Cache entity properties that are never expected to change in the base class (#95315)

This commit is contained in:
J. Nick Koston 2023-09-14 17:48:48 -05:00 committed by GitHub
parent 5f20725fd5
commit 042776ebb8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 146 additions and 48 deletions

View file

@ -5,18 +5,18 @@ from collections.abc import Callable
from types import GenericAlias from types import GenericAlias
from typing import Any, Generic, Self, TypeVar, overload from typing import Any, Generic, Self, TypeVar, overload
_T = TypeVar("_T") _T_co = TypeVar("_T_co", covariant=True)
class cached_property(Generic[_T]): class cached_property(Generic[_T_co]): # pylint: disable=invalid-name
"""Backport of Python 3.12's cached_property. """Backport of Python 3.12's cached_property.
Includes https://github.com/python/cpython/pull/101890/files Includes https://github.com/python/cpython/pull/101890/files
""" """
def __init__(self, func: Callable[[Any], _T]) -> None: def __init__(self, func: Callable[[Any], _T_co]) -> None:
"""Initialize.""" """Initialize."""
self.func: Callable[[Any], _T] = func self.func: Callable[[Any], _T_co] = func
self.attrname: str | None = None self.attrname: str | None = None
self.__doc__ = func.__doc__ self.__doc__ = func.__doc__
@ -35,12 +35,12 @@ class cached_property(Generic[_T]):
... ...
@overload @overload
def __get__(self, instance: Any, owner: type[Any] | None = None) -> _T: def __get__(self, instance: Any, owner: type[Any] | None = None) -> _T_co:
... ...
def __get__( def __get__(
self, instance: Any | None, owner: type[Any] | None = None self, instance: Any | None, owner: type[Any] | None = None
) -> _T | Self: ) -> _T_co | Self:
"""Get.""" """Get."""
if instance is None: if instance is None:
return self return self

View file

@ -50,7 +50,9 @@ class AbodeBinarySensor(AbodeDevice, BinarySensorEntity):
"""Return True if the binary sensor is on.""" """Return True if the binary sensor is on."""
return cast(bool, self._device.is_on) return cast(bool, self._device.is_on)
@property @property # type: ignore[override]
# We don't know if the class may be set late here
# so we need to override the property to disable the cache.
def device_class(self) -> BinarySensorDeviceClass | None: def device_class(self) -> BinarySensorDeviceClass | None:
"""Return the class of the binary sensor.""" """Return the class of the binary sensor."""
if self._device.get_value("is_window") == "1": if self._device.get_value("is_window") == "1":

View file

@ -9,6 +9,7 @@ from typing import Literal, final
import voluptuous as vol import voluptuous as vol
from homeassistant.backports.functools import cached_property
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@ -197,7 +198,7 @@ class BinarySensorEntity(Entity):
""" """
return self.device_class is not None return self.device_class is not None
@property @cached_property
def device_class(self) -> BinarySensorDeviceClass | None: def device_class(self) -> BinarySensorDeviceClass | None:
"""Return the class of this entity.""" """Return the class of this entity."""
if hasattr(self, "_attr_device_class"): if hasattr(self, "_attr_device_class"):

View file

@ -9,6 +9,7 @@ from typing import final
import voluptuous as vol import voluptuous as vol
from homeassistant.backports.functools import cached_property
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.config_validation import ( # noqa: F401 from homeassistant.helpers.config_validation import ( # noqa: F401
@ -96,7 +97,7 @@ class ButtonEntity(RestoreEntity):
""" """
return self.device_class is not None return self.device_class is not None
@property @cached_property
def device_class(self) -> ButtonDeviceClass | None: def device_class(self) -> ButtonDeviceClass | None:
"""Return the class of this entity.""" """Return the class of this entity."""
if hasattr(self, "_attr_device_class"): if hasattr(self, "_attr_device_class"):

View file

@ -11,6 +11,7 @@ from typing import Any, ParamSpec, TypeVar, final
import voluptuous as vol import voluptuous as vol
from homeassistant.backports.functools import cached_property
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
SERVICE_CLOSE_COVER, SERVICE_CLOSE_COVER,
@ -250,7 +251,7 @@ class CoverEntity(Entity):
""" """
return self._attr_current_cover_tilt_position return self._attr_current_cover_tilt_position
@property @cached_property
def device_class(self) -> CoverDeviceClass | None: def device_class(self) -> CoverDeviceClass | None:
"""Return the class of this entity.""" """Return the class of this entity."""
if hasattr(self, "_attr_device_class"): if hasattr(self, "_attr_device_class"):

View file

@ -8,6 +8,7 @@ from typing import final
import voluptuous as vol import voluptuous as vol
from homeassistant.backports.functools import cached_property
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_DATE from homeassistant.const import ATTR_DATE
from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.core import HomeAssistant, ServiceCall
@ -75,7 +76,7 @@ class DateEntity(Entity):
_attr_native_value: date | None _attr_native_value: date | None
_attr_state: None = None _attr_state: None = None
@property @cached_property
@final @final
def device_class(self) -> None: def device_class(self) -> None:
"""Return the device class for the entity.""" """Return the device class for the entity."""

View file

@ -8,6 +8,7 @@ from typing import final
import voluptuous as vol import voluptuous as vol
from homeassistant.backports.functools import cached_property
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
@ -86,7 +87,7 @@ class DateTimeEntity(Entity):
_attr_state: None = None _attr_state: None = None
_attr_native_value: datetime | None _attr_native_value: datetime | None
@property @cached_property
@final @final
def device_class(self) -> None: def device_class(self) -> None:
"""Return entity device class.""" """Return entity device class."""

View file

@ -592,7 +592,10 @@ class DSMREntity(SensorEntity):
"""Entity is only available if there is a telegram.""" """Entity is only available if there is a telegram."""
return self.telegram is not None return self.telegram is not None
@property @property # type: ignore[override]
# The device class can change at runtime from GAS to ENERGY
# when new data is received. This should be remembered and restored
# at startup, but the integration currently doesn't support that.
def device_class(self) -> SensorDeviceClass | None: def device_class(self) -> SensorDeviceClass | None:
"""Return the device class of this entity.""" """Return the device class of this entity."""
device_class = super().device_class device_class = super().device_class

View file

@ -7,6 +7,7 @@ from enum import StrEnum
import logging import logging
from typing import Any, Self, final from typing import Any, Self, final
from homeassistant.backports.functools import cached_property
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.config_validation import ( # noqa: F401 from homeassistant.helpers.config_validation import ( # noqa: F401
@ -114,7 +115,7 @@ class EventEntity(RestoreEntity):
__last_event_type: str | None = None __last_event_type: str | None = None
__last_event_attributes: dict[str, Any] | None = None __last_event_attributes: dict[str, Any] | None = None
@property @cached_property
def device_class(self) -> EventDeviceClass | None: def device_class(self) -> EventDeviceClass | None:
"""Return the class of this entity.""" """Return the class of this entity."""
if hasattr(self, "_attr_device_class"): if hasattr(self, "_attr_device_class"):

View file

@ -220,10 +220,17 @@ class SensorFilter(SensorEntity):
self._state: StateType = None self._state: StateType = None
self._filters = filters self._filters = filters
self._attr_icon = None self._attr_icon = None
self._attr_device_class = None self._device_class = None
self._attr_state_class = None self._attr_state_class = None
self._attr_extra_state_attributes = {ATTR_ENTITY_ID: entity_id} self._attr_extra_state_attributes = {ATTR_ENTITY_ID: entity_id}
@property
# This property is not cached because the underlying source may
# not always be available.
def device_class(self) -> SensorDeviceClass | None: # type: ignore[override]
"""Return the device class of the sensor."""
return self._device_class
@callback @callback
def _update_filter_sensor_state_event( def _update_filter_sensor_state_event(
self, event: EventType[EventStateChangedData] self, event: EventType[EventStateChangedData]
@ -283,7 +290,7 @@ class SensorFilter(SensorEntity):
self._state = temp_state.state self._state = temp_state.state
self._attr_icon = new_state.attributes.get(ATTR_ICON, ICON) self._attr_icon = new_state.attributes.get(ATTR_ICON, ICON)
self._attr_device_class = new_state.attributes.get(ATTR_DEVICE_CLASS) self._device_class = new_state.attributes.get(ATTR_DEVICE_CLASS)
self._attr_state_class = new_state.attributes.get(ATTR_STATE_CLASS) self._attr_state_class = new_state.attributes.get(ATTR_STATE_CLASS)
if self._attr_native_unit_of_measurement != new_state.attributes.get( if self._attr_native_unit_of_measurement != new_state.attributes.get(

View file

@ -5,6 +5,7 @@ from typing import Any
import voluptuous as vol import voluptuous as vol
from homeassistant.backports.functools import cached_property
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
DEVICE_CLASSES_SCHEMA, DEVICE_CLASSES_SCHEMA,
DOMAIN as BINARY_SENSOR_DOMAIN, DOMAIN as BINARY_SENSOR_DOMAIN,
@ -147,7 +148,7 @@ class BinarySensorGroup(GroupEntity, BinarySensorEntity):
# Set as ON if any / all member is ON # Set as ON if any / all member is ON
self._attr_is_on = self.mode(state == STATE_ON for state in states) self._attr_is_on = self.mode(state == STATE_ON for state in states)
@property @cached_property
def device_class(self) -> BinarySensorDeviceClass | None: def device_class(self) -> BinarySensorDeviceClass | None:
"""Return the sensor class of the binary sensor.""" """Return the sensor class of the binary sensor."""
return self._device_class return self._device_class

View file

@ -360,7 +360,10 @@ class SensorGroup(GroupEntity, SensorEntity):
"""Return the state attributes of the sensor.""" """Return the state attributes of the sensor."""
return {ATTR_ENTITY_ID: self._entity_ids, **self._extra_state_attribute} return {ATTR_ENTITY_ID: self._entity_ids, **self._extra_state_attribute}
@property @property # type: ignore[override]
# Because the device class is calculated, there is no guarantee that the
# sensors will be available when the entity is created so we do not want to
# cache the value.
def device_class(self) -> SensorDeviceClass | None: def device_class(self) -> SensorDeviceClass | None:
"""Return device class.""" """Return device class."""
if self._attr_device_class is not None: if self._attr_device_class is not None:

View file

@ -154,7 +154,10 @@ class HERETravelTimeSensor(
) )
self.async_write_ha_state() self.async_write_ha_state()
@property @property # type: ignore[override]
# This property is not cached because the attribute can change
# at run time. This is not expected, but it is currently how
# the HERE integration works.
def attribution(self) -> str | None: def attribution(self) -> str | None:
"""Return the attribution.""" """Return the attribution."""
if self.coordinator.data is not None: if self.coordinator.data is not None:

View file

@ -760,7 +760,9 @@ class HuaweiLteSensor(HuaweiLteBaseEntityWithDevice, SensorEntity):
return self.entity_description.icon_fn(self.state) return self.entity_description.icon_fn(self.state)
return self.entity_description.icon return self.entity_description.icon
@property @property # type: ignore[override]
# The device class might change at run time of the signal
# is not a number, so we override here.
def device_class(self) -> SensorDeviceClass | None: def device_class(self) -> SensorDeviceClass | None:
"""Return device class for sensor.""" """Return device class for sensor."""
if self.entity_description.device_class_fn: if self.entity_description.device_class_fn:

View file

@ -9,6 +9,7 @@ from typing import Any, final
import voluptuous as vol import voluptuous as vol
from homeassistant.backports.functools import cached_property
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
ATTR_MODE, ATTR_MODE,
@ -158,7 +159,7 @@ class HumidifierEntity(ToggleEntity):
return data return data
@property @cached_property
def device_class(self) -> HumidifierDeviceClass | None: def device_class(self) -> HumidifierDeviceClass | None:
"""Return the class of this entity.""" """Return the class of this entity."""
if hasattr(self, "_attr_device_class"): if hasattr(self, "_attr_device_class"):

View file

@ -10,6 +10,7 @@ from typing import Any, Final, TypedDict, final
import voluptuous as vol import voluptuous as vol
from homeassistant.backports.functools import cached_property
from homeassistant.components.camera import Image from homeassistant.components.camera import Image
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_ENTITY_ID,
@ -156,7 +157,7 @@ class ImageProcessingEntity(Entity):
return self.entity_description.confidence return self.entity_description.confidence
return None return None
@property @cached_property
def device_class(self) -> ImageProcessingDeviceClass | None: def device_class(self) -> ImageProcessingDeviceClass | None:
"""Return the class of this entity.""" """Return the class of this entity."""
if hasattr(self, "_attr_device_class"): if hasattr(self, "_attr_device_class"):

View file

@ -242,6 +242,14 @@ class IntegrationSensor(RestoreSensor):
self._source_entity: str = source_entity self._source_entity: str = source_entity
self._last_valid_state: Decimal | None = None self._last_valid_state: Decimal | None = None
self._attr_device_info = device_info self._attr_device_info = device_info
self._device_class: SensorDeviceClass | None = None
@property # type: ignore[override]
# The underlying source data may be unavailable at startup, so the device
# class may be set late so we need to override the property to disable the cache.
def device_class(self) -> SensorDeviceClass | None:
"""Return the device class of the sensor."""
return self._device_class
def _unit(self, source_unit: str) -> str: def _unit(self, source_unit: str) -> str:
"""Derive unit from the source sensor, SI prefix and time unit.""" """Derive unit from the source sensor, SI prefix and time unit."""
@ -288,7 +296,7 @@ class IntegrationSensor(RestoreSensor):
err, err,
) )
self._attr_device_class = state.attributes.get(ATTR_DEVICE_CLASS) self._device_class = state.attributes.get(ATTR_DEVICE_CLASS)
self._unit_of_measurement = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) self._unit_of_measurement = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
@callback @callback
@ -319,7 +327,7 @@ class IntegrationSensor(RestoreSensor):
and new_state.attributes.get(ATTR_DEVICE_CLASS) and new_state.attributes.get(ATTR_DEVICE_CLASS)
== SensorDeviceClass.POWER == SensorDeviceClass.POWER
): ):
self._attr_device_class = SensorDeviceClass.ENERGY self._device_class = SensorDeviceClass.ENERGY
self._attr_icon = None self._attr_icon = None
self.async_write_ha_state() self.async_write_ha_state()

View file

@ -22,6 +22,7 @@ from aiohttp.typedefs import LooseHeaders
import voluptuous as vol import voluptuous as vol
from yarl import URL from yarl import URL
from homeassistant.backports.functools import cached_property
from homeassistant.components import websocket_api from homeassistant.components import websocket_api
from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView
from homeassistant.components.websocket_api import ERR_NOT_SUPPORTED, ERR_UNKNOWN_ERROR from homeassistant.components.websocket_api import ERR_NOT_SUPPORTED, ERR_UNKNOWN_ERROR
@ -495,7 +496,7 @@ class MediaPlayerEntity(Entity):
_attr_volume_level: float | None = None _attr_volume_level: float | None = None
# Implement these for your media player # Implement these for your media player
@property @cached_property
def device_class(self) -> MediaPlayerDeviceClass | None: def device_class(self) -> MediaPlayerDeviceClass | None:
"""Return the class of this entity.""" """Return the class of this entity."""
if hasattr(self, "_attr_device_class"): if hasattr(self, "_attr_device_class"):

View file

@ -67,7 +67,7 @@ async def async_setup_entry(
) )
class MobileAppBinarySensor(MobileAppEntity, BinarySensorEntity): class MobileAppBinarySensor(MobileAppEntity, BinarySensorEntity): # type: ignore[misc]
"""Representation of an mobile app binary sensor.""" """Representation of an mobile app binary sensor."""
@property @property

View file

@ -69,7 +69,9 @@ class MobileAppEntity(RestoreEntity):
"""Return if entity should be enabled by default.""" """Return if entity should be enabled by default."""
return not self._config.get(ATTR_SENSOR_DISABLED) return not self._config.get(ATTR_SENSOR_DISABLED)
@property @property # type: ignore[override,unused-ignore]
# Because the device class is received later from the mobile app
# we do not want to cache the property
def device_class(self): def device_class(self):
"""Return the device class.""" """Return the device class."""
return self._config.get(ATTR_SENSOR_DEVICE_CLASS) return self._config.get(ATTR_SENSOR_DEVICE_CLASS)

View file

@ -76,7 +76,7 @@ async def async_setup_entry(
) )
class MobileAppSensor(MobileAppEntity, RestoreSensor): class MobileAppSensor(MobileAppEntity, RestoreSensor): # type: ignore[misc]
"""Representation of an mobile app sensor.""" """Representation of an mobile app sensor."""
async def async_restore_last_state(self, last_state): async def async_restore_last_state(self, last_state):

View file

@ -12,6 +12,7 @@ from typing import Any, Self, final
import voluptuous as vol import voluptuous as vol
from homeassistant.backports.functools import cached_property
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_MODE, CONF_UNIT_OF_MEASUREMENT, UnitOfTemperature from homeassistant.const import ATTR_MODE, CONF_UNIT_OF_MEASUREMENT, UnitOfTemperature
from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.core import HomeAssistant, ServiceCall, callback
@ -231,7 +232,7 @@ class NumberEntity(Entity):
""" """
return self.device_class is not None return self.device_class is not None
@property @cached_property
def device_class(self) -> NumberDeviceClass | None: def device_class(self) -> NumberDeviceClass | None:
"""Return the class of this entity.""" """Return the class of this entity."""
if hasattr(self, "_attr_device_class"): if hasattr(self, "_attr_device_class"):

