Nothing imports AlarmStyle via the styles namespace (blink/simple both
import from styles.base directly), so the redundant-alias re-export was
dead weight that only existed to silence ruff F401.
Add an Alarm Styles section (simple vs blink), show --style in manual usage,
and update the GraphQL examples (setAlarm/getAlarms/startRinging) to include
the style field/argument. Note backward compat for legacy cron entries.
- crontab_manager.get_alarms now parses --style from the cron command
(defaults to 'blink' for legacy entries without --style)
- GraphQL Alarm type gains a 'style' field
- setAlarm accepts a 'style' arg (default 'simple') and validates it
against the styles registry; unknown styles raise a GraphQL error
- _default_command embeds --style <name> into the cron command
- startRinging accepts 'style' (default 'simple') and passes --style to
the spawned wecker.py
The crontab remains the single source of truth; the style simply rides
in the cron command alongside --music-file.
Introduce a styles/ plugin package:
- styles/base.py: AlarmStyle ABC (injected set_led, update()->bool contract)
- styles/blink.py: BlinkStyle, the existing count-the-blinks puzzle moved
out of wecker.py's AlarmClock
- styles/simple.py: SimpleStyle, press-once-to-stop (the new default style)
- styles/__init__.py: STYLES registry + get_style() validator + LEGACY_STYLE
wecker.py now resolves the style via the registry and owns only the shared
music/button/cleanup loop; the --style CLI arg defaults to blink so existing
cron entries keep their behaviour. Unknown styles raise before hardware init.
The default wecker.py shell command was duplicated across set_alarm()
and start_ringing(). Extracted a shared _default_command() helper so
both callers use the same definition.
- Add startWecker mutation: starts wecker.py via subprocess if not already ringing
- Add stopWecker mutation: kills the running wecker process via SIGTERM if ringing
- Both mutations handle the 'already ringing' / 'not ringing' edge cases gracefully
- Update README with API documentation for the new mutations
- Add comprehensive tests (unit + GraphQL endpoint)
Features:
- New GraphQL query 'isRinging' returns true/false if wecker is active
- Checks the wecker process via PID file (PID_FILE in common.py)
DRY refactoring:
- Extract PID_FILE into shared common.py module
- Both wecker.py and api/schema.py import from common
- DRY enforcement tests verify identity (is) not just equality
Tests:
- test_is_ringing_returns_false_when_not_running
- test_is_ringing_returns_true_when_running
- test_pid_file_defined_once_across_modules (DRY enforcement)
- test_pid_file_same_shared_constant_in_api (DRY enforcement)
- Cleaned up unused imports in test_single_instance.py
Docs:
- Updated README.md with isRinging query documentation
- Fixed api/schema.py to generate cron jobs using '>> wecker.log' so logs aren't truncated.
- Added test_bugfix_default_command_uses_append_for_logs in tests/test_api.py to prevent regression.
- Added RPi.GPIO and pygame to pyproject.toml via 'uv add' to fix ModuleNotFoundError in cron jobs.
- Updated README.md to reflect that system python packages are no longer needed.
When transitioning from STATE_BLINKING to STATE_WAIT_FOR_INPUT, the script used a stale timestamp `now` from before the blocking `blink_led` function. This caused the puzzle to immediately evaluate as failed. Updated to use `time.time()` after blinking finishes to ensure the user gets the full 3 seconds to enter their answer.