From 8158ca7c69240014275417b92a05e12804df1184 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sun, 22 Sep 2024 16:55:31 +0200 Subject: [PATCH] Add connection test feature to assist_satellite (#126256) * Add connection test feature to assist_satellite * Add http to assist_satellite dependencies * Remove extra logging * Incorporate feedback * Fix tests * ruff * Apply suggestions from code review Co-authored-by: Bram Kragten * Use asyncio.Event instead of dispatcher * Respond asap * Update homeassistant/components/assist_satellite/websocket_api.py Co-authored-by: Martin Hjelmare --------- Co-authored-by: Michael Hansen Co-authored-by: Paulus Schoutsen Co-authored-by: Bram Kragten Co-authored-by: Martin Hjelmare --- .../components/assist_satellite/__init__.py | 10 +- .../assist_satellite/connection_test.mp3 | Bin 0 -> 36780 bytes .../assist_satellite/connection_test.py | 43 ++++++ .../components/assist_satellite/const.py | 4 + .../components/assist_satellite/manifest.json | 2 +- .../assist_satellite/websocket_api.py | 69 ++++++++- tests/components/assist_satellite/conftest.py | 2 +- .../assist_satellite/test_websocket_api.py | 133 +++++++++++++++++- 8 files changed, 258 insertions(+), 5 deletions(-) create mode 100755 homeassistant/components/assist_satellite/connection_test.mp3 create mode 100644 homeassistant/components/assist_satellite/connection_test.py diff --git a/homeassistant/components/assist_satellite/__init__.py b/homeassistant/components/assist_satellite/__init__.py index 3f322beef29..6932fa3180c 100644 --- a/homeassistant/components/assist_satellite/__init__.py +++ b/homeassistant/components/assist_satellite/__init__.py @@ -10,7 +10,13 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType -from .const import DOMAIN, DOMAIN_DATA, AssistSatelliteEntityFeature +from .connection_test import ConnectionTestView +from .const import ( + CONNECTION_TEST_DATA, + DOMAIN, + DOMAIN_DATA, + AssistSatelliteEntityFeature, +) from .entity import ( AssistSatelliteAnnouncement, AssistSatelliteConfiguration, @@ -57,7 +63,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: "async_internal_announce", [AssistSatelliteEntityFeature.ANNOUNCE], ) + hass.data[CONNECTION_TEST_DATA] = {} async_register_websocket_api(hass) + hass.http.register_view(ConnectionTestView()) return True diff --git a/homeassistant/components/assist_satellite/connection_test.mp3 b/homeassistant/components/assist_satellite/connection_test.mp3 new file mode 100755 index 0000000000000000000000000000000000000000..5fd79ce86095ad7800c1684cb8392c0ff89d6175 GIT binary patch literal 36780 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 literal 0 HcmV?d00001 diff --git a/homeassistant/components/assist_satellite/connection_test.py b/homeassistant/components/assist_satellite/connection_test.py new file mode 100644 index 00000000000..956542dacf3 --- /dev/null +++ b/homeassistant/components/assist_satellite/connection_test.py @@ -0,0 +1,43 @@ +"""Assist satellite connection test.""" + +import logging +from pathlib import Path + +from aiohttp import web + +from homeassistant.components.http import KEY_HASS, HomeAssistantView + +from .const import CONNECTION_TEST_DATA + +_LOGGER = logging.getLogger(__name__) + +CONNECTION_TEST_CONTENT_TYPE = "audio/mpeg" +CONNECTION_TEST_FILENAME = "connection_test.mp3" +CONNECTION_TEST_URL_BASE = "/api/assist_satellite/connection_test" + + +class ConnectionTestView(HomeAssistantView): + """View to serve an audio sample for connection test.""" + + requires_auth = False + url = f"{CONNECTION_TEST_URL_BASE}/{{connection_id}}" + name = "api:assist_satellite_connection_test" + + async def get(self, request: web.Request, connection_id: str) -> web.Response: + """Start a get request.""" + _LOGGER.debug("Request for connection test with id %s", connection_id) + + hass = request.app[KEY_HASS] + connection_test_data = hass.data[CONNECTION_TEST_DATA] + + connection_test_event = connection_test_data.pop(connection_id, None) + + if connection_test_event is None: + return web.Response(status=404) + + connection_test_event.set() + + audio_path = Path(__file__).parent / CONNECTION_TEST_FILENAME + audio_data = await hass.async_add_executor_job(audio_path.read_bytes) + + return web.Response(body=audio_data, content_type=CONNECTION_TEST_CONTENT_TYPE) diff --git a/homeassistant/components/assist_satellite/const.py b/homeassistant/components/assist_satellite/const.py index bd5453e06de..73bc126f7ba 100644 --- a/homeassistant/components/assist_satellite/const.py +++ b/homeassistant/components/assist_satellite/const.py @@ -2,6 +2,7 @@ from __future__ import annotations +import asyncio from enum import IntFlag from typing import TYPE_CHECKING @@ -15,6 +16,9 @@ if TYPE_CHECKING: DOMAIN = "assist_satellite" DOMAIN_DATA: HassKey[EntityComponent[AssistSatelliteEntity]] = HassKey(DOMAIN) +CONNECTION_TEST_DATA: HassKey[dict[str, asyncio.Event]] = HassKey( + f"{DOMAIN}_connection_tests" +) class AssistSatelliteEntityFeature(IntFlag): diff --git a/homeassistant/components/assist_satellite/manifest.json b/homeassistant/components/assist_satellite/manifest.json index b4f89456351..68a3ceafd4f 100644 --- a/homeassistant/components/assist_satellite/manifest.json +++ b/homeassistant/components/assist_satellite/manifest.json @@ -2,7 +2,7 @@ "domain": "assist_satellite", "name": "Assist Satellite", "codeowners": ["@home-assistant/core", "@synesthesiam"], - "dependencies": ["assist_pipeline", "stt", "tts"], + "dependencies": ["assist_pipeline", "http", "stt", "tts"], "documentation": "https://www.home-assistant.io/integrations/assist_satellite", "integration_type": "entity", "quality_scale": "internal" diff --git a/homeassistant/components/assist_satellite/websocket_api.py b/homeassistant/components/assist_satellite/websocket_api.py index ee7bef7e4e8..741f4364e7f 100644 --- a/homeassistant/components/assist_satellite/websocket_api.py +++ b/homeassistant/components/assist_satellite/websocket_api.py @@ -1,5 +1,6 @@ """Assist satellite Websocket API.""" +import asyncio from dataclasses import asdict, replace from typing import Any @@ -9,8 +10,19 @@ from homeassistant.components import websocket_api from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.util import uuid as uuid_util -from .const import DOMAIN, DOMAIN_DATA +from .connection_test import CONNECTION_TEST_URL_BASE +from .const import ( + CONNECTION_TEST_DATA, + DOMAIN, + DOMAIN_DATA, + AssistSatelliteEntityFeature, +) +from .entity import AssistSatelliteEntity + +CONNECTION_TEST_TIMEOUT = 30 @callback @@ -19,6 +31,7 @@ def async_register_websocket_api(hass: HomeAssistant) -> None: websocket_api.async_register_command(hass, websocket_intercept_wake_word) websocket_api.async_register_command(hass, websocket_get_configuration) websocket_api.async_register_command(hass, websocket_set_wake_words) + websocket_api.async_register_command(hass, websocket_test_connection) @callback @@ -138,3 +151,57 @@ async def websocket_set_wake_words( replace(config, active_wake_words=actual_ids) ) connection.send_result(msg["id"]) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "assist_satellite/test_connection", + vol.Required("entity_id"): cv.entity_domain(DOMAIN), + } +) +@websocket_api.async_response +async def websocket_test_connection( + hass: HomeAssistant, + connection: websocket_api.connection.ActiveConnection, + msg: dict[str, Any], +) -> None: + """Test the connection between the device and Home Assistant. + + Send an announcement to the device with a special media id. + """ + component: EntityComponent[AssistSatelliteEntity] = hass.data[DOMAIN] + satellite = component.get_entity(msg["entity_id"]) + if satellite is None: + connection.send_error( + msg["id"], websocket_api.ERR_NOT_FOUND, "Entity not found" + ) + return + if not (satellite.supported_features or 0) & AssistSatelliteEntityFeature.ANNOUNCE: + connection.send_error( + msg["id"], + websocket_api.ERR_NOT_SUPPORTED, + "Entity does not support announce", + ) + return + + # Announce and wait for event + connection_test_data = hass.data[CONNECTION_TEST_DATA] + connection_id = uuid_util.random_uuid_hex() + connection_test_event = asyncio.Event() + connection_test_data[connection_id] = connection_test_event + + hass.async_create_background_task( + satellite.async_internal_announce( + media_id=f"{CONNECTION_TEST_URL_BASE}/{connection_id}" + ), + f"assist_satellite_connection_test_{msg['entity_id']}", + ) + + try: + async with asyncio.timeout(CONNECTION_TEST_TIMEOUT): + await connection_test_event.wait() + connection.send_result(msg["id"], {"status": "success"}) + except TimeoutError: + connection.send_result(msg["id"], {"status": "timeout"}) + finally: + connection_test_data.pop(connection_id, None) diff --git a/tests/components/assist_satellite/conftest.py b/tests/components/assist_satellite/conftest.py index 489460f8e2c..9e9bfd959e6 100644 --- a/tests/components/assist_satellite/conftest.py +++ b/tests/components/assist_satellite/conftest.py @@ -44,7 +44,7 @@ class MockAssistSatellite(AssistSatelliteEntity): def __init__(self) -> None: """Initialize the mock entity.""" self.events = [] - self.announcements = [] + self.announcements: list[AssistSatelliteAnnouncement] = [] self.config = AssistSatelliteConfiguration( available_wake_words=[ AssistSatelliteWakeWord( diff --git a/tests/components/assist_satellite/test_websocket_api.py b/tests/components/assist_satellite/test_websocket_api.py index 709005e38cf..257961a5b32 100644 --- a/tests/components/assist_satellite/test_websocket_api.py +++ b/tests/components/assist_satellite/test_websocket_api.py @@ -1,11 +1,16 @@ """Test WebSocket API.""" import asyncio +from http import HTTPStatus from unittest.mock import patch +from freezegun.api import FrozenDateTimeFactory import pytest from homeassistant.components.assist_pipeline import PipelineStage +from homeassistant.components.assist_satellite.websocket_api import ( + CONNECTION_TEST_TIMEOUT, +) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -13,7 +18,7 @@ from . import ENTITY_ID from .conftest import MockAssistSatellite from tests.common import MockUser -from tests.typing import WebSocketGenerator +from tests.typing import ClientSessionGenerator, WebSocketGenerator async def test_intercept_wake_word( @@ -385,3 +390,129 @@ async def test_set_wake_words_bad_id( "code": "not_supported", "message": "Wake word id is not supported: abcd", } + + +async def test_connection_test( + hass: HomeAssistant, + init_components: ConfigEntry, + entity: MockAssistSatellite, + hass_ws_client: WebSocketGenerator, + hass_client: ClientSessionGenerator, +) -> None: + """Test connection test.""" + ws_client = await hass_ws_client(hass) + + await ws_client.send_json_auto_id( + { + "type": "assist_satellite/test_connection", + "entity_id": ENTITY_ID, + } + ) + + for _ in range(3): + await asyncio.sleep(0) + + assert len(entity.announcements) == 1 + assert entity.announcements[0].message == "" + announcement_media_id = entity.announcements[0].media_id + hass_url = "http://10.10.10.10:8123" + assert announcement_media_id.startswith( + f"{hass_url}/api/assist_satellite/connection_test/" + ) + + # Fake satellite fetches the URL + client = await hass_client() + resp = await client.get(announcement_media_id[len(hass_url) :]) + assert resp.status == HTTPStatus.OK + + response = await ws_client.receive_json() + assert response["success"] + assert response["result"] == {"status": "success"} + + +async def test_connection_test_timeout( + hass: HomeAssistant, + init_components: ConfigEntry, + entity: MockAssistSatellite, + hass_ws_client: WebSocketGenerator, + hass_client: ClientSessionGenerator, + freezer: FrozenDateTimeFactory, +) -> None: + """Test connection test timeout.""" + ws_client = await hass_ws_client(hass) + + await ws_client.send_json_auto_id( + { + "type": "assist_satellite/test_connection", + "entity_id": ENTITY_ID, + } + ) + + for _ in range(3): + await asyncio.sleep(0) + + assert len(entity.announcements) == 1 + assert entity.announcements[0].message == "" + announcement_media_id = entity.announcements[0].media_id + hass_url = "http://10.10.10.10:8123" + assert announcement_media_id.startswith( + f"{hass_url}/api/assist_satellite/connection_test/" + ) + + freezer.tick(CONNECTION_TEST_TIMEOUT + 1) + + # Timeout + response = await ws_client.receive_json() + assert response["success"] + assert response["result"] == {"status": "timeout"} + + +async def test_connection_test_invalid_satellite( + hass: HomeAssistant, + init_components: ConfigEntry, + entity: MockAssistSatellite, + hass_ws_client: WebSocketGenerator, +) -> None: + """Test connection test with unknown entity id.""" + ws_client = await hass_ws_client(hass) + + await ws_client.send_json_auto_id( + { + "type": "assist_satellite/test_connection", + "entity_id": "assist_satellite.invalid", + } + ) + response = await ws_client.receive_json() + + assert not response["success"] + assert response["error"] == { + "code": "not_found", + "message": "Entity not found", + } + + +async def test_connection_test_timeout_announcement_unsupported( + hass: HomeAssistant, + init_components: ConfigEntry, + entity: MockAssistSatellite, + hass_ws_client: WebSocketGenerator, +) -> None: + """Test connection test entity which does not support announce.""" + ws_client = await hass_ws_client(hass) + + # Disable announce support + entity.supported_features = 0 + + await ws_client.send_json_auto_id( + { + "type": "assist_satellite/test_connection", + "entity_id": ENTITY_ID, + } + ) + response = await ws_client.receive_json() + + assert not response["success"] + assert response["error"] == { + "code": "not_supported", + "message": "Entity does not support announce", + }