View file

@ -11,6 +11,7 @@ import logging
from math import ceil, floor, isfinite, log10 from math import ceil, floor, isfinite, log10
from typing import Any, Final, Self, cast, final from typing import Any, Final, Self, cast, final
from homeassistant.backports.functools import cached_property
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
# pylint: disable-next=hass-deprecated-import # pylint: disable-next=hass-deprecated-import
@ -259,7 +260,7 @@ class SensorEntity(Entity):
""" """
return self.device_class not in (None, SensorDeviceClass.ENUM) return self.device_class not in (None, SensorDeviceClass.ENUM)
@property @cached_property
def device_class(self) -> SensorDeviceClass | None: def device_class(self) -> SensorDeviceClass | None:
"""Return the class of this entity.""" """Return the class of this entity."""
if hasattr(self, "_attr_device_class"): if hasattr(self, "_attr_device_class"):

View file

@ -393,7 +393,9 @@ class StatisticsSensor(SensorEntity):
unit = base_unit + "/s" unit = base_unit + "/s"
return unit return unit
@property @property # type: ignore[override]
# Since the underlying data source may not be available at startup
# we disable the caching of device_class.
def device_class(self) -> SensorDeviceClass | None: def device_class(self) -> SensorDeviceClass | None:
"""Return the class of this device.""" """Return the class of this device."""
if self._state_characteristic in STATS_DATETIME: if self._state_characteristic in STATS_DATETIME:

