Bump ZHA to 2.1.0 (#177520)

Co-authored-by: TheJulianJES <TheJulianJES@users.noreply.github.com>
This commit is contained in:
puddly
2026-07-29 08:34:22 +02:00
committed by GitHub
co-authored by TheJulianJES
parent 25ca9c4dd7
commit a5c5d3a976
22 changed files with 367 additions and 354 deletions
+4 -5
View File
@@ -227,6 +227,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
ha_zha_data.gateway_proxy = ZHAGatewayProxy(hass, config_entry, zha_gateway)
# Ensure the gateway is torn down if setup fails after this point
config_entry.async_on_unload(ha_zha_data.gateway_proxy.shutdown)
manufacturer = zha_gateway.state.node_info.manufacturer
model = zha_gateway.state.node_info.model
@@ -285,11 +288,7 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
ha_zha_data = get_zha_data(hass)
ha_zha_data.config_entry = None
if ha_zha_data.gateway_proxy is not None:
await ha_zha_data.gateway_proxy.shutdown()
ha_zha_data.gateway_proxy = None
ha_zha_data.gateway_proxy = None
ha_zha_data.update_coordinator = None
# clean up any remaining entity metadata
@@ -4,6 +4,7 @@ import functools
from typing import override
from zha.application.platforms.alarm_control_panel.const import (
AlarmControlPanelEntityFeature as ZHAAlarmControlPanelEntityFeature,
AlarmState as ZHAAlarmState,
)
@@ -19,7 +20,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .entity import ZHAEntity
from .entity import ZHASupportedFeaturesEntity
from .helpers import (
SIGNAL_ADD_ENTITIES,
async_add_entities as zha_async_add_entities,
@@ -64,23 +65,42 @@ async def async_setup_entry(
config_entry.async_on_unload(unsub)
class ZHAAlarmControlPanel(ZHAEntity, AlarmControlPanelEntity):
class ZHAAlarmControlPanel(ZHASupportedFeaturesEntity, AlarmControlPanelEntity):
"""Entity for ZHA alarm control devices."""
_attr_translation_key: str = "alarm_control_panel"
_attr_code_format = CodeFormat.TEXT
_attr_supported_features = (
AlarmControlPanelEntityFeature.ARM_HOME
| AlarmControlPanelEntityFeature.ARM_AWAY
| AlarmControlPanelEntityFeature.ARM_NIGHT
| AlarmControlPanelEntityFeature.TRIGGER
)
@staticmethod
@functools.cache
@override
def _convert_supported_features(
zha_features: int,
) -> AlarmControlPanelEntityFeature:
"""Convert ZHA alarm control panel features to HA ones."""
zha_flags = ZHAAlarmControlPanelEntityFeature(zha_features)
features = AlarmControlPanelEntityFeature(0)
if ZHAAlarmControlPanelEntityFeature.ARM_HOME in zha_flags:
features |= AlarmControlPanelEntityFeature.ARM_HOME
if ZHAAlarmControlPanelEntityFeature.ARM_AWAY in zha_flags:
features |= AlarmControlPanelEntityFeature.ARM_AWAY
if ZHAAlarmControlPanelEntityFeature.ARM_NIGHT in zha_flags:
features |= AlarmControlPanelEntityFeature.ARM_NIGHT
if ZHAAlarmControlPanelEntityFeature.TRIGGER in zha_flags:
features |= AlarmControlPanelEntityFeature.TRIGGER
if ZHAAlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS in zha_flags:
features |= AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS
if ZHAAlarmControlPanelEntityFeature.ARM_VACATION in zha_flags:
features |= AlarmControlPanelEntityFeature.ARM_VACATION
return features
@property
@override
def code_arm_required(self) -> bool:
"""Whether the code is required for arm actions."""
return self.entity_data.entity.code_arm_required
return self._zha_state.code_arm_required
@convert_zha_error_to_ha_error()
@override
@@ -121,4 +141,4 @@ class ZHAAlarmControlPanel(ZHAEntity, AlarmControlPanelEntity):
@override
def alarm_state(self) -> AlarmControlPanelState | None:
"""Return the state of the entity."""
return ZHA_STATE_TO_ALARM_STATE_MAP.get(self.entity_data.entity.state["state"])
return ZHA_STATE_TO_ALARM_STATE_MAP.get(self._zha_state.alarm_state)
@@ -47,13 +47,13 @@ class BinarySensor(ZHAEntity, BinarySensorEntity):
def __init__(self, entity_data: EntityData) -> None:
"""Initialize the ZHA binary sensor."""
super().__init__(entity_data)
if self.entity_data.entity.info_object.device_class is not None:
if self._zha_state.device_class is not None:
self._attr_device_class = BinarySensorDeviceClass(
self.entity_data.entity.info_object.device_class
self._zha_state.device_class
)
@property
@override
def is_on(self) -> bool:
"""Return True if the switch is on based on the state machine."""
return self.entity_data.entity.is_on
return self._zha_state.is_on
+2 -4
View File
@@ -48,10 +48,8 @@ class ZHAButton(ZHAEntity, ButtonEntity):
def __init__(self, entity_data: EntityData) -> None:
"""Initialize the ZHA binary sensor."""
super().__init__(entity_data)
if self.entity_data.entity.info_object.device_class is not None:
self._attr_device_class = ButtonDeviceClass(
self.entity_data.entity.info_object.device_class
)
if self._zha_state.device_class is not None:
self._attr_device_class = ButtonDeviceClass(self._zha_state.device_class)
@convert_zha_error_to_ha_error()
@override
+49 -67
View File
@@ -8,6 +8,7 @@ from collections.abc import Mapping
import functools
from typing import Any, override
from zha.application.platforms.climate import ThermostatState
from zha.application.platforms.climate.const import (
ClimateEntityFeature as ZHAClimateEntityFeature,
HVACAction as ZHAHVACAction,
@@ -26,14 +27,13 @@ from homeassistant.components.climate import (
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import PRECISION_TENTHS, Platform, UnitOfTemperature
from homeassistant.core import HomeAssistant, callback
from homeassistant.core import HomeAssistant
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .entity import ZHAEntity
from .entity import ZHASupportedFeaturesEntity
from .helpers import (
SIGNAL_ADD_ENTITIES,
EntityData,
async_add_entities as zha_async_add_entities,
convert_zha_error_to_ha_error,
exclude_none_values,
@@ -80,30 +80,21 @@ async def async_setup_entry(
config_entry.async_on_unload(unsub)
class Thermostat(ZHAEntity, ClimateEntity):
class Thermostat(ZHASupportedFeaturesEntity, ClimateEntity):
"""Representation of a ZHA Thermostat device."""
_attr_precision = PRECISION_TENTHS
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_translation_key: str = "thermostat"
def __init__(self, entity_data: EntityData, **kwargs: Any) -> None:
"""Initialize the ZHA thermostat entity."""
super().__init__(entity_data, **kwargs)
self._attr_hvac_modes = [
ZHA_TO_HA_HVAC_MODE[mode] for mode in self.entity_data.entity.hvac_modes
]
self._attr_hvac_mode = ZHA_TO_HA_HVAC_MODE.get(
self.entity_data.entity.hvac_mode
)
self._attr_hvac_action = ZHA_TO_HA_HVAC_ACTION.get(
self.entity_data.entity.hvac_action
)
features: ClimateEntityFeature = ClimateEntityFeature(0)
zha_features: ZHAClimateEntityFeature = (
self.entity_data.entity.supported_features
)
@staticmethod
@functools.cache
@override
def _convert_supported_features(
zha_features: ZHAClimateEntityFeature,
) -> ClimateEntityFeature:
"""Convert ZHA climate features to HA climate features."""
features = ClimateEntityFeature(0)
if ZHAClimateEntityFeature.TARGET_TEMPERATURE in zha_features:
features |= ClimateEntityFeature.TARGET_TEMPERATURE
@@ -122,24 +113,39 @@ class Thermostat(ZHAEntity, ClimateEntity):
if ZHAClimateEntityFeature.TURN_ON in zha_features:
features |= ClimateEntityFeature.TURN_ON
self._attr_supported_features = features
return features
@override
def _update_capability_attrs(self) -> None:
"""Re-derive capability attributes from the cached state."""
super()._update_capability_attrs()
state = self._zha_state
self._attr_hvac_modes = [ZHA_TO_HA_HVAC_MODE[mode] for mode in state.hvac_modes]
self._attr_fan_modes = state.fan_modes
self._attr_preset_modes = state.preset_modes
self._attr_min_temp = state.min_temp
self._attr_max_temp = state.max_temp
@property
@override
def extra_state_attributes(self) -> Mapping[str, Any] | None:
"""Return entity specific state attributes."""
state = self.entity_data.entity.state
state = self._zha_state
if not isinstance(state, ThermostatState):
return None
return exclude_none_values(
{
"occupancy": state.get("occupancy"),
"occupied_cooling_setpoint": state.get("occupied_cooling_setpoint"),
"occupied_heating_setpoint": state.get("occupied_heating_setpoint"),
"pi_cooling_demand": state.get("pi_cooling_demand"),
"pi_heating_demand": state.get("pi_heating_demand"),
"system_mode": state.get("system_mode"),
"unoccupied_cooling_setpoint": state.get("unoccupied_cooling_setpoint"),
"unoccupied_heating_setpoint": state.get("unoccupied_heating_setpoint"),
"occupancy": state.occupancy,
"occupied_cooling_setpoint": state.occupied_cooling_setpoint,
"occupied_heating_setpoint": state.occupied_heating_setpoint,
"pi_cooling_demand": state.pi_cooling_demand,
"pi_heating_demand": state.pi_heating_demand,
"system_mode": state.sys_mode,
"unoccupied_cooling_setpoint": state.unoccupied_cooling_setpoint,
"unoccupied_heating_setpoint": state.unoccupied_heating_setpoint,
}
)
@@ -147,73 +153,49 @@ class Thermostat(ZHAEntity, ClimateEntity):
@override
def current_temperature(self) -> float | None:
"""Return the current temperature."""
return self.entity_data.entity.current_temperature
return self._zha_state.current_temperature
@property
@override
def fan_mode(self) -> str | None:
"""Return current FAN mode."""
return self.entity_data.entity.fan_mode
@property
@override
def fan_modes(self) -> list[str] | None:
"""Return supported FAN modes."""
return self.entity_data.entity.fan_modes
return self._zha_state.fan_mode
@property
@override
def preset_mode(self) -> str:
"""Return current preset mode."""
return self.entity_data.entity.preset_mode
@property
@override
def preset_modes(self) -> list[str] | None:
"""Return supported preset modes."""
return self.entity_data.entity.preset_modes
return self._zha_state.preset_mode
@property
@override
def target_temperature(self) -> float | None:
"""Return the temperature we try to reach."""
return self.entity_data.entity.target_temperature
return self._zha_state.target_temperature
@property
@override
def target_temperature_high(self) -> float | None:
"""Return the upper bound temperature we try to reach."""
return self.entity_data.entity.target_temperature_high
return self._zha_state.target_temperature_high
@property
@override
def target_temperature_low(self) -> float | None:
"""Return the lower bound temperature we try to reach."""
return self.entity_data.entity.target_temperature_low
return self._zha_state.target_temperature_low
@property
@override
def max_temp(self) -> float:
"""Return the maximum temperature."""
return self.entity_data.entity.max_temp
def hvac_mode(self) -> HVACMode | None:
"""Return HVAC operation mode."""
return ZHA_TO_HA_HVAC_MODE.get(self._zha_state.hvac_mode)
@property
@override
def min_temp(self) -> float:
"""Return the minimum temperature."""
return self.entity_data.entity.min_temp
@callback
@override
def _handle_entity_events(self, event: Any) -> None:
"""Entity state changed."""
self._attr_hvac_mode = self._attr_hvac_mode = ZHA_TO_HA_HVAC_MODE.get(
self.entity_data.entity.hvac_mode
)
self._attr_hvac_action = ZHA_TO_HA_HVAC_ACTION.get(
self.entity_data.entity.hvac_action
)
super()._handle_entity_events(event)
def hvac_action(self) -> HVACAction | None:
"""Return the current HVAC action."""
return ZHA_TO_HA_HVAC_ACTION.get(self._zha_state.hvac_action)
@convert_zha_error_to_ha_error()
@override
+17 -22
View File
@@ -22,10 +22,9 @@ from homeassistant.core import HomeAssistant, State, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .entity import ZHAEntity
from .entity import ZHASupportedFeaturesEntity
from .helpers import (
SIGNAL_ADD_ENTITIES,
EntityData,
async_add_entities as zha_async_add_entities,
convert_zha_error_to_ha_error,
get_zha_data,
@@ -53,19 +52,12 @@ async def async_setup_entry(
config_entry.async_on_unload(unsub)
class ZhaCover(ZHAEntity, CoverEntity):
class ZhaCover(ZHASupportedFeaturesEntity, CoverEntity):
"""Representation of a ZHA cover."""
def __init__(self, entity_data: EntityData) -> None:
"""Initialize the ZHA cover."""
super().__init__(entity_data)
if self.entity_data.entity.info_object.device_class is not None:
self._attr_device_class = CoverDeviceClass(
self.entity_data.entity.info_object.device_class
)
@staticmethod
@functools.cache
@override
def _convert_supported_features(
zha_features: ZHACoverEntityFeature,
) -> CoverEntityFeature:
@@ -91,42 +83,45 @@ class ZhaCover(ZHAEntity, CoverEntity):
return features
@property
@override
def supported_features(self) -> CoverEntityFeature:
"""Return the supported features."""
zha_features: ZHACoverEntityFeature = self.entity_data.entity.supported_features
return self._convert_supported_features(zha_features)
def _update_capability_attrs(self) -> None:
"""Re-derive capability attributes from the cached state."""
super()._update_capability_attrs()
device_class = self._zha_state.device_class
self._attr_device_class = (
CoverDeviceClass(device_class) if device_class is not None else None
)
@property
@override
def is_closed(self) -> bool | None:
"""Return True if the cover is closed."""
return self.entity_data.entity.is_closed
return self._zha_state.is_closed
@property
@override
def is_opening(self) -> bool:
"""Return if the cover is opening or not."""
return self.entity_data.entity.is_opening
return self._zha_state.is_opening
@property
@override
def is_closing(self) -> bool:
"""Return if the cover is closing or not."""
return self.entity_data.entity.is_closing
return self._zha_state.is_closing
@property
@override
def current_cover_position(self) -> int | None:
"""Return the current position of ZHA cover."""
return self.entity_data.entity.current_cover_position
return self._zha_state.current_position
@property
@override
def current_cover_tilt_position(self) -> int | None:
"""Return the current tilt position of the cover."""
return self.entity_data.entity.current_cover_tilt_position
return self._zha_state.current_tilt_position
@convert_zha_error_to_ha_error()
@override
@@ -51,7 +51,7 @@ class ZHADeviceScannerEntity(ScannerEntity, ZHAEntity):
@override
def is_connected(self) -> bool:
"""Return true if the device is connected to the network."""
return self.entity_data.entity.is_connected
return self._zha_state.connected
@property
@override
@@ -60,7 +60,7 @@ class ZHADeviceScannerEntity(ScannerEntity, ZHAEntity):
Percentage from 0-100.
"""
return self.entity_data.entity.battery_level
return self._zha_state.battery_level
@property # type: ignore[misc]
@override
+44 -13
View File
@@ -2,11 +2,14 @@
import asyncio
from collections.abc import Callable
import dataclasses
from enum import IntFlag
from functools import partial
import logging
from typing import Any, override
from propcache.api import cached_property
from zha.application.platforms import EntityStateChangedEvent
from zha.mixins import LogMixin
from homeassistant.const import (
@@ -47,12 +50,13 @@ class ZHAEntity(LogMixin, RestoreEntity, Entity):
super().__init__(*args, **kwargs)
self.entity_data: EntityData = entity_data
self._unsubs: list[Callable[[], None]] = []
self._zha_state = self.entity_data.entity.state
if self.entity_data.entity.icon is not None:
# Only custom quirks will realistically set an icon
self._attr_icon = self.entity_data.entity.icon
meta = self.entity_data.entity.info_object
meta = self._zha_state
self._attr_unique_id = meta.unique_id
if self.entity_data.is_group_entity:
@@ -60,7 +64,7 @@ class ZHAEntity(LogMixin, RestoreEntity, Entity):
assert group_proxy is not None
platform = self.entity_data.entity.PLATFORM
unique_ids = [
entity.info_object.unique_id
entity.identifiers.unique_id
for member in group_proxy.group.members
for entity in member.associated_entities
if platform == entity.PLATFORM
@@ -80,6 +84,8 @@ class ZHAEntity(LogMixin, RestoreEntity, Entity):
if meta.translation_placeholders is not None:
self._attr_translation_placeholders = meta.translation_placeholders
self._update_capability_attrs()
@cached_property
@override
def name(self) -> str | UndefinedType | None:
@@ -91,7 +97,7 @@ class ZHAEntity(LogMixin, RestoreEntity, Entity):
If a device class is set but no translation key,
the device class name is used.
"""
meta = self.entity_data.entity.info_object
meta = self._zha_state
if meta.primary:
self._attr_name = None
return super().name
@@ -120,7 +126,7 @@ class ZHAEntity(LogMixin, RestoreEntity, Entity):
@override
def available(self) -> bool:
"""Return entity availability."""
return self.entity_data.entity.available
return self._zha_state.available
@property
@override
@@ -144,19 +150,21 @@ class ZHAEntity(LogMixin, RestoreEntity, Entity):
)
return device_info
def _update_capability_attrs(self) -> None:
"""Re-derive capability `_attr_*` attributes from the cached state."""
@callback
def _handle_entity_events(self, event: Any) -> None:
"""Entity state changed."""
def _handle_zha_entity_state_changed(self, event: EntityStateChangedEvent) -> None:
"""Handle a state change reported by the ZHA library entity."""
self.debug("Handling event from entity: %s", event)
self._zha_state = dataclasses.replace(self._zha_state, **event.state_diff)
self._update_capability_attrs()
self.async_write_ha_state()
@override
async def async_added_to_hass(self) -> None:
"""Run when about to be added to hass."""
self.remove_future = self.hass.loop.create_future()
self._unsubs.append(
self.entity_data.entity.on_all_events(self._handle_entity_events)
)
remove_signal = (
f"{SIGNAL_REMOVE_ENTITIES}_group_{self.entity_data.group_proxy.group.group_id}"
if self.entity_data.is_group_entity
@@ -187,10 +195,16 @@ class ZHAEntity(LogMixin, RestoreEntity, Entity):
self.remove_future,
)
if (state := await self.async_get_last_state()) is None:
return
if (state := await self.async_get_last_state()) is not None:
self.restore_external_state_attributes(state)
self.restore_external_state_attributes(state)
# The subscription synchronously delivers the full current state as its
# first event, establishing a baseline coherent with subsequent diffs.
self._unsubs.append(
self.entity_data.entity.subscribe_state(
self._handle_zha_entity_state_changed
)
)
@callback
def restore_external_state_attributes(self, state: State) -> None:
@@ -221,8 +235,25 @@ class ZHAEntity(LogMixin, RestoreEntity, Entity):
"""Log a message."""
if not _LOGGER.isEnabledFor(level):
# Avoid building the prefixed message and args tuple for disabled
# levels; this runs for every entity event via _handle_entity_events.
# levels; this runs for every entity event via
# _handle_zha_entity_state_changed.
return
msg = f"%s: {msg}"
args = (self.entity_id, *args)
_LOGGER.log(level, msg, *args, **kwargs)
class ZHASupportedFeaturesEntity(ZHAEntity):
"""ZHA entity whose state carries a `supported_features` flag to translate."""
@override
def _update_capability_attrs(self) -> None:
"""Re-derive capability `_attr_*` attributes from the cached state."""
self._attr_supported_features = self._convert_supported_features(
self._zha_state.supported_features
)
@staticmethod
def _convert_supported_features(zha_features: IntFlag) -> IntFlag:
"""Translate ZHA feature flags into their HA equivalents."""
raise NotImplementedError
+16 -14
View File
@@ -12,10 +12,9 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .entity import ZHAEntity
from .entity import ZHASupportedFeaturesEntity
from .helpers import (
SIGNAL_ADD_ENTITIES,
EntityData,
async_add_entities as zha_async_add_entities,
convert_zha_error_to_ha_error,
get_zha_data,
@@ -41,16 +40,19 @@ async def async_setup_entry(
config_entry.async_on_unload(unsub)
class ZhaFan(FanEntity, ZHAEntity):
class ZhaFan(FanEntity, ZHASupportedFeaturesEntity):
"""Representation of a ZHA fan."""
_attr_translation_key: str = "fan"
def __init__(self, entity_data: EntityData) -> None:
"""Initialize the ZHA fan."""
super().__init__(entity_data)
@staticmethod
@functools.cache
@override
def _convert_supported_features(
zha_features: ZHAFanEntityFeature,
) -> FanEntityFeature:
"""Convert ZHA fan features to HA fan features."""
features = FanEntityFeature(0)
zha_features: ZHAFanEntityFeature = self.entity_data.entity.supported_features
if ZHAFanEntityFeature.DIRECTION in zha_features:
features |= FanEntityFeature.DIRECTION
@@ -65,35 +67,35 @@ class ZhaFan(FanEntity, ZHAEntity):
if ZHAFanEntityFeature.TURN_OFF in zha_features:
features |= FanEntityFeature.TURN_OFF
self._attr_supported_features = features
return features
@property
@override
def preset_mode(self) -> str | None:
"""Return the current preset mode."""
return self.entity_data.entity.preset_mode
return self._zha_state.preset_mode
@property
@override
def preset_modes(self) -> list[str]:
"""Return the available preset modes."""
return self.entity_data.entity.preset_modes
return self._zha_state.preset_modes
@property
def default_on_percentage(self) -> int:
"""Return the default on percentage."""
return self.entity_data.entity.default_on_percentage
return self._zha_state.default_on_percentage
@property
def speed_range(self) -> tuple[int, int]:
"""Return the range of speeds the fan supports. Off is not included."""
return self.entity_data.entity.speed_range
return self._zha_state.speed_range
@property
@override
def speed_count(self) -> int:
"""Return the number of speeds the fan supports."""
return self.entity_data.entity.speed_count
return self._zha_state.speed_count
@convert_zha_error_to_ha_error()
@override
@@ -134,4 +136,4 @@ class ZhaFan(FanEntity, ZHAEntity):
@override
def percentage(self) -> int | None:
"""Return the current speed percentage."""
return self.entity_data.entity.percentage
return self._zha_state.percentage
+17 -19
View File
@@ -11,7 +11,6 @@ import itertools
import logging
import queue
import re
import time
from types import MappingProxyType
from typing import TYPE_CHECKING, Any, NamedTuple, cast, override
from zoneinfo import ZoneInfo
@@ -357,26 +356,25 @@ class ZHADeviceProxy(EventBase):
@property
def device_info(self) -> dict[str, Any]:
"""Return a device description for device."""
ieee = str(self.device.ieee)
time_struct = time.localtime(self.device.last_seen)
update_time = time.strftime("%Y-%m-%dT%H:%M:%S", time_struct)
info = self.device.device_info
ieee = str(info.ieee)
return {
ATTR_IEEE: ieee,
ATTR_NWK: self.device.nwk,
ATTR_MANUFACTURER: self.device.manufacturer,
ATTR_MODEL: self.device.model,
ATTR_NAME: self.device.name or ieee,
ATTR_QUIRK_APPLIED: self.device.quirk_applied,
ATTR_QUIRK_CLASS: self.device.quirk_class,
ATTR_EXPOSES_FEATURES: self.device.exposes_features,
ATTR_MANUFACTURER_CODE: self.device.manufacturer_code,
ATTR_POWER_SOURCE: self.device.power_source,
ATTR_LQI: self.device.lqi,
ATTR_RSSI: self.device.rssi,
ATTR_LAST_SEEN: update_time,
ATTR_AVAILABLE: self.device.available,
ATTR_DEVICE_TYPE: self.device.device_type,
ATTR_SIGNATURE: self.device.zigbee_signature,
ATTR_NWK: info.nwk,
ATTR_MANUFACTURER: info.manufacturer,
ATTR_MODEL: info.model,
ATTR_NAME: info.name or ieee,
ATTR_QUIRK_APPLIED: info.quirk_applied,
ATTR_QUIRK_CLASS: info.quirk_class,
ATTR_EXPOSES_FEATURES: info.exposes_features,
ATTR_MANUFACTURER_CODE: info.manufacturer_code,
ATTR_POWER_SOURCE: info.power_source,
ATTR_LQI: info.lqi,
ATTR_RSSI: info.rssi,
ATTR_LAST_SEEN: info.last_seen,
ATTR_AVAILABLE: info.available,
ATTR_DEVICE_TYPE: info.device_type,
ATTR_SIGNATURE: info.signature,
}
@property
+45 -56
View File
@@ -29,10 +29,9 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util import color as color_util
from .entity import ZHAEntity
from .entity import ZHASupportedFeaturesEntity
from .helpers import (
SIGNAL_ADD_ENTITIES,
EntityData,
async_add_entities as zha_async_add_entities,
convert_zha_error_to_ha_error,
get_zha_data,
@@ -73,30 +72,17 @@ async def async_setup_entry(
config_entry.async_on_unload(unsub)
class Light(LightEntity, ZHAEntity):
class Light(LightEntity, ZHASupportedFeaturesEntity):
"""Representation of a ZHA or ZLL light."""
def __init__(self, entity_data: EntityData) -> None:
"""Initialize the ZHA light."""
super().__init__(entity_data)
color_modes: set[ColorMode] = set()
has_brightness = False
for color_mode in self.entity_data.entity.supported_color_modes:
if color_mode == ZhaColorMode.BRIGHTNESS:
has_brightness = True
if color_mode not in (ZhaColorMode.BRIGHTNESS, ZhaColorMode.ONOFF):
color_modes.add(ZHA_TO_HA_COLOR_MODE[color_mode])
if color_modes:
self._attr_supported_color_modes = color_modes
elif has_brightness:
color_modes.add(ColorMode.BRIGHTNESS)
self._attr_supported_color_modes = color_modes
else:
color_modes.add(ColorMode.ONOFF)
self._attr_supported_color_modes = color_modes
@staticmethod
@functools.cache
@override
def _convert_supported_features(
zha_features: ZhaLightEntityFeature,
) -> LightEntityFeature:
"""Convert ZHA light features to HA light features."""
features = LightEntityFeature(0)
zha_features: ZhaLightEntityFeature = self.entity_data.entity.supported_features
if ZhaLightEntityFeature.EFFECT in zha_features:
features |= LightEntityFeature.EFFECT
@@ -105,51 +91,60 @@ class Light(LightEntity, ZHAEntity):
if ZhaLightEntityFeature.TRANSITION in zha_features:
features |= LightEntityFeature.TRANSITION
self._attr_supported_features = features
return features
@override
def _update_capability_attrs(self) -> None:
"""Re-derive capability attributes from the cached state."""
super()._update_capability_attrs()
state = self._zha_state
color_modes: set[ColorMode] = set()
has_brightness = False
for color_mode in state.supported_color_modes:
if color_mode == ZhaColorMode.BRIGHTNESS:
has_brightness = True
if color_mode not in (ZhaColorMode.BRIGHTNESS, ZhaColorMode.ONOFF):
color_modes.add(ZHA_TO_HA_COLOR_MODE[color_mode])
if not color_modes:
color_modes.add(ColorMode.BRIGHTNESS if has_brightness else ColorMode.ONOFF)
self._attr_supported_color_modes = color_modes
self._attr_max_color_temp_kelvin = color_util.color_temperature_mired_to_kelvin(
state.min_mireds
)
self._attr_min_color_temp_kelvin = color_util.color_temperature_mired_to_kelvin(
state.max_mireds
)
self._attr_effect_list = state.effect_list
@property
@override
def extra_state_attributes(self) -> Mapping[str, Any] | None:
"""Return entity specific state attributes."""
state = self.entity_data.entity.state
state = self._zha_state
return {
"off_with_transition": state.get("off_with_transition"),
"off_brightness": state.get("off_brightness"),
"off_with_transition": state.off_with_transition,
"off_brightness": state.off_brightness,
}
@property
@override
def is_on(self) -> bool:
"""Return true if entity is on."""
return self.entity_data.entity.is_on
return self._zha_state.on
@property
@override
def brightness(self) -> int:
"""Return the brightness of this light."""
return self.entity_data.entity.brightness
@property
@override
def max_color_temp_kelvin(self) -> int:
"""Return the coldest color_temp_kelvin that this light supports."""
return color_util.color_temperature_mired_to_kelvin(
self.entity_data.entity.min_mireds
)
@property
@override
def min_color_temp_kelvin(self) -> int:
"""Return the warmest color_temp_kelvin that this light supports."""
return color_util.color_temperature_mired_to_kelvin(
self.entity_data.entity.max_mireds
)
return self._zha_state.brightness
@property
@override
def xy_color(self) -> tuple[float, float] | None:
"""Return the xy color value [float, float]."""
return self.entity_data.entity.xy_color
return self._zha_state.xy_color
@property
@override
@@ -157,7 +152,7 @@ class Light(LightEntity, ZHAEntity):
"""Return the color temperature value in Kelvin."""
return (
color_util.color_temperature_mired_to_kelvin(mireds)
if (mireds := self.entity_data.entity.color_temp)
if (mireds := self._zha_state.color_temp)
else None
)
@@ -165,21 +160,15 @@ class Light(LightEntity, ZHAEntity):
@override
def color_mode(self) -> ColorMode:
"""Return the color mode."""
if self.entity_data.entity.color_mode is None:
if self._zha_state.color_mode is None:
return ColorMode.UNKNOWN
return ZHA_TO_HA_COLOR_MODE[self.entity_data.entity.color_mode]
@property
@override
def effect_list(self) -> list[str] | None:
"""Return the list of supported effects."""
return self.entity_data.entity.effect_list
return ZHA_TO_HA_COLOR_MODE[self._zha_state.color_mode]
@property
@override
def effect(self) -> str | None:
"""Return the current effect."""
return self.entity_data.entity.effect
return self._zha_state.effect
@convert_zha_error_to_ha_error()
@override
+1 -1
View File
@@ -93,7 +93,7 @@ class ZhaDoorLock(ZHAEntity, LockEntity):
@override
def is_locked(self) -> bool:
"""Return true if entity is locked."""
return self.entity_data.entity.is_locked
return self._zha_state.is_locked
@convert_zha_error_to_ha_error()
@override
+1 -1
View File
@@ -23,7 +23,7 @@
"universal_silabs_flasher",
"serialx"
],
"requirements": ["zha==2.0.1", "zha-quirks==2.2.0"],
"requirements": ["zha==2.1.0", "zha-quirks==2.2.0"],
"usb": [
{
"description": "*2652*",
+11 -26
View File
@@ -51,37 +51,22 @@ class ZhaNumber(ZHAEntity, RestoreNumber):
entity = entity_data.entity
if entity.device_class is not None:
self._attr_device_class = NumberDeviceClass(entity.device_class)
self._attr_mode = NumberMode(entity.mode)
@override
def _update_capability_attrs(self) -> None:
"""Re-derive capability attributes from the cached state."""
state = self._zha_state
self._attr_mode = NumberMode(state.mode)
self._attr_native_min_value = state.native_min_value
self._attr_native_max_value = state.native_max_value
self._attr_native_step = state.native_step
self._attr_native_unit_of_measurement = state.native_unit_of_measurement
@property
@override
def native_value(self) -> float | None:
"""Return the current value."""
return self.entity_data.entity.native_value
@property
@override
def native_min_value(self) -> float:
"""Return the minimum value."""
return self.entity_data.entity.native_min_value
@property
@override
def native_max_value(self) -> float:
"""Return the maximum value."""
return self.entity_data.entity.native_max_value
@property
@override
def native_step(self) -> float | None:
"""Return the value step."""
return self.entity_data.entity.native_step
@property
@override
def native_unit_of_measurement(self) -> str | None:
"""Return the unit the value is expressed in."""
return self.entity_data.entity.native_unit_of_measurement
return self._zha_state.native_value
@convert_zha_error_to_ha_error()
@override
+6 -7
View File
@@ -2,7 +2,7 @@
import functools
import logging
from typing import Any, override
from typing import override
from homeassistant.components.select import SelectEntity
from homeassistant.config_entries import ConfigEntry
@@ -14,7 +14,6 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .entity import ZHAEntity
from .helpers import (
SIGNAL_ADD_ENTITIES,
EntityData,
async_add_entities as zha_async_add_entities,
convert_zha_error_to_ha_error,
get_zha_data,
@@ -48,16 +47,16 @@ async def async_setup_entry(
class ZHAEnumSelectEntity(ZHAEntity, SelectEntity):
"""Representation of a ZHA select entity."""
def __init__(self, entity_data: EntityData, **kwargs: Any) -> None:
"""Initialize the ZHA select entity."""
super().__init__(entity_data, **kwargs)
self._attr_options = self.entity_data.entity.info_object.options
@override
def _update_capability_attrs(self) -> None:
"""Re-derive capability attributes from the cached state."""
self._attr_options = self._zha_state.options
@property
@override
def current_option(self) -> str | None:
"""Return the selected entity option to represent the entity state."""
return self.entity_data.entity.current_option
return self._zha_state.current_option
@convert_zha_error_to_ha_error()
@override
+19 -20
View File
@@ -103,14 +103,14 @@ class Sensor(ZHAEntity, SensorEntity):
super().__init__(entity_data, **kwargs)
entity = self.entity_data.entity
if entity.device_class is not None:
self._attr_device_class = SensorDeviceClass(entity.device_class)
if self._zha_state.device_class is not None:
self._attr_device_class = SensorDeviceClass(self._zha_state.device_class)
if entity.state_class is not None:
self._attr_state_class = SensorStateClass(entity.state_class)
if self._zha_state.state_class is not None:
self._attr_state_class = SensorStateClass(self._zha_state.state_class)
if hasattr(entity.info_object, "unit") and entity.info_object.unit is not None:
self._attr_native_unit_of_measurement = entity.info_object.unit
if hasattr(self._zha_state, "unit") and self._zha_state.unit is not None:
self._attr_native_unit_of_measurement = self._zha_state.unit
if (
hasattr(entity, "entity_description")
@@ -136,35 +136,34 @@ class Sensor(ZHAEntity, SensorEntity):
entity_description.device_class.value
)
if entity.info_object.suggested_display_precision is not None:
if self._zha_state.suggested_display_precision is not None:
self._attr_suggested_display_precision = (
entity.info_object.suggested_display_precision
self._zha_state.suggested_display_precision
)
if hasattr(self._zha_state, "options"):
self._attr_options = self._zha_state.options
@property
@override
def native_value(self) -> StateType:
"""Return the state of the entity."""
return self.entity_data.entity.native_value
return self._zha_state.native_value
@property
@override
def extra_state_attributes(self) -> Mapping[str, Any] | None:
"""Return entity specific state attributes."""
entity = self.entity_data.entity
if entity.extra_state_attribute_names is None:
if not self._zha_state.extra_state_attribute_names:
return None
if not entity.extra_state_attribute_names <= _EXTRA_STATE_ATTRIBUTES:
extra_state_attributes = self._zha_state.extra_state_attributes
if not extra_state_attributes.keys() <= _EXTRA_STATE_ATTRIBUTES:
_LOGGER.warning(
"Unexpected extra state attributes found for sensor %s: %s",
entity,
entity.extra_state_attribute_names - _EXTRA_STATE_ATTRIBUTES,
self.entity_data.entity,
extra_state_attributes.keys() - _EXTRA_STATE_ATTRIBUTES,
)
return exclude_none_values(
{
name: entity.state.get(name)
for name in entity.extra_state_attribute_names
}
)
return exclude_none_values(extra_state_attributes)
+12 -11
View File
@@ -21,10 +21,9 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .entity import ZHAEntity
from .entity import ZHASupportedFeaturesEntity
from .helpers import (
SIGNAL_ADD_ENTITIES,
EntityData,
async_add_entities as zha_async_add_entities,
convert_zha_error_to_ha_error,
get_zha_data,
@@ -50,7 +49,7 @@ async def async_setup_entry(
config_entry.async_on_unload(unsub)
class ZHASiren(ZHAEntity, SirenEntity):
class ZHASiren(ZHASupportedFeaturesEntity, SirenEntity):
"""Representation of a ZHA siren."""
_attr_available_tones: list[int | str] | dict[int, str] | None = {
@@ -62,12 +61,14 @@ class ZHASiren(ZHAEntity, SirenEntity):
WarningMode.Emergency_Panic: "Emergency Panic",
}
def __init__(self, entity_data: EntityData, **kwargs: Any) -> None:
"""Initialize the ZHA siren."""
super().__init__(entity_data, **kwargs)
features: SirenEntityFeature = SirenEntityFeature(0)
zha_features: ZHASirenEntityFeature = self.entity_data.entity.supported_features
@staticmethod
@functools.cache
@override
def _convert_supported_features(
zha_features: ZHASirenEntityFeature,
) -> SirenEntityFeature:
"""Convert ZHA siren features to HA siren features."""
features = SirenEntityFeature(0)
if ZHASirenEntityFeature.TURN_ON in zha_features:
features |= SirenEntityFeature.TURN_ON
@@ -80,13 +81,13 @@ class ZHASiren(ZHAEntity, SirenEntity):
if ZHASirenEntityFeature.DURATION in zha_features:
features |= SirenEntityFeature.DURATION
self._attr_supported_features = features
return features
@property
@override
def is_on(self) -> bool:
"""Return True if entity is on."""
return self.entity_data.entity.is_on
return self._zha_state.is_on
@convert_zha_error_to_ha_error()
@override
+1 -1
View File
@@ -48,7 +48,7 @@ class Switch(ZHAEntity, SwitchEntity):
@override
def is_on(self) -> bool:
"""Return if the switch is on based on the statemachine."""
return self.entity_data.entity.is_on
return self._zha_state.is_on
@convert_zha_error_to_ha_error()
@override
+36 -15
View File
@@ -4,6 +4,9 @@ import functools
import logging
from typing import Any, override
from zha.application.platforms.update import (
UpdateEntityFeature as ZHAUpdateEntityFeature,
)
from zha.exceptions import ZHAException
from zigpy.application import ControllerApplication
@@ -23,7 +26,7 @@ from homeassistant.helpers.update_coordinator import (
DataUpdateCoordinator,
)
from .entity import ZHAEntity
from .entity import ZHASupportedFeaturesEntity
from .helpers import (
SIGNAL_ADD_ENTITIES,
EntityData,
@@ -99,19 +102,37 @@ class ZHAFirmwareUpdateCoordinator(DataUpdateCoordinator[None]): # pylint: disa
class ZHAFirmwareUpdateEntity(
ZHAEntity, CoordinatorEntity[ZHAFirmwareUpdateCoordinator], UpdateEntity
ZHASupportedFeaturesEntity,
CoordinatorEntity[ZHAFirmwareUpdateCoordinator],
UpdateEntity,
):
"""Representation of a ZHA firmware update entity."""
_attr_device_class = UpdateDeviceClass.FIRMWARE
_attr_supported_features = (
UpdateEntityFeature.INSTALL
| UpdateEntityFeature.PROGRESS
| UpdateEntityFeature.SPECIFIC_VERSION
| UpdateEntityFeature.RELEASE_NOTES
)
_attr_display_precision = 2 # 40 byte chunks with ~200KB files increments by 0.02%
@staticmethod
@functools.cache
@override
def _convert_supported_features(
zha_features: ZHAUpdateEntityFeature,
) -> UpdateEntityFeature:
"""Convert ZHA update features to HA update features."""
features = UpdateEntityFeature(0)
if ZHAUpdateEntityFeature.INSTALL in zha_features:
features |= UpdateEntityFeature.INSTALL
if ZHAUpdateEntityFeature.SPECIFIC_VERSION in zha_features:
features |= UpdateEntityFeature.SPECIFIC_VERSION
if ZHAUpdateEntityFeature.PROGRESS in zha_features:
features |= UpdateEntityFeature.PROGRESS
if ZHAUpdateEntityFeature.BACKUP in zha_features:
features |= UpdateEntityFeature.BACKUP
if ZHAUpdateEntityFeature.RELEASE_NOTES in zha_features:
features |= UpdateEntityFeature.RELEASE_NOTES
return features
def __init__(self, entity_data: EntityData, **kwargs: Any) -> None:
"""Initialize the ZHA siren."""
zha_data = get_zha_data(entity_data.device_proxy.gateway_proxy.hass)
@@ -124,7 +145,7 @@ class ZHAFirmwareUpdateEntity(
@override
def installed_version(self) -> str | None:
"""Version installed and in use."""
return self.entity_data.entity.installed_version
return self._zha_state.installed_version
@property
@override
@@ -133,7 +154,7 @@ class ZHAFirmwareUpdateEntity(
Should return a boolean (True if in progress, False if not).
"""
return self.entity_data.entity.in_progress
return self._zha_state.in_progress
@property
@override
@@ -144,13 +165,13 @@ class ZHAFirmwareUpdateEntity(
Can either return a number to indicate the progress from 0 to 100% or None.
"""
return self.entity_data.entity.update_percentage
return self._zha_state.update_percentage
@property
@override
def latest_version(self) -> str | None:
"""Latest version available for install."""
return self.entity_data.entity.latest_version
return self._zha_state.latest_version
@property
@override
@@ -160,7 +181,7 @@ class ZHAFirmwareUpdateEntity(
This is not suitable for long changelogs, but merely suitable
for a short excerpt update description of max 255 characters.
"""
return self.entity_data.entity.release_summary
return self._zha_state.release_summary
@override
async def async_release_notes(self) -> str | None:
@@ -179,13 +200,13 @@ class ZHAFirmwareUpdateEntity(
"</ha-alert>"
)
return f"{header}\n\n{self.entity_data.entity.release_notes or ''}"
return f"{header}\n\n{self._zha_state.release_notes or ''}"
@property
@override
def release_url(self) -> str | None:
"""URL to the full release notes of the latest version available."""
return self.entity_data.entity.release_url
return self._zha_state.release_url
# We explicitly convert ZHA exceptions to HA exceptions here so there is no need to
# use the `@convert_zha_error_to_ha_error()` decorator.
+1 -1
View File
@@ -3476,7 +3476,7 @@ zeversolar==0.3.2
zha-quirks==2.2.0
# homeassistant.components.zha
zha==2.0.1
zha==2.1.0
# homeassistant.components.zhong_hong
zhong-hong-hvac==1.0.13
@@ -246,68 +246,60 @@
'routes': list([
]),
'rssi': None,
'version': 2,
'version': 3,
'zha_lib_entities': dict({
'alarm_control_panel': list([
dict({
'info_object': dict({
'available': True,
'class_name': 'AlarmControlPanel',
'code_arm_required': False,
'code_format': 'number',
'device_class': None,
'device_ieee': '**REDACTED**',
'enabled': True,
'endpoint_id': 1,
'entity_category': None,
'entity_registry_enabled_default': True,
'fallback_name': None,
'group_id': None,
'migrate_unique_ids': list([
]),
'platform': 'alarm_control_panel',
'primary': False,
'state_class': None,
'supported_features': 15,
'translation_key': 'alarm_control_panel',
'translation_placeholders': None,
'unique_id': '**REDACTED**',
}),
'state': dict({
'available': True,
'class_name': 'AlarmControlPanel',
'state': 'disarmed',
}),
'alarm_state': 'disarmed',
'available': True,
'class_name': 'AlarmControlPanel',
'code_arm_required': False,
'code_format': 'number',
'device_class': None,
'device_ieee': '**REDACTED**',
'enabled': True,
'endpoint_id': 1,
'entity_category': None,
'entity_registry_enabled_default': True,
'extra_state_attribute_names': list([
]),
'fallback_name': None,
'group_id': None,
'migrate_unique_ids': list([
]),
'platform': 'alarm_control_panel',
'primary': False,
'state_class': None,
'supported_features': 15,
'translation_key': 'alarm_control_panel',
'translation_placeholders': None,
'unique_id': '**REDACTED**',
}),
]),
'binary_sensor': list([
dict({
'info_object': dict({
'attribute_name': 'zone_status',
'available': True,
'class_name': 'IASZone',
'device_class': None,
'device_ieee': '**REDACTED**',
'enabled': True,
'endpoint_id': 1,
'entity_category': None,
'entity_registry_enabled_default': True,
'fallback_name': None,
'group_id': None,
'migrate_unique_ids': list([
]),
'platform': 'binary_sensor',
'primary': True,
'state_class': None,
'translation_key': 'ias_zone',
'translation_placeholders': None,
'unique_id': '**REDACTED**',
}),
'state': dict({
'available': True,
'class_name': 'IASZone',
'state': False,
}),
'attribute_name': 'zone_status',
'available': True,
'class_name': 'IASZone',
'device_class': None,
'device_ieee': '**REDACTED**',
'enabled': True,
'endpoint_id': 1,
'entity_category': None,
'entity_registry_enabled_default': True,
'extra_state_attribute_names': list([
]),
'fallback_name': None,
'group_id': None,
'is_on': False,
'migrate_unique_ids': list([
]),
'platform': 'binary_sensor',
'primary': True,
'state_class': None,
'translation_key': 'ias_zone',
'translation_placeholders': None,
'unique_id': '**REDACTED**',
}),
]),
}),
+3 -1
View File
@@ -11,7 +11,7 @@ from zigpy.zcl import Cluster
from zigpy.zcl.clusters import general, homeautomation, hvac, measurement, smartenergy
from zigpy.zcl.clusters.hvac import Thermostat
from homeassistant.components.sensor import SensorDeviceClass
from homeassistant.components.sensor import ATTR_OPTIONS, SensorDeviceClass
from homeassistant.components.zha.helpers import get_zha_gateway
from homeassistant.const import (
ATTR_DEVICE_CLASS,
@@ -308,6 +308,8 @@ async def async_test_setpoint_change_source(
)
hass_state = hass.states.get(entity_id)
assert hass_state.state == "Schedule"
assert hass_state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.ENUM
assert hass_state.attributes[ATTR_OPTIONS] == ["Manual", "Schedule", "External"]
async def async_test_pi_heating_demand(