1
0
mirror of https://github.com/home-assistant/core.git synced 2026-04-02 00:20:30 +01:00

Add trigger humidifier.mode_changed (#166241)

Co-authored-by: Norbert Rittel <norbert@rittel.de>
This commit is contained in:
Erik Montnemery
2026-03-26 10:03:49 +01:00
committed by GitHub
parent 0f41a311c8
commit ad522d723c
5 changed files with 203 additions and 9 deletions

View File

@@ -67,6 +67,9 @@
}
},
"triggers": {
"mode_changed": {
"trigger": "mdi:air-humidifier"
},
"started_drying": {
"trigger": "mdi:arrow-down-bold"
},

View File

@@ -201,6 +201,20 @@
},
"title": "Humidifier",
"triggers": {
"mode_changed": {
"description": "Triggers after the operation mode of one or more humidifiers changes.",
"fields": {
"behavior": {
"description": "[%key:component::humidifier::common::trigger_behavior_description%]",
"name": "[%key:component::humidifier::common::trigger_behavior_name%]"
},
"mode": {
"description": "The operation modes to trigger on.",
"name": "Mode"
}
},
"name": "Humidifier mode changed"
},
"started_drying": {
"description": "Triggers after one or more humidifiers start drying.",
"fields": {

View File

@@ -1,13 +1,65 @@
"""Provides triggers for humidifiers."""
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant
from homeassistant.helpers.automation import DomainSpec
from homeassistant.helpers.trigger import Trigger, make_entity_target_state_trigger
import voluptuous as vol
from homeassistant.const import ATTR_MODE, CONF_OPTIONS, 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.entity import get_supported_features
from homeassistant.helpers.trigger import (
ENTITY_STATE_TRIGGER_SCHEMA_FIRST_LAST,
EntityTargetStateTriggerBase,
Trigger,
TriggerConfig,
make_entity_target_state_trigger,
)
from .const import ATTR_ACTION, DOMAIN, HumidifierAction, HumidifierEntityFeature
CONF_MODE = "mode"
MODE_CHANGED_TRIGGER_SCHEMA = ENTITY_STATE_TRIGGER_SCHEMA_FIRST_LAST.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 ModeChangedTrigger(EntityTargetStateTriggerBase):
"""Trigger for humidifier mode changes."""
_domain_specs = {DOMAIN: DomainSpec(value_source=ATTR_MODE)}
_schema = MODE_CHANGED_TRIGGER_SCHEMA
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
"""Initialize the mode trigger."""
super().__init__(hass, config)
self._to_states = set(self._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, DOMAIN, HumidifierAction
TRIGGERS: dict[str, type[Trigger]] = {
"mode_changed": ModeChangedTrigger,
"started_drying": make_entity_target_state_trigger(
{DOMAIN: DomainSpec(value_source=ATTR_ACTION)}, HumidifierAction.DRYING
),

View File

@@ -1,9 +1,9 @@
.trigger_common: &trigger_common
target:
target: &trigger_humidifier_target
entity:
domain: humidifier
fields:
behavior:
behavior: &trigger_behavior
required: true
default: any
selector:
@@ -18,3 +18,16 @@ started_drying: *trigger_common
started_humidifying: *trigger_common
turned_on: *trigger_common
turned_off: *trigger_common
mode_changed:
target: *trigger_humidifier_target
fields:
behavior: *trigger_behavior
mode:
context:
filter_target: target
required: true
selector:
state:
attribute: available_modes
multiple: true

View File

@@ -1,12 +1,28 @@
"""Test humidifier trigger."""
from contextlib import AbstractContextManager, nullcontext as does_not_raise
from typing import Any
import pytest
import voluptuous as vol
from homeassistant.components.humidifier.const import ATTR_ACTION, HumidifierAction
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.components.humidifier.const import (
ATTR_ACTION,
HumidifierAction,
HumidifierEntityFeature,
)
from homeassistant.components.humidifier.trigger import CONF_MODE
from homeassistant.const import (
ATTR_MODE,
ATTR_SUPPORTED_FEATURES,
CONF_ENTITY_ID,
CONF_OPTIONS,
CONF_TARGET,
STATE_OFF,
STATE_ON,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.trigger import async_validate_trigger_config
from tests.components.common import (
TriggerStateDescription,
@@ -29,6 +45,7 @@ async def target_humidifiers(hass: HomeAssistant) -> dict[str, list[str]]:
@pytest.mark.parametrize(
"trigger_key",
[
"humidifier.mode_changed",
"humidifier.started_drying",
"humidifier.started_humidifying",
"humidifier.turned_off",
@@ -103,6 +120,21 @@ async def test_humidifier_state_trigger_behavior_any(
target_states=[(STATE_ON, {ATTR_ACTION: HumidifierAction.HUMIDIFYING})],
other_states=[(STATE_ON, {ATTR_ACTION: HumidifierAction.IDLE})],
),
*parametrize_trigger_states(
trigger="humidifier.mode_changed",
trigger_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
},
trigger_from_none=False,
),
],
)
async def test_humidifier_state_attribute_trigger_behavior_any(
@@ -189,6 +221,21 @@ async def test_humidifier_state_trigger_behavior_first(
target_states=[(STATE_ON, {ATTR_ACTION: HumidifierAction.HUMIDIFYING})],
other_states=[(STATE_ON, {ATTR_ACTION: HumidifierAction.IDLE})],
),
*parametrize_trigger_states(
trigger="humidifier.mode_changed",
trigger_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
},
trigger_from_none=False,
),
],
)
async def test_humidifier_state_attribute_trigger_behavior_first(
@@ -275,6 +322,21 @@ async def test_humidifier_state_trigger_behavior_last(
target_states=[(STATE_ON, {ATTR_ACTION: HumidifierAction.HUMIDIFYING})],
other_states=[(STATE_ON, {ATTR_ACTION: HumidifierAction.IDLE})],
),
*parametrize_trigger_states(
trigger="humidifier.mode_changed",
trigger_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
},
trigger_from_none=False,
),
],
)
async def test_humidifier_state_attribute_trigger_behavior_last(
@@ -298,3 +360,53 @@ async def test_humidifier_state_attribute_trigger_behavior_last(
trigger_options=trigger_options,
states=states,
)
@pytest.mark.usefixtures("enable_labs_preview_features")
@pytest.mark.parametrize(
("trigger", "trigger_options", "expected_result"),
[
# Valid configurations
(
"humidifier.mode_changed",
{CONF_MODE: ["eco", "sleep"]},
does_not_raise(),
),
(
"humidifier.mode_changed",
{CONF_MODE: "eco"},
does_not_raise(),
),
# Invalid configurations
(
"humidifier.mode_changed",
# Empty mode list
{CONF_MODE: []},
pytest.raises(vol.Invalid),
),
(
"humidifier.mode_changed",
# Missing CONF_MODE
{},
pytest.raises(vol.Invalid),
),
],
)
async def test_humidifier_mode_changed_trigger_validation(
hass: HomeAssistant,
trigger: str,
trigger_options: dict[str, Any],
expected_result: AbstractContextManager,
) -> None:
"""Test humidifier mode_changed trigger config validation."""
with expected_result:
await async_validate_trigger_config(
hass,
[
{
"platform": trigger,
CONF_TARGET: {CONF_ENTITY_ID: "humidifier.test"},
CONF_OPTIONS: trigger_options,
}
],
)