From fb65cf48c977d651e62c05d3c7bdcf1e2b53ce10 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 26 Mar 2026 17:14:11 +0100 Subject: [PATCH] Add condition humidifier.is_mode (#166610) --- .../components/humidifier/condition.py | 63 ++++++++++++- .../components/humidifier/conditions.yaml | 13 +++ .../components/humidifier/icons.json | 3 + .../components/humidifier/strings.json | 14 +++ tests/components/humidifier/test_condition.py | 92 ++++++++++++++++++- 5 files changed, 182 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/humidifier/condition.py b/homeassistant/components/humidifier/condition.py index 0795291ae97..2a96eaffe37 100644 --- a/homeassistant/components/humidifier/condition.py +++ b/homeassistant/components/humidifier/condition.py @@ -1,15 +1,73 @@ """Provides conditions for humidifiers.""" -from homeassistant.const import PERCENTAGE, STATE_OFF, STATE_ON +from typing import TYPE_CHECKING + +import voluptuous as vol + +from homeassistant.const import ATTR_MODE, CONF_OPTIONS, PERCENTAGE, STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.automation import DomainSpec from homeassistant.helpers.condition import ( + ENTITY_STATE_CONDITION_SCHEMA_ANY_ALL, Condition, + ConditionConfig, + EntityStateConditionBase, make_entity_numerical_condition, make_entity_state_condition, ) +from homeassistant.helpers.entity import get_supported_features + +from .const import ( + ATTR_ACTION, + ATTR_HUMIDITY, + DOMAIN, + HumidifierAction, + HumidifierEntityFeature, +) + +CONF_MODE = "mode" + +IS_MODE_CONDITION_SCHEMA = ENTITY_STATE_CONDITION_SCHEMA_ANY_ALL.extend( + { + vol.Required(CONF_OPTIONS): { + vol.Required(CONF_MODE): vol.All(cv.ensure_list, vol.Length(min=1), [str]), + }, + } +) + + +def _supports_feature(hass: HomeAssistant, entity_id: str, features: int) -> bool: + """Test if an entity supports the specified features.""" + try: + return bool(get_supported_features(hass, entity_id) & features) + except HomeAssistantError: + return False + + +class IsModeCondition(EntityStateConditionBase): + """Condition for humidifier mode.""" + + _domain_specs = {DOMAIN: DomainSpec(value_source=ATTR_MODE)} + _schema = IS_MODE_CONDITION_SCHEMA + + def __init__(self, hass: HomeAssistant, config: ConditionConfig) -> None: + """Initialize the mode condition.""" + super().__init__(hass, config) + if TYPE_CHECKING: + assert config.options is not None + self._states = set(config.options[CONF_MODE]) + + def entity_filter(self, entities: set[str]) -> set[str]: + """Filter entities of this domain.""" + entities = super().entity_filter(entities) + return { + entity_id + for entity_id in entities + if _supports_feature(self._hass, entity_id, HumidifierEntityFeature.MODES) + } -from .const import ATTR_ACTION, ATTR_HUMIDITY, DOMAIN, HumidifierAction CONDITIONS: dict[str, type[Condition]] = { "is_off": make_entity_state_condition(DOMAIN, STATE_OFF), @@ -20,6 +78,7 @@ CONDITIONS: dict[str, type[Condition]] = { "is_humidifying": make_entity_state_condition( {DOMAIN: DomainSpec(value_source=ATTR_ACTION)}, HumidifierAction.HUMIDIFYING ), + "is_mode": IsModeCondition, "is_target_humidity": make_entity_numerical_condition( {DOMAIN: DomainSpec(value_source=ATTR_HUMIDITY)}, valid_unit=PERCENTAGE, diff --git a/homeassistant/components/humidifier/conditions.yaml b/homeassistant/components/humidifier/conditions.yaml index bc10ab1db65..25c29301f26 100644 --- a/homeassistant/components/humidifier/conditions.yaml +++ b/homeassistant/components/humidifier/conditions.yaml @@ -32,6 +32,19 @@ is_on: *condition_common is_drying: *condition_common is_humidifying: *condition_common +is_mode: + target: *condition_humidifier_target + fields: + behavior: *condition_behavior + mode: + context: + filter_target: target + required: true + selector: + state: + attribute: available_modes + multiple: true + is_target_humidity: target: *condition_humidifier_target fields: diff --git a/homeassistant/components/humidifier/icons.json b/homeassistant/components/humidifier/icons.json index 778aa6d0f47..8f4e3f89a11 100644 --- a/homeassistant/components/humidifier/icons.json +++ b/homeassistant/components/humidifier/icons.json @@ -6,6 +6,9 @@ "is_humidifying": { "condition": "mdi:arrow-up-bold" }, + "is_mode": { + "condition": "mdi:air-humidifier" + }, "is_off": { "condition": "mdi:air-humidifier-off" }, diff --git a/homeassistant/components/humidifier/strings.json b/homeassistant/components/humidifier/strings.json index 6acd851b3de..82ae8b57436 100644 --- a/homeassistant/components/humidifier/strings.json +++ b/homeassistant/components/humidifier/strings.json @@ -28,6 +28,20 @@ }, "name": "Humidifier is humidifying" }, + "is_mode": { + "description": "Tests if one or more humidifiers are set to a specific mode.", + "fields": { + "behavior": { + "description": "[%key:component::humidifier::common::condition_behavior_description%]", + "name": "[%key:component::humidifier::common::condition_behavior_name%]" + }, + "mode": { + "description": "The operation modes to check for.", + "name": "Mode" + } + }, + "name": "Humidifier is in mode" + }, "is_off": { "description": "Tests if one or more humidifiers are off.", "fields": { diff --git a/tests/components/humidifier/test_condition.py b/tests/components/humidifier/test_condition.py index 98e27a406a9..b45f8882964 100644 --- a/tests/components/humidifier/test_condition.py +++ b/tests/components/humidifier/test_condition.py @@ -1,16 +1,29 @@ """Test humidifier conditions.""" +from contextlib import AbstractContextManager, nullcontext as does_not_raise from typing import Any import pytest +import voluptuous as vol +from homeassistant.components.humidifier.condition import CONF_MODE from homeassistant.components.humidifier.const import ( ATTR_ACTION, ATTR_HUMIDITY, HumidifierAction, + HumidifierEntityFeature, +) +from homeassistant.const import ( + ATTR_MODE, + ATTR_SUPPORTED_FEATURES, + CONF_ENTITY_ID, + CONF_OPTIONS, + CONF_TARGET, + STATE_OFF, + STATE_ON, ) -from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant +from homeassistant.helpers.condition import async_validate_condition_config from tests.components.common import ( ConditionStateDescription, @@ -39,6 +52,7 @@ async def target_humidifiers(hass: HomeAssistant) -> dict[str, list[str]]: "humidifier.is_on", "humidifier.is_drying", "humidifier.is_humidifying", + "humidifier.is_mode", "humidifier.is_target_humidity", ], ) @@ -153,6 +167,20 @@ async def test_humidifier_state_condition_behavior_all( target_states=[(STATE_ON, {ATTR_ACTION: HumidifierAction.HUMIDIFYING})], other_states=[(STATE_ON, {ATTR_ACTION: HumidifierAction.IDLE})], ), + *parametrize_condition_states_any( + condition="humidifier.is_mode", + condition_options={CONF_MODE: ["eco", "sleep"]}, + target_states=[ + (STATE_ON, {ATTR_MODE: "eco"}), + (STATE_ON, {ATTR_MODE: "sleep"}), + ], + other_states=[ + (STATE_ON, {ATTR_MODE: "normal"}), + ], + required_filter_attributes={ + ATTR_SUPPORTED_FEATURES: HumidifierEntityFeature.MODES + }, + ), ], ) async def test_humidifier_attribute_condition_behavior_any( @@ -196,6 +224,20 @@ async def test_humidifier_attribute_condition_behavior_any( target_states=[(STATE_ON, {ATTR_ACTION: HumidifierAction.HUMIDIFYING})], other_states=[(STATE_ON, {ATTR_ACTION: HumidifierAction.IDLE})], ), + *parametrize_condition_states_all( + condition="humidifier.is_mode", + condition_options={CONF_MODE: ["eco", "sleep"]}, + target_states=[ + (STATE_ON, {ATTR_MODE: "eco"}), + (STATE_ON, {ATTR_MODE: "sleep"}), + ], + other_states=[ + (STATE_ON, {ATTR_MODE: "normal"}), + ], + required_filter_attributes={ + ATTR_SUPPORTED_FEATURES: HumidifierEntityFeature.MODES + }, + ), ], ) async def test_humidifier_attribute_condition_behavior_all( @@ -291,3 +333,51 @@ async def test_humidifier_numerical_condition_behavior_all( condition_options=condition_options, states=states, ) + + +@pytest.mark.usefixtures("enable_labs_preview_features") +@pytest.mark.parametrize( + ("condition", "condition_options", "expected_result"), + [ + # Valid configurations + ( + "humidifier.is_mode", + {CONF_MODE: ["eco", "sleep"]}, + does_not_raise(), + ), + ( + "humidifier.is_mode", + {CONF_MODE: "eco"}, + does_not_raise(), + ), + # Invalid configurations + ( + "humidifier.is_mode", + # Empty mode list + {CONF_MODE: []}, + pytest.raises(vol.Invalid), + ), + ( + "humidifier.is_mode", + # Missing CONF_MODE + {}, + pytest.raises(vol.Invalid), + ), + ], +) +async def test_humidifier_is_mode_condition_validation( + hass: HomeAssistant, + condition: str, + condition_options: dict[str, Any], + expected_result: AbstractContextManager, +) -> None: + """Test humidifier is_mode condition config validation.""" + with expected_result: + await async_validate_condition_config( + hass, + { + "condition": condition, + CONF_TARGET: {CONF_ENTITY_ID: "humidifier.test"}, + CONF_OPTIONS: condition_options, + }, + )