from abc import ABC, abstractmethod from typing import Callable, Protocol class AlarmSound(Protocol): """Audio capability injected into a style so it can own its ringing tone. Styles stay free of pygame/GPIO imports (the API process imports them only for name validation); the runner supplies a concrete implementation. """ def play_music(self, path: str) -> None: """Load and play a music file on an endless loop.""" ... def play_beep(self) -> None: """Play one short beep tone.""" ... class AlarmStyle(ABC): """A pluggable alarm behaviour. A style owns *its own ringing tone* (via the injected ``sound``) and decides when the alarm stops and what the LED does. It never touches hardware directly: ``set_led`` and ``sound`` are injected by the runner, so styles stay import-safe in the API process (no GPIO/pygame). Lifecycle: construct once with ``set_led``/``sound``/``music_file``; call ``start()`` to begin ringing (kick off the tone and initial LED); then call ``update`` every tick. Return ``False`` from ``update`` to stop the alarm. """ def __init__( self, set_led: Callable[[bool], None], sound: AlarmSound, music_file: str | None = None, ): self.set_led = set_led self.sound = sound self.music_file = music_file def start(self) -> None: """Begin ringing. Override to start the style's tone / initial LED.""" @abstractmethod def update(self, now: float, is_pressed: bool) -> bool: """Advance the style by one tick. Args: now: current time (``time.time()`` from the runner). is_pressed: whether the button is currently held down. Returns: True to keep ringing, False to stop. """