From c41cf570d3f312ec3e1f5d0701b789d3e45771ff Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 5 Dec 2024 20:37:17 +0100 Subject: [PATCH] Remove deprecated supported features warning in `ClimateEntity` (#132206) * Remove deprecated features from ClimateEntity * Remove not needed tests * Remove add_to_platform_start --- homeassistant/components/climate/__init__.py | 111 ------- tests/components/climate/test_init.py | 293 +------------------ 2 files changed, 2 insertions(+), 402 deletions(-) diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index 045003dcd0f..ca85979f19a 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -2,7 +2,6 @@ from __future__ import annotations -import asyncio from datetime import timedelta import functools as ft import logging @@ -28,7 +27,6 @@ from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import config_validation as cv, issue_registry as ir from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.entity_platform import EntityPlatform from homeassistant.helpers.temperature import display_temp as show_temp from homeassistant.helpers.typing import ConfigType from homeassistant.loader import async_get_issue_tracker, async_suggest_report_issue @@ -303,115 +301,6 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): __climate_reported_legacy_aux = False - __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. - - Modify return of `supported_features` to - include `_mod_supported_features` if attribute is set. - """ - if name != "supported_features": - return super().__getattribute__(name) - - # Convert the supported features to ClimateEntityFeature. - # Remove this compatibility shim in 2025.1 or later. - _supported_features: ClimateEntityFeature = super().__getattribute__( - "supported_features" - ) - _mod_supported_features: ClimateEntityFeature = super().__getattribute__( - "_ClimateEntity__mod_supported_features" - ) - if type(_supported_features) is int: # noqa: E721 - _features = ClimateEntityFeature(_supported_features) - self._report_deprecated_supported_features_values(_features) - else: - _features = _supported_features - - if not _mod_supported_features: - return _features - - # Add automatically calculated ClimateEntityFeature.TURN_OFF/TURN_ON to - # supported features and return it - return _features | _mod_supported_features - - @callback - def add_to_platform_start( - self, - hass: HomeAssistant, - platform: EntityPlatform, - parallel_updates: asyncio.Semaphore | None, - ) -> None: - """Start adding an entity to a platform.""" - super().add_to_platform_start(hass, platform, parallel_updates) - - def _report_turn_on_off(feature: str, method: str) -> None: - """Log warning not implemented turn on/off feature.""" - report_issue = self._suggest_report_issue() - if feature.startswith("TURN"): - message = ( - "Entity %s (%s) does not set ClimateEntityFeature.%s" - " but implements the %s method. Please %s" - ) - else: - message = ( - "Entity %s (%s) implements HVACMode(s): %s and therefore implicitly" - " supports the %s methods without setting the proper" - " ClimateEntityFeature. Please %s" - ) - _LOGGER.warning( - message, - self.entity_id, - type(self), - feature, - method, - report_issue, - ) - - # Adds ClimateEntityFeature.TURN_OFF/TURN_ON depending on service calls implemented - # This should be removed in 2025.1. - if self._enable_turn_on_off_backwards_compatibility is False: - # Return if integration has migrated already - return - - supported_features = self.supported_features - if supported_features & CHECK_TURN_ON_OFF_FEATURE_FLAG: - # The entity supports both turn_on and turn_off, the backwards compatibility - # checks are not needed - return - - if not 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 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 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 - ) - def _report_legacy_aux(self) -> None: """Log warning and create an issue if the entity implements legacy auxiliary heater.""" diff --git a/tests/components/climate/test_init.py b/tests/components/climate/test_init.py index a7f47668612..8851b2d60c5 100644 --- a/tests/components/climate/test_init.py +++ b/tests/components/climate/test_init.py @@ -4,7 +4,7 @@ from __future__ import annotations from enum import Enum from typing import Any -from unittest.mock import MagicMock, Mock, patch +from unittest.mock import MagicMock, Mock import pytest import voluptuous as vol @@ -38,13 +38,7 @@ from homeassistant.components.climate.const import ( ClimateEntityFeature, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_TEMPERATURE, - PRECISION_WHOLE, - SERVICE_TURN_OFF, - SERVICE_TURN_ON, - UnitOfTemperature, -) +from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import issue_registry as ir @@ -430,289 +424,6 @@ async def test_mode_validation( assert exc.value.translation_key == "not_valid_fan_mode" -@pytest.mark.parametrize( - "supported_features_at_int", - [ - ClimateEntityFeature.TARGET_TEMPERATURE.value, - ClimateEntityFeature.TARGET_TEMPERATURE.value - | ClimateEntityFeature.TURN_ON.value - | ClimateEntityFeature.TURN_OFF.value, - ], -) -def test_deprecated_supported_features_ints( - caplog: pytest.LogCaptureFixture, supported_features_at_int: int -) -> None: - """Test deprecated supported features ints.""" - - class MockClimateEntity(ClimateEntity): - @property - def supported_features(self) -> int: - """Return supported features.""" - return supported_features_at_int - - entity = MockClimateEntity() - assert entity.supported_features is ClimateEntityFeature(supported_features_at_int) - assert "MockClimateEntity" in caplog.text - assert "is using deprecated supported features values" in caplog.text - assert "Instead it should use" in caplog.text - assert "ClimateEntityFeature.TARGET_TEMPERATURE" in caplog.text - caplog.clear() - assert entity.supported_features is ClimateEntityFeature(supported_features_at_int) - assert "is using deprecated supported features values" not in caplog.text - - -async def test_warning_not_implemented_turn_on_off_feature( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - register_test_integration: MockConfigEntry, -) -> None: - """Test adding feature flag and warn if missing when methods are set.""" - - called = [] - - class MockClimateEntityTest(MockClimateEntity): - """Mock Climate device.""" - - def turn_on(self) -> None: - """Turn on.""" - called.append("turn_on") - - def turn_off(self) -> None: - """Turn off.""" - called.append("turn_off") - - climate_entity = MockClimateEntityTest(name="test", entity_id="climate.test") - - with patch.object( - MockClimateEntityTest, "__module__", "tests.custom_components.climate.test_init" - ): - setup_test_component_platform( - hass, DOMAIN, entities=[climate_entity], from_config_entry=True - ) - await hass.config_entries.async_setup(register_test_integration.entry_id) - await hass.async_block_till_done() - - state = hass.states.get("climate.test") - assert state is not None - - assert ( - "Entity climate.test (.MockClimateEntityTest'>)" - " does not set ClimateEntityFeature.TURN_OFF but implements the turn_off method." - " Please report it to the author of the 'test' custom integration" - in caplog.text - ) - assert ( - "Entity climate.test (.MockClimateEntityTest'>)" - " does not set ClimateEntityFeature.TURN_ON but implements the turn_on method." - " Please report it to the author of the 'test' custom integration" - in caplog.text - ) - - await hass.services.async_call( - DOMAIN, - SERVICE_TURN_ON, - { - "entity_id": "climate.test", - }, - blocking=True, - ) - await hass.services.async_call( - DOMAIN, - SERVICE_TURN_OFF, - { - "entity_id": "climate.test", - }, - blocking=True, - ) - - assert len(called) == 2 - assert "turn_on" in called - assert "turn_off" in called - - -async def test_implicit_warning_not_implemented_turn_on_off_feature( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - register_test_integration: MockConfigEntry, -) -> None: - """Test adding feature flag and warn if missing when methods are not set. - - (implicit by hvac mode) - """ - - class MockClimateEntityTest(MockEntity, ClimateEntity): - """Mock Climate device.""" - - _attr_temperature_unit = UnitOfTemperature.CELSIUS - - @property - def hvac_mode(self) -> HVACMode: - """Return hvac operation ie. heat, cool mode. - - Need to be one of HVACMode.*. - """ - return HVACMode.HEAT - - @property - def hvac_modes(self) -> list[HVACMode]: - """Return the list of available hvac operation modes. - - Need to be a subset of HVAC_MODES. - """ - return [HVACMode.OFF, HVACMode.HEAT] - - climate_entity = MockClimateEntityTest(name="test", entity_id="climate.test") - - with patch.object( - MockClimateEntityTest, "__module__", "tests.custom_components.climate.test_init" - ): - setup_test_component_platform( - hass, DOMAIN, entities=[climate_entity], from_config_entry=True - ) - await hass.config_entries.async_setup(register_test_integration.entry_id) - await hass.async_block_till_done() - - state = hass.states.get("climate.test") - assert state is not None - - assert ( - "Entity climate.test (.MockClimateEntityTest'>)" - " 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 - ) - - -async def test_no_warning_implemented_turn_on_off_feature( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - register_test_integration: MockConfigEntry, -) -> None: - """Test no warning when feature flags are set.""" - - class MockClimateEntityTest(MockClimateEntity): - """Mock Climate device.""" - - _attr_supported_features = ( - ClimateEntityFeature.FAN_MODE - | ClimateEntityFeature.PRESET_MODE - | ClimateEntityFeature.SWING_MODE - | ClimateEntityFeature.TURN_OFF - | ClimateEntityFeature.TURN_ON - ) - - climate_entity = MockClimateEntityTest(name="test", entity_id="climate.test") - - with patch.object( - MockClimateEntityTest, "__module__", "tests.custom_components.climate.test_init" - ): - setup_test_component_platform( - hass, DOMAIN, entities=[climate_entity], from_config_entry=True - ) - await hass.config_entries.async_setup(register_test_integration.entry_id) - await hass.async_block_till_done() - - state = hass.states.get("climate.test") - assert state is not None - - assert ( - "does not set ClimateEntityFeature.TURN_OFF but implements the turn_off method." - not in caplog.text - ) - assert ( - "does not set ClimateEntityFeature.TURN_ON but implements the turn_on method." - not in caplog.text - ) - assert ( - " implements HVACMode(s): off, heat and therefore implicitly supports the off, heat methods" - not in caplog.text - ) - - -async def test_no_warning_integration_has_migrated( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - register_test_integration: MockConfigEntry, -) -> None: - """Test no warning when integration migrated using `_enable_turn_on_off_backwards_compatibility`.""" - - class MockClimateEntityTest(MockClimateEntity): - """Mock Climate device.""" - - _enable_turn_on_off_backwards_compatibility = False - _attr_supported_features = ( - ClimateEntityFeature.FAN_MODE - | ClimateEntityFeature.PRESET_MODE - | ClimateEntityFeature.SWING_MODE - ) - - climate_entity = MockClimateEntityTest(name="test", entity_id="climate.test") - - with patch.object( - MockClimateEntityTest, "__module__", "tests.custom_components.climate.test_init" - ): - setup_test_component_platform( - hass, DOMAIN, entities=[climate_entity], from_config_entry=True - ) - await hass.config_entries.async_setup(register_test_integration.entry_id) - await hass.async_block_till_done() - - state = hass.states.get("climate.test") - assert state is not None - - assert ( - "does not set ClimateEntityFeature.TURN_OFF but implements the turn_off method." - not in caplog.text - ) - assert ( - "does not set ClimateEntityFeature.TURN_ON but implements the turn_on method." - not in caplog.text - ) - assert ( - " implements HVACMode(s): off, heat and therefore implicitly supports the off, heat methods" - not in caplog.text - ) - - -async def test_no_warning_integration_implement_feature_flags( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - register_test_integration: MockConfigEntry, -) -> None: - """Test no warning when integration uses the correct feature flags.""" - - class MockClimateEntityTest(MockClimateEntity): - """Mock Climate device.""" - - _attr_supported_features = ( - ClimateEntityFeature.FAN_MODE - | ClimateEntityFeature.PRESET_MODE - | ClimateEntityFeature.SWING_MODE - | ClimateEntityFeature.TURN_OFF - | ClimateEntityFeature.TURN_ON - ) - - climate_entity = MockClimateEntityTest(name="test", entity_id="climate.test") - - with patch.object( - MockClimateEntityTest, "__module__", "tests.custom_components.climate.test_init" - ): - setup_test_component_platform( - hass, DOMAIN, entities=[climate_entity], from_config_entry=True - ) - await hass.config_entries.async_setup(register_test_integration.entry_id) - await hass.async_block_till_done() - - state = hass.states.get("climate.test") - assert state is not None - - assert "does not set ClimateEntityFeature" not in caplog.text - assert "implements HVACMode(s):" not in caplog.text - - async def test_turn_on_off_toggle(hass: HomeAssistant) -> None: """Test turn_on/turn_off/toggle methods."""