Do not break Alexa sync when encounter bad entity (#39380)
This commit is contained in:
parent
b4db9f615d
commit
ba75856f2b
4 changed files with 107 additions and 29 deletions
|
@ -1,5 +1,6 @@
|
||||||
"""Alexa capabilities."""
|
"""Alexa capabilities."""
|
||||||
import logging
|
import logging
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
from homeassistant.components import (
|
from homeassistant.components import (
|
||||||
cover,
|
cover,
|
||||||
|
@ -36,6 +37,7 @@ from homeassistant.const import (
|
||||||
STATE_UNKNOWN,
|
STATE_UNKNOWN,
|
||||||
STATE_UNLOCKED,
|
STATE_UNLOCKED,
|
||||||
)
|
)
|
||||||
|
from homeassistant.core import State
|
||||||
import homeassistant.util.color as color_util
|
import homeassistant.util.color as color_util
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
|
|
||||||
|
@ -71,32 +73,32 @@ class AlexaCapability:
|
||||||
|
|
||||||
supported_locales = {"en-US"}
|
supported_locales = {"en-US"}
|
||||||
|
|
||||||
def __init__(self, entity, instance=None):
|
def __init__(self, entity: State, instance: Optional[str] = None):
|
||||||
"""Initialize an Alexa capability."""
|
"""Initialize an Alexa capability."""
|
||||||
self.entity = entity
|
self.entity = entity
|
||||||
self.instance = instance
|
self.instance = instance
|
||||||
|
|
||||||
def name(self):
|
def name(self) -> str:
|
||||||
"""Return the Alexa API name of this interface."""
|
"""Return the Alexa API name of this interface."""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def properties_supported():
|
def properties_supported() -> List[dict]:
|
||||||
"""Return what properties this entity supports."""
|
"""Return what properties this entity supports."""
|
||||||
return []
|
return []
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def properties_proactively_reported():
|
def properties_proactively_reported() -> bool:
|
||||||
"""Return True if properties asynchronously reported."""
|
"""Return True if properties asynchronously reported."""
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def properties_retrievable():
|
def properties_retrievable() -> bool:
|
||||||
"""Return True if properties can be retrieved."""
|
"""Return True if properties can be retrieved."""
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def properties_non_controllable():
|
def properties_non_controllable() -> bool:
|
||||||
"""Return True if non controllable."""
|
"""Return True if non controllable."""
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -237,8 +239,22 @@ class AlexaCapability:
|
||||||
"""Return properties serialized for an API response."""
|
"""Return properties serialized for an API response."""
|
||||||
for prop in self.properties_supported():
|
for prop in self.properties_supported():
|
||||||
prop_name = prop["name"]
|
prop_name = prop["name"]
|
||||||
|
try:
|
||||||
prop_value = self.get_property(prop_name)
|
prop_value = self.get_property(prop_name)
|
||||||
if prop_value is not None:
|
except UnsupportedProperty:
|
||||||
|
raise
|
||||||
|
except Exception: # pylint: disable=broad-except
|
||||||
|
_LOGGER.exception(
|
||||||
|
"Unexpected error getting %s.%s property from %s",
|
||||||
|
self.name(),
|
||||||
|
prop_name,
|
||||||
|
self.entity,
|
||||||
|
)
|
||||||
|
prop_value = None
|
||||||
|
|
||||||
|
if prop_value is None:
|
||||||
|
continue
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
"name": prop_name,
|
"name": prop_name,
|
||||||
"namespace": self.name(),
|
"namespace": self.name(),
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
"""Alexa entity adapters."""
|
"""Alexa entity adapters."""
|
||||||
import logging
|
import logging
|
||||||
from typing import List
|
from typing import TYPE_CHECKING, List
|
||||||
|
|
||||||
from homeassistant.components import (
|
from homeassistant.components import (
|
||||||
alarm_control_panel,
|
alarm_control_panel,
|
||||||
|
@ -34,7 +34,7 @@ from homeassistant.const import (
|
||||||
TEMP_CELSIUS,
|
TEMP_CELSIUS,
|
||||||
TEMP_FAHRENHEIT,
|
TEMP_FAHRENHEIT,
|
||||||
)
|
)
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import HomeAssistant, State, callback
|
||||||
from homeassistant.helpers import network
|
from homeassistant.helpers import network
|
||||||
from homeassistant.util.decorator import Registry
|
from homeassistant.util.decorator import Registry
|
||||||
|
|
||||||
|
@ -42,6 +42,7 @@ from .capabilities import (
|
||||||
Alexa,
|
Alexa,
|
||||||
AlexaBrightnessController,
|
AlexaBrightnessController,
|
||||||
AlexaCameraStreamController,
|
AlexaCameraStreamController,
|
||||||
|
AlexaCapability,
|
||||||
AlexaChannelController,
|
AlexaChannelController,
|
||||||
AlexaColorController,
|
AlexaColorController,
|
||||||
AlexaColorTemperatureController,
|
AlexaColorTemperatureController,
|
||||||
|
@ -72,6 +73,9 @@ from .capabilities import (
|
||||||
)
|
)
|
||||||
from .const import CONF_DESCRIPTION, CONF_DISPLAY_CATEGORIES
|
from .const import CONF_DESCRIPTION, CONF_DISPLAY_CATEGORIES
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .config import AbstractConfig
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
ENTITY_ADAPTERS = Registry()
|
ENTITY_ADAPTERS = Registry()
|
||||||
|
@ -203,7 +207,7 @@ class AlexaEntity:
|
||||||
The API handlers should manipulate entities only through this interface.
|
The API handlers should manipulate entities only through this interface.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, hass, config, entity):
|
def __init__(self, hass: HomeAssistant, config: "AbstractConfig", entity: State):
|
||||||
"""Initialize Alexa Entity."""
|
"""Initialize Alexa Entity."""
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
self.config = config
|
self.config = config
|
||||||
|
@ -246,13 +250,13 @@ class AlexaEntity:
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def get_interface(self, capability):
|
def get_interface(self, capability) -> AlexaCapability:
|
||||||
"""Return the given AlexaInterface.
|
"""Return the given AlexaInterface.
|
||||||
|
|
||||||
Raises _UnsupportedInterface.
|
Raises _UnsupportedInterface.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def interfaces(self):
|
def interfaces(self) -> List[AlexaCapability]:
|
||||||
"""Return a list of supported interfaces.
|
"""Return a list of supported interfaces.
|
||||||
|
|
||||||
Used for discovery. The list should contain AlexaInterface instances.
|
Used for discovery. The list should contain AlexaInterface instances.
|
||||||
|
@ -280,11 +284,18 @@ class AlexaEntity:
|
||||||
}
|
}
|
||||||
|
|
||||||
locale = self.config.locale
|
locale = self.config.locale
|
||||||
capabilities = [
|
capabilities = []
|
||||||
i.serialize_discovery()
|
|
||||||
for i in self.interfaces()
|
for i in self.interfaces():
|
||||||
if locale in i.supported_locales
|
if locale not in i.supported_locales:
|
||||||
]
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
capabilities.append(i.serialize_discovery())
|
||||||
|
except Exception: # pylint: disable=broad-except
|
||||||
|
_LOGGER.exception(
|
||||||
|
"Error serializing %s discovery for %s", i.name(), self.entity
|
||||||
|
)
|
||||||
|
|
||||||
result["capabilities"] = capabilities
|
result["capabilities"] = capabilities
|
||||||
|
|
||||||
|
|
|
@ -33,6 +33,7 @@ from . import (
|
||||||
reported_properties,
|
reported_properties,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from tests.async_mock import patch
|
||||||
from tests.common import async_mock_service
|
from tests.common import async_mock_service
|
||||||
|
|
||||||
|
|
||||||
|
@ -756,3 +757,25 @@ async def test_report_image_processing(hass):
|
||||||
"humanPresenceDetectionState",
|
"humanPresenceDetectionState",
|
||||||
{"value": "DETECTED"},
|
{"value": "DETECTED"},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_get_property_blowup(hass, caplog):
|
||||||
|
"""Test we handle a property blowing up."""
|
||||||
|
hass.states.async_set(
|
||||||
|
"climate.downstairs",
|
||||||
|
climate.HVAC_MODE_AUTO,
|
||||||
|
{
|
||||||
|
"friendly_name": "Climate Downstairs",
|
||||||
|
"supported_features": 91,
|
||||||
|
climate.ATTR_CURRENT_TEMPERATURE: 34,
|
||||||
|
ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.alexa.capabilities.float",
|
||||||
|
side_effect=Exception("Boom Fail"),
|
||||||
|
):
|
||||||
|
properties = await reported_properties(hass, "climate.downstairs")
|
||||||
|
properties.assert_not_has_property("Alexa.ThermostatController", "temperature")
|
||||||
|
|
||||||
|
assert "Boom Fail" in caplog.text
|
||||||
|
|
|
@ -3,6 +3,8 @@ from homeassistant.components.alexa import smart_home
|
||||||
|
|
||||||
from . import DEFAULT_CONFIG, get_new_request
|
from . import DEFAULT_CONFIG, get_new_request
|
||||||
|
|
||||||
|
from tests.async_mock import patch
|
||||||
|
|
||||||
|
|
||||||
async def test_unsupported_domain(hass):
|
async def test_unsupported_domain(hass):
|
||||||
"""Discovery ignores entities of unknown domains."""
|
"""Discovery ignores entities of unknown domains."""
|
||||||
|
@ -16,3 +18,29 @@ async def test_unsupported_domain(hass):
|
||||||
msg = msg["event"]
|
msg = msg["event"]
|
||||||
|
|
||||||
assert not msg["payload"]["endpoints"]
|
assert not msg["payload"]["endpoints"]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_serialize_discovery_recovers(hass, caplog):
|
||||||
|
"""Test we handle an interface raising unexpectedly during serialize discovery."""
|
||||||
|
request = get_new_request("Alexa.Discovery", "Discover")
|
||||||
|
|
||||||
|
hass.states.async_set("switch.bla", "on", {"friendly_name": "Boop Woz"})
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.alexa.capabilities.AlexaPowerController.serialize_discovery",
|
||||||
|
side_effect=TypeError,
|
||||||
|
):
|
||||||
|
msg = await smart_home.async_handle_message(hass, DEFAULT_CONFIG, request)
|
||||||
|
|
||||||
|
assert "event" in msg
|
||||||
|
msg = msg["event"]
|
||||||
|
|
||||||
|
interfaces = {
|
||||||
|
ifc["interface"] for ifc in msg["payload"]["endpoints"][0]["capabilities"]
|
||||||
|
}
|
||||||
|
|
||||||
|
assert "Alexa.PowerController" not in interfaces
|
||||||
|
assert (
|
||||||
|
f"Error serializing Alexa.PowerController discovery for {hass.states.get('switch.bla')}"
|
||||||
|
in caplog.text
|
||||||
|
)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue