From 5f1201dbbe8b5f3b0be03c68981f441c49869a7c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 6 May 2026 13:10:53 +0200 Subject: [PATCH] Exclude incompatible entities from temperature automations (#169901) --- .../components/temperature/condition.py | 15 +++++++++++++++ homeassistant/components/temperature/trigger.py | 17 +++++++++++++++++ tests/components/temperature/test_condition.py | 6 ++++++ tests/components/temperature/test_trigger.py | 12 ++++++++++++ 4 files changed, 50 insertions(+) diff --git a/homeassistant/components/temperature/condition.py b/homeassistant/components/temperature/condition.py index acb87a1cdc0..589883baf57 100644 --- a/homeassistant/components/temperature/condition.py +++ b/homeassistant/components/temperature/condition.py @@ -46,6 +46,21 @@ class TemperatureCondition(EntityNumericalConditionWithUnitBase): _domain_specs = TEMPERATURE_DOMAIN_SPECS _unit_converter = TemperatureConverter + def _should_include(self, state: State) -> bool: + """Skip attribute-source entities that lack the temperature attribute. + + Mirrors the temperature trigger: for climate / water_heater / + 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 + def _get_entity_unit(self, entity_state: State) -> str | None: """Get the temperature unit of an entity from its state.""" if entity_state.domain == SENSOR_DOMAIN: diff --git a/homeassistant/components/temperature/trigger.py b/homeassistant/components/temperature/trigger.py index 02430d14ef2..a3a4aa9777c 100644 --- a/homeassistant/components/temperature/trigger.py +++ b/homeassistant/components/temperature/trigger.py @@ -46,6 +46,23 @@ class _TemperatureTriggerMixin(EntityNumericalStateTriggerWithUnitBase): _domain_specs = TEMPERATURE_DOMAIN_SPECS _unit_converter = TemperatureConverter + def _should_include(self, state: State) -> bool: + """Skip attribute-source entities that lack the temperature attribute. + + For domains whose tracked value comes from an attribute + (climate / water_heater / weather), require the attribute to be + present; otherwise the all/count check would treat an entity that + cannot report a temperature 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 + def _get_entity_unit(self, state: State) -> str | None: """Get the temperature unit of an entity from its state.""" if state.domain == SENSOR_DOMAIN: diff --git a/tests/components/temperature/test_condition.py b/tests/components/temperature/test_condition.py index 9224f3d664a..7e77dda53b9 100644 --- a/tests/components/temperature/test_condition.py +++ b/tests/components/temperature/test_condition.py @@ -180,6 +180,7 @@ async def test_temperature_sensor_condition_behavior_all( HVACMode.AUTO, "current_temperature", threshold_unit=UnitOfTemperature.CELSIUS, + attribute_required=True, ), ) async def test_temperature_climate_condition_behavior_any( @@ -217,6 +218,7 @@ async def test_temperature_climate_condition_behavior_any( HVACMode.AUTO, "current_temperature", threshold_unit=UnitOfTemperature.CELSIUS, + attribute_required=True, ), ) async def test_temperature_climate_condition_behavior_all( @@ -254,6 +256,7 @@ async def test_temperature_climate_condition_behavior_all( "eco", "current_temperature", threshold_unit=UnitOfTemperature.CELSIUS, + attribute_required=True, ), ) async def test_temperature_water_heater_condition_behavior_any( @@ -291,6 +294,7 @@ async def test_temperature_water_heater_condition_behavior_any( "eco", "current_temperature", threshold_unit=UnitOfTemperature.CELSIUS, + attribute_required=True, ), ) async def test_temperature_water_heater_condition_behavior_all( @@ -329,6 +333,7 @@ async def test_temperature_water_heater_condition_behavior_all( "temperature", threshold_unit=UnitOfTemperature.CELSIUS, unit_attributes=_WEATHER_UNIT_ATTRIBUTES, + attribute_required=True, ), ) async def test_temperature_weather_condition_behavior_any( @@ -367,6 +372,7 @@ async def test_temperature_weather_condition_behavior_any( "temperature", threshold_unit=UnitOfTemperature.CELSIUS, unit_attributes=_WEATHER_UNIT_ATTRIBUTES, + attribute_required=True, ), ) async def test_temperature_weather_condition_behavior_all( diff --git a/tests/components/temperature/test_trigger.py b/tests/components/temperature/test_trigger.py index 993bda6068b..8de4317a1f5 100644 --- a/tests/components/temperature/test_trigger.py +++ b/tests/components/temperature/test_trigger.py @@ -263,12 +263,14 @@ async def test_temperature_trigger_sensor_crossed_threshold_behavior_last( HVACMode.AUTO, CLIMATE_ATTR_CURRENT_TEMPERATURE, threshold_unit=UnitOfTemperature.CELSIUS, + attribute_required=True, ), *parametrize_numerical_attribute_crossed_threshold_trigger_states( "temperature.crossed_threshold", HVACMode.AUTO, CLIMATE_ATTR_CURRENT_TEMPERATURE, threshold_unit=UnitOfTemperature.CELSIUS, + attribute_required=True, ), ], ) @@ -308,6 +310,7 @@ async def test_temperature_trigger_climate_behavior_any( HVACMode.AUTO, CLIMATE_ATTR_CURRENT_TEMPERATURE, threshold_unit=UnitOfTemperature.CELSIUS, + attribute_required=True, ), ], ) @@ -347,6 +350,7 @@ async def test_temperature_trigger_climate_crossed_threshold_behavior_first( HVACMode.AUTO, CLIMATE_ATTR_CURRENT_TEMPERATURE, threshold_unit=UnitOfTemperature.CELSIUS, + attribute_required=True, ), ], ) @@ -389,12 +393,14 @@ async def test_temperature_trigger_climate_crossed_threshold_behavior_last( "eco", WATER_HEATER_ATTR_CURRENT_TEMPERATURE, threshold_unit=UnitOfTemperature.CELSIUS, + attribute_required=True, ), *parametrize_numerical_attribute_crossed_threshold_trigger_states( "temperature.crossed_threshold", "eco", WATER_HEATER_ATTR_CURRENT_TEMPERATURE, threshold_unit=UnitOfTemperature.CELSIUS, + attribute_required=True, ), ], ) @@ -434,6 +440,7 @@ async def test_temperature_trigger_water_heater_behavior_any( "eco", WATER_HEATER_ATTR_CURRENT_TEMPERATURE, threshold_unit=UnitOfTemperature.CELSIUS, + attribute_required=True, ), ], ) @@ -473,6 +480,7 @@ async def test_temperature_trigger_water_heater_crossed_threshold_behavior_first "eco", WATER_HEATER_ATTR_CURRENT_TEMPERATURE, threshold_unit=UnitOfTemperature.CELSIUS, + attribute_required=True, ), ], ) @@ -516,6 +524,7 @@ async def test_temperature_trigger_water_heater_crossed_threshold_behavior_last( ATTR_WEATHER_TEMPERATURE, threshold_unit=UnitOfTemperature.CELSIUS, unit_attributes=_WEATHER_UNIT_ATTRIBUTES, + attribute_required=True, ), *parametrize_numerical_attribute_crossed_threshold_trigger_states( "temperature.crossed_threshold", @@ -523,6 +532,7 @@ async def test_temperature_trigger_water_heater_crossed_threshold_behavior_last( ATTR_WEATHER_TEMPERATURE, threshold_unit=UnitOfTemperature.CELSIUS, unit_attributes=_WEATHER_UNIT_ATTRIBUTES, + attribute_required=True, ), ], ) @@ -563,6 +573,7 @@ async def test_temperature_trigger_weather_behavior_any( ATTR_WEATHER_TEMPERATURE, threshold_unit=UnitOfTemperature.CELSIUS, unit_attributes=_WEATHER_UNIT_ATTRIBUTES, + attribute_required=True, ), ], ) @@ -603,6 +614,7 @@ async def test_temperature_trigger_weather_crossed_threshold_behavior_first( ATTR_WEATHER_TEMPERATURE, threshold_unit=UnitOfTemperature.CELSIUS, unit_attributes=_WEATHER_UNIT_ATTRIBUTES, + attribute_required=True, ), ], )