Stabilize alexa discovery (#108787)
This commit is contained in:
parent
5467fe8ff1
commit
a90d8b6a0c
3 changed files with 126 additions and 49 deletions
|
@ -380,12 +380,17 @@ def async_get_entities(
|
||||||
if state.domain not in ENTITY_ADAPTERS:
|
if state.domain not in ENTITY_ADAPTERS:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
alexa_entity = ENTITY_ADAPTERS[state.domain](hass, config, state)
|
try:
|
||||||
|
alexa_entity = ENTITY_ADAPTERS[state.domain](hass, config, state)
|
||||||
if not list(alexa_entity.interfaces()):
|
interfaces = list(alexa_entity.interfaces())
|
||||||
continue
|
except Exception as exc: # pylint: disable=broad-except
|
||||||
|
_LOGGER.exception(
|
||||||
entities.append(alexa_entity)
|
"Unable to serialize %s for discovery: %s", state.entity_id, exc
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
if not interfaces:
|
||||||
|
continue
|
||||||
|
entities.append(alexa_entity)
|
||||||
|
|
||||||
return entities
|
return entities
|
||||||
|
|
||||||
|
@ -406,13 +411,11 @@ class GenericCapabilities(AlexaEntity):
|
||||||
|
|
||||||
return [DisplayCategory.OTHER]
|
return [DisplayCategory.OTHER]
|
||||||
|
|
||||||
def interfaces(self) -> list[AlexaCapability]:
|
def interfaces(self) -> Generator[AlexaCapability, None, None]:
|
||||||
"""Yield the supported interfaces."""
|
"""Yield the supported interfaces."""
|
||||||
return [
|
yield AlexaPowerController(self.entity)
|
||||||
AlexaPowerController(self.entity),
|
yield AlexaEndpointHealth(self.hass, self.entity)
|
||||||
AlexaEndpointHealth(self.hass, self.entity),
|
yield Alexa(self.entity)
|
||||||
Alexa(self.entity),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
@ENTITY_ADAPTERS.register(input_boolean.DOMAIN)
|
@ENTITY_ADAPTERS.register(input_boolean.DOMAIN)
|
||||||
|
@ -431,14 +434,12 @@ class SwitchCapabilities(AlexaEntity):
|
||||||
|
|
||||||
return [DisplayCategory.SWITCH]
|
return [DisplayCategory.SWITCH]
|
||||||
|
|
||||||
def interfaces(self) -> list[AlexaCapability]:
|
def interfaces(self) -> Generator[AlexaCapability, None, None]:
|
||||||
"""Yield the supported interfaces."""
|
"""Yield the supported interfaces."""
|
||||||
return [
|
yield AlexaPowerController(self.entity)
|
||||||
AlexaPowerController(self.entity),
|
yield AlexaContactSensor(self.hass, self.entity)
|
||||||
AlexaContactSensor(self.hass, self.entity),
|
yield AlexaEndpointHealth(self.hass, self.entity)
|
||||||
AlexaEndpointHealth(self.hass, self.entity),
|
yield Alexa(self.entity)
|
||||||
Alexa(self.entity),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
@ENTITY_ADAPTERS.register(button.DOMAIN)
|
@ENTITY_ADAPTERS.register(button.DOMAIN)
|
||||||
|
@ -450,14 +451,12 @@ class ButtonCapabilities(AlexaEntity):
|
||||||
"""Return the display categories for this entity."""
|
"""Return the display categories for this entity."""
|
||||||
return [DisplayCategory.ACTIVITY_TRIGGER]
|
return [DisplayCategory.ACTIVITY_TRIGGER]
|
||||||
|
|
||||||
def interfaces(self) -> list[AlexaCapability]:
|
def interfaces(self) -> Generator[AlexaCapability, None, None]:
|
||||||
"""Yield the supported interfaces."""
|
"""Yield the supported interfaces."""
|
||||||
return [
|
yield AlexaSceneController(self.entity, supports_deactivation=False)
|
||||||
AlexaSceneController(self.entity, supports_deactivation=False),
|
yield AlexaEventDetectionSensor(self.hass, self.entity)
|
||||||
AlexaEventDetectionSensor(self.hass, self.entity),
|
yield AlexaEndpointHealth(self.hass, self.entity)
|
||||||
AlexaEndpointHealth(self.hass, self.entity),
|
yield Alexa(self.entity)
|
||||||
Alexa(self.entity),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
@ENTITY_ADAPTERS.register(climate.DOMAIN)
|
@ENTITY_ADAPTERS.register(climate.DOMAIN)
|
||||||
|
@ -676,13 +675,11 @@ class LockCapabilities(AlexaEntity):
|
||||||
"""Return the display categories for this entity."""
|
"""Return the display categories for this entity."""
|
||||||
return [DisplayCategory.SMARTLOCK]
|
return [DisplayCategory.SMARTLOCK]
|
||||||
|
|
||||||
def interfaces(self) -> list[AlexaCapability]:
|
def interfaces(self) -> Generator[AlexaCapability, None, None]:
|
||||||
"""Yield the supported interfaces."""
|
"""Yield the supported interfaces."""
|
||||||
return [
|
yield AlexaLockController(self.entity)
|
||||||
AlexaLockController(self.entity),
|
yield AlexaEndpointHealth(self.hass, self.entity)
|
||||||
AlexaEndpointHealth(self.hass, self.entity),
|
yield Alexa(self.entity)
|
||||||
Alexa(self.entity),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
@ENTITY_ADAPTERS.register(media_player.const.DOMAIN)
|
@ENTITY_ADAPTERS.register(media_player.const.DOMAIN)
|
||||||
|
@ -767,12 +764,10 @@ class SceneCapabilities(AlexaEntity):
|
||||||
"""Return the display categories for this entity."""
|
"""Return the display categories for this entity."""
|
||||||
return [DisplayCategory.SCENE_TRIGGER]
|
return [DisplayCategory.SCENE_TRIGGER]
|
||||||
|
|
||||||
def interfaces(self) -> list[AlexaCapability]:
|
def interfaces(self) -> Generator[AlexaCapability, None, None]:
|
||||||
"""Yield the supported interfaces."""
|
"""Yield the supported interfaces."""
|
||||||
return [
|
yield AlexaSceneController(self.entity, supports_deactivation=False)
|
||||||
AlexaSceneController(self.entity, supports_deactivation=False),
|
yield Alexa(self.entity)
|
||||||
Alexa(self.entity),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
@ENTITY_ADAPTERS.register(script.DOMAIN)
|
@ENTITY_ADAPTERS.register(script.DOMAIN)
|
||||||
|
@ -783,12 +778,10 @@ class ScriptCapabilities(AlexaEntity):
|
||||||
"""Return the display categories for this entity."""
|
"""Return the display categories for this entity."""
|
||||||
return [DisplayCategory.ACTIVITY_TRIGGER]
|
return [DisplayCategory.ACTIVITY_TRIGGER]
|
||||||
|
|
||||||
def interfaces(self) -> list[AlexaCapability]:
|
def interfaces(self) -> Generator[AlexaCapability, None, None]:
|
||||||
"""Yield the supported interfaces."""
|
"""Yield the supported interfaces."""
|
||||||
return [
|
yield AlexaSceneController(self.entity, supports_deactivation=True)
|
||||||
AlexaSceneController(self.entity, supports_deactivation=True),
|
yield Alexa(self.entity)
|
||||||
Alexa(self.entity),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
@ENTITY_ADAPTERS.register(sensor.DOMAIN)
|
@ENTITY_ADAPTERS.register(sensor.DOMAIN)
|
||||||
|
|
|
@ -119,11 +119,18 @@ async def async_api_discovery(
|
||||||
|
|
||||||
Async friendly.
|
Async friendly.
|
||||||
"""
|
"""
|
||||||
discovery_endpoints = [
|
discovery_endpoints: list[dict[str, Any]] = []
|
||||||
alexa_entity.serialize_discovery()
|
for alexa_entity in async_get_entities(hass, config):
|
||||||
for alexa_entity in async_get_entities(hass, config)
|
if not config.should_expose(alexa_entity.entity_id):
|
||||||
if 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(
|
return directive.response(
|
||||||
name="Discover.Response",
|
name="Discover.Response",
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
"""Test Alexa entity representation."""
|
"""Test Alexa entity representation."""
|
||||||
|
from typing import Any
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components.alexa import smart_home
|
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.core import HomeAssistant
|
||||||
from homeassistant.helpers import entity_registry as er
|
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:
|
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")
|
request = get_new_request("Alexa.Discovery", "Discover")
|
||||||
|
|
||||||
hass.states.async_set("switch.bla", "on", {"friendly_name": "Boop Woz"})
|
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(
|
async def test_serialize_discovery_recovers(
|
||||||
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
|
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue