mirror of
https://github.com/home-assistant/core.git
synced 2026-05-06 08:36:42 +02:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 522bfaff07 |
@@ -13,8 +13,8 @@ from homeassistant.helpers.condition import (
|
||||
Condition,
|
||||
ConditionConfig,
|
||||
EntityConditionBase,
|
||||
EntityNumericalConditionBase,
|
||||
EntityNumericalConditionWithUnitBase,
|
||||
make_entity_numerical_condition,
|
||||
make_entity_state_condition,
|
||||
)
|
||||
from homeassistant.util.unit_conversion import TemperatureConverter
|
||||
@@ -65,6 +65,20 @@ class ClimateTargetTemperatureCondition(EntityNumericalConditionWithUnitBase):
|
||||
return self._hass.config.units.temperature_unit
|
||||
|
||||
|
||||
class ClimateTargetHumidityCondition(EntityNumericalConditionBase):
|
||||
"""Condition for climate target humidity."""
|
||||
|
||||
_domain_specs = {DOMAIN: DomainSpec(value_source=ATTR_HUMIDITY)}
|
||||
_valid_unit = "%"
|
||||
|
||||
def _should_include(self, state: State) -> bool:
|
||||
"""Skip climate entities that do not expose a target humidity."""
|
||||
return (
|
||||
super()._should_include(state)
|
||||
and state.attributes.get(ATTR_HUMIDITY) is not None
|
||||
)
|
||||
|
||||
|
||||
CONDITIONS: dict[str, type[Condition]] = {
|
||||
"is_hvac_mode": ClimateHVACModeCondition,
|
||||
"is_off": make_entity_state_condition(DOMAIN, HVACMode.OFF),
|
||||
@@ -88,10 +102,7 @@ CONDITIONS: dict[str, type[Condition]] = {
|
||||
"is_heating": make_entity_state_condition(
|
||||
{DOMAIN: DomainSpec(value_source=ATTR_HVAC_ACTION)}, HVACAction.HEATING
|
||||
),
|
||||
"target_humidity": make_entity_numerical_condition(
|
||||
{DOMAIN: DomainSpec(value_source=ATTR_HUMIDITY)},
|
||||
valid_unit="%",
|
||||
),
|
||||
"target_humidity": ClimateTargetHumidityCondition,
|
||||
"target_temperature": ClimateTargetTemperatureCondition,
|
||||
}
|
||||
|
||||
|
||||
@@ -437,6 +437,9 @@ class EntityConditionBase(Condition):
|
||||
"""Base class for entity conditions."""
|
||||
|
||||
_domain_specs: Mapping[str, DomainSpec]
|
||||
_excluded_states: Final[frozenset[str]] = frozenset(
|
||||
{STATE_UNAVAILABLE, STATE_UNKNOWN}
|
||||
)
|
||||
_schema: vol.Schema = ENTITY_STATE_CONDITION_SCHEMA_ANY_ALL
|
||||
# When True, indirect target expansion (via device/area/floor) skips
|
||||
# entities with an entity_category.
|
||||
@@ -501,7 +504,7 @@ class EntityConditionBase(Condition):
|
||||
"""
|
||||
if (
|
||||
_state is not None
|
||||
and _state.state not in (STATE_UNAVAILABLE, STATE_UNKNOWN)
|
||||
and self._should_include(_state)
|
||||
and self.is_valid_state(_state)
|
||||
):
|
||||
# Only record the time if not already tracked, to avoid
|
||||
@@ -566,6 +569,16 @@ class EntityConditionBase(Condition):
|
||||
return entity_state.state
|
||||
return entity_state.attributes.get(domain_spec.value_source)
|
||||
|
||||
def _should_include(self, _state: State) -> bool:
|
||||
"""Check if an entity should participate in any/all checks.
|
||||
|
||||
The default implementation excludes only entities whose state.state
|
||||
is in `_excluded_states` (unavailable / unknown). Subclasses can
|
||||
override to also exclude entities that lack the optional capability
|
||||
the condition relies on.
|
||||
"""
|
||||
return _state.state not in self._excluded_states
|
||||
|
||||
@abc.abstractmethod
|
||||
def is_valid_state(self, entity_state: State) -> bool:
|
||||
"""Check if the state matches the expected state(s)."""
|
||||
@@ -622,7 +635,7 @@ class EntityConditionBase(Condition):
|
||||
_state
|
||||
for entity_id in filtered_entity_ids
|
||||
if (_state := self._hass.states.get(entity_id))
|
||||
and _state.state not in (STATE_UNAVAILABLE, STATE_UNKNOWN)
|
||||
and self._should_include(_state)
|
||||
]
|
||||
return self._matcher(entity_states)
|
||||
|
||||
|
||||
@@ -332,6 +332,7 @@ async def test_climate_attribute_condition_behavior_all(
|
||||
"climate.target_humidity",
|
||||
HVACMode.AUTO,
|
||||
ATTR_HUMIDITY,
|
||||
attribute_required=True,
|
||||
),
|
||||
*parametrize_numerical_attribute_condition_above_below_any(
|
||||
"climate.target_temperature",
|
||||
@@ -376,6 +377,7 @@ async def test_climate_numerical_condition_behavior_any(
|
||||
"climate.target_humidity",
|
||||
HVACMode.AUTO,
|
||||
ATTR_HUMIDITY,
|
||||
attribute_required=True,
|
||||
),
|
||||
*parametrize_numerical_attribute_condition_above_below_all(
|
||||
"climate.target_temperature",
|
||||
|
||||
@@ -246,6 +246,7 @@ def _parametrize_condition_states(
|
||||
condition_options: dict[str, Any] | None = None,
|
||||
target_states: list[str | None | tuple[str | None, dict]],
|
||||
other_states: list[str | None | tuple[str | None, dict]],
|
||||
extra_excluded_states: list[str | None | tuple[str | None, dict]] | None = None,
|
||||
required_filter_attributes: dict | None,
|
||||
condition_true_if_invalid: bool,
|
||||
excluded_entities_from_other_domain: bool,
|
||||
@@ -261,6 +262,7 @@ def _parametrize_condition_states(
|
||||
|
||||
required_filter_attributes = required_filter_attributes or {}
|
||||
condition_options = condition_options or {}
|
||||
extra_excluded_states = extra_excluded_states or []
|
||||
add_excluded_state = excluded_entities_from_other_domain or bool(
|
||||
required_filter_attributes
|
||||
)
|
||||
@@ -314,6 +316,18 @@ def _parametrize_condition_states(
|
||||
STATE_UNKNOWN, condition_true_if_invalid, True
|
||||
),
|
||||
),
|
||||
# `extra_excluded_states` are filtered by the condition's
|
||||
# `_should_include` override exactly like
|
||||
# missing/unavailable/unknown, so they share the
|
||||
# `condition_true_if_invalid` expectation: vacuous True
|
||||
# under behavior=all (every entity filtered → all-check
|
||||
# vacuous), vacuous False under behavior=any.
|
||||
(
|
||||
state_with_attributes(
|
||||
extra_excluded_state, condition_true_if_invalid, True
|
||||
)
|
||||
for extra_excluded_state in extra_excluded_states
|
||||
),
|
||||
(
|
||||
state_with_attributes(other_state, False, False)
|
||||
for other_state in other_states
|
||||
@@ -342,6 +356,7 @@ def parametrize_condition_states_any(
|
||||
condition_options: dict[str, Any] | None = None,
|
||||
target_states: list[str | None | tuple[str | None, dict]],
|
||||
other_states: list[str | None | tuple[str | None, dict]],
|
||||
extra_excluded_states: list[str | None | tuple[str | None, dict]] | None = None,
|
||||
required_filter_attributes: dict | None = None,
|
||||
excluded_entities_from_other_domain: bool = False,
|
||||
) -> list[tuple[str, dict[str, Any], list[ConditionStateDescription]]]:
|
||||
@@ -364,6 +379,13 @@ def parametrize_condition_states_any(
|
||||
other_states: States the condition is expected to evaluate False for.
|
||||
Same accepted shapes as `target_states`. With behavior=any, an
|
||||
entity in such a state does not satisfy the condition.
|
||||
extra_excluded_states: *Additional* states (on top of the always-
|
||||
included missing/unavailable/unknown) that the condition's
|
||||
`_should_include` override is expected to filter out. Under
|
||||
behavior=any, every targeted entity sitting in a filtered state
|
||||
yields `any([]) → False`, so these share the built-in invalid
|
||||
states' expectation. Set this for conditions whose
|
||||
`_should_include` skips entities lacking the tracked attribute.
|
||||
required_filter_attributes: Attributes that must be present on the
|
||||
entity for the condition's domain filter to accept it. The
|
||||
helper merges these into every generated state so the entity
|
||||
@@ -380,6 +402,7 @@ def parametrize_condition_states_any(
|
||||
condition_options=condition_options,
|
||||
target_states=target_states,
|
||||
other_states=other_states,
|
||||
extra_excluded_states=extra_excluded_states,
|
||||
required_filter_attributes=required_filter_attributes,
|
||||
condition_true_if_invalid=False,
|
||||
excluded_entities_from_other_domain=excluded_entities_from_other_domain,
|
||||
@@ -392,6 +415,7 @@ def parametrize_condition_states_all(
|
||||
condition_options: dict[str, Any] | None = None,
|
||||
target_states: list[str | None | tuple[str | None, dict]],
|
||||
other_states: list[str | None | tuple[str | None, dict]],
|
||||
extra_excluded_states: list[str | None | tuple[str | None, dict]] | None = None,
|
||||
required_filter_attributes: dict | None = None,
|
||||
excluded_entities_from_other_domain: bool = False,
|
||||
) -> list[tuple[str, dict[str, Any], list[ConditionStateDescription]]]:
|
||||
@@ -416,6 +440,13 @@ def parametrize_condition_states_all(
|
||||
for. Same accepted shapes as `target_states`. Under behavior=all,
|
||||
an entity in such a state blocks the all-check (counts toward
|
||||
the check but is not a match).
|
||||
extra_excluded_states: *Additional* states (on top of the always-
|
||||
included missing/unavailable/unknown) that the condition's
|
||||
`_should_include` override is expected to filter out. Under
|
||||
behavior=all, every targeted entity sitting in a filtered state
|
||||
yields `all([]) → True` (vacuous), so these share the built-in
|
||||
invalid states' expectation. Set this for conditions whose
|
||||
`_should_include` skips entities lacking the tracked attribute.
|
||||
required_filter_attributes: Attributes that must be present on the
|
||||
entity for the condition's domain filter to accept it. The
|
||||
helper merges these into every generated state so the entity
|
||||
@@ -432,6 +463,7 @@ def parametrize_condition_states_all(
|
||||
condition_options=condition_options,
|
||||
target_states=target_states,
|
||||
other_states=other_states,
|
||||
extra_excluded_states=extra_excluded_states,
|
||||
required_filter_attributes=required_filter_attributes,
|
||||
condition_true_if_invalid=True,
|
||||
excluded_entities_from_other_domain=excluded_entities_from_other_domain,
|
||||
@@ -2095,6 +2127,7 @@ def parametrize_numerical_attribute_condition_above_below_any(
|
||||
required_filter_attributes: dict | None = None,
|
||||
threshold_unit: str | None | UndefinedType = UNDEFINED,
|
||||
unit_attributes: dict | None = None,
|
||||
attribute_required: bool = False,
|
||||
) -> list[tuple[str, dict[str, Any], list[ConditionStateDescription]]]:
|
||||
"""Parametrize above/below/between threshold cases for attribute-based numerical conditions under behavior=any.
|
||||
|
||||
@@ -2134,9 +2167,18 @@ def parametrize_numerical_attribute_condition_above_below_any(
|
||||
`{ATTR_UNIT_OF_MEASUREMENT: ...}`) merged into every generated
|
||||
state, so the entity carries a unit alongside its tracked
|
||||
attribute.
|
||||
attribute_required: When True, `(state, {attribute: None})` is
|
||||
classified as an *excluded* state (filtered out of the all/any
|
||||
check by the condition's `_should_include` override) rather
|
||||
than treated as just-missing. Set this for conditions whose
|
||||
`_should_include` skips entities lacking the tracked
|
||||
attribute.
|
||||
"""
|
||||
condition_options = condition_options or {}
|
||||
unit_attributes = unit_attributes or {}
|
||||
extra_excluded_states = (
|
||||
[(state, {attribute: None} | unit_attributes)] if attribute_required else None
|
||||
)
|
||||
|
||||
return [
|
||||
*parametrize_condition_states_any(
|
||||
@@ -2158,6 +2200,7 @@ def parametrize_numerical_attribute_condition_above_below_any(
|
||||
(state, {attribute: 10} | unit_attributes),
|
||||
(state, {attribute: 20} | unit_attributes),
|
||||
],
|
||||
extra_excluded_states=extra_excluded_states,
|
||||
required_filter_attributes=required_filter_attributes,
|
||||
),
|
||||
*parametrize_condition_states_any(
|
||||
@@ -2179,6 +2222,7 @@ def parametrize_numerical_attribute_condition_above_below_any(
|
||||
(state, {attribute: 90} | unit_attributes),
|
||||
(state, {attribute: 100} | unit_attributes),
|
||||
],
|
||||
extra_excluded_states=extra_excluded_states,
|
||||
required_filter_attributes=required_filter_attributes,
|
||||
),
|
||||
*parametrize_condition_states_any(
|
||||
@@ -2205,6 +2249,7 @@ def parametrize_numerical_attribute_condition_above_below_any(
|
||||
(state, {attribute: 80} | unit_attributes),
|
||||
(state, {attribute: 100} | unit_attributes),
|
||||
],
|
||||
extra_excluded_states=extra_excluded_states,
|
||||
required_filter_attributes=required_filter_attributes,
|
||||
),
|
||||
]
|
||||
@@ -2219,6 +2264,7 @@ def parametrize_numerical_attribute_condition_above_below_all(
|
||||
required_filter_attributes: dict | None = None,
|
||||
threshold_unit: str | None | UndefinedType = UNDEFINED,
|
||||
unit_attributes: dict | None = None,
|
||||
attribute_required: bool = False,
|
||||
) -> list[tuple[str, dict[str, Any], list[ConditionStateDescription]]]:
|
||||
"""Parametrize above/below/between threshold cases for attribute-based numerical conditions under behavior=all.
|
||||
|
||||
@@ -2256,9 +2302,18 @@ def parametrize_numerical_attribute_condition_above_below_all(
|
||||
`{ATTR_UNIT_OF_MEASUREMENT: ...}`) merged into every generated
|
||||
state, so the entity carries a unit alongside its tracked
|
||||
attribute.
|
||||
attribute_required: When True, `(state, {attribute: None})` is
|
||||
classified as an *excluded* state (filtered out of the all/any
|
||||
check by the condition's `_should_include` override) rather
|
||||
than treated as just-missing. Set this for conditions whose
|
||||
`_should_include` skips entities lacking the tracked
|
||||
attribute.
|
||||
"""
|
||||
condition_options = condition_options or {}
|
||||
unit_attributes = unit_attributes or {}
|
||||
extra_excluded_states = (
|
||||
[(state, {attribute: None} | unit_attributes)] if attribute_required else None
|
||||
)
|
||||
|
||||
return [
|
||||
*parametrize_condition_states_all(
|
||||
@@ -2280,6 +2335,7 @@ def parametrize_numerical_attribute_condition_above_below_all(
|
||||
(state, {attribute: 10} | unit_attributes),
|
||||
(state, {attribute: 20} | unit_attributes),
|
||||
],
|
||||
extra_excluded_states=extra_excluded_states,
|
||||
required_filter_attributes=required_filter_attributes,
|
||||
),
|
||||
*parametrize_condition_states_all(
|
||||
@@ -2301,6 +2357,7 @@ def parametrize_numerical_attribute_condition_above_below_all(
|
||||
(state, {attribute: 90} | unit_attributes),
|
||||
(state, {attribute: 100} | unit_attributes),
|
||||
],
|
||||
extra_excluded_states=extra_excluded_states,
|
||||
required_filter_attributes=required_filter_attributes,
|
||||
),
|
||||
*parametrize_condition_states_all(
|
||||
@@ -2327,6 +2384,7 @@ def parametrize_numerical_attribute_condition_above_below_all(
|
||||
(state, {attribute: 80} | unit_attributes),
|
||||
(state, {attribute: 100} | unit_attributes),
|
||||
],
|
||||
extra_excluded_states=extra_excluded_states,
|
||||
required_filter_attributes=required_filter_attributes,
|
||||
),
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user