View file

@ -8,6 +8,7 @@ import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.backports.functools import cached_property
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
SERVICE_TOGGLE, SERVICE_TOGGLE,
@ -102,7 +103,7 @@ class SwitchEntity(ToggleEntity):
entity_description: SwitchEntityDescription entity_description: SwitchEntityDescription
_attr_device_class: SwitchDeviceClass | None _attr_device_class: SwitchDeviceClass | None
@property @cached_property
def device_class(self) -> SwitchDeviceClass | None: def device_class(self) -> SwitchDeviceClass | None:
"""Return the class of this entity.""" """Return the class of this entity."""
if hasattr(self, "_attr_device_class"): if hasattr(self, "_attr_device_class"):

View file

@ -294,7 +294,9 @@ class WeatherTemplate(TemplateEntity, WeatherEntity):
"""Return the daily forecast in native units.""" """Return the daily forecast in native units."""
return self._forecast_twice_daily return self._forecast_twice_daily
@property @property # type: ignore[override]
# Because attribution is a template, it can change at any time
# and we don't want to cache it.
def attribution(self) -> str | None: def attribution(self) -> str | None:
"""Return the attribution.""" """Return the attribution."""
if self._attribution is None: if self._attribution is None:

View file

@ -8,6 +8,7 @@ from typing import final
import voluptuous as vol import voluptuous as vol
from homeassistant.backports.functools import cached_property
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_TIME from homeassistant.const import ATTR_TIME
from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.core import HomeAssistant, ServiceCall
@ -75,7 +76,7 @@ class TimeEntity(Entity):
_attr_device_class: None = None _attr_device_class: None = None
_attr_state: None = None _attr_state: None = None
@property @cached_property
@final @final
def device_class(self) -> None: def device_class(self) -> None:
"""Return the device class for the entity.""" """Return the device class for the entity."""

