from abc import ABC, abstractmethod from typing import Callable class AlarmStyle(ABC): """A pluggable alarm behaviour. A style owns *its own ringing tone*: it uses the audio engine (pygame) directly and decides when the alarm stops and what the LED does. It never touches hardware directly: ``set_led`` is injected by the runner, and the runner owns the audio engine lifecycle (mixer init/quit). Styles stay import-safe in the API process — ``import pygame`` does not initialise audio, so the API can import styles just to validate names. Lifecycle: construct once with ``set_led`` (plus any style-specific keyword config the runner passes); 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. Only ``set_led`` is universal. Style-specific config (e.g. ``music_file``) belongs on the subclass, not here, so the base contract never grows as styles are added; the runner passes config as keyword arguments and each style declares only the ones it uses (extra kwargs are swallowed). """ def __init__(self, set_led: Callable[[bool], None], **kwargs): self.set_led = set_led 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. """