Bump ZHA to 2.0.0 (#174586)

This commit is contained in:
puddly
2026-06-24 08:52:44 +02:00
committed by GitHub
parent 58603326e3
commit 05016e46c8
8 changed files with 136 additions and 22 deletions
+51 -14
View File
@@ -77,6 +77,8 @@ from zha.zigbee.device import (
ZHAEvent,
)
from zha.zigbee.group import Group, GroupInfo, GroupMember
import zhaquirks
import zhaquirks.legacy
from zigpy.config import (
CONF_DATABASE,
CONF_DEVICE,
@@ -312,6 +314,36 @@ class ZHADeviceProxy(EventBase):
self._unsubs: list[Callable[[], None]] = []
self._unsubs.append(self.device.on_all_events(self._handle_event_protocol))
@callback
def async_rebind_device(self, device: Device) -> None:
"""Repoint this proxy to a replacement device object after a re-interview."""
for unsub in self._unsubs:
unsub()
self._unsubs.clear()
self.device = device
self._unsubs.append(self.device.on_all_events(self._handle_event_protocol))
self.attach_event_handlers()
@callback
def attach_event_handlers(self) -> None:
"""Attach event handlers to the ZHA device."""
device_registry = dr.async_get(self.gateway_proxy.hass)
# Sync the device's firmware version into the device registry
def update_sw_version(event: DeviceFirmwareInfoUpdatedEvent) -> None:
"""Update software version in device registry."""
device_registry.async_update_device(
self.device_id, sw_version=event.new_firmware_version
)
self._unsubs.append(
self.device.on_event(
DeviceFirmwareInfoUpdatedEvent.event_type, update_sw_version
)
)
@property
def device_id(self) -> str:
"""Return the HA device registry device id."""
@@ -726,11 +758,24 @@ class ZHAGatewayProxy(EventBase):
def handle_device_fully_initialized(self, event: DeviceFullInitEvent) -> None:
"""Handle a device fully initialized event."""
zha_device = self.gateway.get_device(event.device_info.ieee)
# If ZHA swaps out the underlying device object, we need to update the proxy to
# point to the new one. The replacement is initialized silently
# (its entity-added events are suppressed), so we repoint the proxy and re-add
# its entities here; the old entities were already removed via teardown events
# on the previous object.
device_proxy = self.device_proxies.get(zha_device.ieee)
swapped_device = (
device_proxy is not None and device_proxy.device is not zha_device
)
zha_device_proxy = self._async_get_or_create_device_proxy(zha_device)
if swapped_device:
zha_device_proxy.async_rebind_device(zha_device)
device_info = zha_device_proxy.zha_device_info
device_info[DEVICE_PAIRING_STATUS] = event.device_info.pairing_status.name
if event.new_join:
if event.new_join or swapped_device:
self._create_entity_metadata(zha_device_proxy)
async_dispatcher_send(self.hass, SIGNAL_ADD_ENTITIES)
async_dispatcher_send(
@@ -870,19 +915,7 @@ class ZHAGatewayProxy(EventBase):
sw_version=zha_device.firmware_version,
)
zha_device_proxy.device_id = device_registry_device.id
def update_sw_version(event: DeviceFirmwareInfoUpdatedEvent) -> None:
"""Update software version in device registry."""
device_registry.async_update_device(
device_registry_device.id,
sw_version=event.new_firmware_version,
)
self._unsubs.append(
zha_device.on_event(
DeviceFirmwareInfoUpdatedEvent.event_type, update_sw_version
)
)
zha_device_proxy.attach_event_handlers()
return zha_device_proxy
@@ -1363,6 +1396,10 @@ def create_zha_config(hass: HomeAssistant, ha_zha_data: HAZHAData) -> ZHAData:
quirks_config: QuirksConfiguration = QuirksConfiguration(
enabled=ha_zha_data.yaml_config.get(CONF_ENABLE_QUIRKS, True),
custom_quirks_path=ha_zha_data.yaml_config.get(CONF_CUSTOM_QUIRKS_PATH),
setup_function=zhaquirks.setup,
uninitialized_packet_handler=(
zhaquirks.legacy.handle_message_from_uninitialized_sender
),
)
overrides_config: dict[str, DeviceOverridesConfiguration] = {}
overrides: dict[str, dict[str, Any]] = cast(
+1 -1
View File
@@ -23,7 +23,7 @@
"universal_silabs_flasher",
"serialx"
],
"requirements": ["zha==1.4.1"],
"requirements": ["zha==2.0.0", "zha-quirks==2.0.0"],
"usb": [
{
"description": "*2652*",
@@ -45,13 +45,18 @@ from .helpers import get_zha_data
RECOMMENDED_RADIOS = (
RadioType.ezsp,
RadioType.znp,
RadioType.ziggurat,
RadioType.deconz,
)
# Only the common radio types will be autoprobed, ordered by new device popularity.
# XBee takes too long to probe since it scans through all possible bauds and likely has
# very few users to begin with.
AUTOPROBE_RADIOS = RECOMMENDED_RADIOS
AUTOPROBE_RADIOS = (
RadioType.ezsp,
RadioType.znp,
RadioType.deconz,
)
CONNECT_DELAY_S = 1.0
RETRY_DELAY_S = 1.0
+4 -1
View File
@@ -3466,7 +3466,10 @@ zeroconf==0.150.0
zeversolar==0.3.2
# homeassistant.components.zha
zha==1.4.1
zha-quirks==2.0.0
# homeassistant.components.zha
zha==2.0.0
# homeassistant.components.zhong_hong
zhong-hong-hvac==1.0.13
+2 -2
View File
@@ -8,6 +8,7 @@ from unittest.mock import AsyncMock, MagicMock, create_autospec, patch
import warnings
import pytest
from zha.quirks import DEVICE_REGISTRY
import zhaquirks
import zigpy
from zigpy.application import ControllerApplication
@@ -18,7 +19,6 @@ import zigpy.device
import zigpy.group
import zigpy.profiles
from zigpy.profiles import zha
import zigpy.quirks
import zigpy.state
import zigpy.types
import zigpy.util
@@ -397,7 +397,7 @@ def zigpy_device_mock(zigpy_app_controller) -> Callable[..., zigpy.device.Device
device = quirk(zigpy_app_controller, device.ieee, device.nwk, device)
else:
# Allow zigpy to apply quirks if we don't pass one explicitly
device = zigpy.quirks.get_device(device)
device = DEVICE_REGISTRY.resolve(device)
if patch_cluster:
for endpoint in (ep for epid, ep in device.endpoints.items() if epid):
@@ -6,7 +6,9 @@ from unittest.mock import patch
import pytest
from zha.application import Platform as ZhaPlatform
from zha.application.platforms import PlatformEntity
from zha.quirks import DEVICE_REGISTRY
from zha.zigbee.device import DeviceEntityAddedEvent, DeviceEntityRemovedEvent
from zhaquirks.builder import QuirkBuilder
from zigpy.device import Device
from zigpy.profiles import zha
from zigpy.zcl.clusters import general
@@ -257,3 +259,71 @@ async def test_remove_entity_reference_when_ieee_already_cleared(
await hass.async_block_till_done()
assert ieee not in gateway_proxy._ha_entity_refs
async def test_reinterview_quirk_class_swap_rebinds_proxy(
hass: HomeAssistant,
setup_zha: Callable[..., Coroutine[None]],
zigpy_device_mock: Callable[..., Device],
) -> None:
"""A re-interview that changes the ZHA `Device` class re-binds the proxy."""
await setup_zha()
gateway = get_zha_gateway(hass)
gateway_proxy = get_zha_gateway_proxy(hass)
endpoints = {
1: {
SIG_EP_INPUT: [general.Basic.cluster_id, general.OnOff.cluster_id],
SIG_EP_OUTPUT: [],
SIG_EP_TYPE: zha.DeviceType.ON_OFF_SWITCH,
SIG_EP_PROFILE: zha.PROFILE_ID,
}
}
# Join with no matching quirk: resolves to the base `Device` class.
zigpy_device = zigpy_device_mock(
endpoints,
ieee="01:2d:6f:00:0a:90:69:e8",
manufacturer="Fake",
model="Model_A",
)
gateway.get_or_create_device(zigpy_device)
await gateway.async_device_initialized(zigpy_device)
await hass.async_block_till_done(wait_background_tasks=True)
proxy = gateway_proxy.get_device_proxy(zigpy_device.ieee)
assert proxy is not None
original_device = proxy.device
assert not original_device.quirk_applied
switch_id = find_entity_id(Platform.SWITCH, proxy, hass)
assert switch_id is not None
assert hass.states.get(switch_id).state == STATE_OFF
with DEVICE_REGISTRY.preserve_state():
# Register a v2 quirk for the re-interviewed model -> `QuirkV2Device`.
(
QuirkBuilder("Fake", "Model_B")
.friendly_name(model="Quirk B", manufacturer="Fake")
.add_to_registry()
)
reinterviewed = zigpy_device_mock(
endpoints,
ieee="01:2d:6f:00:0a:90:69:e8",
manufacturer="Fake",
model="Model_B",
)
gateway.device_reinterviewed(reinterviewed)
await hass.async_block_till_done(wait_background_tasks=True)
# The `Device` object was replaced by a quirked one
new_device = gateway.get_device(zigpy_device.ieee)
assert new_device is not original_device
assert new_device.quirk_applied
# The proxy followed the swap
assert proxy.device is new_device
# And the switch entity was re-materialized on the new object.
assert hass.states.get(switch_id).state == STATE_OFF
+1 -2
View File
@@ -4,10 +4,9 @@ from collections.abc import Callable, Coroutine
from unittest.mock import call, patch
import pytest
from zhaquirks.builder import NumberDeviceClass, QuirkBuilder
from zigpy.device import Device
from zigpy.profiles import zha
from zigpy.quirks.v2 import QuirkBuilder
from zigpy.quirks.v2.homeassistant.number import NumberDeviceClass
from zigpy.typing import UNDEFINED
from zigpy.zcl.clusters import general
import zigpy.zcl.foundation as zcl_f
+1 -1
View File
@@ -4,9 +4,9 @@ from collections.abc import Callable, Coroutine
from unittest.mock import patch
import pytest
from zhaquirks.builder import QuirkBuilder
from zigpy.device import Device
from zigpy.profiles import zha
from zigpy.quirks.v2 import QuirkBuilder
from zigpy.zcl import Cluster
from zigpy.zcl.clusters import general, homeautomation, hvac, measurement, smartenergy
from zigpy.zcl.clusters.hvac import Thermostat