Add default preannounce sound to Assist satellites (#141522)

* Add default preannounce sound

* Allow None to disable sound

* Register static path instead of HTTP view

* Fix path

---------

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
This commit is contained in:
Michael Hansen 2025-03-26 22:45:28 -05:00 committed by Franck Nijhof
parent e7ff0a3f8b
commit ec8363fa49
No known key found for this signature in database
GPG Key ID: D62583BA8AB11CA3
7 changed files with 95 additions and 12 deletions

View File

@ -1,9 +1,11 @@
"""Base class for assist satellite entities.""" """Base class for assist satellite entities."""
import logging import logging
from pathlib import Path
import voluptuous as vol import voluptuous as vol
from homeassistant.components.http import StaticPathConfig
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
@ -15,6 +17,8 @@ from .const import (
CONNECTION_TEST_DATA, CONNECTION_TEST_DATA,
DATA_COMPONENT, DATA_COMPONENT,
DOMAIN, DOMAIN,
PREANNOUNCE_FILENAME,
PREANNOUNCE_URL,
AssistSatelliteEntityFeature, AssistSatelliteEntityFeature,
) )
from .entity import ( from .entity import (
@ -56,7 +60,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
{ {
vol.Optional("message"): str, vol.Optional("message"): str,
vol.Optional("media_id"): str, vol.Optional("media_id"): str,
vol.Optional("preannounce_media_id"): str, vol.Optional("preannounce_media_id"): vol.Any(str, None),
} }
), ),
cv.has_at_least_one_key("message", "media_id"), cv.has_at_least_one_key("message", "media_id"),
@ -71,7 +75,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
{ {
vol.Optional("start_message"): str, vol.Optional("start_message"): str,
vol.Optional("start_media_id"): str, vol.Optional("start_media_id"): str,
vol.Optional("preannounce_media_id"): str, vol.Optional("preannounce_media_id"): vol.Any(str, None),
vol.Optional("extra_system_prompt"): str, vol.Optional("extra_system_prompt"): str,
} }
), ),
@ -84,6 +88,15 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async_register_websocket_api(hass) async_register_websocket_api(hass)
hass.http.register_view(ConnectionTestView()) hass.http.register_view(ConnectionTestView())
# Default preannounce sound
await hass.http.async_register_static_paths(
[
StaticPathConfig(
PREANNOUNCE_URL, str(Path(__file__).parent / PREANNOUNCE_FILENAME)
)
]
)
return True return True

View File

@ -20,6 +20,9 @@ CONNECTION_TEST_DATA: HassKey[dict[str, asyncio.Event]] = HassKey(
f"{DOMAIN}_connection_tests" f"{DOMAIN}_connection_tests"
) )
PREANNOUNCE_FILENAME = "preannounce.mp3"
PREANNOUNCE_URL = f"/api/assist_satellite/static/{PREANNOUNCE_FILENAME}"
class AssistSatelliteEntityFeature(IntFlag): class AssistSatelliteEntityFeature(IntFlag):
"""Supported features of Assist satellite entity.""" """Supported features of Assist satellite entity."""

View File

@ -28,7 +28,7 @@ from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import chat_session, entity from homeassistant.helpers import chat_session, entity
from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.entity import EntityDescription
from .const import AssistSatelliteEntityFeature from .const import PREANNOUNCE_URL, AssistSatelliteEntityFeature
from .errors import AssistSatelliteError, SatelliteBusyError from .errors import AssistSatelliteError, SatelliteBusyError
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -180,7 +180,7 @@ class AssistSatelliteEntity(entity.Entity):
self, self,
message: str | None = None, message: str | None = None,
media_id: str | None = None, media_id: str | None = None,
preannounce_media_id: str | None = None, preannounce_media_id: str | None = PREANNOUNCE_URL,
) -> None: ) -> None:
"""Play and show an announcement on the satellite. """Play and show an announcement on the satellite.
@ -190,7 +190,8 @@ class AssistSatelliteEntity(entity.Entity):
If media_id is provided, it is played directly. It is possible If media_id is provided, it is played directly. It is possible
to omit the message and the satellite will not show any text. to omit the message and the satellite will not show any text.
If preannounce_media_id is provided, it is played before the announcement. If preannounce_media_id is provided, it overrides the default sound.
If preannounce_media_id is None, no sound is played.
Calls async_announce with message and media id. Calls async_announce with message and media id.
""" """
@ -228,7 +229,7 @@ class AssistSatelliteEntity(entity.Entity):
start_message: str | None = None, start_message: str | None = None,
start_media_id: str | None = None, start_media_id: str | None = None,
extra_system_prompt: str | None = None, extra_system_prompt: str | None = None,
preannounce_media_id: str | None = None, preannounce_media_id: str | None = PREANNOUNCE_URL,
) -> None: ) -> None:
"""Start a conversation from the satellite. """Start a conversation from the satellite.
@ -239,6 +240,7 @@ class AssistSatelliteEntity(entity.Entity):
to omit the message and the satellite will not show any text. to omit the message and the satellite will not show any text.
If preannounce_media_id is provided, it is played before the announcement. If preannounce_media_id is provided, it is played before the announcement.
If preannounce_media_id is None, no sound is played.
Calls async_start_conversation. Calls async_start_conversation.
""" """