View file

@ -552,6 +552,7 @@ class ProtectDeviceBinarySensor(ProtectDeviceEntity, BinarySensorEntity):
device: Camera | Light | Sensor device: Camera | Light | Sensor
entity_description: ProtectBinaryEntityDescription entity_description: ProtectBinaryEntityDescription
_device_class: BinarySensorDeviceClass | None
@callback @callback
def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None:
@ -561,9 +562,17 @@ class ProtectDeviceBinarySensor(ProtectDeviceEntity, BinarySensorEntity):
self._attr_is_on = entity_description.get_ufp_value(updated_device) self._attr_is_on = entity_description.get_ufp_value(updated_device)
# UP Sense can be any of the 3 contact sensor device classes # UP Sense can be any of the 3 contact sensor device classes
if entity_description.key == _KEY_DOOR and isinstance(updated_device, Sensor): if entity_description.key == _KEY_DOOR and isinstance(updated_device, Sensor):
entity_description.device_class = MOUNT_DEVICE_CLASS_MAP.get( self._device_class = MOUNT_DEVICE_CLASS_MAP.get(
updated_device.mount_type, BinarySensorDeviceClass.DOOR self.device.mount_type, BinarySensorDeviceClass.DOOR
) )
else:
self._device_class = self.entity_description.device_class
@property # type: ignore[override]
# UFP smart sensors can change device class at runtime
def device_class(self) -> BinarySensorDeviceClass | None:
"""Return the class of this sensor."""
return self._device_class
class ProtectDiskBinarySensor(ProtectNVREntity, BinarySensorEntity): class ProtectDiskBinarySensor(ProtectNVREntity, BinarySensorEntity):

