Remove deprecated supported features warning in ClimateEntity (#132206)

* Remove deprecated features from ClimateEntity

* Remove not needed tests

* Remove add_to_platform_start
This commit is contained in:
G Johansson 2024-12-05 20:37:17 +01:00 committed by GitHub
parent 17afe1ae51
commit c41cf570d3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 2 additions and 402 deletions

View File

@ -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."""

View File

@ -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 (<class 'tests.custom_components.climate.test_init."
"test_warning_not_implemented_turn_on_off_feature.<locals>.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 (<class 'tests.custom_components.climate.test_init."
"test_warning_not_implemented_turn_on_off_feature.<locals>.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 (<class 'tests.custom_components.climate.test_init."
"test_implicit_warning_not_implemented_turn_on_off_feature.<locals>.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."""