From e353ed1e2e9fe9baf55a38cc3e8f8d7c23693243 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=ADlio=20Costa?= Date: Mon, 30 Mar 2026 15:41:08 +0100 Subject: [PATCH] Add counter purpose-specific condition (#166879) --- .../components/automation/__init__.py | 1 + homeassistant/components/counter/condition.py | 15 ++ .../components/counter/conditions.yaml | 25 +++ homeassistant/components/counter/icons.json | 5 + homeassistant/components/counter/strings.json | 22 +++ tests/components/counter/test_condition.py | 177 ++++++++++++++++++ 6 files changed, 245 insertions(+) create mode 100644 homeassistant/components/counter/condition.py create mode 100644 homeassistant/components/counter/conditions.yaml create mode 100644 tests/components/counter/test_condition.py diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 7fa8c5afb6d..8887674dcdb 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -124,6 +124,7 @@ _EXPERIMENTAL_CONDITION_PLATFORMS = { "battery", "calendar", "climate", + "counter", "cover", "device_tracker", "door", diff --git a/homeassistant/components/counter/condition.py b/homeassistant/components/counter/condition.py new file mode 100644 index 00000000000..ce5aa6b3916 --- /dev/null +++ b/homeassistant/components/counter/condition.py @@ -0,0 +1,15 @@ +"""Provides conditions for counters.""" + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.condition import Condition, make_entity_numerical_condition + +DOMAIN = "counter" + +CONDITIONS: dict[str, type[Condition]] = { + "is_value": make_entity_numerical_condition(DOMAIN), +} + + +async def async_get_conditions(hass: HomeAssistant) -> dict[str, type[Condition]]: + """Return the conditions for counters.""" + return CONDITIONS diff --git a/homeassistant/components/counter/conditions.yaml b/homeassistant/components/counter/conditions.yaml new file mode 100644 index 00000000000..6a00235d287 --- /dev/null +++ b/homeassistant/components/counter/conditions.yaml @@ -0,0 +1,25 @@ +is_value: + target: + entity: + - domain: counter + fields: + behavior: + required: true + default: any + selector: + select: + translation_key: condition_behavior + options: + - all + - any + threshold: + required: true + selector: + numeric_threshold: + entity: + - domain: counter + - domain: input_number + - domain: number + mode: is + number: + mode: box diff --git a/homeassistant/components/counter/icons.json b/homeassistant/components/counter/icons.json index fef5b876c73..c2dd1d0afc3 100644 --- a/homeassistant/components/counter/icons.json +++ b/homeassistant/components/counter/icons.json @@ -1,4 +1,9 @@ { + "conditions": { + "is_value": { + "condition": "mdi:counter" + } + }, "services": { "decrement": { "service": "mdi:numeric-negative-1" diff --git a/homeassistant/components/counter/strings.json b/homeassistant/components/counter/strings.json index e09fd1ba9fd..5bede3a676b 100644 --- a/homeassistant/components/counter/strings.json +++ b/homeassistant/components/counter/strings.json @@ -3,6 +3,22 @@ "trigger_behavior_description": "The behavior of the targeted counters to trigger on.", "trigger_behavior_name": "Behavior" }, + "conditions": { + "is_value": { + "description": "Tests the value of one or more counters.", + "fields": { + "behavior": { + "description": "How the state should match on the targeted counters.", + "name": "Behavior" + }, + "threshold": { + "description": "What to test for and threshold values.", + "name": "Threshold" + } + }, + "name": "Counter value" + } + }, "entity_component": { "_": { "name": "[%key:component::counter::title%]", @@ -30,6 +46,12 @@ } }, "selector": { + "condition_behavior": { + "options": { + "all": "All", + "any": "Any" + } + }, "trigger_behavior": { "options": { "any": "Any", diff --git a/tests/components/counter/test_condition.py b/tests/components/counter/test_condition.py new file mode 100644 index 00000000000..c25695edbfb --- /dev/null +++ b/tests/components/counter/test_condition.py @@ -0,0 +1,177 @@ +"""Test counter conditions.""" + +from typing import Any + +import pytest + +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_counters(hass: HomeAssistant) -> dict[str, list[str]]: + """Create multiple counter entities associated with different targets.""" + return await target_entities(hass, "counter") + + +async def test_counter_condition_gated_by_labs_flag( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test the counter condition is gated by the labs flag.""" + await assert_condition_gated_by_labs_flag(hass, caplog, "counter.is_value") + + +@pytest.mark.usefixtures("enable_labs_preview_features") +@pytest.mark.parametrize( + ("condition_target_config", "entity_id", "entities_in_target"), + parametrize_target_entities("counter"), +) +@pytest.mark.parametrize( + ("condition", "condition_options", "states"), + [ + *parametrize_condition_states_any( + condition="counter.is_value", + condition_options={ + "threshold": {"type": "above", "value": {"number": 20}}, + }, + target_states=["21", "50", "100"], + other_states=["0", "10", "20"], + ), + *parametrize_condition_states_any( + condition="counter.is_value", + condition_options={ + "threshold": {"type": "below", "value": {"number": 20}}, + }, + target_states=["0", "10", "19"], + other_states=["20", "50", "100"], + ), + *parametrize_condition_states_any( + condition="counter.is_value", + condition_options={ + "threshold": { + "type": "between", + "value_min": {"number": 10}, + "value_max": {"number": 30}, + }, + }, + target_states=["11", "20", "29"], + other_states=["0", "10", "30", "100"], + ), + *parametrize_condition_states_any( + condition="counter.is_value", + condition_options={ + "threshold": { + "type": "outside", + "value_min": {"number": 10}, + "value_max": {"number": 30}, + }, + }, + target_states=["0", "10", "30", "100"], + other_states=["11", "20", "29"], + ), + ], +) +async def test_counter_is_value_condition_behavior_any( + hass: HomeAssistant, + target_counters: dict[str, list[str]], + condition_target_config: dict[str, Any], + entity_id: str, + entities_in_target: int, + condition: str, + condition_options: dict[str, Any], + states: list[ConditionStateDescription], +) -> None: + """Test the counter is_value condition with 'any' behavior.""" + await assert_condition_behavior_any( + hass, + target_entities=target_counters, + 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("counter"), +) +@pytest.mark.parametrize( + ("condition", "condition_options", "states"), + [ + *parametrize_condition_states_all( + condition="counter.is_value", + condition_options={ + "threshold": {"type": "above", "value": {"number": 20}}, + }, + target_states=["21", "50", "100"], + other_states=["0", "10", "20"], + ), + *parametrize_condition_states_all( + condition="counter.is_value", + condition_options={ + "threshold": {"type": "below", "value": {"number": 20}}, + }, + target_states=["0", "10", "19"], + other_states=["20", "50", "100"], + ), + *parametrize_condition_states_all( + condition="counter.is_value", + condition_options={ + "threshold": { + "type": "between", + "value_min": {"number": 10}, + "value_max": {"number": 30}, + }, + }, + target_states=["11", "20", "29"], + other_states=["0", "10", "30", "100"], + ), + *parametrize_condition_states_all( + condition="counter.is_value", + condition_options={ + "threshold": { + "type": "outside", + "value_min": {"number": 10}, + "value_max": {"number": 30}, + }, + }, + target_states=["0", "10", "30", "100"], + other_states=["11", "20", "29"], + ), + ], +) +async def test_counter_is_value_condition_behavior_all( + hass: HomeAssistant, + target_counters: dict[str, list[str]], + condition_target_config: dict[str, Any], + entity_id: str, + entities_in_target: int, + condition: str, + condition_options: dict[str, Any], + states: list[ConditionStateDescription], +) -> None: + """Test the counter is_value condition with 'all' behavior.""" + await assert_condition_behavior_all( + hass, + target_entities=target_counters, + condition_target_config=condition_target_config, + entity_id=entity_id, + entities_in_target=entities_in_target, + condition=condition, + condition_options=condition_options, + states=states, + )