diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 8980ced5bbd..608a13e6ce9 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -147,6 +147,7 @@ _EXPERIMENTAL_CONDITION_PLATFORMS = { "switch", "temperature", "text", + "timer", "vacuum", "valve", "water_heater", diff --git a/homeassistant/components/timer/condition.py b/homeassistant/components/timer/condition.py new file mode 100644 index 00000000000..130114ca5d0 --- /dev/null +++ b/homeassistant/components/timer/condition.py @@ -0,0 +1,17 @@ +"""Provides conditions for timers.""" + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.condition import Condition, make_entity_state_condition + +from . import DOMAIN, STATUS_ACTIVE, STATUS_IDLE, STATUS_PAUSED + +CONDITIONS: dict[str, type[Condition]] = { + "is_active": make_entity_state_condition(DOMAIN, STATUS_ACTIVE), + "is_paused": make_entity_state_condition(DOMAIN, STATUS_PAUSED), + "is_idle": make_entity_state_condition(DOMAIN, STATUS_IDLE), +} + + +async def async_get_conditions(hass: HomeAssistant) -> dict[str, type[Condition]]: + """Return the timer conditions.""" + return CONDITIONS diff --git a/homeassistant/components/timer/conditions.yaml b/homeassistant/components/timer/conditions.yaml new file mode 100644 index 00000000000..a94cf600933 --- /dev/null +++ b/homeassistant/components/timer/conditions.yaml @@ -0,0 +1,18 @@ +.condition_common: &condition_common + target: + entity: + - domain: timer + fields: + behavior: + required: true + default: any + selector: + select: + translation_key: condition_behavior + options: + - all + - any + +is_active: *condition_common +is_paused: *condition_common +is_idle: *condition_common diff --git a/homeassistant/components/timer/icons.json b/homeassistant/components/timer/icons.json index d2a7160750b..fcc398870aa 100644 --- a/homeassistant/components/timer/icons.json +++ b/homeassistant/components/timer/icons.json @@ -1,4 +1,15 @@ { + "conditions": { + "is_active": { + "condition": "mdi:timer" + }, + "is_idle": { + "condition": "mdi:timer-off" + }, + "is_paused": { + "condition": "mdi:timer-pause" + } + }, "services": { "cancel": { "service": "mdi:cancel" diff --git a/homeassistant/components/timer/strings.json b/homeassistant/components/timer/strings.json index de8518212d5..b1373b4764e 100644 --- a/homeassistant/components/timer/strings.json +++ b/homeassistant/components/timer/strings.json @@ -1,4 +1,40 @@ { + "common": { + "condition_behavior_description": "How the state should match on the targeted timers.", + "condition_behavior_name": "Behavior" + }, + "conditions": { + "is_active": { + "description": "Tests if one or more timers are active.", + "fields": { + "behavior": { + "description": "[%key:component::timer::common::condition_behavior_description%]", + "name": "[%key:component::timer::common::condition_behavior_name%]" + } + }, + "name": "Timer is active" + }, + "is_idle": { + "description": "Tests if one or more timers are idle.", + "fields": { + "behavior": { + "description": "[%key:component::timer::common::condition_behavior_description%]", + "name": "[%key:component::timer::common::condition_behavior_name%]" + } + }, + "name": "Timer is idle" + }, + "is_paused": { + "description": "Tests if one or more timers are paused.", + "fields": { + "behavior": { + "description": "[%key:component::timer::common::condition_behavior_description%]", + "name": "[%key:component::timer::common::condition_behavior_name%]" + } + }, + "name": "Timer is paused" + } + }, "entity_component": { "_": { "name": "Timer", @@ -30,6 +66,14 @@ } } }, + "selector": { + "condition_behavior": { + "options": { + "all": "All", + "any": "Any" + } + } + }, "services": { "cancel": { "description": "Resets a timer's duration to the last known initial value without firing the timer finished event.", diff --git a/tests/components/timer/test_condition.py b/tests/components/timer/test_condition.py new file mode 100644 index 00000000000..3a60edca4c0 --- /dev/null +++ b/tests/components/timer/test_condition.py @@ -0,0 +1,136 @@ +"""Test timer conditions.""" + +from typing import Any + +import pytest + +from homeassistant.components.timer import STATUS_ACTIVE, STATUS_IDLE, STATUS_PAUSED +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_timers(hass: HomeAssistant) -> dict[str, list[str]]: + """Create multiple timer entities associated with different targets.""" + return await target_entities(hass, "timer") + + +@pytest.mark.parametrize( + "condition", + [ + "timer.is_active", + "timer.is_paused", + "timer.is_idle", + ], +) +async def test_timer_conditions_gated_by_labs_flag( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, condition: str +) -> None: + """Test the timer 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("timer"), +) +@pytest.mark.parametrize( + ("condition", "condition_options", "states"), + [ + *parametrize_condition_states_any( + condition="timer.is_active", + target_states=[STATUS_ACTIVE], + other_states=[STATUS_IDLE, STATUS_PAUSED], + ), + *parametrize_condition_states_any( + condition="timer.is_paused", + target_states=[STATUS_PAUSED], + other_states=[STATUS_IDLE, STATUS_ACTIVE], + ), + *parametrize_condition_states_any( + condition="timer.is_idle", + target_states=[STATUS_IDLE], + other_states=[STATUS_ACTIVE, STATUS_PAUSED], + ), + ], +) +async def test_timer_condition_behavior_any( + hass: HomeAssistant, + target_timers: 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 timer condition with 'any' behavior.""" + await assert_condition_behavior_any( + hass, + target_entities=target_timers, + 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("timer"), +) +@pytest.mark.parametrize( + ("condition", "condition_options", "states"), + [ + *parametrize_condition_states_all( + condition="timer.is_active", + target_states=[STATUS_ACTIVE], + other_states=[STATUS_IDLE, STATUS_PAUSED], + ), + *parametrize_condition_states_all( + condition="timer.is_paused", + target_states=[STATUS_PAUSED], + other_states=[STATUS_IDLE, STATUS_ACTIVE], + ), + *parametrize_condition_states_all( + condition="timer.is_idle", + target_states=[STATUS_IDLE], + other_states=[STATUS_ACTIVE, STATUS_PAUSED], + ), + ], +) +async def test_timer_condition_behavior_all( + hass: HomeAssistant, + target_timers: 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 timer condition with 'all' behavior.""" + await assert_condition_behavior_all( + hass, + target_entities=target_timers, + condition_target_config=condition_target_config, + entity_id=entity_id, + entities_in_target=entities_in_target, + condition=condition, + condition_options=condition_options, + states=states, + )