Update __main__.py
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.
This commit is contained in:
@@ -48,7 +48,7 @@ class MediaPlayers:
|
|||||||
tts: MpvMediaPlayer
|
tts: MpvMediaPlayer
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Mic Mute Handler
|
# Mic Mute / Preferences Handler
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
class MicMuteHandler(EventHandler):
|
class MicMuteHandler(EventHandler):
|
||||||
@@ -73,18 +73,18 @@ class MicMuteHandler(EventHandler):
|
|||||||
is_muted = data.get("state", False)
|
is_muted = data.get("state", False)
|
||||||
if self.state.mic_muted != is_muted:
|
if self.state.mic_muted != is_muted:
|
||||||
self.state.mic_muted = is_muted
|
self.state.mic_muted = is_muted
|
||||||
|
|
||||||
# Synchronize the threading event for the audio loop
|
# Synchronize the threading event for the audio loop
|
||||||
if is_muted:
|
if is_muted:
|
||||||
self.state.mic_muted_event.clear() # Pauses audio thread
|
self.state.mic_muted_event.clear() # Pauses audio thread
|
||||||
else:
|
else:
|
||||||
self.state.mic_muted_event.set() # Resumes audio thread
|
self.state.mic_muted_event.set() # Resumes audio thread
|
||||||
|
|
||||||
_LOGGER.debug("Mic muted = %s", is_muted)
|
_LOGGER.debug("Mic muted = %s", is_muted)
|
||||||
|
|
||||||
if self.mqtt_controller:
|
if self.mqtt_controller:
|
||||||
self.mqtt_controller.publish_mute_state(is_muted)
|
self.mqtt_controller.publish_mute_state(is_muted)
|
||||||
|
|
||||||
if is_muted:
|
if is_muted:
|
||||||
self.event_bus.publish("mic_muted")
|
self.event_bus.publish("mic_muted")
|
||||||
else:
|
else:
|
||||||
@@ -98,6 +98,45 @@ class MicMuteHandler(EventHandler):
|
|||||||
self.state.preferences.num_leds = num_leds
|
self.state.preferences.num_leds = num_leds
|
||||||
self.state.save_preferences()
|
self.state.save_preferences()
|
||||||
|
|
||||||
|
@subscribe
|
||||||
|
def set_alarm_duration(self, data: dict):
|
||||||
|
"""
|
||||||
|
Event handler to save alarm_duration_seconds to preferences.
|
||||||
|
|
||||||
|
Expected payload from MQTT controller:
|
||||||
|
{ "alarm_duration_seconds": <int> }
|
||||||
|
|
||||||
|
Semantics:
|
||||||
|
0 -> infinite alarm (only stop via Stop wake word / wake word logic)
|
||||||
|
>0 -> auto-stop alarm after N seconds (plus Stop wake word support)
|
||||||
|
"""
|
||||||
|
duration = data.get("alarm_duration_seconds")
|
||||||
|
if duration is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
duration = int(duration)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Invalid alarm_duration_seconds value received: %r", duration
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if duration < 0:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Negative alarm_duration_seconds (%d) is not allowed; ignoring",
|
||||||
|
duration,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
current = getattr(self.state.preferences, "alarm_duration_seconds", 0)
|
||||||
|
if current != duration:
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Updating alarm_duration_seconds: %s -> %s", current, duration
|
||||||
|
)
|
||||||
|
self.state.preferences.alarm_duration_seconds = duration
|
||||||
|
self.state.save_preferences()
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Main Application
|
# Main Application
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -105,13 +144,13 @@ class MicMuteHandler(EventHandler):
|
|||||||
async def main() -> None:
|
async def main() -> None:
|
||||||
# --- 1. Load Basics ---
|
# --- 1. Load Basics ---
|
||||||
config, loop, event_bus = _init_basics()
|
config, loop, event_bus = _init_basics()
|
||||||
|
|
||||||
# --- 2. Load Preferences ---
|
# --- 2. Load Preferences ---
|
||||||
preferences = _load_preferences(config)
|
preferences = _load_preferences(config)
|
||||||
|
|
||||||
# --- 3. Find Microphone ---
|
# --- 3. Find Microphone ---
|
||||||
mic = _get_microphone(config)
|
mic = _get_microphone(config)
|
||||||
|
|
||||||
# --- 4. Load Wake Words ---
|
# --- 4. Load Wake Words ---
|
||||||
wake_word_data = _load_wake_words(config, preferences)
|
wake_word_data = _load_wake_words(config, preferences)
|
||||||
|
|
||||||
@@ -120,17 +159,17 @@ async def main() -> None:
|
|||||||
|
|
||||||
# --- 6. Create Server State ---
|
# --- 6. Create Server State ---
|
||||||
state = _create_server_state(
|
state = _create_server_state(
|
||||||
config, loop, event_bus, preferences,
|
config, loop, event_bus, preferences,
|
||||||
wake_word_data, media_players
|
wake_word_data, media_players
|
||||||
)
|
)
|
||||||
|
|
||||||
# --- 7. Initialize Controllers ---
|
# --- 7. Initialize Controllers ---
|
||||||
_init_controllers(loop, event_bus, state, config, preferences)
|
_init_controllers(loop, event_bus, state, config, preferences)
|
||||||
|
|
||||||
# --- 8. Start Audio Engine ---
|
# --- 8. Start Audio Engine ---
|
||||||
audio_engine = AudioEngine(state, mic, config.audio.input_block_size)
|
audio_engine = AudioEngine(state, mic, config.audio.input_block_size)
|
||||||
audio_engine.start()
|
audio_engine.start()
|
||||||
|
|
||||||
# --- 9. Run Server ---
|
# --- 9. Run Server ---
|
||||||
try:
|
try:
|
||||||
await _run_server(state, config)
|
await _run_server(state, config)
|
||||||
@@ -138,7 +177,7 @@ async def main() -> None:
|
|||||||
# --- 10. Cleanup ---
|
# --- 10. Cleanup ---
|
||||||
_LOGGER.debug("Shutting down...")
|
_LOGGER.debug("Shutting down...")
|
||||||
audio_engine.stop()
|
audio_engine.stop()
|
||||||
|
|
||||||
if hasattr(state, "mqtt_controller") and state.mqtt_controller:
|
if hasattr(state, "mqtt_controller") and state.mqtt_controller:
|
||||||
_LOGGER.debug("Stopping MQTT controller...")
|
_LOGGER.debug("Stopping MQTT controller...")
|
||||||
state.mqtt_controller.stop()
|
state.mqtt_controller.stop()
|
||||||
@@ -166,7 +205,10 @@ def _init_basics() -> Tuple[Config, asyncio.AbstractEventLoop, EventBus]:
|
|||||||
for idx, mic in enumerate(sc.all_microphones(include_loopback=False)):
|
for idx, mic in enumerate(sc.all_microphones(include_loopback=False)):
|
||||||
print(f"[{idx}]", mic.name)
|
print(f"[{idx}]", mic.name)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
_LOGGER.error("Error listing input devices (ensure audio backend is working): %s", e)
|
_LOGGER.error(
|
||||||
|
"Error listing input devices (ensure audio backend is working): %s",
|
||||||
|
e,
|
||||||
|
)
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
if args.list_output_devices:
|
if args.list_output_devices:
|
||||||
@@ -182,7 +224,7 @@ def _init_basics() -> Tuple[Config, asyncio.AbstractEventLoop, EventBus]:
|
|||||||
|
|
||||||
config_path = args.config
|
config_path = args.config
|
||||||
if not config_path.is_absolute():
|
if not config_path.is_absolute():
|
||||||
config_path = _REPO_DIR / config_path
|
config_path = _REPO_DIR / config_path
|
||||||
config = load_config_from_json(config_path)
|
config = load_config_from_json(config_path)
|
||||||
|
|
||||||
if args.debug:
|
if args.debug:
|
||||||
@@ -197,7 +239,7 @@ def _init_basics() -> Tuple[Config, asyncio.AbstractEventLoop, EventBus]:
|
|||||||
|
|
||||||
loop = asyncio.get_running_loop()
|
loop = asyncio.get_running_loop()
|
||||||
event_bus = EventBus()
|
event_bus = EventBus()
|
||||||
|
|
||||||
return config, loop, event_bus
|
return config, loop, event_bus
|
||||||
|
|
||||||
def _load_preferences(config: Config) -> Preferences:
|
def _load_preferences(config: Config) -> Preferences:
|
||||||
@@ -209,8 +251,14 @@ def _load_preferences(config: Config) -> Preferences:
|
|||||||
preferences = Preferences(**preferences_dict)
|
preferences = Preferences(**preferences_dict)
|
||||||
else:
|
else:
|
||||||
preferences = Preferences()
|
preferences = Preferences()
|
||||||
|
|
||||||
preferences.num_leds = getattr(preferences, 'num_leds', config.led.num_leds)
|
# Backwards-compatible defaults / migrations
|
||||||
|
preferences.num_leds = getattr(preferences, "num_leds", config.led.num_leds)
|
||||||
|
# New: default alarm_duration_seconds, 0 = infinite until Stop/wake word
|
||||||
|
preferences.alarm_duration_seconds = getattr(
|
||||||
|
preferences, "alarm_duration_seconds", 0
|
||||||
|
)
|
||||||
|
|
||||||
return preferences
|
return preferences
|
||||||
|
|
||||||
def _get_microphone(config: Config):
|
def _get_microphone(config: Config):
|
||||||
@@ -224,11 +272,11 @@ def _get_microphone(config: Config):
|
|||||||
mic = sc.get_microphone(config.audio.input_device, include_loopback=False)
|
mic = sc.get_microphone(config.audio.input_device, include_loopback=False)
|
||||||
else:
|
else:
|
||||||
mic = sc.default_microphone()
|
mic = sc.default_microphone()
|
||||||
|
|
||||||
if mic is None:
|
if mic is None:
|
||||||
_LOGGER.critical("No microphone found.")
|
_LOGGER.critical("No microphone found.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
_LOGGER.info("Using audio input device: %s", mic.name)
|
_LOGGER.info("Using audio input device: %s", mic.name)
|
||||||
return mic
|
return mic
|
||||||
|
|
||||||
@@ -236,7 +284,7 @@ def _load_wake_words(config: Config, preferences: Preferences) -> WakeWordData:
|
|||||||
"""Loads all available and active wake word models."""
|
"""Loads all available and active wake word models."""
|
||||||
if not config.wake_word.directories:
|
if not config.wake_word.directories:
|
||||||
config.wake_word.directories = ["wakewords", "wakewords/openWakeWord"]
|
config.wake_word.directories = ["wakewords", "wakewords/openWakeWord"]
|
||||||
|
|
||||||
download_dir = _REPO_DIR / config.wake_word.download_dir
|
download_dir = _REPO_DIR / config.wake_word.download_dir
|
||||||
download_dir.mkdir(parents=True, exist_ok=True)
|
download_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
@@ -254,7 +302,7 @@ def _load_wake_words(config: Config, preferences: Preferences) -> WakeWordData:
|
|||||||
with open(config_path, "r", encoding="utf-8") as f:
|
with open(config_path, "r", encoding="utf-8") as f:
|
||||||
model_config = json.load(f)
|
model_config = json.load(f)
|
||||||
model_type = WakeWordType(model_config["type"])
|
model_type = WakeWordType(model_config["type"])
|
||||||
|
|
||||||
wake_word_path = (
|
wake_word_path = (
|
||||||
config_path.parent / model_config["model"]
|
config_path.parent / model_config["model"]
|
||||||
if model_type == WakeWordType.OPEN_WAKE_WORD
|
if model_type == WakeWordType.OPEN_WAKE_WORD
|
||||||
@@ -271,13 +319,13 @@ def _load_wake_words(config: Config, preferences: Preferences) -> WakeWordData:
|
|||||||
|
|
||||||
active: Set[str] = set()
|
active: Set[str] = set()
|
||||||
models: Dict[str, Union[MicroWakeWord, OpenWakeWord]] = {}
|
models: Dict[str, Union[MicroWakeWord, OpenWakeWord]] = {}
|
||||||
|
|
||||||
if preferences.active_wake_words:
|
if preferences.active_wake_words:
|
||||||
for ww_id in preferences.active_wake_words:
|
for ww_id in preferences.active_wake_words:
|
||||||
if ww_id in available:
|
if ww_id in available:
|
||||||
models[ww_id] = available[ww_id].load()
|
models[ww_id] = available[ww_id].load()
|
||||||
active.add(ww_id)
|
active.add(ww_id)
|
||||||
|
|
||||||
if not models:
|
if not models:
|
||||||
ww_id = config.wake_word.model
|
ww_id = config.wake_word.model
|
||||||
if ww_id in available:
|
if ww_id in available:
|
||||||
@@ -294,17 +342,21 @@ def _load_wake_words(config: Config, preferences: Preferences) -> WakeWordData:
|
|||||||
|
|
||||||
return WakeWordData(available, models, active, stop_model)
|
return WakeWordData(available, models, active, stop_model)
|
||||||
|
|
||||||
def _init_media_players(loop: asyncio.AbstractEventLoop, config: Config, preferences: Preferences) -> MediaPlayers:
|
def _init_media_players(
|
||||||
|
loop: asyncio.AbstractEventLoop,
|
||||||
|
config: Config,
|
||||||
|
preferences: Preferences,
|
||||||
|
) -> MediaPlayers:
|
||||||
"""Initializes the music and TTS media players."""
|
"""Initializes the music and TTS media players."""
|
||||||
music_player = MpvMediaPlayer(
|
music_player = MpvMediaPlayer(
|
||||||
loop=loop,
|
loop=loop,
|
||||||
device=config.audio.output_device,
|
device=config.audio.output_device,
|
||||||
initial_volume=preferences.volume_level
|
initial_volume=preferences.volume_level,
|
||||||
)
|
)
|
||||||
tts_player = MpvMediaPlayer(
|
tts_player = MpvMediaPlayer(
|
||||||
loop=loop,
|
loop=loop,
|
||||||
device=config.audio.output_device,
|
device=config.audio.output_device,
|
||||||
initial_volume=preferences.volume_level
|
initial_volume=preferences.volume_level,
|
||||||
)
|
)
|
||||||
return MediaPlayers(music=music_player, tts=tts_player)
|
return MediaPlayers(music=music_player, tts=tts_player)
|
||||||
|
|
||||||
@@ -375,15 +427,17 @@ def _init_controllers(
|
|||||||
async def _run_server(state: ServerState, config: Config):
|
async def _run_server(state: ServerState, config: Config):
|
||||||
"""Starts the ESPHome server and ZeroConf discovery."""
|
"""Starts the ESPHome server and ZeroConf discovery."""
|
||||||
server = await state.loop.create_server(
|
server = await state.loop.create_server(
|
||||||
lambda: VoiceSatelliteProtocol(state),
|
lambda: VoiceSatelliteProtocol(state),
|
||||||
host=config.esphome.host,
|
host=config.esphome.host,
|
||||||
port=config.esphome.port
|
port=config.esphome.port,
|
||||||
)
|
)
|
||||||
discovery = HomeAssistantZeroconf(port=config.esphome.port, name=config.app.name)
|
discovery = HomeAssistantZeroconf(port=config.esphome.port, name=config.app.name)
|
||||||
await discovery.register_server()
|
await discovery.register_server()
|
||||||
|
|
||||||
async with server:
|
async with server:
|
||||||
_LOGGER.info("Server started (host=%s, port=%s)", config.esphome.host, config.esphome.port)
|
_LOGGER.info(
|
||||||
|
"Server started (host=%s, port=%s)", config.esphome.host, config.esphome.port
|
||||||
|
)
|
||||||
await server.serve_forever()
|
await server.serve_forever()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user