Revert "Remove deprecated supported features warning in ..." (multiple) (#134933)

This commit is contained in:
Franck Nijhof 2025-01-07 00:08:02 +01:00
parent 7a5525951d
commit 298f059488
No known key found for this signature in database
GPG Key ID: D62583BA8AB11CA3
10 changed files with 221 additions and 27 deletions

View File

@ -516,6 +516,19 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
"""Flag supported features."""
return self._attr_supported_features
@property
def supported_features_compat(self) -> CameraEntityFeature:
"""Return the supported features as CameraEntityFeature.
Remove this compatibility shim in 2025.1 or later.
"""
features = self.supported_features
if type(features) is int: # noqa: E721
new_features = CameraEntityFeature(features)
self._report_deprecated_supported_features_values(new_features)
return new_features
return features
@cached_property
def is_recording(self) -> bool:
"""Return true if the device is recording."""
@ -569,7 +582,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
self._deprecate_attr_frontend_stream_type_logged = True
return self._attr_frontend_stream_type
if CameraEntityFeature.STREAM not in self.supported_features:
if CameraEntityFeature.STREAM not in self.supported_features_compat:
return None
if (
self._webrtc_provider
@ -798,7 +811,9 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
async def async_internal_added_to_hass(self) -> None:
"""Run when entity about to be added to hass."""
await super().async_internal_added_to_hass()
self.__supports_stream = self.supported_features & CameraEntityFeature.STREAM
self.__supports_stream = (
self.supported_features_compat & CameraEntityFeature.STREAM
)
await self.async_refresh_providers(write_state=False)
async def async_refresh_providers(self, *, write_state: bool = True) -> None:
@ -838,7 +853,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
self, fn: Callable[[HomeAssistant, Camera], Coroutine[None, None, _T | None]]
) -> _T | None:
"""Get first provider that supports this camera."""
if CameraEntityFeature.STREAM not in self.supported_features:
if CameraEntityFeature.STREAM not in self.supported_features_compat:
return None
return await fn(self.hass, self)
@ -896,7 +911,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
def camera_capabilities(self) -> CameraCapabilities:
"""Return the camera capabilities."""
frontend_stream_types = set()
if CameraEntityFeature.STREAM in self.supported_features:
if CameraEntityFeature.STREAM in self.supported_features_compat:
if self._supports_native_sync_webrtc or self._supports_native_async_webrtc:
# The camera has a native WebRTC implementation
frontend_stream_types.add(StreamType.WEB_RTC)
@ -916,7 +931,8 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
"""
super().async_write_ha_state()
if self.__supports_stream != (
supports_stream := self.supported_features & CameraEntityFeature.STREAM
supports_stream := self.supported_features_compat
& CameraEntityFeature.STREAM
):
self.__supports_stream = supports_stream
self._invalidate_camera_capabilities_cache()

View File

@ -300,6 +300,10 @@ class CoverEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
def supported_features(self) -> CoverEntityFeature:
"""Flag supported features."""
if (features := self._attr_supported_features) is not None:
if type(features) is int: # noqa: E721
new_features = CoverEntityFeature(features)
self._report_deprecated_supported_features_values(new_features)
return new_features
return features
supported_features = (

View File

@ -773,6 +773,19 @@ class MediaPlayerEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
"""Flag media player features that are supported."""
return self._attr_supported_features
@property
def supported_features_compat(self) -> MediaPlayerEntityFeature:
"""Return the supported features as MediaPlayerEntityFeature.
Remove this compatibility shim in 2025.1 or later.
"""
features = self.supported_features
if type(features) is int: # noqa: E721
new_features = MediaPlayerEntityFeature(features)
self._report_deprecated_supported_features_values(new_features)
return new_features
return features
def turn_on(self) -> None:
"""Turn the media player on."""
raise NotImplementedError
@ -912,85 +925,87 @@ class MediaPlayerEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
@property
def support_play(self) -> bool:
"""Boolean if play is supported."""
return MediaPlayerEntityFeature.PLAY in self.supported_features
return MediaPlayerEntityFeature.PLAY in self.supported_features_compat
@final
@property
def support_pause(self) -> bool:
"""Boolean if pause is supported."""
return MediaPlayerEntityFeature.PAUSE in self.supported_features
return MediaPlayerEntityFeature.PAUSE in self.supported_features_compat
@final
@property
def support_stop(self) -> bool:
"""Boolean if stop is supported."""
return MediaPlayerEntityFeature.STOP in self.supported_features
return MediaPlayerEntityFeature.STOP in self.supported_features_compat
@final
@property
def support_seek(self) -> bool:
"""Boolean if seek is supported."""
return MediaPlayerEntityFeature.SEEK in self.supported_features
return MediaPlayerEntityFeature.SEEK in self.supported_features_compat
@final
@property
def support_volume_set(self) -> bool:
"""Boolean if setting volume is supported."""
return MediaPlayerEntityFeature.VOLUME_SET in self.supported_features
return MediaPlayerEntityFeature.VOLUME_SET in self.supported_features_compat
@final
@property
def support_volume_mute(self) -> bool:
"""Boolean if muting volume is supported."""
return MediaPlayerEntityFeature.VOLUME_MUTE in self.supported_features
return MediaPlayerEntityFeature.VOLUME_MUTE in self.supported_features_compat
@final
@property
def support_previous_track(self) -> bool:
"""Boolean if previous track command supported."""
return MediaPlayerEntityFeature.PREVIOUS_TRACK in self.supported_features
return MediaPlayerEntityFeature.PREVIOUS_TRACK in self.supported_features_compat
@final
@property
def support_next_track(self) -> bool:
"""Boolean if next track command supported."""
return MediaPlayerEntityFeature.NEXT_TRACK in self.supported_features
return MediaPlayerEntityFeature.NEXT_TRACK in self.supported_features_compat
@final
@property
def support_play_media(self) -> bool:
"""Boolean if play media command supported."""
return MediaPlayerEntityFeature.PLAY_MEDIA in self.supported_features
return MediaPlayerEntityFeature.PLAY_MEDIA in self.supported_features_compat
@final
@property
def support_select_source(self) -> bool:
"""Boolean if select source command supported."""
return MediaPlayerEntityFeature.SELECT_SOURCE in self.supported_features
return MediaPlayerEntityFeature.SELECT_SOURCE in self.supported_features_compat
@final
@property
def support_select_sound_mode(self) -> bool:
"""Boolean if select sound mode command supported."""
return MediaPlayerEntityFeature.SELECT_SOUND_MODE in self.supported_features
return (
MediaPlayerEntityFeature.SELECT_SOUND_MODE in self.supported_features_compat
)
@final
@property
def support_clear_playlist(self) -> bool:
"""Boolean if clear playlist command supported."""
return MediaPlayerEntityFeature.CLEAR_PLAYLIST in self.supported_features
return MediaPlayerEntityFeature.CLEAR_PLAYLIST in self.supported_features_compat
@final
@property
def support_shuffle_set(self) -> bool:
"""Boolean if shuffle is supported."""
return MediaPlayerEntityFeature.SHUFFLE_SET in self.supported_features
return MediaPlayerEntityFeature.SHUFFLE_SET in self.supported_features_compat
@final
@property
def support_grouping(self) -> bool:
"""Boolean if player grouping is supported."""
return MediaPlayerEntityFeature.GROUPING in self.supported_features
return MediaPlayerEntityFeature.GROUPING in self.supported_features_compat
async def async_toggle(self) -> None:
"""Toggle the power on the media player."""
@ -1019,7 +1034,7 @@ class MediaPlayerEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
if (
self.volume_level is not None
and self.volume_level < 1
and MediaPlayerEntityFeature.VOLUME_SET in self.supported_features
and MediaPlayerEntityFeature.VOLUME_SET in self.supported_features_compat
):
await self.async_set_volume_level(
min(1, self.volume_level + self.volume_step)
@ -1037,7 +1052,7 @@ class MediaPlayerEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
if (
self.volume_level is not None
and self.volume_level > 0
and MediaPlayerEntityFeature.VOLUME_SET in self.supported_features
and MediaPlayerEntityFeature.VOLUME_SET in self.supported_features_compat
):
await self.async_set_volume_level(
max(0, self.volume_level - self.volume_step)
@ -1080,7 +1095,7 @@ class MediaPlayerEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
def capability_attributes(self) -> dict[str, Any]:
"""Return capability attributes."""
data: dict[str, Any] = {}
supported_features = self.supported_features
supported_features = self.supported_features_compat
if (
source_list := self.source_list
@ -1286,7 +1301,7 @@ async def websocket_browse_media(
connection.send_error(msg["id"], "entity_not_found", "Entity not found")
return
if MediaPlayerEntityFeature.BROWSE_MEDIA not in player.supported_features:
if MediaPlayerEntityFeature.BROWSE_MEDIA not in player.supported_features_compat:
connection.send_message(
websocket_api.error_message(
msg["id"], ERR_NOT_SUPPORTED, "Player does not support browsing media"

View File

@ -312,7 +312,7 @@ class StateVacuumEntity(
@property
def capability_attributes(self) -> dict[str, Any] | None:
"""Return capability attributes."""
if VacuumEntityFeature.FAN_SPEED in self.supported_features:
if VacuumEntityFeature.FAN_SPEED in self.supported_features_compat:
return {ATTR_FAN_SPEED_LIST: self.fan_speed_list}
return None
@ -330,7 +330,7 @@ class StateVacuumEntity(
def state_attributes(self) -> dict[str, Any]:
"""Return the state attributes of the vacuum cleaner."""
data: dict[str, Any] = {}
supported_features = self.supported_features
supported_features = self.supported_features_compat
if VacuumEntityFeature.BATTERY in supported_features:
data[ATTR_BATTERY_LEVEL] = self.battery_level
@ -369,6 +369,19 @@ class StateVacuumEntity(
"""Flag vacuum cleaner features that are supported."""
return self._attr_supported_features
@property
def supported_features_compat(self) -> VacuumEntityFeature:
"""Return the supported features as VacuumEntityFeature.
Remove this compatibility shim in 2025.1 or later.
"""
features = self.supported_features
if type(features) is int: # noqa: E721
new_features = VacuumEntityFeature(features)
self._report_deprecated_supported_features_values(new_features)
return new_features
return features
def stop(self, **kwargs: Any) -> None:
"""Stop the vacuum cleaner."""
raise NotImplementedError

View File

@ -7,7 +7,7 @@ import asyncio
from collections import deque
from collections.abc import Callable, Coroutine, Iterable, Mapping
import dataclasses
from enum import Enum, auto
from enum import Enum, IntFlag, auto
import functools as ft
import logging
import math
@ -1639,6 +1639,31 @@ class Entity(
self.hass, integration_domain=platform_name, module=type(self).__module__
)
@callback
def _report_deprecated_supported_features_values(
self, replacement: IntFlag
) -> None:
"""Report deprecated supported features values."""
if self._deprecated_supported_features_reported is True:
return
self._deprecated_supported_features_reported = True
report_issue = self._suggest_report_issue()
report_issue += (
" and reference "
"https://developers.home-assistant.io/blog/2023/12/28/support-feature-magic-numbers-deprecation"
)
_LOGGER.warning(
(
"Entity %s (%s) is using deprecated supported features"
" values which will be removed in HA Core 2025.1. Instead it should use"
" %s, please %s"
),
self.entity_id,
type(self),
repr(replacement),
report_issue,
)
class ToggleEntityDescription(EntityDescription, frozen_or_thawed=True):
"""A class that describes toggle entities."""

View File

@ -826,6 +826,26 @@ def test_deprecated_state_constants(
import_and_test_deprecated_constant_enum(caplog, module, enum, "STATE_", "2025.10")
def test_deprecated_supported_features_ints(caplog: pytest.LogCaptureFixture) -> None:
"""Test deprecated supported features ints."""
class MockCamera(camera.Camera):
@property
def supported_features(self) -> int:
"""Return supported features."""
return 1
entity = MockCamera()
assert entity.supported_features_compat is camera.CameraEntityFeature(1)
assert "MockCamera" in caplog.text
assert "is using deprecated supported features values" in caplog.text
assert "Instead it should use" in caplog.text
assert "CameraEntityFeature.ON_OFF" in caplog.text
caplog.clear()
assert entity.supported_features_compat is camera.CameraEntityFeature(1)
assert "is using deprecated supported features values" not in caplog.text
@pytest.mark.usefixtures("mock_camera")
async def test_entity_picture_url_changes_on_token_update(hass: HomeAssistant) -> None:
"""Test the token is rotated and entity entity picture cache is cleared."""

View File

@ -2,6 +2,8 @@
from enum import Enum
import pytest
from homeassistant.components import cover
from homeassistant.components.cover import CoverState
from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM, SERVICE_TOGGLE
@ -153,3 +155,20 @@ def _create_tuples(enum: type[Enum], constant_prefix: str) -> list[tuple[Enum, s
def test_all() -> None:
"""Test module.__all__ is correctly set."""
help_test_all(cover)
def test_deprecated_supported_features_ints(caplog: pytest.LogCaptureFixture) -> None:
"""Test deprecated supported features ints."""
class MockCoverEntity(cover.CoverEntity):
_attr_supported_features = 1
entity = MockCoverEntity()
assert entity.supported_features is cover.CoverEntityFeature(1)
assert "MockCoverEntity" in caplog.text
assert "is using deprecated supported features values" in caplog.text
assert "Instead it should use" in caplog.text
assert "CoverEntityFeature.OPEN" in caplog.text
caplog.clear()
assert entity.supported_features is cover.CoverEntityFeature(1)
assert "is using deprecated supported features values" not in caplog.text

View File

@ -129,7 +129,7 @@ def test_support_properties(property_suffix: str) -> None:
entity3 = MediaPlayerEntity()
entity3._attr_supported_features = feature
entity4 = MediaPlayerEntity()
entity4._attr_supported_features = all_features & ~feature
entity4._attr_supported_features = all_features - feature
assert getattr(entity1, f"support_{property_suffix}") is False
assert getattr(entity2, f"support_{property_suffix}") is True
@ -447,3 +447,23 @@ async def test_get_async_get_browse_image_quoting(
url = player.get_browse_image_url("album", media_content_id)
await client.get(url)
mock_browse_image.assert_called_with("album", media_content_id, None)
def test_deprecated_supported_features_ints(caplog: pytest.LogCaptureFixture) -> None:
"""Test deprecated supported features ints."""
class MockMediaPlayerEntity(MediaPlayerEntity):
@property
def supported_features(self) -> int:
"""Return supported features."""
return 1
entity = MockMediaPlayerEntity()
assert entity.supported_features_compat is MediaPlayerEntityFeature(1)
assert "MockMediaPlayerEntity" in caplog.text
assert "is using deprecated supported features values" in caplog.text
assert "Instead it should use" in caplog.text
assert "MediaPlayerEntityFeature.PAUSE" in caplog.text
caplog.clear()
assert entity.supported_features_compat is MediaPlayerEntityFeature(1)
assert "is using deprecated supported features values" not in caplog.text

View File

@ -272,6 +272,42 @@ async def test_send_command(hass: HomeAssistant, config_flow_fixture: None) -> N
assert "test" in strings
async def test_supported_features_compat(hass: HomeAssistant) -> None:
"""Test StateVacuumEntity using deprecated feature constants features."""
features = (
VacuumEntityFeature.BATTERY
| VacuumEntityFeature.FAN_SPEED
| VacuumEntityFeature.START
| VacuumEntityFeature.STOP
| VacuumEntityFeature.PAUSE
)
class _LegacyConstantsStateVacuum(StateVacuumEntity):
_attr_supported_features = int(features)
_attr_fan_speed_list = ["silent", "normal", "pet hair"]
entity = _LegacyConstantsStateVacuum()
assert isinstance(entity.supported_features, int)
assert entity.supported_features == int(features)
assert entity.supported_features_compat is (
VacuumEntityFeature.BATTERY
| VacuumEntityFeature.FAN_SPEED
| VacuumEntityFeature.START
| VacuumEntityFeature.STOP
| VacuumEntityFeature.PAUSE
)
assert entity.state_attributes == {
"battery_level": None,
"battery_icon": "mdi:battery-unknown",
"fan_speed": None,
}
assert entity.capability_attributes == {
"fan_speed_list": ["silent", "normal", "pet hair"]
}
assert entity._deprecated_supported_features_reported
async def test_vacuum_not_log_deprecated_state_warning(
hass: HomeAssistant,
mock_vacuum_entity: MockVacuum,

View File

@ -4,6 +4,7 @@ import asyncio
from collections.abc import Iterable
import dataclasses
from datetime import timedelta
from enum import IntFlag
import logging
import threading
from typing import Any
@ -2485,6 +2486,31 @@ async def test_cached_entity_property_override(hass: HomeAssistant) -> None:
return "🤡"
async def test_entity_report_deprecated_supported_features_values(
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test reporting deprecated supported feature values only happens once."""
ent = entity.Entity()
class MockEntityFeatures(IntFlag):
VALUE1 = 1
VALUE2 = 2
ent._report_deprecated_supported_features_values(MockEntityFeatures(2))
assert (
"is using deprecated supported features values which will be removed"
in caplog.text
)
assert "MockEntityFeatures.VALUE2" in caplog.text
caplog.clear()
ent._report_deprecated_supported_features_values(MockEntityFeatures(2))
assert (
"is using deprecated supported features values which will be removed"
not in caplog.text
)
async def test_remove_entity_registry(
hass: HomeAssistant, entity_registry: er.EntityRegistry
) -> None: