Added a new ButtonConfig dataclass:
enabled: bool = False
pin: int = 17 (BCM pin for the button)
long_press_seconds: float = 1.0
Extended the main Config dataclass with a button: ButtonConfig field.
Updated load_config_from_json() to construct button_config from the "button" section in config.json.
This allows enabling/disabling the button, changing the GPIO pin, and tuning the long-press threshold (and optional poll interval) without code changes.
Added a new user preference: alarm_duration_seconds (0 = infinite / Stop-only, >0 = auto-stop after N seconds).
Exposed that preference via MQTT/HA as a number entity (Alarm Duration) in seconds.
Updated the satellite timer logic so the alarm:
loops as before,
stops on the Stop wake word or wake wake word (existing semantics),
and optionally auto-stops after alarm_duration_seconds if configured.
Ensured all paths that stop the alarm cleanly restore audio state and wake word activation.
Added internal tracking for an alarm auto-stop:
self._timer_auto_stop_handle: Optional[asyncio.TimerHandle]
On VOICE_ASSISTANT_TIMER_FINISHED:
enables the Stop wake word,
starts the repeating timer_finished_sound alarm as before,
reads state.preferences.alarm_duration_seconds and, if > 0, schedules an auto-stop callback after that duration.
Centralized alarm stopping in helper methods:
_clear_timer_auto_stop() – cancels any pending auto-stop timer.
_stop_timer_alarm(reason: str) – stops the alarm sound, clears flags, unducks music, and removes the Stop wake word from active_wake_words.
_auto_stop_timer_alarm() – invoked by the scheduled timer, calls _stop_timer_alarm("auto_timeout").
Updated behavior paths:
wakeup() now stops a ringing alarm instead of starting a new conversation if a timer is active.
stop() (Stop wake word) stops a ringing alarm first; only falls back to stopping TTS if no alarm is active.
_play_timer_finished() continues to loop the sound until _timer_finished is cleared, then unducks and exits cleanly.
Added MQTT topics for the new alarm duration control:
lva/<device_id>/alarm_duration/set
lva/<device_id>/alarm_duration/state
Published a new Home Assistant number discovery config:
number.<device_id>_alarm_duration (0–3600 seconds, step 5, unit s).
On /alarm_duration/set:
validates and parses the payload as a non-negative integer,
publishes a set_alarm_duration event on the EventBus,
echoes the value back to /alarm_duration/state.
On connect, publishes the current alarm_duration_seconds from preferences to the state topic for retained startup behavior.
Updated Preferences dataclass with a new field:
alarm_duration_seconds: int = 0
Kept ServerState.save_preferences() unchanged so alarm_duration_seconds is automatically serialized into preferences.json.
Extended Preferences loading to initialize and backfill a new alarm_duration_seconds field (default 0).
Added a set_alarm_duration event handler to MicMuteHandler which:
validates and normalizes the duration value,
updates state.preferences.alarm_duration_seconds,
persists the updated preferences to disk.
Kept the existing “single writer” pattern (MicMuteHandler) for preferences/state updates.
File: linux_voice_assistant/__main__.py
In _init_basics(), the --list-output-devices block is updated so that:
It exits with 0 on success.
It exits with 1 only if an exception occurs.
Wrap the board import in a try/except and introduce a _BOARD_AVAILABLE flag.
If board (or Blinka/platformdetect) is not available or fails, log a clear warning and skip hardware LED initialization.
Keep the event subscriptions intact, but ensure all LED actions become no-ops when _is_ready is False.
Removed the home-grown backend probing / priority logic (pipewire → pulse → alsa) when no device is explicitly configured.
Kept support for explicitly specifying an audio_output_device in the config/CLI; if provided, it is passed through to mpv.
Added debug logging after mpv initialization to show:
current ao
current audio-device
the full audio-device-list
New CLI helpers
Added support for:
script/run --list-input-devices
script/run --list-output-devices
These code paths:
Initialize just enough of the audio stack to enumerate devices.
Print a clean, human-readable list of input/output devices (including PipeWire / PulseAudio virtual devices like Echo-Cancel Source and pipewire/echo-cancel-sink).
Exit cleanly with status 0 instead of trying to start the full LVA process afterward (fixing the previous CalledProcessError when --list-output-devices was used).
Behavior change
__main__.py now treats the “list devices” modes as early-exit utility modes, not “run LVA and then also print devices”.
This makes it practical to use these commands in setup docs and shell scripts.
Behavioral & logging changes
Improved the wake word processing loop to make the following clearly visible in DEBUG logs:
The current set of active wake word IDs (MWW/OWW).
When the Stop wake word:
is activated by the model,
is accepted (i.e. meets refractory timing + is currently allowed), and
is ignored because it is no longer in active_wake_words.
Ensured the special stop model:
Is only treated as “active” when state.stop_word.is_active is True.
Is flipped back to inactive immediately after satellite.stop() is called, so:
The first “Stop” that passes all checks cancels playback and alarm tones.
Subsequent detections in the same moment appear as “TRIGGERED but IGNORED” (expected and now clearly logged).
Why this matters
When the LVA is playing audio (TTS or timer alarm), you can now:
See exactly when the Stop model fires.
Confirm that playback is being canceled at the right time.
Understand why you might see multiple “Stop wake word ACTIVATED” messages even though the alarm already stopped (model still “hearing” the tail, but logically ignored).
This matches the behavior seen in your logs:
At lower volumes / with AEC, stop is reliably accepted and playback stops immediately.
At high volumes without AEC, the model sees far less “clean” speech, so activations are rare or absent.