Modify climate turn_on/off backwards compatibility check (#109195)

* Modify climate turn_on/off backwards compatibility check

* Fix logger message

* Comments

* Fix demo

* devolo

* ecobee

* Some more

* Fix missing feature flag

* some more

* and some more

* Remove demo change

* Add back demo change

* Fix demo

* Update comments
This commit is contained in:
G Johansson 2024-01-31 16:29:36 +01:00 committed by GitHub
parent 816c2e9500
commit ddb56fe20d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 71 additions and 85 deletions

View file

@ -301,6 +301,9 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
_attr_temperature_unit: str _attr_temperature_unit: str
__mod_supported_features: ClimateEntityFeature = ClimateEntityFeature(0) __mod_supported_features: ClimateEntityFeature = ClimateEntityFeature(0)
# Integrations should set `_enable_turn_on_off_backwards_compatibility` to False
# once migrated and set the feature flags TURN_ON/TURN_OFF as needed.
_enable_turn_on_off_backwards_compatibility: bool = True
def __getattribute__(self, __name: str) -> Any: def __getattribute__(self, __name: str) -> Any:
"""Get attribute. """Get attribute.
@ -345,7 +348,7 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
else: else:
message = ( message = (
"Entity %s (%s) implements HVACMode(s): %s and therefore implicitly" "Entity %s (%s) implements HVACMode(s): %s and therefore implicitly"
" supports the %s service without setting the proper" " supports the %s methods without setting the proper"
" ClimateEntityFeature. Please %s" " ClimateEntityFeature. Please %s"
) )
_LOGGER.warning( _LOGGER.warning(
@ -353,48 +356,44 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
self.entity_id, self.entity_id,
type(self), type(self),
feature, feature,
feature.lower(), method,
report_issue, report_issue,
) )
# Adds ClimateEntityFeature.TURN_OFF/TURN_ON depending on service calls implemented # Adds ClimateEntityFeature.TURN_OFF/TURN_ON depending on service calls implemented
# This should be removed in 2025.1. # This should be removed in 2025.1.
if not self.supported_features & ClimateEntityFeature.TURN_OFF: if self._enable_turn_on_off_backwards_compatibility is False:
if ( # Return if integration has migrated already
type(self).async_turn_off is not ClimateEntity.async_turn_off return
or type(self).turn_off is not ClimateEntity.turn_off
):
# turn_off implicitly supported by implementing turn_off method
_report_turn_on_off("TURN_OFF", "turn_off")
self.__mod_supported_features |= ( # pylint: disable=unused-private-member
ClimateEntityFeature.TURN_OFF
)
elif self.hvac_modes and HVACMode.OFF in self.hvac_modes:
# turn_off implicitly supported by including HVACMode.OFF
_report_turn_on_off("off", "turn_off")
self.__mod_supported_features |= ( # pylint: disable=unused-private-member
ClimateEntityFeature.TURN_OFF
)
if not self.supported_features & ClimateEntityFeature.TURN_ON: if not self.supported_features & ClimateEntityFeature.TURN_OFF and (
if ( type(self).async_turn_off is not ClimateEntity.async_turn_off
type(self).async_turn_on is not ClimateEntity.async_turn_on or type(self).turn_off is not ClimateEntity.turn_off
or type(self).turn_on is not ClimateEntity.turn_on ):
): # turn_off implicitly supported by implementing turn_off method
# turn_on implicitly supported by implementing turn_on method _report_turn_on_off("TURN_OFF", "turn_off")
_report_turn_on_off("TURN_ON", "turn_on") self.__mod_supported_features |= ( # pylint: disable=unused-private-member
self.__mod_supported_features |= ( # pylint: disable=unused-private-member ClimateEntityFeature.TURN_OFF
ClimateEntityFeature.TURN_ON )
)
elif self.hvac_modes and any( if not self.supported_features & ClimateEntityFeature.TURN_ON and (
_mode != HVACMode.OFF and _mode is not None for _mode in self.hvac_modes type(self).async_turn_on is not ClimateEntity.async_turn_on
): or type(self).turn_on is not ClimateEntity.turn_on
# turn_on implicitly supported by including any other HVACMode than HVACMode.OFF ):
_modes = [_mode for _mode in self.hvac_modes if _mode != HVACMode.OFF] # turn_on implicitly supported by implementing turn_on method
_report_turn_on_off(", ".join(_modes or []), "turn_on") _report_turn_on_off("TURN_ON", "turn_on")
self.__mod_supported_features |= ( # pylint: disable=unused-private-member self.__mod_supported_features |= ( # pylint: disable=unused-private-member
ClimateEntityFeature.TURN_ON ClimateEntityFeature.TURN_ON
) )
if (modes := self.hvac_modes) and len(modes) >= 2 and HVACMode.OFF in modes:
# turn_on/off implicitly supported by including more modes than 1 and one of these
# are HVACMode.OFF
_modes = [_mode for _mode in self.hvac_modes if _mode is not None]
_report_turn_on_off(", ".join(_modes or []), "turn_on/turn_off")
self.__mod_supported_features |= ( # pylint: disable=unused-private-member
ClimateEntityFeature.TURN_ON | ClimateEntityFeature.TURN_OFF
)
@final @final
@property @property

View file

@ -531,16 +531,9 @@ async def test_implicit_warning_not_implemented_turn_on_off_feature(
assert ( assert (
"Entity climate.test (<class 'tests.components.climate.test_init." "Entity climate.test (<class 'tests.components.climate.test_init."
"test_implicit_warning_not_implemented_turn_on_off_feature.<locals>.MockClimateEntityTest'>)" "test_implicit_warning_not_implemented_turn_on_off_feature.<locals>.MockClimateEntityTest'>)"
" implements HVACMode(s): off and therefore implicitly supports the off service without setting" " implements HVACMode(s): off, heat and therefore implicitly supports the turn_on/turn_off"
" the proper ClimateEntityFeature. Please report it to the author of the 'test' custom integration" " methods without setting the proper ClimateEntityFeature. Please report it to the author"
in caplog.text " of the 'test' custom integration" in caplog.text
)
assert (
"Entity climate.test (<class 'tests.components.climate.test_init."
"test_implicit_warning_not_implemented_turn_on_off_feature.<locals>.MockClimateEntityTest'>)"
" implements HVACMode(s): heat and therefore implicitly supports the heat service without setting"
" the proper ClimateEntityFeature. Please report it to the author of the 'test' custom integration"
in caplog.text
) )
@ -608,10 +601,6 @@ async def test_no_warning_implemented_turn_on_off_feature(
not in caplog.text not in caplog.text
) )
assert ( assert (
"implements HVACMode.off and therefore implicitly implements the off method without setting" " implements HVACMode(s): off, heat and therefore implicitly supports the off, heat methods"
not in caplog.text
)
assert (
"implements HVACMode.heat and therefore implicitly implements the heat method without setting"
not in caplog.text not in caplog.text
) )

View file

@ -9,7 +9,7 @@
]), ]),
'max_temp': 24, 'max_temp': 24,
'min_temp': 4, 'min_temp': 4,
'supported_features': <ClimateEntityFeature: 257>, 'supported_features': <ClimateEntityFeature: 1>,
'target_temp_step': 0.5, 'target_temp_step': 0.5,
'temperature': 20, 'temperature': 20,
}), }),
@ -52,7 +52,7 @@
'original_name': None, 'original_name': None,
'platform': 'devolo_home_control', 'platform': 'devolo_home_control',
'previous_unique_id': None, 'previous_unique_id': None,
'supported_features': <ClimateEntityFeature: 257>, 'supported_features': <ClimateEntityFeature: 1>,
'translation_key': None, 'translation_key': None,
'unique_id': 'Test', 'unique_id': 'Test',
'unit_of_measurement': None, 'unit_of_measurement': None,

