mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-11-10 19:07:16 +01:00
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
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:
parent
cf2d917b33
commit
deb89b3e03
@ -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
|
||||
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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()
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
Loading…
Reference in New Issue
Block a user