View File

@ -23,7 +23,11 @@ from homeassistant.helpers.network import (
from .const import CONTENT_AUTH_EXPIRY_TIME, MediaClass, MediaType from .const import CONTENT_AUTH_EXPIRY_TIME, MediaClass, MediaType
# Paths that we don't need to sign # Paths that we don't need to sign
PATHS_WITHOUT_AUTH = ("/api/tts_proxy/", "/api/esphome/ffmpeg_proxy/") PATHS_WITHOUT_AUTH = (
"/api/tts_proxy/",
"/api/esphome/ffmpeg_proxy/",
"/api/assist_satellite/static/",
)
@callback @callback

View File

@ -22,6 +22,7 @@ from homeassistant.components.assist_satellite import (
AssistSatelliteAnnouncement, AssistSatelliteAnnouncement,
SatelliteBusyError, SatelliteBusyError,
) )
from homeassistant.components.assist_satellite.const import PREANNOUNCE_URL
from homeassistant.components.assist_satellite.entity import AssistSatelliteState from homeassistant.components.assist_satellite.entity import AssistSatelliteState
from homeassistant.components.media_source import PlayMedia from homeassistant.components.media_source import PlayMedia
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
@ -185,7 +186,7 @@ async def test_new_pipeline_cancels_pipeline(
("service_data", "expected_params"), ("service_data", "expected_params"),
[ [
( (
{"message": "Hello"}, {"message": "Hello", "preannounce_media_id": None},
AssistSatelliteAnnouncement( AssistSatelliteAnnouncement(
message="Hello", message="Hello",
media_id="http://10.10.10.10:8123/api/tts_proxy/test-token", media_id="http://10.10.10.10:8123/api/tts_proxy/test-token",
@ -198,6 +199,7 @@ async def test_new_pipeline_cancels_pipeline(
{ {
"message": "Hello", "message": "Hello",
"media_id": "media-source://given", "media_id": "media-source://given",
"preannounce_media_id": None,
}, },
AssistSatelliteAnnouncement( AssistSatelliteAnnouncement(
message="Hello", message="Hello",
@ -208,7 +210,7 @@ async def test_new_pipeline_cancels_pipeline(
), ),
), ),
( (
{"media_id": "http://example.com/bla.mp3"}, {"media_id": "http://example.com/bla.mp3", "preannounce_media_id": None},
AssistSatelliteAnnouncement( AssistSatelliteAnnouncement(
message="", message="",
media_id="http://example.com/bla.mp3", media_id="http://example.com/bla.mp3",
@ -368,6 +370,24 @@ async def test_announce_cancels_pipeline(
mock_async_announce.assert_called_once() mock_async_announce.assert_called_once()
async def test_announce_default_preannounce(
hass: HomeAssistant, init_components: ConfigEntry, entity: MockAssistSatellite
) -> None:
"""Test announcing on a device with the default preannouncement sound."""
async def async_announce(announcement):
assert announcement.preannounce_media_id.endswith(PREANNOUNCE_URL)
with patch.object(entity, "async_announce", new=async_announce):
await hass.services.async_call(
"assist_satellite",
"announce",
{"media_id": "test-media-id"},
target={"entity_id": "assist_satellite.test_entity"},
blocking=True,
)
async def test_context_refresh( async def test_context_refresh(
hass: HomeAssistant, init_components: ConfigEntry, entity: MockAssistSatellite hass: HomeAssistant, init_components: ConfigEntry, entity: MockAssistSatellite
) -> None: ) -> None:
@ -521,6 +541,7 @@ async def test_vad_sensitivity_entity_not_found(
{ {
"start_message": "Hello", "start_message": "Hello",
"extra_system_prompt": "Better system prompt", "extra_system_prompt": "Better system prompt",
"preannounce_media_id": None,
}, },
( (
"mock-conversation-id", "mock-conversation-id",
@ -538,6 +559,7 @@ async def test_vad_sensitivity_entity_not_found(
{ {
"start_message": "Hello", "start_message": "Hello",
"start_media_id": "media-source://given", "start_media_id": "media-source://given",
"preannounce_media_id": None,
}, },
( (
"mock-conversation-id", "mock-conversation-id",
@ -552,7 +574,10 @@ async def test_vad_sensitivity_entity_not_found(
), ),
), ),
( (
{"start_media_id": "http://example.com/given.mp3"}, {
"start_media_id": "http://example.com/given.mp3",
"preannounce_media_id": None,
},
( (
"mock-conversation-id", "mock-conversation-id",
None, None,
@ -657,6 +682,32 @@ async def test_start_conversation_reject_builtin_agent(
) )
async def test_start_conversation_default_preannounce(
hass: HomeAssistant, init_components: ConfigEntry, entity: MockAssistSatellite
) -> None:
"""Test starting a conversation on a device with the default preannouncement sound."""
async def async_start_conversation(start_announcement):
assert PREANNOUNCE_URL in start_announcement.preannounce_media_id
await async_update_pipeline(
hass,
async_get_pipeline(hass),
conversation_engine="conversation.some_llm",
)
with (
patch.object(entity, "async_start_conversation", new=async_start_conversation),
):
await hass.services.async_call(
"assist_satellite",
"start_conversation",
{"start_media_id": "test-media-id"},
target={"entity_id": "assist_satellite.test_entity"},
blocking=True,
)
async def test_wake_word_start_keeps_responding( async def test_wake_word_start_keeps_responding(
hass: HomeAssistant, init_components: ConfigEntry, entity: MockAssistSatellite hass: HomeAssistant, init_components: ConfigEntry, entity: MockAssistSatellite
) -> None: ) -> None:

View File

@ -1249,7 +1249,11 @@ async def test_announce_message(
await hass.services.async_call( await hass.services.async_call(
assist_satellite.DOMAIN, assist_satellite.DOMAIN,
"announce", "announce",
{"entity_id": satellite.entity_id, "message": "test-text"}, {
"entity_id": satellite.entity_id,
"message": "test-text",
"preannounce_media_id": None,
},
blocking=True, blocking=True,
) )
await done.wait() await done.wait()
@ -1338,6 +1342,7 @@ async def test_announce_media_id(
{ {
"entity_id": satellite.entity_id, "entity_id": satellite.entity_id,
"media_id": "https://www.home-assistant.io/resolved.mp3", "media_id": "https://www.home-assistant.io/resolved.mp3",
"preannounce_media_id": None,
}, },
blocking=True, blocking=True,
) )
@ -1545,7 +1550,11 @@ async def test_start_conversation_message(
await hass.services.async_call( await hass.services.async_call(
assist_satellite.DOMAIN, assist_satellite.DOMAIN,
"start_conversation", "start_conversation",
{"entity_id": satellite.entity_id, "start_message": "test-text"}, {
"entity_id": satellite.entity_id,
"start_message": "test-text",
"preannounce_media_id": None,
},
blocking=True, blocking=True,
) )
await done.wait() await done.wait()
@ -1653,6 +1662,7 @@ async def test_start_conversation_media_id(
{ {
"entity_id": satellite.entity_id, "entity_id": satellite.entity_id,
"start_media_id": "https://www.home-assistant.io/resolved.mp3", "start_media_id": "https://www.home-assistant.io/resolved.mp3",
"preannounce_media_id": None,
}, },
blocking=True, blocking=True,
) )