From c2f7e5840bd3f2244f338ab6c15c2c3dc41f4427 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 27 Jun 2023 09:28:54 +0200 Subject: [PATCH] Use entity registry id in climate device actions (#95268) Co-authored-by: Franck Nijhof --- .../components/climate/device_action.py | 28 ++- .../components/device_automation/helpers.py | 1 + .../components/climate/test_device_action.py | 208 +++++++++++++++++- 3 files changed, 218 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/climate/device_action.py b/homeassistant/components/climate/device_action.py index 0119ad65801..6714e0bf35a 100644 --- a/homeassistant/components/climate/device_action.py +++ b/homeassistant/components/climate/device_action.py @@ -3,6 +3,10 @@ from __future__ import annotations import voluptuous as vol +from homeassistant.components.device_automation import ( + async_get_entity_registry_entry_or_raise, + async_validate_entity_schema, +) from homeassistant.const import ( ATTR_ENTITY_ID, CONF_DEVICE_ID, @@ -24,7 +28,7 @@ ACTION_TYPES = {"set_hvac_mode", "set_preset_mode"} SET_HVAC_MODE_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( { vol.Required(CONF_TYPE): "set_hvac_mode", - vol.Required(CONF_ENTITY_ID): cv.entity_domain(DOMAIN), + vol.Required(CONF_ENTITY_ID): cv.entity_id_or_uuid, vol.Required(const.ATTR_HVAC_MODE): vol.In(const.HVAC_MODES), } ) @@ -32,12 +36,19 @@ SET_HVAC_MODE_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( SET_PRESET_MODE_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( { vol.Required(CONF_TYPE): "set_preset_mode", - vol.Required(CONF_ENTITY_ID): cv.entity_domain(DOMAIN), + vol.Required(CONF_ENTITY_ID): cv.entity_id_or_uuid, vol.Required(const.ATTR_PRESET_MODE): str, } ) -ACTION_SCHEMA = vol.Any(SET_HVAC_MODE_SCHEMA, SET_PRESET_MODE_SCHEMA) +_ACTION_SCHEMA = vol.Any(SET_HVAC_MODE_SCHEMA, SET_PRESET_MODE_SCHEMA) + + +async def async_validate_action_config( + hass: HomeAssistant, config: ConfigType +) -> ConfigType: + """Validate config.""" + return async_validate_entity_schema(hass, config, _ACTION_SCHEMA) async def async_get_actions( @@ -57,7 +68,7 @@ async def async_get_actions( base_action = { CONF_DEVICE_ID: device_id, CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, + CONF_ENTITY_ID: entry.id, } actions.append({**base_action, CONF_TYPE: "set_hvac_mode"}) @@ -93,23 +104,24 @@ async def async_get_action_capabilities( ) -> dict[str, vol.Schema]: """List action capabilities.""" action_type = config[CONF_TYPE] + entity_id_or_uuid = config[CONF_ENTITY_ID] fields = {} if action_type == "set_hvac_mode": try: + entry = async_get_entity_registry_entry_or_raise(hass, entity_id_or_uuid) hvac_modes = ( - get_capability(hass, config[ATTR_ENTITY_ID], const.ATTR_HVAC_MODES) - or [] + get_capability(hass, entry.entity_id, const.ATTR_HVAC_MODES) or [] ) except HomeAssistantError: hvac_modes = [] fields[vol.Required(const.ATTR_HVAC_MODE)] = vol.In(hvac_modes) elif action_type == "set_preset_mode": try: + entry = async_get_entity_registry_entry_or_raise(hass, entity_id_or_uuid) preset_modes = ( - get_capability(hass, config[ATTR_ENTITY_ID], const.ATTR_PRESET_MODES) - or [] + get_capability(hass, entry.entity_id, const.ATTR_PRESET_MODES) or [] ) except HomeAssistantError: preset_modes = [] diff --git a/homeassistant/components/device_automation/helpers.py b/homeassistant/components/device_automation/helpers.py index 858dc466587..2734f7c58aa 100644 --- a/homeassistant/components/device_automation/helpers.py +++ b/homeassistant/components/device_automation/helpers.py @@ -28,6 +28,7 @@ STATIC_VALIDATOR = { ENTITY_PLATFORMS = { Platform.ALARM_CONTROL_PANEL.value, Platform.BUTTON.value, + Platform.CLIMATE.value, Platform.COVER.value, Platform.FAN.value, Platform.HUMIDIFIER.value, diff --git a/tests/components/climate/test_device_action.py b/tests/components/climate/test_device_action.py index 3f3f9b407b6..05c5c4cdebb 100644 --- a/tests/components/climate/test_device_action.py +++ b/tests/components/climate/test_device_action.py @@ -5,7 +5,9 @@ import voluptuous_serialize import homeassistant.components.automation as automation from homeassistant.components.climate import DOMAIN, HVACMode, const, device_action -from homeassistant.components.device_automation import DeviceAutomationType +from homeassistant.components.device_automation import ( + DeviceAutomationType, +) from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers import ( @@ -63,7 +65,7 @@ async def test_get_actions( config_entry_id=config_entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) - entity_registry.async_get_or_create( + entity_entry = entity_registry.async_get_or_create( DOMAIN, "test", "5678", @@ -82,7 +84,7 @@ async def test_get_actions( "domain": DOMAIN, "type": action, "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_5678", + "entity_id": entity_entry.id, "metadata": {"secondary": False}, } for action in expected_action_types @@ -117,7 +119,7 @@ async def test_get_actions_hidden_auxiliary( config_entry_id=config_entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) - entity_registry.async_get_or_create( + entity_entry = entity_registry.async_get_or_create( DOMAIN, "test", "5678", @@ -132,7 +134,7 @@ async def test_get_actions_hidden_auxiliary( "domain": DOMAIN, "type": action, "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_5678", + "entity_id": entity_entry.id, "metadata": {"secondary": True}, } for action in ["set_hvac_mode"] @@ -143,10 +145,12 @@ async def test_get_actions_hidden_auxiliary( assert actions == unordered(expected_actions) -async def test_action(hass: HomeAssistant) -> None: +async def test_action(hass: HomeAssistant, entity_registry: er.EntityRegistry) -> None: """Test for actions.""" + entry = entity_registry.async_get_or_create(DOMAIN, "test", "5678") + hass.states.async_set( - "climate.entity", + entry.entity_id, HVACMode.COOL, { const.ATTR_HVAC_MODES: [HVACMode.COOL, HVACMode.OFF], @@ -167,7 +171,7 @@ async def test_action(hass: HomeAssistant) -> None: "action": { "domain": DOMAIN, "device_id": "abcdefgh", - "entity_id": "climate.entity", + "entity_id": entry.id, "type": "set_hvac_mode", "hvac_mode": HVACMode.OFF, }, @@ -180,7 +184,7 @@ async def test_action(hass: HomeAssistant) -> None: "action": { "domain": DOMAIN, "device_id": "abcdefgh", - "entity_id": "climate.entity", + "entity_id": entry.id, "type": "set_preset_mode", "preset_mode": const.PRESET_AWAY, }, @@ -202,6 +206,70 @@ async def test_action(hass: HomeAssistant) -> None: assert len(set_hvac_mode_calls) == 1 assert len(set_preset_mode_calls) == 1 + assert set_hvac_mode_calls[0].domain == DOMAIN + assert set_hvac_mode_calls[0].service == "set_hvac_mode" + assert set_hvac_mode_calls[0].data == { + "entity_id": entry.entity_id, + "hvac_mode": const.HVAC_MODE_OFF, + } + assert set_preset_mode_calls[0].domain == DOMAIN + assert set_preset_mode_calls[0].service == "set_preset_mode" + assert set_preset_mode_calls[0].data == { + "entity_id": entry.entity_id, + "preset_mode": const.PRESET_AWAY, + } + + +async def test_action_legacy( + hass: HomeAssistant, entity_registry: er.EntityRegistry +) -> None: + """Test for actions.""" + entry = entity_registry.async_get_or_create(DOMAIN, "test", "5678") + + hass.states.async_set( + entry.entity_id, + HVACMode.COOL, + { + const.ATTR_HVAC_MODES: [HVACMode.COOL, HVACMode.OFF], + const.ATTR_PRESET_MODES: [const.PRESET_HOME, const.PRESET_AWAY], + }, + ) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "event", + "event_type": "test_event_set_hvac_mode", + }, + "action": { + "domain": DOMAIN, + "device_id": "abcdefgh", + "entity_id": entry.entity_id, + "type": "set_hvac_mode", + "hvac_mode": HVACMode.OFF, + }, + }, + ] + }, + ) + + set_hvac_mode_calls = async_mock_service(hass, "climate", "set_hvac_mode") + + hass.bus.async_fire("test_event_set_hvac_mode") + await hass.async_block_till_done() + assert len(set_hvac_mode_calls) == 1 + + assert set_hvac_mode_calls[0].domain == DOMAIN + assert set_hvac_mode_calls[0].service == "set_hvac_mode" + assert set_hvac_mode_calls[0].data == { + "entity_id": entry.entity_id, + "hvac_mode": const.HVAC_MODE_OFF, + } + @pytest.mark.parametrize( ( @@ -287,7 +355,7 @@ async def test_capabilities( config_entry_id=config_entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) - entity_registry.async_get_or_create( + entity_entry = entity_registry.async_get_or_create( DOMAIN, "test", "5678", @@ -306,7 +374,125 @@ async def test_capabilities( { "domain": DOMAIN, "device_id": "abcdefgh", - "entity_id": f"{DOMAIN}.test_5678", + "entity_id": entity_entry.id, + "type": action, + }, + ) + + assert capabilities and "extra_fields" in capabilities + + assert ( + voluptuous_serialize.convert( + capabilities["extra_fields"], custom_serializer=cv.custom_serializer + ) + == expected_capabilities + ) + + +@pytest.mark.parametrize( + ( + "set_state", + "capabilities_reg", + "capabilities_state", + "action", + "expected_capabilities", + ), + [ + ( + False, + {const.ATTR_HVAC_MODES: [HVACMode.COOL, HVACMode.OFF]}, + {}, + "set_hvac_mode", + [ + { + "name": "hvac_mode", + "options": [("cool", "cool"), ("off", "off")], + "required": True, + "type": "select", + } + ], + ), + ( + False, + {const.ATTR_PRESET_MODES: [const.PRESET_HOME, const.PRESET_AWAY]}, + {}, + "set_preset_mode", + [ + { + "name": "preset_mode", + "options": [("home", "home"), ("away", "away")], + "required": True, + "type": "select", + } + ], + ), + ( + True, + {}, + {const.ATTR_HVAC_MODES: [HVACMode.COOL, HVACMode.OFF]}, + "set_hvac_mode", + [ + { + "name": "hvac_mode", + "options": [("cool", "cool"), ("off", "off")], + "required": True, + "type": "select", + } + ], + ), + ( + True, + {}, + {const.ATTR_PRESET_MODES: [const.PRESET_HOME, const.PRESET_AWAY]}, + "set_preset_mode", + [ + { + "name": "preset_mode", + "options": [("home", "home"), ("away", "away")], + "required": True, + "type": "select", + } + ], + ), + ], +) +async def test_capabilities_legacy( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + set_state, + capabilities_reg, + capabilities_state, + action, + expected_capabilities, +) -> None: + """Test getting capabilities.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_entry = entity_registry.async_get_or_create( + DOMAIN, + "test", + "5678", + device_id=device_entry.id, + capabilities=capabilities_reg, + ) + if set_state: + hass.states.async_set( + f"{DOMAIN}.test_5678", + HVACMode.COOL, + capabilities_state, + ) + + capabilities = await device_action.async_get_action_capabilities( + hass, + { + "domain": DOMAIN, + "device_id": "abcdefgh", + "entity_id": entity_entry.entity_id, "type": action, }, )