Relaxed days from freedays.WORKDAYS; delete the dead rtcwake mixin and its stale ALARM_DAYS
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (flutter, flutter-analyze, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (mypy, mypy, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (pylint, pylint, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (rest, , mypy,pylint,flutter-analyze) (push) Waiting to run
Tests / test (push) Waiting to run
Tests / lock-popup-safety (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (0) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (1) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (2) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (3) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (4) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (5) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (6) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (7) (push) Waiting to run
Tests / flutter-coverage (push) Blocked by required conditions
Tests / web-coverage (push) Waiting to run
release-apk / release (APK) (push) Waiting to run

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Krzysztof Rudnicki 2026-09-27 20:24:01 +02:00
parent cf2d917b33
commit deb89b3e03
8 changed files with 9 additions and 300 deletions

View File

@ -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

View File

@ -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]

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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()

View File

@ -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

View File

@ -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