diff --git a/homeassistant/const.py b/homeassistant/const.py index 765b7b0077a..0ab11a2971f 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -61,6 +61,7 @@ MATCH_ALL: Final = "*" # Entity target all constant ENTITY_MATCH_NONE: Final = "none" ENTITY_MATCH_ALL: Final = "all" +ENTITY_MATCH_ANY: Final = "any" # If no name is specified DEVICE_DEFAULT_NAME: Final = "Unnamed Device" @@ -172,6 +173,7 @@ CONF_LIGHTS: Final = "lights" CONF_LOCATION: Final = "location" CONF_LONGITUDE: Final = "longitude" CONF_MAC: Final = "mac" +CONF_MATCH: Final = "match" CONF_MAXIMUM: Final = "maximum" CONF_MEDIA_DIRS: Final = "media_dirs" CONF_METHOD: Final = "method" diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index 71e238938cb..ddd521af802 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -29,10 +29,13 @@ from homeassistant.const import ( CONF_DEVICE_ID, CONF_ENTITY_ID, CONF_ID, + CONF_MATCH, CONF_STATE, CONF_VALUE_TEMPLATE, CONF_WEEKDAY, CONF_ZONE, + ENTITY_MATCH_ALL, + ENTITY_MATCH_ANY, STATE_UNAVAILABLE, STATE_UNKNOWN, SUN_EVENT_SUNRISE, @@ -524,6 +527,7 @@ def state_from_config(config: ConfigType) -> ConditionCheckerType: req_states: str | list[str] = config.get(CONF_STATE, []) for_period = config.get("for") attribute = config.get(CONF_ATTRIBUTE) + match = config.get(CONF_MATCH, ENTITY_MATCH_ALL) if not isinstance(req_states, list): req_states = [req_states] @@ -532,10 +536,13 @@ def state_from_config(config: ConfigType) -> ConditionCheckerType: def if_state(hass: HomeAssistant, variables: TemplateVarsType = None) -> bool: """Test if condition.""" errors = [] + result: bool = match != ENTITY_MATCH_ANY for index, entity_id in enumerate(entity_ids): try: with trace_path(["entity_id", str(index)]), trace_condition(variables): - if not state(hass, entity_id, req_states, for_period, attribute): + if state(hass, entity_id, req_states, for_period, attribute): + result = True + elif match == ENTITY_MATCH_ALL: return False except ConditionError as ex: errors.append( @@ -548,7 +555,7 @@ def state_from_config(config: ConfigType) -> ConditionCheckerType: if errors: raise ConditionErrorContainer("state", errors=errors) - return True + return result return if_state diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index fb920c4ef1b..c7c7e9aae4a 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -49,6 +49,7 @@ from homeassistant.const import ( CONF_EVENT_DATA_TEMPLATE, CONF_FOR, CONF_ID, + CONF_MATCH, CONF_PLATFORM, CONF_REPEAT, CONF_SCAN_INTERVAL, @@ -68,6 +69,7 @@ from homeassistant.const import ( CONF_WAIT_TEMPLATE, CONF_WHILE, ENTITY_MATCH_ALL, + ENTITY_MATCH_ANY, ENTITY_MATCH_NONE, SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET, @@ -1105,6 +1107,9 @@ STATE_CONDITION_BASE_SCHEMA = { **CONDITION_BASE_SCHEMA, vol.Required(CONF_CONDITION): "state", vol.Required(CONF_ENTITY_ID): entity_ids_or_uuids, + vol.Optional(CONF_MATCH, default=ENTITY_MATCH_ALL): vol.All( + vol.Lower, vol.Any(ENTITY_MATCH_ALL, ENTITY_MATCH_ANY) + ), vol.Optional(CONF_ATTRIBUTE): str, vol.Optional(CONF_FOR): positive_time_period, # To support use_trigger_value in automation diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py index 2b5c35f06ad..6d0ecfbd053 100644 --- a/tests/helpers/test_condition.py +++ b/tests/helpers/test_condition.py @@ -15,6 +15,7 @@ from homeassistant.const import ( SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET, ) +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConditionError, HomeAssistantError from homeassistant.helpers import ( condition, @@ -1020,6 +1021,40 @@ async def test_state_multiple_entities(hass): assert not test(hass) +async def test_state_multiple_entities_match_any(hass: HomeAssistant) -> None: + """Test with multiple entities in condition with match any.""" + config = { + "condition": "and", + "conditions": [ + { + "condition": "state", + "entity_id": ["sensor.temperature_1", "sensor.temperature_2"], + "match": "any", + "state": "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.temperature_1", 100) + hass.states.async_set("sensor.temperature_2", 100) + assert test(hass) + + hass.states.async_set("sensor.temperature_1", 101) + hass.states.async_set("sensor.temperature_2", 100) + assert test(hass) + + hass.states.async_set("sensor.temperature_1", 100) + hass.states.async_set("sensor.temperature_2", 101) + assert test(hass) + + hass.states.async_set("sensor.temperature_1", 101) + hass.states.async_set("sensor.temperature_2", 101) + assert not test(hass) + + async def test_multiple_states(hass): """Test with multiple states in condition.""" config = {