From deb89b3e030dc0196910a698d2f8d54bdb08f617 Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Sun, 27 Sep 2026 20:24:01 +0200 Subject: [PATCH] Relaxed days from freedays.WORKDAYS; delete the dead rtcwake mixin and its stale ALARM_DAYS Co-Authored-By: Claude Opus 5 (1M context) --- .pre-commit-config.yaml | 4 +- pyproject.toml | 2 +- requirements.txt | 2 +- screen_locker/_constants.py | 10 -- screen_locker/_shutdown.py | 3 +- screen_locker/_wake_alarm.py | 93 ----------- screen_locker/_weekly_check.py | 6 +- screen_locker/tests/test_wake_shutdown.py | 189 ---------------------- 8 files changed, 9 insertions(+), 300 deletions(-) delete mode 100644 screen_locker/_wake_alarm.py delete mode 100644 screen_locker/tests/test_wake_shutdown.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b317423..cc24777 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -128,7 +128,7 @@ repos: # than merely an old one. - gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.8.1#subdirectory=gatelock - crdt-sync @ git+https://github.com/kuhyx/utils@crdt-sync-v0.9.1#subdirectory=crdt-sync - - freedays @ git+https://github.com/kuhyx/utils@freedays-v0.2.0#subdirectory=freedays + - freedays @ git+https://github.com/kuhyx/utils@freedays-v0.3.0#subdirectory=freedays - mcp>=2,<3 - pydantic - id: pylint @@ -148,7 +148,7 @@ repos: # than merely an old one. - gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.8.1#subdirectory=gatelock - crdt-sync @ git+https://github.com/kuhyx/utils@crdt-sync-v0.9.1#subdirectory=crdt-sync - - freedays @ git+https://github.com/kuhyx/utils@freedays-v0.2.0#subdirectory=freedays + - freedays @ git+https://github.com/kuhyx/utils@freedays-v0.3.0#subdirectory=freedays - mcp>=2,<3 - pydantic diff --git a/pyproject.toml b/pyproject.toml index 46d145a..57517cd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ requires-python = ">=3.14" dependencies = [ "gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.8.2#subdirectory=gatelock", "crdt-sync @ git+https://github.com/kuhyx/utils@crdt-sync-v0.9.1#subdirectory=crdt-sync", - "freedays @ git+https://github.com/kuhyx/utils@freedays-v0.2.0#subdirectory=freedays", + "freedays @ git+https://github.com/kuhyx/utils@freedays-v0.3.0#subdirectory=freedays", ] [project.scripts] diff --git a/requirements.txt b/requirements.txt index 532df53..db32cf3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ bandit==1.9.4 codespell==2.4.3 coverage==7.16.2 crdt-sync @ git+https://github.com/kuhyx/utils@crdt-sync-v0.9.1#subdirectory=crdt-sync -freedays @ git+https://github.com/kuhyx/utils@freedays-v0.2.0#subdirectory=freedays +freedays @ git+https://github.com/kuhyx/utils@freedays-v0.3.0#subdirectory=freedays gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.8.2#subdirectory=gatelock mcp==2.2.0 mypy==2.3.1 diff --git a/screen_locker/_constants.py b/screen_locker/_constants.py index f83ae63..4029ec1 100644 --- a/screen_locker/_constants.py +++ b/screen_locker/_constants.py @@ -128,16 +128,6 @@ SYNC_TOMBSTONES_FILE = Path(__file__).resolve().parent / "sync_tombstones.json" # it's a same-day pending flag, checked against its own "date" field. EARLY_BIRD_PENDING_FILE = Path(__file__).resolve().parent / "early_bird_pending.json" -# --------------------------------------------------------------------------- -# Wake-alarm integration (originally from wake_alarm._constants / _state). -# These must match the values used by the companion wake_alarm service. -# --------------------------------------------------------------------------- -# Days on which the wake alarm fires (0=Mon … 6=Sun). -ALARM_DAYS: frozenset[int] = frozenset({0, 4, 5, 6}) -# How many hours after midnight the alarm triggers (configurable in wake_alarm). -WAKE_AFTER_HOURS: int = 8 -# Path to the rtcwake binary. -RTCWAKE_BIN: str = "/usr/sbin/rtcwake" # The PC's copy of the phone's morning session, written HMAC-signed by # wake-alarm (`python -m wake_alarm._session --refresh`, every 2 min through # the morning). Read here, never fetched: wake-alarm is the only Firebase diff --git a/screen_locker/_shutdown.py b/screen_locker/_shutdown.py index 5eccc59..4dcbd20 100644 --- a/screen_locker/_shutdown.py +++ b/screen_locker/_shutdown.py @@ -12,7 +12,6 @@ from screen_locker._constants import ( ) from screen_locker._day import today_str from screen_locker._shutdown_sick_state import SickDayStateMixin -from screen_locker._wake_alarm import WakeAlarmMixin from screen_locker._workout_credit import FIRST_WORKOUT_BONUS_HOURS if TYPE_CHECKING: @@ -49,7 +48,7 @@ def read_shutdown_config(path: Path) -> tuple[int, int, int] | None: ) -class ShutdownMixin(SickDayStateMixin, WakeAlarmMixin): +class ShutdownMixin(SickDayStateMixin): """Mixin providing shutdown schedule adjustment functionality.""" def _apply_earlier_shutdown(self, today: str) -> bool: diff --git a/screen_locker/_wake_alarm.py b/screen_locker/_wake_alarm.py deleted file mode 100644 index cc4845c..0000000 --- a/screen_locker/_wake_alarm.py +++ /dev/null @@ -1,93 +0,0 @@ -"""rtcwake wake-alarm scheduling, split out of the shutdown mixin. - -Split out of :mod:`screen_locker._shutdown` to keep every file under the -250-line cap. Composed back into ``ShutdownMixin`` there, so callers see no -change. -""" - -from __future__ import annotations - -import calendar -from datetime import UTC, datetime, timedelta -import logging -import subprocess - -from screen_locker._constants import ALARM_DAYS, RTCWAKE_BIN, WAKE_AFTER_HOURS - -_logger = logging.getLogger(__name__) - - -class WakeAlarmMixin: - """Schedules an RTC wake alarm for the next alarm day.""" - - # ------------------------------------------------------------------ - # rtcwake integration for weekend wake alarm - # ------------------------------------------------------------------ - - @staticmethod - def _is_tomorrow_alarm_day() -> bool: - """Check if tomorrow is an alarm day.""" - tomorrow = datetime.now(tz=UTC) + timedelta(days=1) - return tomorrow.weekday() in ALARM_DAYS - - @staticmethod - def _compute_wake_timestamp() -> int: - """Compute the UTC epoch timestamp for the next wake alarm. - - Returns: - Epoch seconds WAKE_AFTER_HOURS from now. - """ - wake_time = datetime.now(tz=UTC) + timedelta( - hours=WAKE_AFTER_HOURS, - ) - return calendar.timegm(wake_time.utctimetuple()) - - @staticmethod - def _schedule_rtcwake() -> bool: - """Set rtcwake to power on the PC after WAKE_AFTER_HOURS. - - Uses ``rtcwake -m disk`` to hibernate immediately while programming - the RTC to restore power at wake_epoch. Hibernate is completely - silent and dark (state written to swap file), making it suitable - when the PC is in a bedroom. - - Returns: - True if rtcwake was set successfully, False otherwise. - """ - wake_epoch = WakeAlarmMixin._compute_wake_timestamp() - cmd = [ - "/usr/bin/sudo", - RTCWAKE_BIN, - "-m", - "disk", - "-t", - str(wake_epoch), - ] - try: - subprocess.run( - cmd, - check=True, - capture_output=True, - text=True, - ) - except subprocess.SubprocessError as exc: - _logger.warning("Failed to set rtcwake: %s", exc) - return False - _logger.info( - "rtcwake set: PC will wake at epoch %d", - wake_epoch, - ) - return True - - def schedule_wake_if_needed(self) -> bool: - """Schedule rtcwake if tomorrow is an alarm day. - - Call this at shutdown time. - - Returns: - True if wake was scheduled, False if not needed or failed. - """ - if not self._is_tomorrow_alarm_day(): - _logger.info("Tomorrow is not an alarm day — skipping rtcwake") - return False - return self._schedule_rtcwake() diff --git a/screen_locker/_weekly_check.py b/screen_locker/_weekly_check.py index 4614dfb..a031994 100644 --- a/screen_locker/_weekly_check.py +++ b/screen_locker/_weekly_check.py @@ -16,6 +16,8 @@ from datetime import UTC, datetime, timedelta import logging from typing import TYPE_CHECKING, Any +import freedays + from screen_locker._log_io import load_workout_log from screen_locker._workday_penalty import workday_penalty_today @@ -26,8 +28,8 @@ _logger = logging.getLogger(__name__) WEEKLY_WORKOUT_MINIMUM: int = 5 -# Python weekday(): Mon=0, Tue=1, Wed=2, Thu=3, Fri=4, Sat=5, Sun=6 -_RELAXED_WEEKDAYS: frozenset[int] = frozenset({1, 2, 3}) # Tue, Wed, Thu +# Tue/Wed/Thu: the shared workday set every gate app reads (freedays). +_RELAXED_WEEKDAYS: frozenset[int] = freedays.WORKDAYS # A StrongLifts session performed on the PC itself (the Linux build of # workout_app). Machine-checked exactly like ``phone_verified`` -- same diff --git a/screen_locker/tests/test_wake_shutdown.py b/screen_locker/tests/test_wake_shutdown.py deleted file mode 100644 index 06c62a6..0000000 --- a/screen_locker/tests/test_wake_shutdown.py +++ /dev/null @@ -1,189 +0,0 @@ -"""Tests for rtcwake integration in ShutdownMixin.""" - -from __future__ import annotations - -from datetime import UTC -from typing import TYPE_CHECKING -from unittest.mock import MagicMock, patch - -from screen_locker.tests.conftest import create_locker - -if TYPE_CHECKING: - from pathlib import Path - - -class TestIsTomorrowAlarmDay: - """Tests for _is_tomorrow_alarm_day.""" - - def test_sunday_evening_means_monday_alarm( - self, - mock_tk: MagicMock, - mock_sys_exit: MagicMock, - tmp_path: Path, - ) -> None: - """Sunday evening → Monday is alarm day (weekday=0).""" - locker = create_locker(mock_tk, tmp_path) - from datetime import datetime - - # Sunday 2026-04-12 → tomorrow Monday - with patch( - "screen_locker._wake_alarm.datetime", - ) as mock_dt: - mock_dt.now.return_value = datetime(2026, 4, 12, 23, 0, tzinfo=UTC) - mock_dt.side_effect = datetime - from datetime import timedelta - - # Ensure timedelta works - with patch( - "screen_locker._wake_alarm.timedelta", - timedelta, - ): - assert locker._is_tomorrow_alarm_day() is True - - def test_monday_evening_is_not_alarm_next( - self, - mock_tk: MagicMock, - mock_sys_exit: MagicMock, - tmp_path: Path, - ) -> None: - """Monday evening → Tuesday is NOT an alarm day.""" - locker = create_locker(mock_tk, tmp_path) - from datetime import datetime, timedelta - - # Monday 2026-04-13 → tomorrow Tuesday (weekday=1) - with ( - patch( - "screen_locker._wake_alarm.datetime", - ) as mock_dt, - patch( - "screen_locker._wake_alarm.timedelta", - timedelta, - ), - ): - mock_dt.now.return_value = datetime(2026, 4, 13, 23, 0, tzinfo=UTC) - mock_dt.side_effect = datetime - assert locker._is_tomorrow_alarm_day() is False - - def test_thursday_evening_friday_is_alarm( - self, - mock_tk: MagicMock, - mock_sys_exit: MagicMock, - tmp_path: Path, - ) -> None: - """Thursday evening → Friday is alarm day (weekday=4).""" - locker = create_locker(mock_tk, tmp_path) - from datetime import datetime, timedelta - - # Thursday 2026-04-16 → tomorrow Friday (weekday=4) - with ( - patch( - "screen_locker._wake_alarm.datetime", - ) as mock_dt, - patch( - "screen_locker._wake_alarm.timedelta", - timedelta, - ), - ): - mock_dt.now.return_value = datetime(2026, 4, 16, 23, 0, tzinfo=UTC) - mock_dt.side_effect = datetime - assert locker._is_tomorrow_alarm_day() is True - - -class TestScheduleRtcwake: - """Tests for _schedule_rtcwake.""" - - def test_success( - self, - mock_tk: MagicMock, - mock_sys_exit: MagicMock, - tmp_path: Path, - ) -> None: - """Successful rtcwake call returns True.""" - locker = create_locker(mock_tk, tmp_path) - with patch( - "screen_locker._wake_alarm.subprocess.run", - ) as mock_run: - mock_run.return_value = MagicMock(returncode=0) - assert locker._schedule_rtcwake() is True - mock_run.assert_called_once() - cmd = mock_run.call_args[0][0] - assert "rtcwake" in cmd[1] - - def test_failure_returns_false( - self, - mock_tk: MagicMock, - mock_sys_exit: MagicMock, - tmp_path: Path, - ) -> None: - """Failed rtcwake call returns False.""" - locker = create_locker(mock_tk, tmp_path) - import subprocess - - with patch( - "screen_locker._wake_alarm.subprocess.run", - side_effect=subprocess.SubprocessError("rtcwake failed"), - ): - assert locker._schedule_rtcwake() is False - - -class TestScheduleWakeIfNeeded: - """Tests for schedule_wake_if_needed.""" - - def test_skips_when_not_alarm_day( - self, - mock_tk: MagicMock, - mock_sys_exit: MagicMock, - tmp_path: Path, - ) -> None: - """Returns False when tomorrow is not an alarm day.""" - locker = create_locker(mock_tk, tmp_path) - with patch.object(locker, "_is_tomorrow_alarm_day", return_value=False): - assert locker.schedule_wake_if_needed() is False - - def test_schedules_when_alarm_day( - self, - mock_tk: MagicMock, - mock_sys_exit: MagicMock, - tmp_path: Path, - ) -> None: - """Returns True when tomorrow is an alarm day and rtcwake succeeds.""" - locker = create_locker(mock_tk, tmp_path) - with ( - patch.object(locker, "_is_tomorrow_alarm_day", return_value=True), - patch.object(locker, "_schedule_rtcwake", return_value=True), - ): - assert locker.schedule_wake_if_needed() is True - - def test_returns_false_when_rtcwake_fails( - self, - mock_tk: MagicMock, - mock_sys_exit: MagicMock, - tmp_path: Path, - ) -> None: - """Returns False when rtcwake call fails.""" - locker = create_locker(mock_tk, tmp_path) - with ( - patch.object(locker, "_is_tomorrow_alarm_day", return_value=True), - patch.object(locker, "_schedule_rtcwake", return_value=False), - ): - assert locker.schedule_wake_if_needed() is False - - -class TestComputeWakeTimestamp: - """Tests for _compute_wake_timestamp.""" - - def test_returns_future_epoch( - self, - mock_tk: MagicMock, - mock_sys_exit: MagicMock, - tmp_path: Path, - ) -> None: - """Wake timestamp is roughly 8 hours from now.""" - locker = create_locker(mock_tk, tmp_path) - import time - - now = int(time.time()) - wake = locker._compute_wake_timestamp() - # Should be ~8 hours ahead (within 60 second tolerance) - expected = now + 8 * 3600 - assert abs(wake - expected) < 60