mirror of
https://github.com/home-assistant/core.git
synced 2026-04-17 23:53:49 +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
|
self._active_pipeline_index = 0
|
||||||
|
|
||||||
maybe_pipeline_index = 0
|
maybe_pipeline_index = 0
|
||||||
while True:
|
while ww_entity_id := self.get_wake_word_entity(maybe_pipeline_index):
|
||||||
if not (ww_entity_id := self.get_wake_word_entity(maybe_pipeline_index)):
|
if (
|
||||||
break
|
ww_state := self.hass.states.get(ww_entity_id)
|
||||||
|
) and ww_state.state == wake_word_phrase:
|
||||||
if not (ww_state := self.hass.states.get(ww_entity_id)):
|
|
||||||
continue
|
|
||||||
|
|
||||||
if ww_state.state == wake_word_phrase:
|
|
||||||
# First match
|
# First match
|
||||||
self._active_pipeline_index = maybe_pipeline_index
|
self._active_pipeline_index = maybe_pipeline_index
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -2091,6 +2091,129 @@ async def test_secondary_pipeline(
|
|||||||
assert (await get_pipeline(None)) == "Primary 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(
|
async def test_custom_wake_words(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
mock_client: APIClient,
|
mock_client: APIClient,
|
||||||
|
|||||||
Reference in New Issue
Block a user