View file

@ -12,7 +12,7 @@
]), ]),
'max_temp': 35.0, 'max_temp': 35.0,
'min_temp': 5.0, 'min_temp': 5.0,
'supported_features': <ClimateEntityFeature: 259>, 'supported_features': <ClimateEntityFeature: 3>,
'target_temp_high': 30.0, 'target_temp_high': 30.0,
'target_temp_low': 21.0, 'target_temp_low': 21.0,
'target_temp_step': 0.5, 'target_temp_step': 0.5,
@ -36,7 +36,7 @@
]), ]),
'max_temp': 35.0, 'max_temp': 35.0,
'min_temp': 5.0, 'min_temp': 5.0,
'supported_features': <ClimateEntityFeature: 259>, 'supported_features': <ClimateEntityFeature: 3>,
'target_temp_step': 0.5, 'target_temp_step': 0.5,
}), }),
'context': <ANY>, 'context': <ANY>,
@ -59,7 +59,7 @@
]), ]),
'max_temp': 35.0, 'max_temp': 35.0,
'min_temp': 5.0, 'min_temp': 5.0,
'supported_features': <ClimateEntityFeature: 259>, 'supported_features': <ClimateEntityFeature: 3>,
'target_temp_high': 30.0, 'target_temp_high': 30.0,
'target_temp_low': 21.0, 'target_temp_low': 21.0,
'target_temp_step': 0.5, 'target_temp_step': 0.5,
@ -83,7 +83,7 @@
]), ]),
'max_temp': 35.0, 'max_temp': 35.0,
'min_temp': 5.0, 'min_temp': 5.0,
'supported_features': <ClimateEntityFeature: 259>, 'supported_features': <ClimateEntityFeature: 3>,
'target_temp_step': 0.5, 'target_temp_step': 0.5,
}), }),
'context': <ANY>, 'context': <ANY>,
@ -112,7 +112,7 @@
]), ]),
'max_temp': 35.0, 'max_temp': 35.0,
'min_temp': 5.0, 'min_temp': 5.0,
'supported_features': <ClimateEntityFeature: 259>, 'supported_features': <ClimateEntityFeature: 3>,
'target_temp_high': 30.0, 'target_temp_high': 30.0,
'target_temp_low': 21.0, 'target_temp_low': 21.0,
'target_temp_step': 0.5, 'target_temp_step': 0.5,
@ -138,7 +138,7 @@
]), ]),
'max_temp': 35.0, 'max_temp': 35.0,
'min_temp': 5.0, 'min_temp': 5.0,
'supported_features': <ClimateEntityFeature: 259>, 'supported_features': <ClimateEntityFeature: 3>,
'target_temp_high': None, 'target_temp_high': None,
'target_temp_low': None, 'target_temp_low': None,
'target_temp_step': 0.5, 'target_temp_step': 0.5,
@ -164,7 +164,7 @@
]), ]),
'max_temp': 35.0, 'max_temp': 35.0,
'min_temp': 5.0, 'min_temp': 5.0,
'supported_features': <ClimateEntityFeature: 259>, 'supported_features': <ClimateEntityFeature: 3>,
'target_temp_high': None, 'target_temp_high': None,
'target_temp_low': None, 'target_temp_low': None,
'target_temp_step': 0.5, 'target_temp_step': 0.5,
@ -190,7 +190,7 @@
]), ]),
'max_temp': 35.0, 'max_temp': 35.0,
'min_temp': 5.0, 'min_temp': 5.0,
'supported_features': <ClimateEntityFeature: 259>, 'supported_features': <ClimateEntityFeature: 3>,
'target_temp_high': 30.0, 'target_temp_high': 30.0,
'target_temp_low': 21.0, 'target_temp_low': 21.0,
'target_temp_step': 0.5, 'target_temp_step': 0.5,
@ -216,7 +216,7 @@
]), ]),
'max_temp': 35.0, 'max_temp': 35.0,
'min_temp': 5.0, 'min_temp': 5.0,
'supported_features': <ClimateEntityFeature: 259>, 'supported_features': <ClimateEntityFeature: 3>,
'target_temp_high': 30.0, 'target_temp_high': 30.0,
'target_temp_low': 21.0, 'target_temp_low': 21.0,
'target_temp_step': 0.5, 'target_temp_step': 0.5,
@ -242,7 +242,7 @@
]), ]),
'max_temp': 35.0, 'max_temp': 35.0,
'min_temp': 5.0, 'min_temp': 5.0,
'supported_features': <ClimateEntityFeature: 259>, 'supported_features': <ClimateEntityFeature: 3>,
'target_temp_high': 30.0, 'target_temp_high': 30.0,
'target_temp_low': 21.0, 'target_temp_low': 21.0,
'target_temp_step': 0.5, 'target_temp_step': 0.5,
@ -268,7 +268,7 @@
]), ]),
'max_temp': 35.0, 'max_temp': 35.0,
'min_temp': 5.0, 'min_temp': 5.0,
'supported_features': <ClimateEntityFeature: 259>, 'supported_features': <ClimateEntityFeature: 3>,
'target_temp_high': None, 'target_temp_high': None,
'target_temp_low': None, 'target_temp_low': None,
'target_temp_step': 0.5, 'target_temp_step': 0.5,
@ -294,7 +294,7 @@
]), ]),
'max_temp': 35.0, 'max_temp': 35.0,
'min_temp': 5.0, 'min_temp': 5.0,
'supported_features': <ClimateEntityFeature: 259>, 'supported_features': <ClimateEntityFeature: 3>,
'target_temp_high': None, 'target_temp_high': None,
'target_temp_low': None, 'target_temp_low': None,
'target_temp_step': 0.5, 'target_temp_step': 0.5,
@ -320,7 +320,7 @@
]), ]),
'max_temp': 35.0, 'max_temp': 35.0,
'min_temp': 5.0, 'min_temp': 5.0,
'supported_features': <ClimateEntityFeature: 259>, 'supported_features': <ClimateEntityFeature: 3>,
'target_temp_high': 30.0, 'target_temp_high': 30.0,
'target_temp_low': 21.0, 'target_temp_low': 21.0,
'target_temp_step': 0.5, 'target_temp_step': 0.5,
@ -346,7 +346,7 @@
]), ]),
'max_temp': 35.0, 'max_temp': 35.0,
'min_temp': 5.0, 'min_temp': 5.0,
'supported_features': <ClimateEntityFeature: 259>, 'supported_features': <ClimateEntityFeature: 3>,
'target_temp_high': None, 'target_temp_high': None,
'target_temp_low': None, 'target_temp_low': None,
'target_temp_step': 0.5, 'target_temp_step': 0.5,
@ -372,7 +372,7 @@
]), ]),
'max_temp': 35.0, 'max_temp': 35.0,
'min_temp': 5.0, 'min_temp': 5.0,
'supported_features': <ClimateEntityFeature: 259>, 'supported_features': <ClimateEntityFeature: 3>,
'target_temp_high': None, 'target_temp_high': None,
'target_temp_low': None, 'target_temp_low': None,
'target_temp_step': 0.5, 'target_temp_step': 0.5,
@ -398,7 +398,7 @@
]), ]),
'max_temp': 35.0, 'max_temp': 35.0,
'min_temp': 5.0, 'min_temp': 5.0,
'supported_features': <ClimateEntityFeature: 259>, 'supported_features': <ClimateEntityFeature: 3>,
'target_temp_high': 30.0, 'target_temp_high': 30.0,
'target_temp_low': 21.0, 'target_temp_low': 21.0,
'target_temp_step': 0.5, 'target_temp_step': 0.5,
@ -424,7 +424,7 @@
]), ]),
'max_temp': 35.0, 'max_temp': 35.0,
'min_temp': 5.0, 'min_temp': 5.0,
'supported_features': <ClimateEntityFeature: 259>, 'supported_features': <ClimateEntityFeature: 3>,
'target_temp_high': 30.0, 'target_temp_high': 30.0,
'target_temp_low': 21.0, 'target_temp_low': 21.0,
'target_temp_step': 0.5, 'target_temp_step': 0.5,
@ -450,7 +450,7 @@
]), ]),
'max_temp': 35.0, 'max_temp': 35.0,
'min_temp': 5.0, 'min_temp': 5.0,
'supported_features': <ClimateEntityFeature: 259>, 'supported_features': <ClimateEntityFeature: 3>,
'target_temp_high': 30.0, 'target_temp_high': 30.0,
'target_temp_low': 21.0, 'target_temp_low': 21.0,
'target_temp_step': 0.5, 'target_temp_step': 0.5,
@ -476,7 +476,7 @@
]), ]),
'max_temp': 35.0, 'max_temp': 35.0,
'min_temp': 5.0, 'min_temp': 5.0,
'supported_features': <ClimateEntityFeature: 259>, 'supported_features': <ClimateEntityFeature: 3>,
'target_temp_high': None, 'target_temp_high': None,
'target_temp_low': None, 'target_temp_low': None,
'target_temp_step': 0.5, 'target_temp_step': 0.5,
@ -502,7 +502,7 @@
]), ]),
'max_temp': 35.0, 'max_temp': 35.0,
'min_temp': 5.0, 'min_temp': 5.0,
'supported_features': <ClimateEntityFeature: 259>, 'supported_features': <ClimateEntityFeature: 3>,
'target_temp_high': None, 'target_temp_high': None,
'target_temp_low': None, 'target_temp_low': None,
'target_temp_step': 0.5, 'target_temp_step': 0.5,