View file

@ -11,6 +11,7 @@ from typing import Any, Final, final
from awesomeversion import AwesomeVersion, AwesomeVersionCompareException from awesomeversion import AwesomeVersion, AwesomeVersionCompareException
import voluptuous as vol import voluptuous as vol
from homeassistant.backports.functools import cached_property
from homeassistant.components import websocket_api from homeassistant.components import websocket_api
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import STATE_OFF, STATE_ON, EntityCategory from homeassistant.const import STATE_OFF, STATE_ON, EntityCategory
@ -223,7 +224,7 @@ class UpdateEntity(RestoreEntity):
""" """
return self.device_class is not None return self.device_class is not None
@property @cached_property
def device_class(self) -> UpdateDeviceClass | None: def device_class(self) -> UpdateDeviceClass | None:
"""Return the class of this entity.""" """Return the class of this entity."""
if hasattr(self, "_attr_device_class"): if hasattr(self, "_attr_device_class"):

View file

@ -8,6 +8,7 @@ import zigpy.types as t
from zigpy.zcl.clusters.general import OnOff from zigpy.zcl.clusters.general import OnOff
from zigpy.zcl.clusters.security import IasZone from zigpy.zcl.clusters.security import IasZone
from homeassistant.backports.functools import cached_property
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass, BinarySensorDeviceClass,
BinarySensorEntity, BinarySensorEntity,
@ -195,7 +196,7 @@ class IASZone(BinarySensor):
zone_type = self._cluster_handler.cluster.get("zone_type") zone_type = self._cluster_handler.cluster.get("zone_type")
return IAS_ZONE_NAME_MAPPING.get(zone_type, "iaszone") return IAS_ZONE_NAME_MAPPING.get(zone_type, "iaszone")
@property @cached_property
def device_class(self) -> BinarySensorDeviceClass | None: def device_class(self) -> BinarySensorDeviceClass | None:
"""Return device class from component DEVICE_CLASSES.""" """Return device class from component DEVICE_CLASSES."""
zone_type = self._cluster_handler.cluster.get("zone_type") zone_type = self._cluster_handler.cluster.get("zone_type")

View file

@ -645,6 +645,13 @@ class ZwaveSensor(ZWaveBaseEntity, SensorEntity):
return None return None
return str(self.info.primary_value.metadata.unit) return str(self.info.primary_value.metadata.unit)
@property # type: ignore[override]
# fget is used in the child classes which is not compatible with cached_property
# mypy also doesn't know about fget: https://github.com/python/mypy/issues/6185
def device_class(self) -> SensorDeviceClass | None:
"""Return device class of sensor."""
return super().device_class
class ZWaveNumericSensor(ZwaveSensor): class ZWaveNumericSensor(ZwaveSensor):
"""Representation of a Z-Wave Numeric sensor.""" """Representation of a Z-Wave Numeric sensor."""
@ -737,7 +744,9 @@ class ZWaveListSensor(ZwaveSensor):
return list(self.info.primary_value.metadata.states.values()) return list(self.info.primary_value.metadata.states.values())
return None return None
@property @property # type: ignore[override]
# fget is used which is not compatible with cached_property
# mypy also doesn't know about fget: https://github.com/python/mypy/issues/6185
def device_class(self) -> SensorDeviceClass | None: def device_class(self) -> SensorDeviceClass | None:
"""Return sensor device class.""" """Return sensor device class."""
if (device_class := super().device_class) is not None: if (device_class := super().device_class) is not None:
@ -781,7 +790,7 @@ class ZWaveConfigParameterSensor(ZWaveListSensor):
additional_info=[property_key_name] if property_key_name else None, additional_info=[property_key_name] if property_key_name else None,
) )
@property @property # type: ignore[override]
def device_class(self) -> SensorDeviceClass | None: def device_class(self) -> SensorDeviceClass | None:
"""Return sensor device class.""" """Return sensor device class."""
# mypy doesn't know about fget: https://github.com/python/mypy/issues/6185 # mypy doesn't know about fget: https://github.com/python/mypy/issues/6185

