From b9c8e6684cd4d3724406bb2b084a9c81bb312d6e Mon Sep 17 00:00:00 2001 From: Jose Date: Sun, 5 Jul 2026 12:23:44 +0200 Subject: [PATCH] Notify the user when voice training fails Training runs in the background when the main entry is set up or its options change. On failure (backend unreachable, audio files not found, ...) the only signal was a log line, so users just saw no recognition result later with no idea why. Track the last training error on the recognition instance and surface it as a persistent notification, dismissed automatically on the next successful train. Co-Authored-By: Claude Opus 4.8 --- .../speaker_recognition/__init__.py | 29 ++++++++++++++++++- .../speaker_recognition/recognition.py | 11 +++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/custom_components/speaker_recognition/__init__.py b/custom_components/speaker_recognition/__init__.py index d8ef158..218ddfa 100644 --- a/custom_components/speaker_recognition/__init__.py +++ b/custom_components/speaker_recognition/__init__.py @@ -2,9 +2,10 @@ from __future__ import annotations +from homeassistant.components import persistent_notification from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from .const import ( CONF_BACKEND_URL, @@ -18,6 +19,31 @@ type SpeakerRecognitionConfigEntry = ConfigEntry[SpeakerRecognition] +TRAINING_NOTIFICATION_ID = "speaker_recognition_training" + + +@callback +def _async_update_training_notification( + hass: HomeAssistant, recognition: SpeakerRecognition +) -> None: + """Notify the user if the last training attempt failed; clear it otherwise. + + Training happens in the background, so without this a failure (backend + unreachable, audio files not found, ...) would only appear in the logs. + """ + if recognition.last_train_error: + persistent_notification.async_create( + hass, + ( + "Speaker Recognition could not train the configured voices:\n\n" + f"{recognition.last_train_error}" + ), + title="Speaker Recognition", + notification_id=TRAINING_NOTIFICATION_ID, + ) + else: + persistent_notification.async_dismiss(hass, TRAINING_NOTIFICATION_ID) + def _get_main_entry(hass: HomeAssistant) -> ConfigEntry | None: """Get the main config entry.""" @@ -50,6 +76,7 @@ async def async_setup_main_entry( if voice_samples: await recognition.async_train() + _async_update_training_notification(hass, recognition) entry.runtime_data = recognition entry.async_on_unload(entry.add_update_listener(async_update_main_listener)) diff --git a/custom_components/speaker_recognition/recognition.py b/custom_components/speaker_recognition/recognition.py index f474660..0f5ffbd 100644 --- a/custom_components/speaker_recognition/recognition.py +++ b/custom_components/speaker_recognition/recognition.py @@ -43,10 +43,15 @@ def __init__( self.hass = hass self.voice_samples = voice_samples self._trained = False + # Human-readable reason the last training attempt failed, or None if the + # last attempt succeeded (or there was nothing to train). Surfaced to the + # user as a notification by __init__.py. + self.last_train_error: str | None = None self._client = SpeakerRecognitionClient(base_url=base_url, timeout=300.0) async def async_train(self) -> None: """Train the speaker recognition model with configured voice samples.""" + self.last_train_error = None _LOGGER.debug( "Training speaker recognition with %d voice samples", len(self.voice_samples), @@ -91,6 +96,11 @@ async def async_train(self) -> None: if not voice_sample_models: _LOGGER.warning("No valid training samples prepared") self._trained = False + self.last_train_error = ( + "No valid audio files were found for the configured voice " + "samples. Make sure the selected files exist under Home " + "Assistant's media folder." + ) return request = TrainingRequest(voice_samples=voice_sample_models) @@ -99,6 +109,7 @@ async def async_train(self) -> None: except (OSError, ValueError, TypeError) as error: _LOGGER.error("Error during training: %s", error) self._trained = False + self.last_train_error = str(error) else: self._trained = True _LOGGER.info(