Add announce support to ESPHome Assist Satellite platform (#125157)

Rebuild
This commit is contained in:
Michael Hansen 2024-09-09 07:25:25 -05:00 committed by GitHub
parent 056e6eae82
commit 1dc496a2dd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 169 additions and 0 deletions

View File

@ -74,6 +74,8 @@ _TIMER_EVENT_TYPES: EsphomeEnumMapper[VoiceAssistantTimerEventType, TimerEventTy
) )
) )
_ANNOUNCEMENT_TIMEOUT_SEC = 5 * 60 # 5 minutes
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
@ -183,6 +185,12 @@ class EsphomeAssistSatellite(
) )
) )
if feature_flags & VoiceAssistantFeature.ANNOUNCE:
# Device supports announcements
self._attr_supported_features |= (
assist_satellite.AssistSatelliteEntityFeature.ANNOUNCE
)
async def async_will_remove_from_hass(self) -> None: async def async_will_remove_from_hass(self) -> None:
"""Run when entity will be removed from hass.""" """Run when entity will be removed from hass."""
await super().async_will_remove_from_hass() await super().async_will_remove_from_hass()
@ -251,6 +259,20 @@ class EsphomeAssistSatellite(
self.cli.send_voice_assistant_event(event_type, data_to_send) self.cli.send_voice_assistant_event(event_type, data_to_send)
async def async_announce(self, message: str, media_id: str) -> None:
"""Announce media on the satellite.
Should block until the announcement is done playing.
"""
_LOGGER.debug(
"Waiting for announcement to finished (message=%s, media_id=%s)",
message,
media_id,
)
await self.cli.send_voice_assistant_announcement_await_response(
media_id, _ANNOUNCEMENT_TIMEOUT_SEC, message
)
async def handle_pipeline_start( async def handle_pipeline_start(
self, self,
conversation_id: str, conversation_id: str,

View File

@ -27,6 +27,7 @@ from homeassistant.components import assist_satellite, tts
from homeassistant.components.assist_pipeline import PipelineEvent, PipelineEventType from homeassistant.components.assist_pipeline import PipelineEvent, PipelineEventType
from homeassistant.components.assist_satellite.entity import ( from homeassistant.components.assist_satellite.entity import (
AssistSatelliteEntity, AssistSatelliteEntity,
AssistSatelliteEntityFeature,
AssistSatelliteState, AssistSatelliteState,
) )
from homeassistant.components.esphome import DOMAIN from homeassistant.components.esphome import DOMAIN
@ -34,6 +35,7 @@ from homeassistant.components.esphome.assist_satellite import (
EsphomeAssistSatellite, EsphomeAssistSatellite,
VoiceAssistantUDPServer, VoiceAssistantUDPServer,
) )
from homeassistant.components.media_source import PlayMedia
from homeassistant.const import Platform from homeassistant.const import Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er, intent as intent_helper from homeassistant.helpers import entity_registry as er, intent as intent_helper
@ -891,3 +893,148 @@ async def test_tts_format_from_media_player(
tts.ATTR_PREFERRED_SAMPLE_CHANNELS: 1, tts.ATTR_PREFERRED_SAMPLE_CHANNELS: 1,
tts.ATTR_PREFERRED_SAMPLE_BYTES: 2, tts.ATTR_PREFERRED_SAMPLE_BYTES: 2,
} }
async def test_announce_supported_features(
hass: HomeAssistant,
mock_client: APIClient,
mock_esphome_device: Callable[
[APIClient, list[EntityInfo], list[UserService], list[EntityState]],
Awaitable[MockESPHomeDevice],
],
) -> None:
"""Test that the announce supported feature is set by flags."""
mock_device: MockESPHomeDevice = await mock_esphome_device(
mock_client=mock_client,
entity_info=[],
user_service=[],
states=[],
device_info={
"voice_assistant_feature_flags": VoiceAssistantFeature.VOICE_ASSISTANT
},
)
await hass.async_block_till_done()
satellite = get_satellite_entity(hass, mock_device.device_info.mac_address)
assert satellite is not None
assert not (satellite.supported_features & AssistSatelliteEntityFeature.ANNOUNCE)
async def test_announce_message(
hass: HomeAssistant,
mock_client: APIClient,
mock_esphome_device: Callable[
[APIClient, list[EntityInfo], list[UserService], list[EntityState]],
Awaitable[MockESPHomeDevice],
],
) -> None:
"""Test announcement with message."""
mock_device: MockESPHomeDevice = await mock_esphome_device(
mock_client=mock_client,
entity_info=[],
user_service=[],
states=[],
device_info={
"voice_assistant_feature_flags": VoiceAssistantFeature.VOICE_ASSISTANT
| VoiceAssistantFeature.SPEAKER
| VoiceAssistantFeature.API_AUDIO
| VoiceAssistantFeature.ANNOUNCE
},
)
await hass.async_block_till_done()
satellite = get_satellite_entity(hass, mock_device.device_info.mac_address)
assert satellite is not None
done = asyncio.Event()
async def send_voice_assistant_announcement_await_response(
media_id: str, timeout: float, text: str
):
assert media_id == "https://www.home-assistant.io/resolved.mp3"
assert text == "test-text"
done.set()
with (
patch(
"homeassistant.components.assist_satellite.entity.tts_generate_media_source_id",
return_value="media-source://bla",
),
patch(
"homeassistant.components.media_source.async_resolve_media",
return_value=PlayMedia(
url="https://www.home-assistant.io/resolved.mp3",
mime_type="audio/mp3",
),
),
patch.object(
mock_client,
"send_voice_assistant_announcement_await_response",
new=send_voice_assistant_announcement_await_response,
),
):
async with asyncio.timeout(1):
await hass.services.async_call(
assist_satellite.DOMAIN,
"announce",
{"entity_id": satellite.entity_id, "message": "test-text"},
blocking=True,
)
await done.wait()
async def test_announce_media_id(
hass: HomeAssistant,
mock_client: APIClient,
mock_esphome_device: Callable[
[APIClient, list[EntityInfo], list[UserService], list[EntityState]],
Awaitable[MockESPHomeDevice],
],
) -> None:
"""Test announcement with media id."""
mock_device: MockESPHomeDevice = await mock_esphome_device(
mock_client=mock_client,
entity_info=[],
user_service=[],
states=[],
device_info={
"voice_assistant_feature_flags": VoiceAssistantFeature.VOICE_ASSISTANT
| VoiceAssistantFeature.SPEAKER
| VoiceAssistantFeature.API_AUDIO
| VoiceAssistantFeature.ANNOUNCE
},
)
await hass.async_block_till_done()
satellite = get_satellite_entity(hass, mock_device.device_info.mac_address)
assert satellite is not None
done = asyncio.Event()
async def send_voice_assistant_announcement_await_response(
media_id: str, timeout: float, text: str
):
assert media_id == "https://www.home-assistant.io/resolved.mp3"
done.set()
with (
patch.object(
mock_client,
"send_voice_assistant_announcement_await_response",
new=send_voice_assistant_announcement_await_response,
),
):
async with asyncio.timeout(1):
await hass.services.async_call(
assist_satellite.DOMAIN,
"announce",
{
"entity_id": satellite.entity_id,
"media_id": "https://www.home-assistant.io/resolved.mp3",
},
blocking=True,
)
await done.wait()