mirror of
https://github.com/home-assistant/core.git
synced 2026-08-24 10:13:52 -05:00
Bump ZHA to 2.1.0 (#177520)
Co-authored-by: TheJulianJES <TheJulianJES@users.noreply.github.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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*",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
Generated
+1
-1
@@ -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**',
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user