diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 8887674dcdb..e6001569bcc 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -143,6 +143,7 @@ _EXPERIMENTAL_CONDITION_PLATFORMS = { "occupancy", "person", "power", + "remote", "schedule", "select", "siren", diff --git a/homeassistant/components/remote/condition.py b/homeassistant/components/remote/condition.py new file mode 100644 index 00000000000..51788c95fa8 --- /dev/null +++ b/homeassistant/components/remote/condition.py @@ -0,0 +1,17 @@ +"""Provides conditions for remotes.""" + +from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.core import HomeAssistant +from homeassistant.helpers.condition import Condition, make_entity_state_condition + +from . import DOMAIN + +CONDITIONS: dict[str, type[Condition]] = { + "is_off": make_entity_state_condition(DOMAIN, STATE_OFF), + "is_on": make_entity_state_condition(DOMAIN, STATE_ON), +} + + +async def async_get_conditions(hass: HomeAssistant) -> dict[str, type[Condition]]: + """Return the remote conditions.""" + return CONDITIONS diff --git a/homeassistant/components/remote/conditions.yaml b/homeassistant/components/remote/conditions.yaml new file mode 100644 index 00000000000..bf61800032e --- /dev/null +++ b/homeassistant/components/remote/conditions.yaml @@ -0,0 +1,17 @@ +.condition_common: &condition_common + target: + entity: + domain: remote + fields: + behavior: + required: true + default: any + selector: + select: + translation_key: condition_behavior + options: + - all + - any + +is_off: *condition_common +is_on: *condition_common diff --git a/homeassistant/components/remote/icons.json b/homeassistant/components/remote/icons.json index 1560336d7c1..1436e21e2b6 100644 --- a/homeassistant/components/remote/icons.json +++ b/homeassistant/components/remote/icons.json @@ -1,4 +1,12 @@ { + "conditions": { + "is_off": { + "condition": "mdi:remote-off" + }, + "is_on": { + "condition": "mdi:remote" + } + }, "entity_component": { "_": { "default": "mdi:remote", diff --git a/homeassistant/components/remote/strings.json b/homeassistant/components/remote/strings.json index fd9c3a779d0..ecd01e20611 100644 --- a/homeassistant/components/remote/strings.json +++ b/homeassistant/components/remote/strings.json @@ -1,7 +1,28 @@ { "common": { + "condition_behavior_name": "Condition passes if", "trigger_behavior_name": "Trigger when" }, + "conditions": { + "is_off": { + "description": "Tests if one or more remotes are off.", + "fields": { + "behavior": { + "name": "[%key:component::remote::common::condition_behavior_name%]" + } + }, + "name": "Remote is off" + }, + "is_on": { + "description": "Tests if one or more remotes are on.", + "fields": { + "behavior": { + "name": "[%key:component::remote::common::condition_behavior_name%]" + } + }, + "name": "Remote is on" + } + }, "device_automation": { "action_type": { "toggle": "[%key:common::device_automation::action_type::toggle%]", @@ -31,6 +52,12 @@ } }, "selector": { + "condition_behavior": { + "options": { + "all": "All", + "any": "Any" + } + }, "trigger_behavior": { "options": { "any": "Any", diff --git a/tests/components/common.py b/tests/components/common.py index b7fe50e1fd5..ddb8c93257f 100644 --- a/tests/components/common.py +++ b/tests/components/common.py @@ -46,13 +46,20 @@ from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry, mock_device_registry -async def target_entities(hass: HomeAssistant, domain: str) -> dict[str, list[str]]: +async def target_entities( + hass: HomeAssistant, domain: str, *, domain_excluded: str | None = None +) -> dict[str, list[str]]: """Create multiple entities associated with different targets. + If `domain_excluded` is provided, entities in excluded_entities will have this + domain, otherwise they will have the same domain as included_entities. + Returns a dict with the following keys: - included_entities: List of entity_ids meant to be targeted. - excluded_entities: List of entity_ids not meant to be targeted. """ + domain_excluded = domain_excluded or domain + config_entry = MockConfigEntry(domain="test") config_entry.add_to_hass(hass) @@ -84,10 +91,10 @@ async def target_entities(hass: HomeAssistant, domain: str) -> dict[str, list[st ) entity_reg.async_update_entity(entity_area.entity_id, area_id=area.id) entity_area_excluded = entity_reg.async_get_or_create( - domain=domain, + domain=domain_excluded, platform="test", - unique_id=f"{domain}_area_excluded", - suggested_object_id=f"area_{domain}_excluded", + unique_id=f"{domain_excluded}_area_excluded", + suggested_object_id=f"area_{domain_excluded}_excluded", ) entity_reg.async_update_entity(entity_area_excluded.entity_id, area_id=area.id) @@ -107,10 +114,10 @@ async def target_entities(hass: HomeAssistant, domain: str) -> dict[str, list[st device_id=device.id, ) entity_reg.async_get_or_create( - domain=domain, + domain=domain_excluded, platform="test", - unique_id=f"{domain}_device_excluded", - suggested_object_id=f"device_{domain}_excluded", + unique_id=f"{domain_excluded}_device_excluded", + suggested_object_id=f"device_{domain_excluded}_excluded", device_id=device.id, ) @@ -123,10 +130,10 @@ async def target_entities(hass: HomeAssistant, domain: str) -> dict[str, list[st ) entity_reg.async_update_entity(entity_label.entity_id, labels={label.label_id}) entity_label_excluded = entity_reg.async_get_or_create( - domain=domain, + domain=domain_excluded, platform="test", - unique_id=f"{domain}_label_excluded", - suggested_object_id=f"label_{domain}_excluded", + unique_id=f"{domain_excluded}_label_excluded", + suggested_object_id=f"label_{domain_excluded}_excluded", ) entity_reg.async_update_entity( entity_label_excluded.entity_id, labels={label.label_id} @@ -143,10 +150,10 @@ async def target_entities(hass: HomeAssistant, domain: str) -> dict[str, list[st f"{domain}.device2_{domain}", ], "excluded_entities": [ - f"{domain}.standalone_{domain}_excluded", - f"{domain}.label_{domain}_excluded", - f"{domain}.area_{domain}_excluded", - f"{domain}.device_{domain}_excluded", + f"{domain_excluded}.standalone_{domain_excluded}_excluded", + f"{domain_excluded}.label_{domain_excluded}_excluded", + f"{domain_excluded}.area_{domain_excluded}_excluded", + f"{domain_excluded}.device_{domain_excluded}_excluded", ], } @@ -215,6 +222,7 @@ def _parametrize_condition_states( other_states: list[str | None | tuple[str | None, dict]], required_filter_attributes: dict | None, condition_true_if_invalid: bool, + excluded_entities_from_other_domain: bool, ) -> list[tuple[str, dict[str, Any], list[ConditionStateDescription]]]: """Parametrize states and expected condition evaluations. @@ -227,7 +235,9 @@ def _parametrize_condition_states( required_filter_attributes = required_filter_attributes or {} condition_options = condition_options or {} - has_required_filter_attributes = bool(required_filter_attributes) + add_excluded_state = excluded_entities_from_other_domain or bool( + required_filter_attributes + ) def state_with_attributes( state: str | None | tuple[str | None, dict], @@ -242,7 +252,7 @@ def _parametrize_condition_states( "attributes": required_filter_attributes, }, "excluded_state": { - "state": state if has_required_filter_attributes else None, + "state": state if add_excluded_state else None, "attributes": {}, }, "condition_true": condition_true, @@ -254,8 +264,8 @@ def _parametrize_condition_states( "attributes": state[1] | required_filter_attributes, }, "excluded_state": { - "state": state[0] if has_required_filter_attributes else None, - "attributes": state[1], + "state": state[0] if add_excluded_state else None, + "attributes": state[1] if add_excluded_state else {}, }, "condition_true": condition_true, "condition_true_first_entity": condition_true_first_entity, @@ -307,6 +317,7 @@ def parametrize_condition_states_any( target_states: list[str | None | tuple[str | None, dict]], other_states: list[str | None | tuple[str | None, dict]], required_filter_attributes: dict | None = None, + excluded_entities_from_other_domain: bool = False, ) -> list[tuple[str, dict[str, Any], list[ConditionStateDescription]]]: """Parametrize states and expected condition evaluations. @@ -324,6 +335,7 @@ def parametrize_condition_states_any( other_states=other_states, required_filter_attributes=required_filter_attributes, condition_true_if_invalid=False, + excluded_entities_from_other_domain=excluded_entities_from_other_domain, ) @@ -334,6 +346,7 @@ def parametrize_condition_states_all( target_states: list[str | None | tuple[str | None, dict]], other_states: list[str | None | tuple[str | None, dict]], required_filter_attributes: dict | None = None, + excluded_entities_from_other_domain: bool = False, ) -> list[tuple[str, dict[str, Any], list[ConditionStateDescription]]]: """Parametrize states and expected condition evaluations. @@ -351,6 +364,7 @@ def parametrize_condition_states_all( other_states=other_states, required_filter_attributes=required_filter_attributes, condition_true_if_invalid=True, + excluded_entities_from_other_domain=excluded_entities_from_other_domain, ) diff --git a/tests/components/remote/test_condition.py b/tests/components/remote/test_condition.py new file mode 100644 index 00000000000..b3052de5bd7 --- /dev/null +++ b/tests/components/remote/test_condition.py @@ -0,0 +1,129 @@ +"""Test remote conditions.""" + +from typing import Any + +import pytest + +from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.core import HomeAssistant + +from tests.components.common import ( + ConditionStateDescription, + assert_condition_behavior_all, + assert_condition_behavior_any, + assert_condition_gated_by_labs_flag, + parametrize_condition_states_all, + parametrize_condition_states_any, + parametrize_target_entities, + target_entities, +) + + +@pytest.fixture +async def target_remotes(hass: HomeAssistant) -> dict[str, list[str]]: + """Create multiple remote entities associated with different targets.""" + return await target_entities(hass, "remote", domain_excluded="switch") + + +@pytest.mark.parametrize( + "condition", + [ + "remote.is_off", + "remote.is_on", + ], +) +async def test_remote_conditions_gated_by_labs_flag( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, condition: str +) -> None: + """Test the remote conditions are gated by the labs flag.""" + await assert_condition_gated_by_labs_flag(hass, caplog, condition) + + +@pytest.mark.usefixtures("enable_labs_preview_features") +@pytest.mark.parametrize( + ("condition_target_config", "entity_id", "entities_in_target"), + parametrize_target_entities("remote"), +) +@pytest.mark.parametrize( + ("condition", "condition_options", "states"), + [ + *parametrize_condition_states_any( + condition="remote.is_on", + target_states=[STATE_ON], + other_states=[STATE_OFF], + excluded_entities_from_other_domain=True, + ), + *parametrize_condition_states_any( + condition="remote.is_off", + target_states=[STATE_OFF], + other_states=[STATE_ON], + excluded_entities_from_other_domain=True, + ), + ], +) +async def test_remote_state_condition_behavior_any( + hass: HomeAssistant, + target_remotes: dict[str, list[str]], + condition_target_config: dict, + entity_id: str, + entities_in_target: int, + condition: str, + condition_options: dict[str, Any], + states: list[ConditionStateDescription], +) -> None: + """Test the remote state condition with the 'any' behavior.""" + await assert_condition_behavior_any( + hass, + target_entities=target_remotes, + condition_target_config=condition_target_config, + entity_id=entity_id, + entities_in_target=entities_in_target, + condition=condition, + condition_options=condition_options, + states=states, + ) + + +@pytest.mark.usefixtures("enable_labs_preview_features") +@pytest.mark.parametrize( + ("condition_target_config", "entity_id", "entities_in_target"), + parametrize_target_entities("remote"), +) +@pytest.mark.parametrize( + ("condition", "condition_options", "states"), + [ + *parametrize_condition_states_all( + condition="remote.is_on", + target_states=[STATE_ON], + other_states=[STATE_OFF], + excluded_entities_from_other_domain=True, + ), + *parametrize_condition_states_all( + condition="remote.is_off", + target_states=[STATE_OFF], + other_states=[STATE_ON], + excluded_entities_from_other_domain=True, + ), + ], +) +async def test_remote_state_condition_behavior_all( + hass: HomeAssistant, + target_remotes: dict[str, list[str]], + condition_target_config: dict, + entity_id: str, + entities_in_target: int, + condition: str, + condition_options: dict[str, Any], + states: list[ConditionStateDescription], +) -> None: + """Test the remote state condition with the 'all' behavior.""" + await assert_condition_behavior_all( + hass, + target_entities=target_remotes, + condition_target_config=condition_target_config, + entity_id=entity_id, + entities_in_target=entities_in_target, + condition=condition, + condition_options=condition_options, + states=states, + )