mirror of
https://github.com/home-assistant/core.git
synced 2026-04-02 08:26:41 +01:00
Fix infinite loop in esphome assist_satellite (#163097)
Co-authored-by: Artur Pragacz <artur@pragacz.com>
This commit is contained in:
committed by
Franck Nijhof
parent
bc9ae3dad6
commit
a4da363ff2
@@ -524,14 +524,10 @@ class EsphomeAssistSatellite(
|
||||
self._active_pipeline_index = 0
|
||||
|
||||
maybe_pipeline_index = 0
|
||||
while True:
|
||||
if not (ww_entity_id := self.get_wake_word_entity(maybe_pipeline_index)):
|
||||
break
|
||||
|
||||
if not (ww_state := self.hass.states.get(ww_entity_id)):
|
||||
continue
|
||||
|
||||
if ww_state.state == wake_word_phrase:
|
||||
while ww_entity_id := self.get_wake_word_entity(maybe_pipeline_index):
|
||||
if (
|
||||
ww_state := self.hass.states.get(ww_entity_id)
|
||||
) and ww_state.state == wake_word_phrase:
|
||||
# First match
|
||||
self._active_pipeline_index = maybe_pipeline_index
|
||||
break
|
||||
|
||||
@@ -2091,6 +2091,129 @@ async def test_secondary_pipeline(
|
||||
assert (await get_pipeline(None)) == "Primary Pipeline"
|
||||
|
||||
|
||||
@pytest.mark.timeout(5)
|
||||
async def test_pipeline_start_missing_wake_word_entity_state(
|
||||
hass: HomeAssistant,
|
||||
mock_client: APIClient,
|
||||
mock_esphome_device: MockESPHomeDeviceType,
|
||||
) -> None:
|
||||
"""Test pipeline selection when a wake word entity has no state.
|
||||
|
||||
Regression test for an infinite loop that occurred when a wake word entity
|
||||
existed in the entity registry but had no state in the state machine.
|
||||
"""
|
||||
assert await async_setup_component(hass, "assist_pipeline", {})
|
||||
pipeline_data = hass.data[KEY_ASSIST_PIPELINE]
|
||||
pipeline_id_to_name: dict[str, str] = {}
|
||||
for pipeline_name in ("Primary Pipeline", "Secondary Pipeline"):
|
||||
pipeline = await pipeline_data.pipeline_store.async_create_item(
|
||||
{
|
||||
"name": pipeline_name,
|
||||
"language": "en-US",
|
||||
"conversation_engine": None,
|
||||
"conversation_language": "en-US",
|
||||
"tts_engine": None,
|
||||
"tts_language": None,
|
||||
"tts_voice": None,
|
||||
"stt_engine": None,
|
||||
"stt_language": None,
|
||||
"wake_word_entity": None,
|
||||
"wake_word_id": None,
|
||||
}
|
||||
)
|
||||
pipeline_id_to_name[pipeline.id] = pipeline_name
|
||||
|
||||
device_config = AssistSatelliteConfiguration(
|
||||
available_wake_words=[
|
||||
AssistSatelliteWakeWord("okay_nabu", "Okay Nabu", ["en"]),
|
||||
AssistSatelliteWakeWord("hey_jarvis", "Hey Jarvis", ["en"]),
|
||||
],
|
||||
active_wake_words=["hey_jarvis"],
|
||||
max_active_wake_words=2,
|
||||
)
|
||||
mock_client.get_voice_assistant_configuration.return_value = device_config
|
||||
|
||||
configuration_set = asyncio.Event()
|
||||
|
||||
async def wrapper(*args, **kwargs):
|
||||
device_config.active_wake_words = kwargs["active_wake_words"]
|
||||
configuration_set.set()
|
||||
|
||||
mock_client.set_voice_assistant_configuration = AsyncMock(side_effect=wrapper)
|
||||
|
||||
mock_device = await mock_esphome_device(
|
||||
mock_client=mock_client,
|
||||
device_info={
|
||||
"voice_assistant_feature_flags": VoiceAssistantFeature.VOICE_ASSISTANT
|
||||
| VoiceAssistantFeature.ANNOUNCE
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
satellite = get_satellite_entity(hass, mock_device.device_info.mac_address)
|
||||
assert satellite is not None
|
||||
|
||||
# Set primary/secondary wake words and assistants
|
||||
await hass.services.async_call(
|
||||
SELECT_DOMAIN,
|
||||
SERVICE_SELECT_OPTION,
|
||||
{ATTR_ENTITY_ID: "select.test_wake_word", "option": "Okay Nabu"},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.services.async_call(
|
||||
SELECT_DOMAIN,
|
||||
SERVICE_SELECT_OPTION,
|
||||
{ATTR_ENTITY_ID: "select.test_assistant", "option": "Primary Pipeline"},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.services.async_call(
|
||||
SELECT_DOMAIN,
|
||||
SERVICE_SELECT_OPTION,
|
||||
{ATTR_ENTITY_ID: "select.test_wake_word_2", "option": "Hey Jarvis"},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.services.async_call(
|
||||
SELECT_DOMAIN,
|
||||
SERVICE_SELECT_OPTION,
|
||||
{
|
||||
ATTR_ENTITY_ID: "select.test_assistant_2",
|
||||
"option": "Secondary Pipeline",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Remove state for primary wake word entity to simulate the bug scenario:
|
||||
# entity exists in the registry but has no state in the state machine.
|
||||
hass.states.async_remove("select.test_wake_word")
|
||||
|
||||
async def get_pipeline(wake_word_phrase):
|
||||
with patch(
|
||||
"homeassistant.components.assist_satellite.entity.async_pipeline_from_audio_stream",
|
||||
) as mock_pipeline_from_audio_stream:
|
||||
await satellite.handle_pipeline_start(
|
||||
conversation_id="",
|
||||
flags=0,
|
||||
audio_settings=VoiceAssistantAudioSettings(),
|
||||
wake_word_phrase=wake_word_phrase,
|
||||
)
|
||||
|
||||
mock_pipeline_from_audio_stream.assert_called_once()
|
||||
kwargs = mock_pipeline_from_audio_stream.call_args_list[0].kwargs
|
||||
return pipeline_id_to_name[kwargs["pipeline_id"]]
|
||||
|
||||
# The primary wake word entity has no state, so the loop must skip it.
|
||||
# The secondary wake word entity still has state, so "Hey Jarvis" matches.
|
||||
assert (await get_pipeline("Hey Jarvis")) == "Secondary Pipeline"
|
||||
|
||||
# "Okay Nabu" can't match because its entity has no state — falls back to
|
||||
# default pipeline (index 0).
|
||||
assert (await get_pipeline("Okay Nabu")) == "Primary Pipeline"
|
||||
|
||||
# No wake word phrase also falls back to default.
|
||||
assert (await get_pipeline(None)) == "Primary Pipeline"
|
||||
|
||||
|
||||
async def test_custom_wake_words(
|
||||
hass: HomeAssistant,
|
||||
mock_client: APIClient,
|
||||
|
||||
Reference in New Issue
Block a user