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(