diff --git a/homeassistant/components/alarm_control_panel/device_condition.py b/homeassistant/components/alarm_control_panel/device_condition.py index 6eb0144a4b4..cc01f4a5954 100644 --- a/homeassistant/components/alarm_control_panel/device_condition.py +++ b/homeassistant/components/alarm_control_panel/device_condition.py @@ -27,7 +27,7 @@ from homeassistant.const import ( STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import condition, config_validation as cv, entity_registry from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA from homeassistant.helpers.entity import get_supported_features @@ -104,7 +104,10 @@ async def async_get_conditions( return conditions -def async_condition_from_config(config: ConfigType) -> condition.ConditionCheckerType: +@callback +def async_condition_from_config( + hass: HomeAssistant, config: ConfigType +) -> condition.ConditionCheckerType: """Create a function to test a device condition.""" if config[CONF_TYPE] == CONDITION_TRIGGERED: state = STATE_ALARM_TRIGGERED diff --git a/homeassistant/components/binary_sensor/device_condition.py b/homeassistant/components/binary_sensor/device_condition.py index e6202ef66a4..6f1a0ba4f5f 100644 --- a/homeassistant/components/binary_sensor/device_condition.py +++ b/homeassistant/components/binary_sensor/device_condition.py @@ -264,7 +264,9 @@ async def async_get_conditions( @callback -def async_condition_from_config(config: ConfigType) -> condition.ConditionCheckerType: +def async_condition_from_config( + hass: HomeAssistant, config: ConfigType +) -> condition.ConditionCheckerType: """Evaluate state based on configuration.""" condition_type = config[CONF_TYPE] if condition_type in IS_ON: @@ -279,6 +281,7 @@ def async_condition_from_config(config: ConfigType) -> condition.ConditionChecke if CONF_FOR in config: state_config[CONF_FOR] = config[CONF_FOR] state_config = cv.STATE_CONDITION_SCHEMA(state_config) + state_config = condition.state_validate_config(hass, state_config) return condition.state_from_config(state_config) diff --git a/homeassistant/components/climate/device_condition.py b/homeassistant/components/climate/device_condition.py index 03e456965fe..f3e01b5a387 100644 --- a/homeassistant/components/climate/device_condition.py +++ b/homeassistant/components/climate/device_condition.py @@ -70,7 +70,9 @@ async def async_get_conditions( @callback -def async_condition_from_config(config: ConfigType) -> condition.ConditionCheckerType: +def async_condition_from_config( + hass: HomeAssistant, config: ConfigType +) -> condition.ConditionCheckerType: """Create a function to test a device condition.""" if config[CONF_TYPE] == "is_hvac_mode": attribute = const.ATTR_HVAC_MODE diff --git a/homeassistant/components/cover/device_condition.py b/homeassistant/components/cover/device_condition.py index d719c4835e2..cca608187a2 100644 --- a/homeassistant/components/cover/device_condition.py +++ b/homeassistant/components/cover/device_condition.py @@ -119,7 +119,9 @@ async def async_get_condition_capabilities( @callback -def async_condition_from_config(config: ConfigType) -> condition.ConditionCheckerType: +def async_condition_from_config( + hass: HomeAssistant, config: ConfigType +) -> condition.ConditionCheckerType: """Create a function to test a device condition.""" if config[CONF_TYPE] in STATE_CONDITION_TYPES: if config[CONF_TYPE] == "is_open": diff --git a/homeassistant/components/device_automation/toggle_entity.py b/homeassistant/components/device_automation/toggle_entity.py index f9f7555eeb6..2d0254b9a0a 100644 --- a/homeassistant/components/device_automation/toggle_entity.py +++ b/homeassistant/components/device_automation/toggle_entity.py @@ -127,7 +127,9 @@ async def async_call_action_from_config( @callback -def async_condition_from_config(config: ConfigType) -> condition.ConditionCheckerType: +def async_condition_from_config( + hass: HomeAssistant, config: ConfigType +) -> condition.ConditionCheckerType: """Evaluate state based on configuration.""" if config[CONF_TYPE] == CONF_IS_ON: stat = "on" @@ -142,6 +144,7 @@ def async_condition_from_config(config: ConfigType) -> condition.ConditionChecke state_config[CONF_FOR] = config[CONF_FOR] state_config = cv.STATE_CONDITION_SCHEMA(state_config) + state_config = condition.state_validate_config(hass, state_config) return condition.state_from_config(state_config) diff --git a/homeassistant/components/device_tracker/device_condition.py b/homeassistant/components/device_tracker/device_condition.py index 78cedd6e900..2762a271cab 100644 --- a/homeassistant/components/device_tracker/device_condition.py +++ b/homeassistant/components/device_tracker/device_condition.py @@ -55,7 +55,9 @@ async def async_get_conditions( @callback -def async_condition_from_config(config: ConfigType) -> condition.ConditionCheckerType: +def async_condition_from_config( + hass: HomeAssistant, config: ConfigType +) -> condition.ConditionCheckerType: """Create a function to test a device condition.""" reverse = config[CONF_TYPE] == "is_not_home" diff --git a/homeassistant/components/fan/device_condition.py b/homeassistant/components/fan/device_condition.py index 4e3aed026dc..b0882137d7f 100644 --- a/homeassistant/components/fan/device_condition.py +++ b/homeassistant/components/fan/device_condition.py @@ -55,7 +55,9 @@ async def async_get_conditions( @callback -def async_condition_from_config(config: ConfigType) -> condition.ConditionCheckerType: +def async_condition_from_config( + hass: HomeAssistant, config: ConfigType +) -> condition.ConditionCheckerType: """Create a function to test a device condition.""" if config[CONF_TYPE] == "is_on": state = STATE_ON diff --git a/homeassistant/components/humidifier/device_condition.py b/homeassistant/components/humidifier/device_condition.py index 28d74d1efff..c8204c91a29 100644 --- a/homeassistant/components/humidifier/device_condition.py +++ b/homeassistant/components/humidifier/device_condition.py @@ -65,12 +65,14 @@ async def async_get_conditions( @callback -def async_condition_from_config(config: ConfigType) -> condition.ConditionCheckerType: +def async_condition_from_config( + hass: HomeAssistant, config: ConfigType +) -> condition.ConditionCheckerType: """Create a function to test a device condition.""" if config[CONF_TYPE] == "is_mode": attribute = ATTR_MODE else: - return toggle_entity.async_condition_from_config(config) + return toggle_entity.async_condition_from_config(hass, config) def test_is_state(hass: HomeAssistant, variables: TemplateVarsType) -> bool: """Test if an entity is a certain state.""" diff --git a/homeassistant/components/light/device_condition.py b/homeassistant/components/light/device_condition.py index 908af72ecfd..12e86c1e23d 100644 --- a/homeassistant/components/light/device_condition.py +++ b/homeassistant/components/light/device_condition.py @@ -19,9 +19,11 @@ CONDITION_SCHEMA = toggle_entity.CONDITION_SCHEMA.extend( @callback -def async_condition_from_config(config: ConfigType) -> ConditionCheckerType: +def async_condition_from_config( + hass: HomeAssistant, config: ConfigType +) -> ConditionCheckerType: """Evaluate state based on configuration.""" - return toggle_entity.async_condition_from_config(config) + return toggle_entity.async_condition_from_config(hass, config) async def async_get_conditions( diff --git a/homeassistant/components/lock/device_condition.py b/homeassistant/components/lock/device_condition.py index b3fe4ffcfe1..a818a2b5fa4 100644 --- a/homeassistant/components/lock/device_condition.py +++ b/homeassistant/components/lock/device_condition.py @@ -67,7 +67,9 @@ async def async_get_conditions( @callback -def async_condition_from_config(config: ConfigType) -> condition.ConditionCheckerType: +def async_condition_from_config( + hass: HomeAssistant, config: ConfigType +) -> condition.ConditionCheckerType: """Create a function to test a device condition.""" if config[CONF_TYPE] == "is_jammed": state = STATE_JAMMED diff --git a/homeassistant/components/media_player/device_condition.py b/homeassistant/components/media_player/device_condition.py index 41034bbe870..d099eb9a8a4 100644 --- a/homeassistant/components/media_player/device_condition.py +++ b/homeassistant/components/media_player/device_condition.py @@ -59,7 +59,9 @@ async def async_get_conditions( @callback -def async_condition_from_config(config: ConfigType) -> condition.ConditionCheckerType: +def async_condition_from_config( + hass: HomeAssistant, config: ConfigType +) -> condition.ConditionCheckerType: """Create a function to test a device condition.""" if config[CONF_TYPE] == "is_playing": state = STATE_PLAYING diff --git a/homeassistant/components/remote/device_condition.py b/homeassistant/components/remote/device_condition.py index ffbfc1c1061..33f680e6829 100644 --- a/homeassistant/components/remote/device_condition.py +++ b/homeassistant/components/remote/device_condition.py @@ -19,9 +19,11 @@ CONDITION_SCHEMA = toggle_entity.CONDITION_SCHEMA.extend( @callback -def async_condition_from_config(config: ConfigType) -> ConditionCheckerType: +def async_condition_from_config( + hass: HomeAssistant, config: ConfigType +) -> ConditionCheckerType: """Evaluate state based on configuration.""" - return toggle_entity.async_condition_from_config(config) + return toggle_entity.async_condition_from_config(hass, config) async def async_get_conditions( diff --git a/homeassistant/components/select/device_condition.py b/homeassistant/components/select/device_condition.py index eadfebbc711..aa22f117ea8 100644 --- a/homeassistant/components/select/device_condition.py +++ b/homeassistant/components/select/device_condition.py @@ -52,7 +52,9 @@ async def async_get_conditions( @callback -def async_condition_from_config(config: ConfigType) -> condition.ConditionCheckerType: +def async_condition_from_config( + hass: HomeAssistant, config: ConfigType +) -> condition.ConditionCheckerType: """Create a function to test a device condition.""" @callback diff --git a/homeassistant/components/sensor/device_condition.py b/homeassistant/components/sensor/device_condition.py index 6444f58a1de..612ebe0abd5 100644 --- a/homeassistant/components/sensor/device_condition.py +++ b/homeassistant/components/sensor/device_condition.py @@ -186,7 +186,9 @@ async def async_get_conditions( @callback -def async_condition_from_config(config: ConfigType) -> condition.ConditionCheckerType: +def async_condition_from_config( + hass: HomeAssistant, config: ConfigType +) -> condition.ConditionCheckerType: """Evaluate state based on configuration.""" numeric_state_config = { condition.CONF_CONDITION: "numeric_state", @@ -198,6 +200,9 @@ def async_condition_from_config(config: ConfigType) -> condition.ConditionChecke numeric_state_config[condition.CONF_BELOW] = config[CONF_BELOW] numeric_state_config = cv.NUMERIC_STATE_CONDITION_SCHEMA(numeric_state_config) + numeric_state_config = condition.numeric_state_validate_config( + hass, numeric_state_config + ) return condition.async_numeric_state_from_config(numeric_state_config) diff --git a/homeassistant/components/switch/device_condition.py b/homeassistant/components/switch/device_condition.py index 94469275ab3..7f47983ba67 100644 --- a/homeassistant/components/switch/device_condition.py +++ b/homeassistant/components/switch/device_condition.py @@ -19,9 +19,11 @@ CONDITION_SCHEMA = toggle_entity.CONDITION_SCHEMA.extend( @callback -def async_condition_from_config(config: ConfigType) -> ConditionCheckerType: +def async_condition_from_config( + hass: HomeAssistant, config: ConfigType +) -> ConditionCheckerType: """Evaluate state based on configuration.""" - return toggle_entity.async_condition_from_config(config) + return toggle_entity.async_condition_from_config(hass, config) async def async_get_conditions( diff --git a/homeassistant/components/vacuum/device_condition.py b/homeassistant/components/vacuum/device_condition.py index 1c7b0c93332..7a973c93694 100644 --- a/homeassistant/components/vacuum/device_condition.py +++ b/homeassistant/components/vacuum/device_condition.py @@ -53,7 +53,9 @@ async def async_get_conditions( @callback -def async_condition_from_config(config: ConfigType) -> condition.ConditionCheckerType: +def async_condition_from_config( + hass: HomeAssistant, config: ConfigType +) -> condition.ConditionCheckerType: """Create a function to test a device condition.""" if config[CONF_TYPE] == "is_docked": test_states = [STATE_DOCKED] diff --git a/homeassistant/components/zwave_js/device_condition.py b/homeassistant/components/zwave_js/device_condition.py index 4258a0b0892..4b1843782e2 100644 --- a/homeassistant/components/zwave_js/device_condition.py +++ b/homeassistant/components/zwave_js/device_condition.py @@ -147,7 +147,9 @@ async def async_get_conditions( @callback -def async_condition_from_config(config: ConfigType) -> condition.ConditionCheckerType: +def async_condition_from_config( + hass: HomeAssistant, config: ConfigType +) -> condition.ConditionCheckerType: """Create a function to test a device condition.""" condition_type = config[CONF_TYPE] device_id = config[CONF_DEVICE_ID] diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index a4c62709778..57e8f8e5ba2 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -51,7 +51,7 @@ from homeassistant.exceptions import ( HomeAssistantError, TemplateError, ) -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.sun import get_astral_event_date from homeassistant.helpers.template import Template from homeassistant.helpers.typing import ConfigType, TemplateVarsType @@ -71,8 +71,9 @@ from .trace import ( # mypy: disallow-any-generics -FROM_CONFIG_FORMAT = "{}_from_config" ASYNC_FROM_CONFIG_FORMAT = "async_{}_from_config" +FROM_CONFIG_FORMAT = "{}_from_config" +VALIDATE_CONFIG_FORMAT = "{}_validate_config" _LOGGER = logging.getLogger(__name__) @@ -885,7 +886,7 @@ async def async_device_from_config( return trace_condition_function( cast( ConditionCheckerType, - platform.async_condition_from_config(config), # type: ignore + platform.async_condition_from_config(hass, config), # type: ignore ) ) @@ -908,6 +909,30 @@ async def async_trigger_from_config( return trigger_if +def numeric_state_validate_config( + hass: HomeAssistant, config: ConfigType +) -> ConfigType: + """Validate numeric_state condition config.""" + + registry = er.async_get(hass) + config = dict(config) + config[CONF_ENTITY_ID] = er.async_resolve_entity_ids( + registry, cv.entity_ids_or_uuids(config[CONF_ENTITY_ID]) + ) + return config + + +def state_validate_config(hass: HomeAssistant, config: ConfigType) -> ConfigType: + """Validate state condition config.""" + + registry = er.async_get(hass) + config = dict(config) + config[CONF_ENTITY_ID] = er.async_resolve_entity_ids( + registry, cv.entity_ids_or_uuids(config[CONF_ENTITY_ID]) + ) + return config + + async def async_validate_condition_config( hass: HomeAssistant, config: ConfigType | Template ) -> ConfigType | Template: @@ -933,6 +958,12 @@ async def async_validate_condition_config( return await platform.async_validate_condition_config(hass, config) # type: ignore return cast(ConfigType, platform.CONDITION_SCHEMA(config)) # type: ignore + if condition in ("numeric_state", "state"): + validator = getattr( + sys.modules[__name__], VALIDATE_CONFIG_FORMAT.format(condition) + ) + return validator(hass, config) # type: ignore + return config diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 8357746c2cd..cbcfb551dad 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -1028,7 +1028,7 @@ NUMERIC_STATE_CONDITION_SCHEMA = vol.All( { **CONDITION_BASE_SCHEMA, vol.Required(CONF_CONDITION): "numeric_state", - vol.Required(CONF_ENTITY_ID): entity_ids, + vol.Required(CONF_ENTITY_ID): entity_ids_or_uuids, vol.Optional(CONF_ATTRIBUTE): str, CONF_BELOW: NUMERIC_STATE_THRESHOLD_SCHEMA, CONF_ABOVE: NUMERIC_STATE_THRESHOLD_SCHEMA, @@ -1041,7 +1041,7 @@ NUMERIC_STATE_CONDITION_SCHEMA = vol.All( STATE_CONDITION_BASE_SCHEMA = { **CONDITION_BASE_SCHEMA, vol.Required(CONF_CONDITION): "state", - vol.Required(CONF_ENTITY_ID): entity_ids, + vol.Required(CONF_ENTITY_ID): entity_ids_or_uuids, vol.Optional(CONF_ATTRIBUTE): str, vol.Optional(CONF_FOR): positive_time_period, # To support use_trigger_value in automation diff --git a/script/scaffold/templates/device_condition/integration/device_condition.py b/script/scaffold/templates/device_condition/integration/device_condition.py index ca0eda163b8..6f129289af8 100644 --- a/script/scaffold/templates/device_condition/integration/device_condition.py +++ b/script/scaffold/templates/device_condition/integration/device_condition.py @@ -58,7 +58,9 @@ async def async_get_conditions( @callback -def async_condition_from_config(config: ConfigType) -> condition.ConditionCheckerType: +def async_condition_from_config( + hass: HomeAssistant, config: ConfigType +) -> condition.ConditionCheckerType: """Create a function to test a device condition.""" if config[CONF_TYPE] == "is_on": state = STATE_ON diff --git a/tests/components/zwave_js/test_device_condition.py b/tests/components/zwave_js/test_device_condition.py index 1f6a9d9ca90..725d9574605 100644 --- a/tests/components/zwave_js/test_device_condition.py +++ b/tests/components/zwave_js/test_device_condition.py @@ -552,7 +552,7 @@ async def test_failure_scenarios(hass, client, hank_binary_switch, integration): with pytest.raises(HomeAssistantError): await device_condition.async_condition_from_config( - {"type": "failed.test", "device_id": device.id} + hass, {"type": "failed.test", "device_id": device.id} ) with patch( diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py index 92bad8777bd..c4ceff89b64 100644 --- a/tests/helpers/test_condition.py +++ b/tests/helpers/test_condition.py @@ -16,7 +16,12 @@ from homeassistant.const import ( SUN_EVENT_SUNSET, ) from homeassistant.exceptions import ConditionError, HomeAssistantError -from homeassistant.helpers import condition, config_validation as cv, trace +from homeassistant.helpers import ( + condition, + config_validation as cv, + entity_registry as er, + trace, +) from homeassistant.helpers.template import Template from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -1107,6 +1112,29 @@ async def test_state_attribute_boolean(hass): assert test(hass) +async def test_state_entity_registry_id(hass): + """Test with entity specified by entity registry id.""" + registry = er.async_get(hass) + entry = registry.async_get_or_create( + "switch", "hue", "1234", suggested_object_id="test" + ) + assert entry.entity_id == "switch.test" + config = { + "condition": "state", + "entity_id": entry.id, + "state": "on", + } + config = cv.CONDITION_SCHEMA(config) + config = await condition.async_validate_condition_config(hass, config) + test = await condition.async_from_config(hass, config) + + hass.states.async_set("switch.test", "on") + assert test(hass) + + hass.states.async_set("switch.test", "off") + assert not test(hass) + + async def test_state_using_input_entities(hass): """Test state conditions using input_* entities.""" await async_setup_component( @@ -1419,6 +1447,29 @@ async def test_numeric_state_attribute(hass): assert not test(hass) +async def test_numeric_state_entity_registry_id(hass): + """Test with entity specified by entity registry id.""" + registry = er.async_get(hass) + entry = registry.async_get_or_create( + "sensor", "hue", "1234", suggested_object_id="test" + ) + assert entry.entity_id == "sensor.test" + config = { + "condition": "numeric_state", + "entity_id": entry.id, + "above": 100, + } + config = cv.CONDITION_SCHEMA(config) + config = await condition.async_validate_condition_config(hass, config) + test = await condition.async_from_config(hass, config) + + hass.states.async_set("sensor.test", "110") + assert test(hass) + + hass.states.async_set("sensor.test", "90") + assert not test(hass) + + async def test_numeric_state_using_input_number(hass): """Test numeric_state conditions using input_number entities.""" hass.states.async_set("number.low", 10) diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index 23768cd95ce..962fe4b1366 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -25,7 +25,13 @@ from homeassistant.const import ( ) from homeassistant.core import SERVICE_CALL_LIMIT, Context, CoreState, callback from homeassistant.exceptions import ConditionError, ServiceNotFound -from homeassistant.helpers import config_validation as cv, script, template, trace +from homeassistant.helpers import ( + config_validation as cv, + entity_registry as er, + script, + template, + trace, +) from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -1492,6 +1498,81 @@ async def test_condition_basic(hass, caplog): ) +async def test_condition_validation(hass, caplog): + """Test if we can use conditions which validate late in a script.""" + registry = er.async_get(hass) + entry = registry.async_get_or_create( + "test", "hue", "1234", suggested_object_id="entity" + ) + assert entry.entity_id == "test.entity" + event = "test_event" + events = async_capture_events(hass, event) + alias = "condition step" + sequence = cv.SCRIPT_SCHEMA( + [ + {"event": event}, + { + "alias": alias, + "condition": "state", + "entity_id": entry.id, + "state": "hello", + }, + {"event": event}, + ] + ) + sequence = await script.async_validate_actions_config(hass, sequence) + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") + + hass.states.async_set("test.entity", "hello") + await script_obj.async_run(context=Context()) + await hass.async_block_till_done() + + assert f"Test condition {alias}: True" in caplog.text + caplog.clear() + assert len(events) == 2 + + assert_action_trace( + { + "0": [{"result": {"event": "test_event", "event_data": {}}}], + "1": [{"result": {"result": True}}], + "1/entity_id/0": [ + {"result": {"result": True, "state": "hello", "wanted_state": "hello"}} + ], + "2": [{"result": {"event": "test_event", "event_data": {}}}], + } + ) + + hass.states.async_set("test.entity", "goodbye") + + await script_obj.async_run(context=Context()) + await hass.async_block_till_done() + + assert f"Test condition {alias}: False" in caplog.text + assert len(events) == 3 + + assert_action_trace( + { + "0": [{"result": {"event": "test_event", "event_data": {}}}], + "1": [ + { + "error_type": script._StopScript, + "result": {"result": False}, + } + ], + "1/entity_id/0": [ + { + "result": { + "result": False, + "state": "goodbye", + "wanted_state": "hello", + } + } + ], + }, + expected_script_execution="aborted", + ) + + @patch("homeassistant.helpers.script.condition.async_from_config") async def test_condition_created_once(async_from_config, hass): """Test that the conditions do not get created multiple times."""