diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index af2fd61081c..d7641c34316 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -349,9 +349,10 @@ def async_validate_entity_schema( config = schema(config) registry = er.async_get(hass) - config[CONF_ENTITY_ID] = er.async_resolve_entity_id( - registry, config[CONF_ENTITY_ID] - ) + if CONF_ENTITY_ID in config: + config[CONF_ENTITY_ID] = er.async_resolve_entity_id( + registry, config[CONF_ENTITY_ID] + ) return config diff --git a/homeassistant/components/zwave_js/device_action.py b/homeassistant/components/zwave_js/device_action.py index 18a3ccef7d8..04db33fdff6 100644 --- a/homeassistant/components/zwave_js/device_action.py +++ b/homeassistant/components/zwave_js/device_action.py @@ -12,6 +12,7 @@ from zwave_js_server.const.command_class.meter import CC_SPECIFIC_METER_TYPE from zwave_js_server.model.value import get_value_id_str from zwave_js_server.util.command_class.meter import get_meter_type +from homeassistant.components.device_automation import async_validate_entity_schema from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import ( @@ -70,7 +71,7 @@ ACTION_TYPES = { CLEAR_LOCK_USERCODE_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( { vol.Required(CONF_TYPE): SERVICE_CLEAR_LOCK_USERCODE, - vol.Required(CONF_ENTITY_ID): cv.entity_domain(LOCK_DOMAIN), + vol.Required(CONF_ENTITY_ID): cv.entity_id_or_uuid, vol.Required(ATTR_CODE_SLOT): vol.Coerce(int), } ) @@ -84,7 +85,7 @@ PING_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( REFRESH_VALUE_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( { vol.Required(CONF_TYPE): SERVICE_REFRESH_VALUE, - vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_ENTITY_ID): cv.entity_id_or_uuid, vol.Optional(ATTR_REFRESH_ALL_VALUES, default=False): cv.boolean, } ) @@ -92,7 +93,7 @@ REFRESH_VALUE_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( RESET_METER_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( { vol.Required(CONF_TYPE): SERVICE_RESET_METER, - vol.Required(CONF_ENTITY_ID): cv.entity_domain(SENSOR_DOMAIN), + vol.Required(CONF_ENTITY_ID): cv.entity_id_or_uuid, vol.Optional(ATTR_METER_TYPE): vol.Coerce(int), vol.Optional(ATTR_VALUE): vol.Coerce(int), } @@ -112,7 +113,7 @@ SET_CONFIG_PARAMETER_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( SET_LOCK_USERCODE_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( { vol.Required(CONF_TYPE): SERVICE_SET_LOCK_USERCODE, - vol.Required(CONF_ENTITY_ID): cv.entity_domain(LOCK_DOMAIN), + vol.Required(CONF_ENTITY_ID): cv.entity_id_or_uuid, vol.Required(ATTR_CODE_SLOT): vol.Coerce(int), vol.Required(ATTR_USERCODE): cv.string, } @@ -130,7 +131,7 @@ SET_VALUE_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( } ) -ACTION_SCHEMA = vol.Any( +_ACTION_SCHEMA = vol.Any( CLEAR_LOCK_USERCODE_SCHEMA, PING_SCHEMA, REFRESH_VALUE_SCHEMA, @@ -141,6 +142,13 @@ ACTION_SCHEMA = vol.Any( ) +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( hass: HomeAssistant, device_id: str ) -> list[dict[str, Any]]: @@ -192,7 +200,7 @@ async def async_get_actions( or state.state == STATE_UNAVAILABLE ): continue - entity_action = {**base_action, CONF_ENTITY_ID: entry.entity_id} + entity_action = {**base_action, CONF_ENTITY_ID: entry.id} actions.append({**entity_action, CONF_TYPE: SERVICE_REFRESH_VALUE}) if entry.domain == LOCK_DOMAIN: actions.extend( @@ -213,9 +221,7 @@ async def async_get_actions( # action for it if CC_SPECIFIC_METER_TYPE in value.metadata.cc_specific: endpoint_idx = value.endpoint or 0 - meter_endpoints[endpoint_idx].setdefault( - CONF_ENTITY_ID, entry.entity_id - ) + meter_endpoints[endpoint_idx].setdefault(CONF_ENTITY_ID, entry.id) meter_endpoints[endpoint_idx].setdefault(ATTR_METER_TYPE, set()).add( get_meter_type(value) ) diff --git a/tests/components/zwave_js/test_device_action.py b/tests/components/zwave_js/test_device_action.py index b5d4149a526..ce2b916b7a1 100644 --- a/tests/components/zwave_js/test_device_action.py +++ b/tests/components/zwave_js/test_device_action.py @@ -15,7 +15,11 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import config_validation as cv, device_registry as dr +from homeassistant.helpers import ( + config_validation as cv, + device_registry as dr, + entity_registry as er, +) from homeassistant.setup import async_setup_component from tests.common import async_get_device_automations @@ -27,6 +31,7 @@ async def test_get_actions( lock_schlage_be469: Node, integration: ConfigEntry, device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, ) -> None: """Test we get the expected actions from a zwave_js node.""" node = lock_schlage_be469 @@ -34,33 +39,39 @@ async def test_get_actions( assert driver device = device_registry.async_get_device(identifiers={get_device_id(driver, node)}) assert device + binary_sensor = entity_registry.async_get( + "binary_sensor.touchscreen_deadbolt_low_battery_level" + ) + assert binary_sensor + lock = entity_registry.async_get("lock.touchscreen_deadbolt") + assert lock expected_actions = [ { "domain": DOMAIN, "type": "clear_lock_usercode", "device_id": device.id, - "entity_id": "lock.touchscreen_deadbolt", + "entity_id": lock.id, "metadata": {"secondary": False}, }, { "domain": DOMAIN, "type": "set_lock_usercode", "device_id": device.id, - "entity_id": "lock.touchscreen_deadbolt", + "entity_id": lock.id, "metadata": {"secondary": False}, }, { "domain": DOMAIN, "type": "refresh_value", "device_id": device.id, - "entity_id": "binary_sensor.touchscreen_deadbolt_low_battery_level", + "entity_id": binary_sensor.id, "metadata": {"secondary": True}, }, { "domain": DOMAIN, "type": "refresh_value", "device_id": device.id, - "entity_id": "lock.touchscreen_deadbolt", + "entity_id": lock.id, "metadata": {"secondary": False}, }, { @@ -129,6 +140,7 @@ async def test_actions( climate_radio_thermostat_ct100_plus: Node, integration: ConfigEntry, device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, ) -> None: """Test actions.""" node = climate_radio_thermostat_ct100_plus @@ -138,6 +150,9 @@ async def test_actions( device = device_registry.async_get_device(identifiers={device_id}) assert device + climate = entity_registry.async_get("climate.z_wave_thermostat") + assert climate + assert await async_setup_component( hass, automation.DOMAIN, @@ -152,7 +167,7 @@ async def test_actions( "domain": DOMAIN, "type": "refresh_value", "device_id": device.id, - "entity_id": "climate.z_wave_thermostat", + "entity_id": climate.id, }, }, { @@ -273,14 +288,15 @@ async def test_actions( assert args[2] == 1 -async def test_actions_multiple_calls( +async def test_actions_legacy( hass: HomeAssistant, client: Client, climate_radio_thermostat_ct100_plus: Node, integration: ConfigEntry, device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, ) -> None: - """Test actions can be called multiple times and still work.""" + """Test actions.""" node = climate_radio_thermostat_ct100_plus driver = client.driver assert driver @@ -288,6 +304,9 @@ async def test_actions_multiple_calls( device = device_registry.async_get_device(identifiers={device_id}) assert device + climate = entity_registry.async_get("climate.z_wave_thermostat") + assert climate + assert await async_setup_component( hass, automation.DOMAIN, @@ -302,7 +321,64 @@ async def test_actions_multiple_calls( "domain": DOMAIN, "type": "refresh_value", "device_id": device.id, - "entity_id": "climate.z_wave_thermostat", + "entity_id": climate.entity_id, + }, + }, + ] + }, + ) + + with patch("zwave_js_server.model.node.Node.async_poll_value") as mock_call: + hass.bus.async_fire("test_event_refresh_value") + await hass.async_block_till_done() + mock_call.assert_called_once() + args = mock_call.call_args_list[0][0] + assert len(args) == 1 + assert args[0].value_id == "13-64-1-mode" + + # Call action a second time to confirm that it works (this was previously a bug) + with patch("zwave_js_server.model.node.Node.async_poll_value") as mock_call: + hass.bus.async_fire("test_event_refresh_value") + await hass.async_block_till_done() + mock_call.assert_called_once() + args = mock_call.call_args_list[0][0] + assert len(args) == 1 + assert args[0].value_id == "13-64-1-mode" + + +async def test_actions_multiple_calls( + hass: HomeAssistant, + client: Client, + climate_radio_thermostat_ct100_plus: Node, + integration: ConfigEntry, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, +) -> None: + """Test actions can be called multiple times and still work.""" + node = climate_radio_thermostat_ct100_plus + driver = client.driver + assert driver + device_id = get_device_id(driver, node) + device = device_registry.async_get_device({device_id}) + assert device + climate = entity_registry.async_get("climate.z_wave_thermostat") + assert climate + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "event", + "event_type": "test_event_refresh_value", + }, + "action": { + "domain": DOMAIN, + "type": "refresh_value", + "device_id": device.id, + "entity_id": climate.id, }, }, ] @@ -326,6 +402,7 @@ async def test_lock_actions( lock_schlage_be469: Node, integration: ConfigEntry, device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, ) -> None: """Test actions for locks.""" node = lock_schlage_be469 @@ -334,6 +411,8 @@ async def test_lock_actions( device_id = get_device_id(driver, node) device = device_registry.async_get_device(identifiers={device_id}) assert device + lock = entity_registry.async_get("lock.touchscreen_deadbolt") + assert lock assert await async_setup_component( hass, @@ -349,7 +428,7 @@ async def test_lock_actions( "domain": DOMAIN, "type": "clear_lock_usercode", "device_id": device.id, - "entity_id": "lock.touchscreen_deadbolt", + "entity_id": lock.id, "code_slot": 1, }, }, @@ -362,7 +441,7 @@ async def test_lock_actions( "domain": DOMAIN, "type": "set_lock_usercode", "device_id": device.id, - "entity_id": "lock.touchscreen_deadbolt", + "entity_id": lock.id, "code_slot": 1, "usercode": "1234", }, @@ -397,6 +476,7 @@ async def test_reset_meter_action( aeon_smart_switch_6: Node, integration: ConfigEntry, device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, ) -> None: """Test reset_meter action.""" node = aeon_smart_switch_6 @@ -405,6 +485,8 @@ async def test_reset_meter_action( device_id = get_device_id(driver, node) device = device_registry.async_get_device(identifiers={device_id}) assert device + sensor = entity_registry.async_get("sensor.smart_switch_6_electric_consumed_kwh") + assert sensor assert await async_setup_component( hass, @@ -420,7 +502,7 @@ async def test_reset_meter_action( "domain": DOMAIN, "type": "reset_meter", "device_id": device.id, - "entity_id": "sensor.smart_switch_6_electric_consumed_kwh", + "entity_id": sensor.id, }, }, ] @@ -615,9 +697,12 @@ async def test_get_action_capabilities_lock_triggers( lock_schlage_be469: Node, integration: ConfigEntry, device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, ) -> None: """Test we get the expected action capabilities for lock triggers.""" device = dr.async_entries_for_config_entry(device_registry, integration.entry_id)[0] + lock = entity_registry.async_get("lock.touchscreen_deadbolt") + assert lock # Test clear_lock_usercode capabilities = await device_action.async_get_action_capabilities( @@ -626,7 +711,7 @@ async def test_get_action_capabilities_lock_triggers( "platform": "device", "domain": DOMAIN, "device_id": device.id, - "entity_id": "lock.touchscreen_deadbolt", + "entity_id": lock.id, "type": "clear_lock_usercode", }, ) @@ -643,7 +728,7 @@ async def test_get_action_capabilities_lock_triggers( "platform": "device", "domain": DOMAIN, "device_id": device.id, - "entity_id": "lock.touchscreen_deadbolt", + "entity_id": lock.id, "type": "set_lock_usercode", }, ) @@ -663,6 +748,7 @@ async def test_get_action_capabilities_meter_triggers( aeon_smart_switch_6: Node, integration: ConfigEntry, device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, ) -> None: """Test we get the expected action capabilities for meter triggers.""" node = aeon_smart_switch_6 @@ -676,7 +762,7 @@ async def test_get_action_capabilities_meter_triggers( "platform": "device", "domain": DOMAIN, "device_id": device.id, - "entity_id": "sensor.meter", + "entity_id": "123456789", # The entity is not checked "type": "reset_meter", }, ) @@ -716,9 +802,10 @@ async def test_unavailable_entity_actions( lock_schlage_be469: Node, integration: ConfigEntry, device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, ) -> None: """Test unavailable entities are not included in actions list.""" - entity_id_unavailable = "binary_sensor.touchscreen_deadbolt_home_security_intrusion" + entity_id_unavailable = "binary_sensor.touchscreen_deadbolt_low_battery_level" hass.states.async_set(entity_id_unavailable, STATE_UNAVAILABLE, force_update=True) await hass.async_block_till_done() node = lock_schlage_be469 @@ -726,9 +813,12 @@ async def test_unavailable_entity_actions( assert driver device = device_registry.async_get_device(identifiers={get_device_id(driver, node)}) assert device + binary_sensor = entity_registry.async_get(entity_id_unavailable) + assert binary_sensor actions = await async_get_device_automations( hass, DeviceAutomationType.ACTION, device.id ) assert not any( action.get("entity_id") == entity_id_unavailable for action in actions ) + assert not any(action.get("entity_id") == binary_sensor.id for action in actions)