View file

@ -550,7 +550,7 @@ class Entity(ABC):
""" """
return self._attr_device_info return self._attr_device_info
@property @cached_property
def device_class(self) -> str | None: def device_class(self) -> str | None:
"""Return the class of this device, from component DEVICE_CLASSES.""" """Return the class of this device, from component DEVICE_CLASSES."""
if hasattr(self, "_attr_device_class"): if hasattr(self, "_attr_device_class"):
@ -639,7 +639,7 @@ class Entity(ABC):
return self.entity_description.entity_registry_visible_default return self.entity_description.entity_registry_visible_default
return True return True
@property @cached_property
def attribution(self) -> str | None: def attribution(self) -> str | None:
"""Return the attribution.""" """Return the attribution."""
return self._attr_attribution return self._attr_attribution
@ -653,7 +653,7 @@ class Entity(ABC):
return self.entity_description.entity_category return self.entity_description.entity_category
return None return None
@property @cached_property
def translation_key(self) -> str | None: def translation_key(self) -> str | None:
"""Return the translation key to translate the entity's states.""" """Return the translation key to translate the entity's states."""
if hasattr(self, "_attr_translation_key"): if hasattr(self, "_attr_translation_key"):

View file

@ -51,6 +51,7 @@ async def test_event() -> None:
event.event_types event.event_types
# Test retrieving data from entity description # Test retrieving data from entity description
del event.device_class
event.entity_description = EventEntityDescription( event.entity_description = EventEntityDescription(
key="test_event", key="test_event",
event_types=["short_press", "long_press"], event_types=["short_press", "long_press"],
@ -63,6 +64,7 @@ async def test_event() -> None:
event._attr_event_types = ["short_press", "long_press", "double_press"] event._attr_event_types = ["short_press", "long_press", "double_press"]
assert event.event_types == ["short_press", "long_press", "double_press"] assert event.event_types == ["short_press", "long_press", "double_press"]
event._attr_device_class = EventDeviceClass.BUTTON event._attr_device_class = EventDeviceClass.BUTTON
del event.device_class
assert event.device_class == EventDeviceClass.BUTTON assert event.device_class == EventDeviceClass.BUTTON
# Test triggering an event # Test triggering an event

View file

@ -59,11 +59,13 @@ class MockUpdateEntity(UpdateEntity):
"""Mock UpdateEntity to use in tests.""" """Mock UpdateEntity to use in tests."""
async def test_update(hass: HomeAssistant) -> None: def _create_mock_update_entity(
"""Test getting data from the mocked update entity.""" hass: HomeAssistant,
) -> MockUpdateEntity:
mock_platform = MockEntityPlatform(hass)
update = MockUpdateEntity() update = MockUpdateEntity()
update.hass = hass update.hass = hass
update.platform = MockEntityPlatform(hass) update.platform = mock_platform
update._attr_installed_version = "1.0.0" update._attr_installed_version = "1.0.0"
update._attr_latest_version = "1.0.1" update._attr_latest_version = "1.0.1"
@ -71,6 +73,13 @@ async def test_update(hass: HomeAssistant) -> None:
update._attr_release_url = "https://example.com" update._attr_release_url = "https://example.com"
update._attr_title = "Title" update._attr_title = "Title"
return update
async def test_update(hass: HomeAssistant) -> None:
"""Test getting data from the mocked update entity."""
update = _create_mock_update_entity(hass)
assert update.entity_category is EntityCategory.DIAGNOSTIC assert update.entity_category is EntityCategory.DIAGNOSTIC
assert ( assert (
update.entity_picture update.entity_picture
@ -93,7 +102,6 @@ async def test_update(hass: HomeAssistant) -> None:
ATTR_SKIPPED_VERSION: None, ATTR_SKIPPED_VERSION: None,
ATTR_TITLE: "Title", ATTR_TITLE: "Title",
} }
# Test no update available # Test no update available
update._attr_installed_version = "1.0.0" update._attr_installed_version = "1.0.0"
update._attr_latest_version = "1.0.0" update._attr_latest_version = "1.0.0"
@ -120,14 +128,19 @@ async def test_update(hass: HomeAssistant) -> None:
assert update.state is STATE_ON assert update.state is STATE_ON
# Test entity category becomes config when its possible to install # Test entity category becomes config when its possible to install
update = _create_mock_update_entity(hass)
update._attr_supported_features = UpdateEntityFeature.INSTALL update._attr_supported_features = UpdateEntityFeature.INSTALL
assert update.entity_category is EntityCategory.CONFIG assert update.entity_category is EntityCategory.CONFIG
# UpdateEntityDescription was set # UpdateEntityDescription was set
update = _create_mock_update_entity(hass)
update._attr_supported_features = 0 update._attr_supported_features = 0
update.entity_description = UpdateEntityDescription(key="F5 - Its very refreshing") update.entity_description = UpdateEntityDescription(key="F5 - Its very refreshing")
assert update.device_class is None assert update.device_class is None
assert update.entity_category is EntityCategory.CONFIG assert update.entity_category is EntityCategory.CONFIG
update = _create_mock_update_entity(hass)
update._attr_supported_features = 0
update.entity_description = UpdateEntityDescription( update.entity_description = UpdateEntityDescription(
key="F5 - Its very refreshing", key="F5 - Its very refreshing",
device_class=UpdateDeviceClass.FIRMWARE, device_class=UpdateDeviceClass.FIRMWARE,
@ -137,14 +150,24 @@ async def test_update(hass: HomeAssistant) -> None:
assert update.entity_category is None assert update.entity_category is None
# Device class via attribute (override entity description) # Device class via attribute (override entity description)
update = _create_mock_update_entity(hass)
update._attr_supported_features = 0
update._attr_device_class = None update._attr_device_class = None
assert update.device_class is None assert update.device_class is None
update = _create_mock_update_entity(hass)
update._attr_supported_features = 0
update._attr_device_class = UpdateDeviceClass.FIRMWARE update._attr_device_class = UpdateDeviceClass.FIRMWARE
assert update.device_class is UpdateDeviceClass.FIRMWARE assert update.device_class is UpdateDeviceClass.FIRMWARE
# Entity Attribute via attribute (override entity description) # Entity Attribute via attribute (override entity description)
update = _create_mock_update_entity(hass)
update._attr_supported_features = 0
update._attr_entity_category = None update._attr_entity_category = None
assert update.entity_category is None assert update.entity_category is None
update = _create_mock_update_entity(hass)
update._attr_supported_features = 0
update._attr_entity_category = EntityCategory.DIAGNOSTIC update._attr_entity_category = EntityCategory.DIAGNOSTIC
assert update.entity_category is EntityCategory.DIAGNOSTIC assert update.entity_category is EntityCategory.DIAGNOSTIC

View file

@ -98,9 +98,13 @@ class TestHelpersEntity:
def setup_method(self, method): def setup_method(self, method):
"""Set up things to be run when tests are started.""" """Set up things to be run when tests are started."""
self.hass = get_test_home_assistant()
self._create_entity()
def _create_entity(self) -> None:
self.entity = entity.Entity() self.entity = entity.Entity()
self.entity.entity_id = "test.overwrite_hidden_true" self.entity.entity_id = "test.overwrite_hidden_true"
self.hass = self.entity.hass = get_test_home_assistant() self.entity.hass = self.hass
self.entity.schedule_update_ha_state() self.entity.schedule_update_ha_state()
self.hass.block_till_done() self.hass.block_till_done()
@ -123,6 +127,7 @@ class TestHelpersEntity:
with patch( with patch(
"homeassistant.helpers.entity.Entity.device_class", new="test_class" "homeassistant.helpers.entity.Entity.device_class", new="test_class"
): ):
self._create_entity()
self.entity.schedule_update_ha_state() self.entity.schedule_update_ha_state()
self.hass.block_till_done() self.hass.block_till_done()
state = self.hass.states.get(self.entity.entity_id) state = self.hass.states.get(self.entity.entity_id)