View file

@ -44,7 +44,7 @@ async def test_climate_thermostat_run(hass: HomeAssistant) -> None:
"min_temp": 5.0, "min_temp": 5.0,
"preset_mode": "Run Schedule", "preset_mode": "Run Schedule",
"preset_modes": ["Run Schedule", "Temporary Hold", "Permanent Hold"], "preset_modes": ["Run Schedule", "Temporary Hold", "Permanent Hold"],
"supported_features": 273, "supported_features": 17,
"temperature": 22.2, "temperature": 22.2,
} }
# Only test for a subset of attributes in case # Only test for a subset of attributes in case
@ -77,7 +77,7 @@ async def test_climate_thermostat_schedule_hold_unavailable(
"max_temp": 180.6, "max_temp": 180.6,
"min_temp": -6.1, "min_temp": -6.1,
"preset_modes": ["Run Schedule", "Temporary Hold", "Permanent Hold"], "preset_modes": ["Run Schedule", "Temporary Hold", "Permanent Hold"],
"supported_features": 273, "supported_features": 17,
} }
# Only test for a subset of attributes in case # Only test for a subset of attributes in case
# HA changes the implementation and a new one appears # HA changes the implementation and a new one appears
@ -110,7 +110,7 @@ async def test_climate_thermostat_schedule_hold_available(hass: HomeAssistant) -
"min_temp": -6.1, "min_temp": -6.1,
"preset_mode": "Run Schedule", "preset_mode": "Run Schedule",
"preset_modes": ["Run Schedule", "Temporary Hold", "Permanent Hold"], "preset_modes": ["Run Schedule", "Temporary Hold", "Permanent Hold"],
"supported_features": 273, "supported_features": 17,
"temperature": 26.1, "temperature": 26.1,
} }
# Only test for a subset of attributes in case # Only test for a subset of attributes in case
@ -144,7 +144,7 @@ async def test_climate_thermostat_schedule_temporary_hold(hass: HomeAssistant) -
"min_temp": -0.6, "min_temp": -0.6,
"preset_mode": "Run Schedule", "preset_mode": "Run Schedule",
"preset_modes": ["Run Schedule", "Temporary Hold", "Permanent Hold"], "preset_modes": ["Run Schedule", "Temporary Hold", "Permanent Hold"],
"supported_features": 273, "supported_features": 17,
"temperature": 37.2, "temperature": 37.2,
} }
# Only test for a subset of attributes in case # Only test for a subset of attributes in case

