mirror of
https://github.com/home-assistant/core.git
synced 2025-07-16 09:47:13 +00:00
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:
parent
17afe1ae51
commit
c41cf570d3
@ -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."""
|
||||
|
||||
|
@ -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."""
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user