1
0
mirror of https://github.com/home-assistant/core.git synced 2026-05-08 17:49:37 +01:00

Exclude incompatible entities from humidity automations (#169898)

This commit is contained in:
Erik Montnemery
2026-05-06 13:10:24 +02:00
committed by GitHub
parent 824f5205e9
commit 52e1d9443c
4 changed files with 96 additions and 15 deletions
+26 -3
View File
@@ -14,9 +14,9 @@ from homeassistant.components.weather import (
DOMAIN as WEATHER_DOMAIN,
)
from homeassistant.const import PERCENTAGE
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, State
from homeassistant.helpers.automation import DomainSpec
from homeassistant.helpers.condition import Condition, make_entity_numerical_condition
from homeassistant.helpers.condition import Condition, EntityNumericalConditionBase
HUMIDITY_DOMAIN_SPECS = {
CLIMATE_DOMAIN: DomainSpec(
@@ -31,8 +31,31 @@ HUMIDITY_DOMAIN_SPECS = {
),
}
class HumidityCondition(EntityNumericalConditionBase):
"""Condition for humidity value across multiple domains."""
_domain_specs = HUMIDITY_DOMAIN_SPECS
_valid_unit = PERCENTAGE
def _should_include(self, state: State) -> bool:
"""Skip attribute-source entities that lack the humidity attribute.
Mirrors the humidity trigger: for climate / humidifier / weather
(attribute-based), the entity is filtered when the source attribute
is absent; sensor entities (state-value-based) fall through to the
base impl.
"""
if not super()._should_include(state):
return False
domain_spec = self._domain_specs[state.domain]
if domain_spec.value_source is None:
return True
return state.attributes.get(domain_spec.value_source) is not None
CONDITIONS: dict[str, type[Condition]] = {
"is_value": make_entity_numerical_condition(HUMIDITY_DOMAIN_SPECS, PERCENTAGE),
"is_value": HumidityCondition,
}
+43 -9
View File
@@ -13,12 +13,13 @@ from homeassistant.components.weather import (
ATTR_WEATHER_HUMIDITY,
DOMAIN as WEATHER_DOMAIN,
)
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, State
from homeassistant.helpers.automation import DomainSpec
from homeassistant.helpers.trigger import (
EntityNumericalStateChangedTriggerBase,
EntityNumericalStateCrossedThresholdTriggerBase,
EntityNumericalStateTriggerBase,
Trigger,
make_entity_numerical_state_changed_trigger,
make_entity_numerical_state_crossed_threshold_trigger,
)
HUMIDITY_DOMAIN_SPECS: dict[str, DomainSpec] = {
@@ -36,13 +37,46 @@ HUMIDITY_DOMAIN_SPECS: dict[str, DomainSpec] = {
),
}
class _HumidityTriggerMixin(EntityNumericalStateTriggerBase):
"""Mixin for humidity triggers providing entity filtering."""
_domain_specs = HUMIDITY_DOMAIN_SPECS
_valid_unit = "%"
def _should_include(self, state: State) -> bool:
"""Skip attribute-source entities that lack the humidity attribute.
For domains whose tracked value comes from an attribute
(climate / humidifier / weather), require the attribute to be
present; otherwise the all/count check would treat an entity that
cannot report a humidity as a non-match and block behavior=last.
Sensor entities source their value from `state.state`, so they
fall through to the base impl.
"""
if not super()._should_include(state):
return False
domain_spec = self._domain_specs[state.domain]
if domain_spec.value_source is None:
return True
return state.attributes.get(domain_spec.value_source) is not None
class HumidityChangedTrigger(
_HumidityTriggerMixin, EntityNumericalStateChangedTriggerBase
):
"""Trigger for humidity value changes across multiple domains."""
class HumidityCrossedThresholdTrigger(
_HumidityTriggerMixin, EntityNumericalStateCrossedThresholdTriggerBase
):
"""Trigger for humidity value crossing a threshold across multiple domains."""
TRIGGERS: dict[str, type[Trigger]] = {
"changed": make_entity_numerical_state_changed_trigger(
HUMIDITY_DOMAIN_SPECS, valid_unit="%"
),
"crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
HUMIDITY_DOMAIN_SPECS, valid_unit="%"
),
"changed": HumidityChangedTrigger,
"crossed_threshold": HumidityCrossedThresholdTrigger,
}
@@ -179,6 +179,7 @@ async def test_humidity_sensor_condition_behavior_all(
"humidity.is_value",
HVACMode.AUTO,
CLIMATE_ATTR_CURRENT_HUMIDITY,
attribute_required=True,
),
)
async def test_humidity_climate_condition_behavior_any(
@@ -215,6 +216,7 @@ async def test_humidity_climate_condition_behavior_any(
"humidity.is_value",
HVACMode.AUTO,
CLIMATE_ATTR_CURRENT_HUMIDITY,
attribute_required=True,
),
)
async def test_humidity_climate_condition_behavior_all(
@@ -251,6 +253,7 @@ async def test_humidity_climate_condition_behavior_all(
"humidity.is_value",
STATE_ON,
HUMIDIFIER_ATTR_CURRENT_HUMIDITY,
attribute_required=True,
),
)
async def test_humidity_humidifier_condition_behavior_any(
@@ -287,6 +290,7 @@ async def test_humidity_humidifier_condition_behavior_any(
"humidity.is_value",
STATE_ON,
HUMIDIFIER_ATTR_CURRENT_HUMIDITY,
attribute_required=True,
),
)
async def test_humidity_humidifier_condition_behavior_all(
@@ -323,6 +327,7 @@ async def test_humidity_humidifier_condition_behavior_all(
"humidity.is_value",
"sunny",
ATTR_WEATHER_HUMIDITY,
attribute_required=True,
),
)
async def test_humidity_weather_condition_behavior_any(
@@ -359,6 +364,7 @@ async def test_humidity_weather_condition_behavior_any(
"humidity.is_value",
"sunny",
ATTR_WEATHER_HUMIDITY,
attribute_required=True,
),
)
async def test_humidity_weather_condition_behavior_all(
+21 -3
View File
@@ -240,12 +240,16 @@ async def test_humidity_trigger_sensor_crossed_threshold_behavior_last(
("trigger", "trigger_options", "states"),
[
*parametrize_numerical_attribute_changed_trigger_states(
"humidity.changed", HVACMode.AUTO, CLIMATE_ATTR_CURRENT_HUMIDITY
"humidity.changed",
HVACMode.AUTO,
CLIMATE_ATTR_CURRENT_HUMIDITY,
attribute_required=True,
),
*parametrize_numerical_attribute_crossed_threshold_trigger_states(
"humidity.crossed_threshold",
HVACMode.AUTO,
CLIMATE_ATTR_CURRENT_HUMIDITY,
attribute_required=True,
),
],
)
@@ -284,6 +288,7 @@ async def test_humidity_trigger_climate_behavior_any(
"humidity.crossed_threshold",
HVACMode.AUTO,
CLIMATE_ATTR_CURRENT_HUMIDITY,
attribute_required=True,
),
],
)
@@ -322,6 +327,7 @@ async def test_humidity_trigger_climate_crossed_threshold_behavior_first(
"humidity.crossed_threshold",
HVACMode.AUTO,
CLIMATE_ATTR_CURRENT_HUMIDITY,
attribute_required=True,
),
],
)
@@ -360,12 +366,16 @@ async def test_humidity_trigger_climate_crossed_threshold_behavior_last(
("trigger", "trigger_options", "states"),
[
*parametrize_numerical_attribute_changed_trigger_states(
"humidity.changed", STATE_ON, HUMIDIFIER_ATTR_CURRENT_HUMIDITY
"humidity.changed",
STATE_ON,
HUMIDIFIER_ATTR_CURRENT_HUMIDITY,
attribute_required=True,
),
*parametrize_numerical_attribute_crossed_threshold_trigger_states(
"humidity.crossed_threshold",
STATE_ON,
HUMIDIFIER_ATTR_CURRENT_HUMIDITY,
attribute_required=True,
),
],
)
@@ -404,6 +414,7 @@ async def test_humidity_trigger_humidifier_behavior_any(
"humidity.crossed_threshold",
STATE_ON,
HUMIDIFIER_ATTR_CURRENT_HUMIDITY,
attribute_required=True,
),
],
)
@@ -442,6 +453,7 @@ async def test_humidity_trigger_humidifier_crossed_threshold_behavior_first(
"humidity.crossed_threshold",
STATE_ON,
HUMIDIFIER_ATTR_CURRENT_HUMIDITY,
attribute_required=True,
),
],
)
@@ -480,12 +492,16 @@ async def test_humidity_trigger_humidifier_crossed_threshold_behavior_last(
("trigger", "trigger_options", "states"),
[
*parametrize_numerical_attribute_changed_trigger_states(
"humidity.changed", "sunny", ATTR_WEATHER_HUMIDITY
"humidity.changed",
"sunny",
ATTR_WEATHER_HUMIDITY,
attribute_required=True,
),
*parametrize_numerical_attribute_crossed_threshold_trigger_states(
"humidity.crossed_threshold",
"sunny",
ATTR_WEATHER_HUMIDITY,
attribute_required=True,
),
],
)
@@ -524,6 +540,7 @@ async def test_humidity_trigger_weather_behavior_any(
"humidity.crossed_threshold",
"sunny",
ATTR_WEATHER_HUMIDITY,
attribute_required=True,
),
],
)
@@ -562,6 +579,7 @@ async def test_humidity_trigger_weather_crossed_threshold_behavior_first(
"humidity.crossed_threshold",
"sunny",
ATTR_WEATHER_HUMIDITY,
attribute_required=True,
),
],
)