From ddb56fe20d07c44a01eb73b3fde6b5fb21143b72 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Wed, 31 Jan 2024 16:29:36 +0100 Subject: [PATCH] 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 --- homeassistant/components/climate/__init__.py | 73 +++++++++---------- tests/components/climate/test_init.py | 19 +---- .../snapshots/test_climate.ambr | 4 +- .../nibe_heatpump/snapshots/test_climate.ambr | 40 +++++----- tests/components/nuheat/test_climate.py | 8 +- tests/components/plugwise/test_climate.py | 6 +- tests/components/smarttub/test_climate.py | 4 +- tests/components/zwave_js/test_climate.py | 2 +- 8 files changed, 71 insertions(+), 85 deletions(-) diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index 889ff8cddbd..43d98ad6bbd 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -301,6 +301,9 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): _attr_temperature_unit: str __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: """Get attribute. @@ -345,7 +348,7 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): else: message = ( "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" ) _LOGGER.warning( @@ -353,48 +356,44 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): self.entity_id, type(self), feature, - feature.lower(), + method, report_issue, ) # Adds ClimateEntityFeature.TURN_OFF/TURN_ON depending on service calls implemented # This should be removed in 2025.1. - if not self.supported_features & ClimateEntityFeature.TURN_OFF: - if ( - type(self).async_turn_off is not ClimateEntity.async_turn_off - 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 self._enable_turn_on_off_backwards_compatibility is False: + # Return if integration has migrated already + return - if not self.supported_features & ClimateEntityFeature.TURN_ON: - if ( - 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 implementing turn_on method - _report_turn_on_off("TURN_ON", "turn_on") - self.__mod_supported_features |= ( # pylint: disable=unused-private-member - ClimateEntityFeature.TURN_ON - ) - elif self.hvac_modes and any( - _mode != HVACMode.OFF and _mode is not None for _mode in self.hvac_modes - ): - # turn_on implicitly supported by including any other HVACMode than HVACMode.OFF - _modes = [_mode for _mode in self.hvac_modes if _mode != HVACMode.OFF] - _report_turn_on_off(", ".join(_modes or []), "turn_on") - self.__mod_supported_features |= ( # pylint: disable=unused-private-member - ClimateEntityFeature.TURN_ON - ) + if not self.supported_features & ClimateEntityFeature.TURN_OFF and ( + type(self).async_turn_off is not ClimateEntity.async_turn_off + 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 + ) + + if not self.supported_features & ClimateEntityFeature.TURN_ON and ( + 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 implementing turn_on method + _report_turn_on_off("TURN_ON", "turn_on") + self.__mod_supported_features |= ( # pylint: disable=unused-private-member + 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 @property diff --git a/tests/components/climate/test_init.py b/tests/components/climate/test_init.py index 03d571f8529..831a8503b79 100644 --- a/tests/components/climate/test_init.py +++ b/tests/components/climate/test_init.py @@ -531,16 +531,9 @@ async def test_implicit_warning_not_implemented_turn_on_off_feature( assert ( "Entity climate.test (.MockClimateEntityTest'>)" - " implements HVACMode(s): off and therefore implicitly supports the off service without setting" - " the proper ClimateEntityFeature. Please report it to the author of the 'test' custom integration" - in caplog.text - ) - assert ( - "Entity climate.test (.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 + " implements HVACMode(s): off, heat and therefore implicitly supports the turn_on/turn_off" + " methods 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 ) assert ( - "implements HVACMode.off and therefore implicitly implements the off method without setting" - not in caplog.text - ) - assert ( - "implements HVACMode.heat and therefore implicitly implements the heat method without setting" + " implements HVACMode(s): off, heat and therefore implicitly supports the off, heat methods" not in caplog.text ) diff --git a/tests/components/devolo_home_control/snapshots/test_climate.ambr b/tests/components/devolo_home_control/snapshots/test_climate.ambr index 26effb7cac6..0e7c5ba547e 100644 --- a/tests/components/devolo_home_control/snapshots/test_climate.ambr +++ b/tests/components/devolo_home_control/snapshots/test_climate.ambr @@ -9,7 +9,7 @@ ]), 'max_temp': 24, 'min_temp': 4, - 'supported_features': , + 'supported_features': , 'target_temp_step': 0.5, 'temperature': 20, }), @@ -52,7 +52,7 @@ 'original_name': None, 'platform': 'devolo_home_control', 'previous_unique_id': None, - 'supported_features': , + 'supported_features': , 'translation_key': None, 'unique_id': 'Test', 'unit_of_measurement': None, diff --git a/tests/components/nibe_heatpump/snapshots/test_climate.ambr b/tests/components/nibe_heatpump/snapshots/test_climate.ambr index d7fced91e68..f19fd69c47d 100644 --- a/tests/components/nibe_heatpump/snapshots/test_climate.ambr +++ b/tests/components/nibe_heatpump/snapshots/test_climate.ambr @@ -12,7 +12,7 @@ ]), 'max_temp': 35.0, 'min_temp': 5.0, - 'supported_features': , + 'supported_features': , 'target_temp_high': 30.0, 'target_temp_low': 21.0, 'target_temp_step': 0.5, @@ -36,7 +36,7 @@ ]), 'max_temp': 35.0, 'min_temp': 5.0, - 'supported_features': , + 'supported_features': , 'target_temp_step': 0.5, }), 'context': , @@ -59,7 +59,7 @@ ]), 'max_temp': 35.0, 'min_temp': 5.0, - 'supported_features': , + 'supported_features': , 'target_temp_high': 30.0, 'target_temp_low': 21.0, 'target_temp_step': 0.5, @@ -83,7 +83,7 @@ ]), 'max_temp': 35.0, 'min_temp': 5.0, - 'supported_features': , + 'supported_features': , 'target_temp_step': 0.5, }), 'context': , @@ -112,7 +112,7 @@ ]), 'max_temp': 35.0, 'min_temp': 5.0, - 'supported_features': , + 'supported_features': , 'target_temp_high': 30.0, 'target_temp_low': 21.0, 'target_temp_step': 0.5, @@ -138,7 +138,7 @@ ]), 'max_temp': 35.0, 'min_temp': 5.0, - 'supported_features': , + 'supported_features': , 'target_temp_high': None, 'target_temp_low': None, 'target_temp_step': 0.5, @@ -164,7 +164,7 @@ ]), 'max_temp': 35.0, 'min_temp': 5.0, - 'supported_features': , + 'supported_features': , 'target_temp_high': None, 'target_temp_low': None, 'target_temp_step': 0.5, @@ -190,7 +190,7 @@ ]), 'max_temp': 35.0, 'min_temp': 5.0, - 'supported_features': , + 'supported_features': , 'target_temp_high': 30.0, 'target_temp_low': 21.0, 'target_temp_step': 0.5, @@ -216,7 +216,7 @@ ]), 'max_temp': 35.0, 'min_temp': 5.0, - 'supported_features': , + 'supported_features': , 'target_temp_high': 30.0, 'target_temp_low': 21.0, 'target_temp_step': 0.5, @@ -242,7 +242,7 @@ ]), 'max_temp': 35.0, 'min_temp': 5.0, - 'supported_features': , + 'supported_features': , 'target_temp_high': 30.0, 'target_temp_low': 21.0, 'target_temp_step': 0.5, @@ -268,7 +268,7 @@ ]), 'max_temp': 35.0, 'min_temp': 5.0, - 'supported_features': , + 'supported_features': , 'target_temp_high': None, 'target_temp_low': None, 'target_temp_step': 0.5, @@ -294,7 +294,7 @@ ]), 'max_temp': 35.0, 'min_temp': 5.0, - 'supported_features': , + 'supported_features': , 'target_temp_high': None, 'target_temp_low': None, 'target_temp_step': 0.5, @@ -320,7 +320,7 @@ ]), 'max_temp': 35.0, 'min_temp': 5.0, - 'supported_features': , + 'supported_features': , 'target_temp_high': 30.0, 'target_temp_low': 21.0, 'target_temp_step': 0.5, @@ -346,7 +346,7 @@ ]), 'max_temp': 35.0, 'min_temp': 5.0, - 'supported_features': , + 'supported_features': , 'target_temp_high': None, 'target_temp_low': None, 'target_temp_step': 0.5, @@ -372,7 +372,7 @@ ]), 'max_temp': 35.0, 'min_temp': 5.0, - 'supported_features': , + 'supported_features': , 'target_temp_high': None, 'target_temp_low': None, 'target_temp_step': 0.5, @@ -398,7 +398,7 @@ ]), 'max_temp': 35.0, 'min_temp': 5.0, - 'supported_features': , + 'supported_features': , 'target_temp_high': 30.0, 'target_temp_low': 21.0, 'target_temp_step': 0.5, @@ -424,7 +424,7 @@ ]), 'max_temp': 35.0, 'min_temp': 5.0, - 'supported_features': , + 'supported_features': , 'target_temp_high': 30.0, 'target_temp_low': 21.0, 'target_temp_step': 0.5, @@ -450,7 +450,7 @@ ]), 'max_temp': 35.0, 'min_temp': 5.0, - 'supported_features': , + 'supported_features': , 'target_temp_high': 30.0, 'target_temp_low': 21.0, 'target_temp_step': 0.5, @@ -476,7 +476,7 @@ ]), 'max_temp': 35.0, 'min_temp': 5.0, - 'supported_features': , + 'supported_features': , 'target_temp_high': None, 'target_temp_low': None, 'target_temp_step': 0.5, @@ -502,7 +502,7 @@ ]), 'max_temp': 35.0, 'min_temp': 5.0, - 'supported_features': , + 'supported_features': , 'target_temp_high': None, 'target_temp_low': None, 'target_temp_step': 0.5, diff --git a/tests/components/nuheat/test_climate.py b/tests/components/nuheat/test_climate.py index 73a07efcc3d..7a0e21485c8 100644 --- a/tests/components/nuheat/test_climate.py +++ b/tests/components/nuheat/test_climate.py @@ -44,7 +44,7 @@ async def test_climate_thermostat_run(hass: HomeAssistant) -> None: "min_temp": 5.0, "preset_mode": "Run Schedule", "preset_modes": ["Run Schedule", "Temporary Hold", "Permanent Hold"], - "supported_features": 273, + "supported_features": 17, "temperature": 22.2, } # 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, "min_temp": -6.1, "preset_modes": ["Run Schedule", "Temporary Hold", "Permanent Hold"], - "supported_features": 273, + "supported_features": 17, } # Only test for a subset of attributes in case # 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, "preset_mode": "Run Schedule", "preset_modes": ["Run Schedule", "Temporary Hold", "Permanent Hold"], - "supported_features": 273, + "supported_features": 17, "temperature": 26.1, } # 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, "preset_mode": "Run Schedule", "preset_modes": ["Run Schedule", "Temporary Hold", "Permanent Hold"], - "supported_features": 273, + "supported_features": 17, "temperature": 37.2, } # Only test for a subset of attributes in case diff --git a/tests/components/plugwise/test_climate.py b/tests/components/plugwise/test_climate.py index 1be4cc2a34f..c5ab3a209c2 100644 --- a/tests/components/plugwise/test_climate.py +++ b/tests/components/plugwise/test_climate.py @@ -34,7 +34,7 @@ async def test_adam_climate_entity_attributes( assert state.attributes["current_temperature"] == 20.9 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["min_temp"] == 0.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["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_low"] == 20.5 assert state.attributes["min_temp"] == 4 @@ -325,7 +325,7 @@ async def test_anna_2_climate_entity_attributes( HVACMode.AUTO, 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_low"] == 20.5 diff --git a/tests/components/smarttub/test_climate.py b/tests/components/smarttub/test_climate.py index cafb156d113..40e3c05b509 100644 --- a/tests/components/smarttub/test_climate.py +++ b/tests/components/smarttub/test_climate.py @@ -52,9 +52,7 @@ async def test_thermostat_update( assert state.state == HVACMode.HEAT assert ( state.attributes[ATTR_SUPPORTED_FEATURES] - == ClimateEntityFeature.PRESET_MODE - | ClimateEntityFeature.TARGET_TEMPERATURE - | ClimateEntityFeature.TURN_ON + == ClimateEntityFeature.PRESET_MODE | ClimateEntityFeature.TARGET_TEMPERATURE ) assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 38 assert state.attributes[ATTR_TEMPERATURE] == 39 diff --git a/tests/components/zwave_js/test_climate.py b/tests/components/zwave_js/test_climate.py index 5f75f7c8307..fdbb2ef7f4c 100644 --- a/tests/components/zwave_js/test_climate.py +++ b/tests/components/zwave_js/test_climate.py @@ -332,7 +332,7 @@ async def test_setpoint_thermostat( assert state.attributes[ATTR_HVAC_MODES] == [HVACMode.HEAT] assert ( state.attributes[ATTR_SUPPORTED_FEATURES] - == ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.TURN_ON + == ClimateEntityFeature.TARGET_TEMPERATURE ) client.async_send_command_no_wait.reset_mock()