From b2bd1a4aa0058d7766d647d0bc95e21217ebca75 Mon Sep 17 00:00:00 2001 From: abmantis Date: Thu, 7 Aug 2025 19:11:50 +0100 Subject: [PATCH] Fix target key; add other platform entities on tests --- homeassistant/components/light/condition.py | 27 +++++--- tests/components/light/test_condition.py | 75 ++++++++++++++------- 2 files changed, 70 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/light/condition.py b/homeassistant/components/light/condition.py index 9a0b993a306..5d6c49ba5a8 100644 --- a/homeassistant/components/light/condition.py +++ b/homeassistant/components/light/condition.py @@ -1,10 +1,16 @@ """Provides conditions for lights.""" -from typing import Final +from typing import Final, override import voluptuous as vol -from homeassistant.const import CONF_CONDITION, CONF_STATE, STATE_OFF, STATE_ON +from homeassistant.const import ( + CONF_CONDITION, + CONF_STATE, + CONF_TARGET, + STATE_OFF, + STATE_ON, +) from homeassistant.core import HomeAssistant, split_entity_id from homeassistant.helpers import config_validation as cv, target from homeassistant.helpers.condition import ( @@ -21,18 +27,17 @@ BEHAVIOR_ONE: Final = "one" BEHAVIOR_ANY: Final = "any" BEHAVIOR_ALL: Final = "all" -STATE_CONDITION_TYPE = f"{DOMAIN}.state" +STATE_CONDITION_TYPE = "state" STATE_CONDITION_SCHEMA = vol.All( { **cv.CONDITION_BASE_SCHEMA, - vol.Required(CONF_CONDITION): STATE_CONDITION_TYPE, + vol.Required(CONF_CONDITION): f"{DOMAIN}.{STATE_CONDITION_TYPE}", vol.Required(CONF_STATE): vol.In([STATE_ON, STATE_OFF]), vol.Required(ATTR_BEHAVIOR, default=BEHAVIOR_ANY): vol.In( [BEHAVIOR_ONE, BEHAVIOR_ANY, BEHAVIOR_ALL] ), - **cv.ENTITY_SERVICE_FIELDS, + vol.Required(CONF_TARGET): cv.ENTITY_SERVICE_FIELDS, }, - cv.has_at_least_one_key(*cv.ENTITY_SERVICE_FIELDS), ) @@ -44,14 +49,16 @@ class StateCondition(Condition): self._hass = hass self._config = config + @override @classmethod - async def async_validate_condition_config( + async def async_validate_config( cls, hass: HomeAssistant, config: ConfigType ) -> ConfigType: """Validate config.""" return STATE_CONDITION_SCHEMA(config) # type: ignore[no-any-return] - async def async_condition_from_config(self) -> ConditionCheckerType: + @override + async def async_get_checker(self) -> ConditionCheckerType: """Wrap action method with zone based condition.""" state = self._config[CONF_STATE] behavior = self._config.get(ATTR_BEHAVIOR) @@ -95,10 +102,12 @@ class StateCondition(Condition): elif behavior == BEHAVIOR_ONE: matcher = check_one_match_state + target_config = self._config.get(CONF_TARGET, {}) + @trace_condition_function def test_state(hass: HomeAssistant, variables: TemplateVarsType = None) -> bool: """Test state condition.""" - selector_data = target.TargetSelectorData(self._config) + selector_data = target.TargetSelectorData(target_config) targeted_entities = target.async_extract_referenced_entity_ids( hass, selector_data, expand_group=False ) diff --git a/tests/components/light/test_condition.py b/tests/components/light/test_condition.py index 77d079bcc8d..8408b1a4307 100644 --- a/tests/components/light/test_condition.py +++ b/tests/components/light/test_condition.py @@ -7,6 +7,7 @@ from homeassistant.const import ( ATTR_LABEL_ID, CONF_CONDITION, CONF_STATE, + CONF_TARGET, STATE_OFF, STATE_ON, STATE_UNAVAILABLE, @@ -24,8 +25,8 @@ def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None: @pytest.fixture -async def label_lights(hass: HomeAssistant) -> None: - """Create multiple light entities associated with labels.""" +async def label_entities(hass: HomeAssistant) -> None: + """Create multiple entities associated with labels.""" await async_setup_component(hass, "light", {}) config_entry = MockConfigEntry(domain="test_labels") @@ -45,6 +46,16 @@ async def label_lights(hass: HomeAssistant) -> None: ) entity_reg.async_update_entity(light_entity.entity_id, labels={label.label_id}) + # Also create switches to test that they don't impact the conditions + for i in range(2): + switch_entity = entity_reg.async_get_or_create( + domain="switch", + platform="test", + unique_id=f"label_switch_{i}", + suggested_object_id=f"label_switch_{i}", + ) + entity_reg.async_update_entity(switch_entity.entity_id, labels={label.label_id}) + return [ "light.label_light_0", "light.label_light_1", @@ -67,14 +78,18 @@ async def has_calls_after_trigger( async def test_light_state_condition_behavior_one( hass: HomeAssistant, service_calls: list[ServiceCall], - label_lights: list[str], + label_entities: list[str], condition_state: str, ) -> None: """Test the light state condition with the 'one' behavior.""" await async_setup_component(hass, "light", {}) + # Set state for two switches to ensure that they don't impact the condition + hass.states.async_set("switch.label_switch_1", STATE_OFF) + hass.states.async_set("switch.label_switch_2", STATE_ON) + reverse_state = STATE_OFF if condition_state == STATE_ON else STATE_ON - for entity_id in label_lights: + for entity_id in label_entities: hass.states.async_set(entity_id, reverse_state) await async_setup_component( @@ -85,7 +100,9 @@ async def test_light_state_condition_behavior_one( "trigger": {"platform": "event", "event_type": "test_event"}, "condition": { CONF_CONDITION: "light.state", - ATTR_LABEL_ID: "test_label", + CONF_TARGET: { + ATTR_LABEL_ID: "test_label", + }, "behavior": "one", CONF_STATE: condition_state, }, @@ -100,20 +117,20 @@ async def test_light_state_condition_behavior_one( assert not await has_calls_after_trigger(hass, service_calls) # Set one light to the condition state -> condition pass - hass.states.async_set(label_lights[0], condition_state) + hass.states.async_set(label_entities[0], condition_state) assert await has_calls_after_trigger(hass, service_calls) # Set second light to the condition state -> condition fail - hass.states.async_set(label_lights[1], condition_state) + hass.states.async_set(label_entities[1], condition_state) assert not await has_calls_after_trigger(hass, service_calls) # Set first light to unavailable -> condition pass again since only the # second light is on the condition state - hass.states.async_set(label_lights[0], STATE_UNAVAILABLE) + hass.states.async_set(label_entities[0], STATE_UNAVAILABLE) assert await has_calls_after_trigger(hass, service_calls) # Set all lights to unavailable -> condition fail - for entity_id in label_lights: + for entity_id in label_entities: hass.states.async_set(entity_id, STATE_UNAVAILABLE) assert not await has_calls_after_trigger(hass, service_calls) @@ -122,14 +139,14 @@ async def test_light_state_condition_behavior_one( async def test_light_state_condition_behavior_any( hass: HomeAssistant, service_calls: list[ServiceCall], - label_lights: list[str], + label_entities: list[str], condition_state: str, ) -> None: """Test the light state condition with the 'any' behavior.""" await async_setup_component(hass, "light", {}) reverse_state = STATE_OFF if condition_state == STATE_ON else STATE_ON - for entity_id in label_lights: + for entity_id in label_entities: hass.states.async_set(entity_id, reverse_state) await async_setup_component( @@ -140,7 +157,9 @@ async def test_light_state_condition_behavior_any( "trigger": {"platform": "event", "event_type": "test_event"}, "condition": { CONF_CONDITION: "light.state", - ATTR_LABEL_ID: "test_label", + CONF_TARGET: { + ATTR_LABEL_ID: "test_label", + }, "behavior": "any", CONF_STATE: condition_state, }, @@ -151,24 +170,28 @@ async def test_light_state_condition_behavior_any( }, ) + # Set state for two switches to ensure that they don't impact the condition + hass.states.async_set("switch.label_switch_1", STATE_OFF) + hass.states.async_set("switch.label_switch_2", STATE_ON) + # No lights on the condition state assert not await has_calls_after_trigger(hass, service_calls) # Set one light to the condition state -> condition pass - hass.states.async_set(label_lights[0], condition_state) + hass.states.async_set(label_entities[0], condition_state) assert await has_calls_after_trigger(hass, service_calls) # Set all lights to the condition state -> condition pass - for entity_id in label_lights: + for entity_id in label_entities: hass.states.async_set(entity_id, condition_state) assert await has_calls_after_trigger(hass, service_calls) # Set one light to unavailable -> condition pass - hass.states.async_set(label_lights[0], STATE_UNAVAILABLE) + hass.states.async_set(label_entities[0], STATE_UNAVAILABLE) assert await has_calls_after_trigger(hass, service_calls) # Set all lights to unavailable -> condition fail - for entity_id in label_lights: + for entity_id in label_entities: hass.states.async_set(entity_id, STATE_UNAVAILABLE) assert not await has_calls_after_trigger(hass, service_calls) @@ -177,14 +200,18 @@ async def test_light_state_condition_behavior_any( async def test_light_state_condition_behavior_all( hass: HomeAssistant, service_calls: list[ServiceCall], - label_lights: list[str], + label_entities: list[str], condition_state: str, ) -> None: """Test the light state condition with the 'all' behavior.""" await async_setup_component(hass, "light", {}) + # Set state for two switches to ensure that they don't impact the condition + hass.states.async_set("switch.label_switch_1", STATE_OFF) + hass.states.async_set("switch.label_switch_2", STATE_ON) + reverse_state = STATE_OFF if condition_state == STATE_ON else STATE_ON - for entity_id in label_lights: + for entity_id in label_entities: hass.states.async_set(entity_id, reverse_state) await async_setup_component( @@ -195,7 +222,9 @@ async def test_light_state_condition_behavior_all( "trigger": {"platform": "event", "event_type": "test_event"}, "condition": { CONF_CONDITION: "light.state", - ATTR_LABEL_ID: "test_label", + CONF_TARGET: { + ATTR_LABEL_ID: "test_label", + }, "behavior": "all", CONF_STATE: condition_state, }, @@ -210,18 +239,18 @@ async def test_light_state_condition_behavior_all( assert not await has_calls_after_trigger(hass, service_calls) # Set one light to the condition state -> condition fail - hass.states.async_set(label_lights[0], condition_state) + hass.states.async_set(label_entities[0], condition_state) assert not await has_calls_after_trigger(hass, service_calls) # Set all lights to the condition state -> condition pass - for entity_id in label_lights: + for entity_id in label_entities: hass.states.async_set(entity_id, condition_state) assert await has_calls_after_trigger(hass, service_calls) # Set one light to unavailable -> condition should still pass - hass.states.async_set(label_lights[0], STATE_UNAVAILABLE) + hass.states.async_set(label_entities[0], STATE_UNAVAILABLE) # Set all lights to unavailable -> condition fail - for entity_id in label_lights: + for entity_id in label_entities: hass.states.async_set(entity_id, STATE_UNAVAILABLE) assert not await has_calls_after_trigger(hass, service_calls)