View file

@ -34,7 +34,7 @@ async def test_adam_climate_entity_attributes(
assert state.attributes["current_temperature"] == 20.9 assert state.attributes["current_temperature"] == 20.9
assert state.attributes["preset_mode"] == "home" assert state.attributes["preset_mode"] == "home"
assert state.attributes["supported_features"] == 273 assert state.attributes["supported_features"] == 17
assert state.attributes["temperature"] == 21.5 assert state.attributes["temperature"] == 21.5
assert state.attributes["min_temp"] == 0.0 assert state.attributes["min_temp"] == 0.0
assert state.attributes["max_temp"] == 35.0 assert state.attributes["max_temp"] == 35.0
@ -303,7 +303,7 @@ async def test_anna_climate_entity_attributes(
assert state.attributes["current_temperature"] == 19.3 assert state.attributes["current_temperature"] == 19.3
assert state.attributes["preset_mode"] == "home" assert state.attributes["preset_mode"] == "home"
assert state.attributes["supported_features"] == 274 assert state.attributes["supported_features"] == 18
assert state.attributes["target_temp_high"] == 30 assert state.attributes["target_temp_high"] == 30
assert state.attributes["target_temp_low"] == 20.5 assert state.attributes["target_temp_low"] == 20.5
assert state.attributes["min_temp"] == 4 assert state.attributes["min_temp"] == 4
@ -325,7 +325,7 @@ async def test_anna_2_climate_entity_attributes(
HVACMode.AUTO, HVACMode.AUTO,
HVACMode.HEAT_COOL, HVACMode.HEAT_COOL,
] ]
assert state.attributes["supported_features"] == 274 assert state.attributes["supported_features"] == 18
assert state.attributes["target_temp_high"] == 30 assert state.attributes["target_temp_high"] == 30
assert state.attributes["target_temp_low"] == 20.5 assert state.attributes["target_temp_low"] == 20.5

View file

@ -52,9 +52,7 @@ async def test_thermostat_update(
assert state.state == HVACMode.HEAT assert state.state == HVACMode.HEAT
assert ( assert (
state.attributes[ATTR_SUPPORTED_FEATURES] state.attributes[ATTR_SUPPORTED_FEATURES]
== ClimateEntityFeature.PRESET_MODE == ClimateEntityFeature.PRESET_MODE | ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.TURN_ON
) )
assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 38 assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 38
assert state.attributes[ATTR_TEMPERATURE] == 39 assert state.attributes[ATTR_TEMPERATURE] == 39

View file

@ -332,7 +332,7 @@ async def test_setpoint_thermostat(
assert state.attributes[ATTR_HVAC_MODES] == [HVACMode.HEAT] assert state.attributes[ATTR_HVAC_MODES] == [HVACMode.HEAT]
assert ( assert (
state.attributes[ATTR_SUPPORTED_FEATURES] state.attributes[ATTR_SUPPORTED_FEATURES]
== ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.TURN_ON == ClimateEntityFeature.TARGET_TEMPERATURE
) )
client.async_send_command_no_wait.reset_mock() client.async_send_command_no_wait.reset_mock()