From 7215f6320e0f41735be850edc3be9b796db5c21b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 27 Apr 2023 13:31:24 +0200 Subject: [PATCH] Avoid exposing unsupported entities to Alexa (#92107) * Avoid exposing unsupported entities to Alexa * Update homeassistant/components/cloud/alexa_config.py Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --------- Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- .../components/cloud/alexa_config.py | 74 +++++++++++++- tests/components/cloud/test_alexa_config.py | 98 +++++++++++++++++++ 2 files changed, 170 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/cloud/alexa_config.py b/homeassistant/components/cloud/alexa_config.py index 44a42c78f09..7acfbbb7f6b 100644 --- a/homeassistant/components/cloud/alexa_config.py +++ b/homeassistant/components/cloud/alexa_config.py @@ -20,14 +20,17 @@ from homeassistant.components.alexa import ( errors as alexa_errors, state_report as alexa_state_report, ) +from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.components.homeassistant.exposed_entities import ( async_get_assistant_settings, async_listen_entity_updates, async_should_expose, ) +from homeassistant.components.sensor import SensorDeviceClass from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES from homeassistant.core import HomeAssistant, callback, split_entity_id from homeassistant.helpers import entity_registry as er, start +from homeassistant.helpers.entity import get_device_class from homeassistant.helpers.event import async_call_later from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow @@ -51,6 +54,69 @@ CLOUD_ALEXA = f"{CLOUD_DOMAIN}.{ALEXA_DOMAIN}" SYNC_DELAY = 1 +SUPPORTED_DOMAINS = { + "alarm_control_panel", + "alert", + "automation", + "button", + "camera", + "climate", + "cover", + "fan", + "group", + "humidifier", + "image_processing", + "input_boolean", + "input_button", + "input_number", + "light", + "lock", + "media_player", + "number", + "scene", + "script", + "switch", + "timer", + "vacuum", +} + +SUPPORTED_BINARY_SENSOR_DEVICE_CLASSES = { + BinarySensorDeviceClass.DOOR, + BinarySensorDeviceClass.GARAGE_DOOR, + BinarySensorDeviceClass.MOTION, + BinarySensorDeviceClass.OPENING, + BinarySensorDeviceClass.PRESENCE, + BinarySensorDeviceClass.WINDOW, +} + +SUPPORTED_SENSOR_DEVICE_CLASSES = { + SensorDeviceClass.TEMPERATURE, +} + + +def _supported_legacy(hass: HomeAssistant, entity_id: str) -> bool: + """Return if the entity is supported. + + This is called when migrating from legacy config format to avoid exposing + all binary sensors and sensors. + """ + domain = split_entity_id(entity_id)[0] + if domain in SUPPORTED_DOMAINS: + return True + + device_class = get_device_class(hass, entity_id) + if ( + domain == "binary_sensor" + and device_class in SUPPORTED_BINARY_SENSOR_DEVICE_CLASSES + ): + return True + + if domain == "sensor" and device_class in SUPPORTED_SENSOR_DEVICE_CLASSES: + return True + + return False + + class CloudAlexaConfig(alexa_config.AbstractConfig): """Alexa Configuration.""" @@ -183,9 +249,13 @@ class CloudAlexaConfig(alexa_config.AbstractConfig): # Backwards compat if (default_expose := self._prefs.alexa_default_expose) is None: - return not auxiliary_entity + return not auxiliary_entity and _supported_legacy(self.hass, entity_id) - return not auxiliary_entity and split_entity_id(entity_id)[0] in default_expose + return ( + not auxiliary_entity + and split_entity_id(entity_id)[0] in default_expose + and _supported_legacy(self.hass, entity_id) + ) def should_expose(self, entity_id): """If an entity should be exposed.""" diff --git a/tests/components/cloud/test_alexa_config.py b/tests/components/cloud/test_alexa_config.py index 2cb363b0420..0e1f941ab64 100644 --- a/tests/components/cloud/test_alexa_config.py +++ b/tests/components/cloud/test_alexa_config.py @@ -650,3 +650,101 @@ async def test_alexa_config_migrate_expose_entity_prefs_default_none( entity_default = entity_registry.async_get(entity_default.entity_id) assert entity_default.options == {"cloud.alexa": {"should_expose": True}} + + +async def test_alexa_config_migrate_expose_entity_prefs_default( + hass: HomeAssistant, + cloud_prefs: CloudPreferences, + cloud_stub, + entity_registry: er.EntityRegistry, +) -> None: + """Test migrating Alexa entity config.""" + + assert await async_setup_component(hass, "homeassistant", {}) + + binary_sensor_supported = entity_registry.async_get_or_create( + "binary_sensor", + "test", + "binary_sensor_supported", + original_device_class="door", + suggested_object_id="supported", + ) + + binary_sensor_unsupported = entity_registry.async_get_or_create( + "binary_sensor", + "test", + "binary_sensor_unsupported", + original_device_class="battery", + suggested_object_id="unsupported", + ) + + light = entity_registry.async_get_or_create( + "light", + "test", + "unique", + suggested_object_id="light", + ) + + sensor_supported = entity_registry.async_get_or_create( + "sensor", + "test", + "sensor_supported", + original_device_class="temperature", + suggested_object_id="supported", + ) + + sensor_unsupported = entity_registry.async_get_or_create( + "sensor", + "test", + "sensor_unsupported", + original_device_class="battery", + suggested_object_id="unsupported", + ) + + water_heater = entity_registry.async_get_or_create( + "water_heater", + "test", + "unique", + suggested_object_id="water_heater", + ) + + await cloud_prefs.async_update( + alexa_enabled=True, + alexa_report_state=False, + alexa_settings_version=1, + ) + + cloud_prefs._prefs[PREF_ALEXA_DEFAULT_EXPOSE] = [ + "binary_sensor", + "light", + "sensor", + "water_heater", + ] + conf = alexa_config.CloudAlexaConfig( + hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, cloud_stub + ) + await conf.async_initialize() + + binary_sensor_supported = entity_registry.async_get( + binary_sensor_supported.entity_id + ) + assert binary_sensor_supported.options == {"cloud.alexa": {"should_expose": True}} + + binary_sensor_unsupported = entity_registry.async_get( + binary_sensor_unsupported.entity_id + ) + assert binary_sensor_unsupported.options == { + "cloud.alexa": {"should_expose": False} + } + + light = entity_registry.async_get(light.entity_id) + assert light.options == {"cloud.alexa": {"should_expose": True}} + + sensor_supported = entity_registry.async_get(sensor_supported.entity_id) + assert sensor_supported.options == {"cloud.alexa": {"should_expose": True}} + + sensor_unsupported = entity_registry.async_get(sensor_unsupported.entity_id) + assert sensor_unsupported.options == {"cloud.alexa": {"should_expose": False}} + + water_heater = entity_registry.async_get(water_heater.entity_id) + assert water_heater.options == {"cloud.alexa": {"should_expose": False}}