From 36734972f04d6a2e010c7fbc409a3f3ac6e3528d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 2 Dec 2021 18:45:40 +0100 Subject: [PATCH] Teach numeric state trigger about entity registry ids (#60835) --- .../components/climate/device_trigger.py | 4 +- .../components/cover/device_trigger.py | 4 +- .../homeassistant/triggers/numeric_state.py | 26 +++++-- .../components/humidifier/device_trigger.py | 6 +- .../components/sensor/device_trigger.py | 4 +- .../triggers/test_numeric_state.py | 72 ++++++++++++++++--- 6 files changed, 99 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/climate/device_trigger.py b/homeassistant/components/climate/device_trigger.py index 3a9e0e45900..6bd6f4c3e02 100644 --- a/homeassistant/components/climate/device_trigger.py +++ b/homeassistant/components/climate/device_trigger.py @@ -159,7 +159,9 @@ async def async_attach_trigger( if CONF_FOR in config: numeric_state_config[CONF_FOR] = config[CONF_FOR] - numeric_state_config = numeric_state_trigger.TRIGGER_SCHEMA(numeric_state_config) + numeric_state_config = await numeric_state_trigger.async_validate_trigger_config( + hass, numeric_state_config + ) return await numeric_state_trigger.async_attach_trigger( hass, numeric_state_config, action, automation_info, platform_type="device" ) diff --git a/homeassistant/components/cover/device_trigger.py b/homeassistant/components/cover/device_trigger.py index b9a0aefb7a2..f960fcdcce6 100644 --- a/homeassistant/components/cover/device_trigger.py +++ b/homeassistant/components/cover/device_trigger.py @@ -192,7 +192,9 @@ async def async_attach_trigger( CONF_ABOVE: min_pos, CONF_VALUE_TEMPLATE: value_template, } - numeric_state_config = numeric_state_trigger.TRIGGER_SCHEMA(numeric_state_config) + numeric_state_config = await numeric_state_trigger.async_validate_trigger_config( + hass, numeric_state_config + ) return await numeric_state_trigger.async_attach_trigger( hass, numeric_state_config, action, automation_info, platform_type="device" ) diff --git a/homeassistant/components/homeassistant/triggers/numeric_state.py b/homeassistant/components/homeassistant/triggers/numeric_state.py index 3f280f581b3..823bb608b4d 100644 --- a/homeassistant/components/homeassistant/triggers/numeric_state.py +++ b/homeassistant/components/homeassistant/triggers/numeric_state.py @@ -13,12 +13,18 @@ from homeassistant.const import ( CONF_PLATFORM, CONF_VALUE_TEMPLATE, ) -from homeassistant.core import CALLBACK_TYPE, HassJob, callback -from homeassistant.helpers import condition, config_validation as cv, template +from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback +from homeassistant.helpers import ( + condition, + config_validation as cv, + entity_registry as er, + template, +) from homeassistant.helpers.event import ( async_track_same_state, async_track_state_change_event, ) +from homeassistant.helpers.typing import ConfigType # mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs # mypy: no-check-untyped-defs @@ -43,11 +49,11 @@ def validate_above_below(value): return value -TRIGGER_SCHEMA = vol.All( +_TRIGGER_SCHEMA = vol.All( cv.TRIGGER_BASE_SCHEMA.extend( { vol.Required(CONF_PLATFORM): "numeric_state", - vol.Required(CONF_ENTITY_ID): cv.entity_ids, + vol.Required(CONF_ENTITY_ID): cv.entity_ids_or_uuids, vol.Optional(CONF_BELOW): cv.NUMERIC_STATE_THRESHOLD_SCHEMA, vol.Optional(CONF_ABOVE): cv.NUMERIC_STATE_THRESHOLD_SCHEMA, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, @@ -62,6 +68,18 @@ TRIGGER_SCHEMA = vol.All( _LOGGER = logging.getLogger(__name__) +async def async_validate_trigger_config( + hass: HomeAssistant, config: ConfigType +) -> ConfigType: + """Validate trigger config.""" + config = _TRIGGER_SCHEMA(config) + registry = er.async_get(hass) + config[CONF_ENTITY_ID] = er.async_resolve_entity_ids( + registry, cv.entity_ids_or_uuids(config[CONF_ENTITY_ID]) + ) + return config + + async def async_attach_trigger( hass, config, action, automation_info, *, platform_type="numeric_state" ) -> CALLBACK_TYPE: diff --git a/homeassistant/components/humidifier/device_trigger.py b/homeassistant/components/humidifier/device_trigger.py index 9c6ca5cea55..1c7a09305e1 100644 --- a/homeassistant/components/humidifier/device_trigger.py +++ b/homeassistant/components/humidifier/device_trigger.py @@ -100,8 +100,10 @@ async def async_attach_trigger( if CONF_FOR in config: numeric_state_config[CONF_FOR] = config[CONF_FOR] - numeric_state_config = numeric_state_trigger.TRIGGER_SCHEMA( - numeric_state_config + numeric_state_config = ( + await numeric_state_trigger.async_validate_trigger_config( + hass, numeric_state_config + ) ) return await numeric_state_trigger.async_attach_trigger( hass, numeric_state_config, action, automation_info, platform_type="device" diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py index 6f82ba1a34c..a9d014e6856 100644 --- a/homeassistant/components/sensor/device_trigger.py +++ b/homeassistant/components/sensor/device_trigger.py @@ -162,7 +162,9 @@ async def async_attach_trigger(hass, config, action, automation_info): if CONF_FOR in config: numeric_state_config[CONF_FOR] = config[CONF_FOR] - numeric_state_config = numeric_state_trigger.TRIGGER_SCHEMA(numeric_state_config) + numeric_state_config = await numeric_state_trigger.async_validate_trigger_config( + hass, numeric_state_config + ) return await numeric_state_trigger.async_attach_trigger( hass, numeric_state_config, action, automation_info, platform_type="device" ) diff --git a/tests/components/homeassistant/triggers/test_numeric_state.py b/tests/components/homeassistant/triggers/test_numeric_state.py index 0e71594937f..c0e5fdbdd99 100644 --- a/tests/components/homeassistant/triggers/test_numeric_state.py +++ b/tests/components/homeassistant/triggers/test_numeric_state.py @@ -12,6 +12,7 @@ from homeassistant.components.homeassistant.triggers import ( ) from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, SERVICE_TURN_OFF from homeassistant.core import Context +from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -126,6 +127,59 @@ async def test_if_fires_on_entity_change_below(hass, calls, below): assert calls[0].data["id"] == 0 +@pytest.mark.parametrize( + "below", (10, "input_number.value_10", "number.value_10", "sensor.value_10") +) +async def test_if_fires_on_entity_change_below_uuid(hass, calls, below): + """Test the firing with changed entity specified by registry entry id.""" + registry = er.async_get(hass) + entry = registry.async_get_or_create( + "test", "hue", "1234", suggested_object_id="entity" + ) + assert entry.entity_id == "test.entity" + + hass.states.async_set("test.entity", 11) + await hass.async_block_till_done() + + context = Context() + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": { + "platform": "numeric_state", + "entity_id": entry.id, + "below": below, + }, + "action": { + "service": "test.automation", + "data_template": {"id": "{{ trigger.id}}"}, + }, + } + }, + ) + # 9 is below 10 + hass.states.async_set("test.entity", 9, context=context) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].context.parent_id == context.id + + # Set above 12 so the automation will fire again + hass.states.async_set("test.entity", 12) + + await hass.services.async_call( + automation.DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_MATCH_ALL}, + blocking=True, + ) + hass.states.async_set("test.entity", 9) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["id"] == 0 + + @pytest.mark.parametrize( "below", (10, "input_number.value_10", "number.value_10", "sensor.value_10") ) @@ -1644,31 +1698,33 @@ async def test_if_fires_on_entities_change_overlap_for_template( assert calls[1].data["some"] == "test.entity_2 - 0:00:10" -def test_below_above(): +async def test_below_above(hass): """Test above cannot be above below.""" with pytest.raises(vol.Invalid): - numeric_state_trigger.TRIGGER_SCHEMA( - {"platform": "numeric_state", "above": 1200, "below": 1000} + await numeric_state_trigger.async_validate_trigger_config( + hass, {"platform": "numeric_state", "above": 1200, "below": 1000} ) -def test_schema_unacceptable_entities(): +async def test_schema_unacceptable_entities(hass): """Test input_number, number & sensor only is accepted for above/below.""" with pytest.raises(vol.Invalid): - numeric_state_trigger.TRIGGER_SCHEMA( + await numeric_state_trigger.async_validate_trigger_config( + hass, { "platform": "numeric_state", "above": "input_datetime.some_input", "below": 1000, - } + }, ) with pytest.raises(vol.Invalid): - numeric_state_trigger.TRIGGER_SCHEMA( + await numeric_state_trigger.async_validate_trigger_config( + hass, { "platform": "numeric_state", "below": "input_datetime.some_input", "above": 1200, - } + }, )