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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 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", + }