diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index 70679f8dafb..939644b4600 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -380,12 +380,17 @@ def async_get_entities( if state.domain not in ENTITY_ADAPTERS: continue - alexa_entity = ENTITY_ADAPTERS[state.domain](hass, config, state) - - if not list(alexa_entity.interfaces()): - continue - - entities.append(alexa_entity) + try: + alexa_entity = ENTITY_ADAPTERS[state.domain](hass, config, state) + interfaces = list(alexa_entity.interfaces()) + except Exception as exc: # pylint: disable=broad-except + _LOGGER.exception( + "Unable to serialize %s for discovery: %s", state.entity_id, exc + ) + else: + if not interfaces: + continue + entities.append(alexa_entity) return entities @@ -406,13 +411,11 @@ class GenericCapabilities(AlexaEntity): return [DisplayCategory.OTHER] - def interfaces(self) -> list[AlexaCapability]: + def interfaces(self) -> Generator[AlexaCapability, None, None]: """Yield the supported interfaces.""" - return [ - AlexaPowerController(self.entity), - AlexaEndpointHealth(self.hass, self.entity), - Alexa(self.entity), - ] + yield AlexaPowerController(self.entity) + yield AlexaEndpointHealth(self.hass, self.entity) + yield Alexa(self.entity) @ENTITY_ADAPTERS.register(input_boolean.DOMAIN) @@ -431,14 +434,12 @@ class SwitchCapabilities(AlexaEntity): return [DisplayCategory.SWITCH] - def interfaces(self) -> list[AlexaCapability]: + def interfaces(self) -> Generator[AlexaCapability, None, None]: """Yield the supported interfaces.""" - return [ - AlexaPowerController(self.entity), - AlexaContactSensor(self.hass, self.entity), - AlexaEndpointHealth(self.hass, self.entity), - Alexa(self.entity), - ] + yield AlexaPowerController(self.entity) + yield AlexaContactSensor(self.hass, self.entity) + yield AlexaEndpointHealth(self.hass, self.entity) + yield Alexa(self.entity) @ENTITY_ADAPTERS.register(button.DOMAIN) @@ -450,14 +451,12 @@ class ButtonCapabilities(AlexaEntity): """Return the display categories for this entity.""" return [DisplayCategory.ACTIVITY_TRIGGER] - def interfaces(self) -> list[AlexaCapability]: + def interfaces(self) -> Generator[AlexaCapability, None, None]: """Yield the supported interfaces.""" - return [ - AlexaSceneController(self.entity, supports_deactivation=False), - AlexaEventDetectionSensor(self.hass, self.entity), - AlexaEndpointHealth(self.hass, self.entity), - Alexa(self.entity), - ] + yield AlexaSceneController(self.entity, supports_deactivation=False) + yield AlexaEventDetectionSensor(self.hass, self.entity) + yield AlexaEndpointHealth(self.hass, self.entity) + yield Alexa(self.entity) @ENTITY_ADAPTERS.register(climate.DOMAIN) @@ -676,13 +675,11 @@ class LockCapabilities(AlexaEntity): """Return the display categories for this entity.""" return [DisplayCategory.SMARTLOCK] - def interfaces(self) -> list[AlexaCapability]: + def interfaces(self) -> Generator[AlexaCapability, None, None]: """Yield the supported interfaces.""" - return [ - AlexaLockController(self.entity), - AlexaEndpointHealth(self.hass, self.entity), - Alexa(self.entity), - ] + yield AlexaLockController(self.entity) + yield AlexaEndpointHealth(self.hass, self.entity) + yield Alexa(self.entity) @ENTITY_ADAPTERS.register(media_player.const.DOMAIN) @@ -767,12 +764,10 @@ class SceneCapabilities(AlexaEntity): """Return the display categories for this entity.""" return [DisplayCategory.SCENE_TRIGGER] - def interfaces(self) -> list[AlexaCapability]: + def interfaces(self) -> Generator[AlexaCapability, None, None]: """Yield the supported interfaces.""" - return [ - AlexaSceneController(self.entity, supports_deactivation=False), - Alexa(self.entity), - ] + yield AlexaSceneController(self.entity, supports_deactivation=False) + yield Alexa(self.entity) @ENTITY_ADAPTERS.register(script.DOMAIN) @@ -783,12 +778,10 @@ class ScriptCapabilities(AlexaEntity): """Return the display categories for this entity.""" return [DisplayCategory.ACTIVITY_TRIGGER] - def interfaces(self) -> list[AlexaCapability]: + def interfaces(self) -> Generator[AlexaCapability, None, None]: """Yield the supported interfaces.""" - return [ - AlexaSceneController(self.entity, supports_deactivation=True), - Alexa(self.entity), - ] + yield AlexaSceneController(self.entity, supports_deactivation=True) + yield Alexa(self.entity) @ENTITY_ADAPTERS.register(sensor.DOMAIN) diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index 463693f7da6..398c6218193 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -119,11 +119,18 @@ async def async_api_discovery( Async friendly. """ - discovery_endpoints = [ - alexa_entity.serialize_discovery() - for alexa_entity in async_get_entities(hass, config) - if config.should_expose(alexa_entity.entity_id) - ] + discovery_endpoints: list[dict[str, Any]] = [] + for alexa_entity in async_get_entities(hass, config): + if not config.should_expose(alexa_entity.entity_id): + continue + try: + discovered_serialized_entity = alexa_entity.serialize_discovery() + except Exception as exc: # pylint: disable=broad-except + _LOGGER.exception( + "Unable to serialize %s for discovery: %s", alexa_entity.entity_id, exc + ) + else: + discovery_endpoints.append(discovered_serialized_entity) return directive.response( name="Discover.Response", diff --git a/tests/components/alexa/test_entities.py b/tests/components/alexa/test_entities.py index 87aab24a3b1..c7949253af0 100644 --- a/tests/components/alexa/test_entities.py +++ b/tests/components/alexa/test_entities.py @@ -1,10 +1,11 @@ """Test Alexa entity representation.""" +from typing import Any from unittest.mock import patch import pytest from homeassistant.components.alexa import smart_home -from homeassistant.const import EntityCategory, __version__ +from homeassistant.const import EntityCategory, UnitOfTemperature, __version__ from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -75,7 +76,7 @@ async def test_categorized_hidden_entities( async def test_serialize_discovery(hass: HomeAssistant) -> None: - """Test we handle an interface raising unexpectedly during serialize discovery.""" + """Test we can serialize a discovery.""" request = get_new_request("Alexa.Discovery", "Discover") hass.states.async_set("switch.bla", "on", {"friendly_name": "Boop Woz"}) @@ -94,6 +95,82 @@ async def test_serialize_discovery(hass: HomeAssistant) -> None: } +async def test_serialize_discovery_partly_fails( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test we can partly serialize a discovery.""" + + async def _mock_discovery() -> dict[str, Any]: + request = get_new_request("Alexa.Discovery", "Discover") + hass.states.async_set("switch.bla", "on", {"friendly_name": "My Switch"}) + hass.states.async_set("fan.bla", "on", {"friendly_name": "My Fan"}) + hass.states.async_set( + "humidifier.bla", "on", {"friendly_name": "My Humidifier"} + ) + hass.states.async_set( + "sensor.bla", + "20.1", + { + "friendly_name": "Livingroom temperature", + "unit_of_measurement": UnitOfTemperature.CELSIUS, + "device_class": "temperature", + }, + ) + return await smart_home.async_handle_message( + hass, get_default_config(hass), request + ) + + msg = await _mock_discovery() + assert "event" in msg + msg = msg["event"] + assert len(msg["payload"]["endpoints"]) == 4 + endpoint_ids = { + attributes["endpointId"] for attributes in msg["payload"]["endpoints"] + } + assert all( + entity in endpoint_ids + for entity in ["switch#bla", "fan#bla", "humidifier#bla", "sensor#bla"] + ) + + # Simulate fetching the interfaces fails for fan entity + with patch( + "homeassistant.components.alexa.entities.FanCapabilities.interfaces", + side_effect=TypeError(), + ): + msg = await _mock_discovery() + assert "event" in msg + msg = msg["event"] + assert len(msg["payload"]["endpoints"]) == 3 + endpoint_ids = { + attributes["endpointId"] for attributes in msg["payload"]["endpoints"] + } + assert all( + entity in endpoint_ids + for entity in ["switch#bla", "humidifier#bla", "sensor#bla"] + ) + assert "Unable to serialize fan.bla for discovery" in caplog.text + caplog.clear() + + # Simulate serializing properties fails for sensor entity + with patch( + "homeassistant.components.alexa.entities.SensorCapabilities.default_display_categories", + side_effect=ValueError(), + ): + msg = await _mock_discovery() + assert "event" in msg + msg = msg["event"] + assert len(msg["payload"]["endpoints"]) == 3 + endpoint_ids = { + attributes["endpointId"] for attributes in msg["payload"]["endpoints"] + } + assert all( + entity in endpoint_ids + for entity in ["switch#bla", "humidifier#bla", "fan#bla"] + ) + assert "Unable to serialize sensor.bla for discovery" in caplog.text + caplog.clear() + + async def test_serialize_discovery_recovers( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: