66
77import asyncio
88from collections import defaultdict
9- from collections .abc import Iterable
9+ from collections .abc import Callable , Iterable
1010import copy
1111import dataclasses
1212from dataclasses import dataclass
6161 UNKNOWN_MODEL ,
6262 ZHA_CLUSTER_HANDLER_CFG_DONE ,
6363 ZHA_CLUSTER_HANDLER_MSG ,
64+ ZHA_DEVICE_UPDATED_EVENT ,
6465 ZHA_EVENT ,
6566)
6667from zha .application .helpers import convert_to_zcl_values , convert_zcl_value
67- from zha .application .platforms import BaseEntityInfo , PlatformEntity
68+ from zha .application .platforms import (
69+ BaseEntityInfo ,
70+ EntityStateChangedEvent ,
71+ PlatformEntity ,
72+ )
73+ from zha .const import STATE_CHANGED
6874from zha .event import EventBase
6975from zha .exceptions import ZHAException
7076from zha .mixins import LogMixin
@@ -147,6 +153,17 @@ class ZHAEvent:
147153 event : Final [str ] = ZHA_EVENT
148154
149155
156+ @dataclass (kw_only = True , frozen = True )
157+ class DeviceFirmwareInfoUpdatedEvent :
158+ """Event generated when the device firmware information has changed."""
159+
160+ event_type : Final [str ] = ZHA_DEVICE_UPDATED_EVENT
161+ event : Final [str ] = ZHA_DEVICE_UPDATED_EVENT
162+
163+ old_firmware_version : str | None
164+ new_firmware_version : str | None
165+
166+
150167@dataclass (kw_only = True , frozen = True )
151168class ClusterHandlerConfigurationComplete :
152169 """Event generated when all cluster handlers are configured."""
@@ -252,7 +269,7 @@ def __init__(
252269 self ._power_config_ch : ClusterHandler | None = None
253270 self ._identify_ch : ClusterHandler | None = None
254271 self ._basic_ch : ClusterHandler | None = None
255- self ._sw_build_id : int | None = None
272+ self ._firmware_version : str | None = None
256273
257274 device_options = _gateway .config .config .device_options
258275 if self .is_mains_powered :
@@ -272,15 +289,20 @@ def __init__(
272289 self ._pending_entities : list [PlatformEntity ] = []
273290 self .semaphore : asyncio .Semaphore = asyncio .Semaphore (3 )
274291
292+ self ._on_remove_callbacks : list [Callable [[], None ]] = []
293+
275294 self ._zdo_handler : ZDOClusterHandler = ZDOClusterHandler (self )
276295 self ._zdo_handler .on_add ()
296+ self ._on_remove_callbacks .append (self ._zdo_handler .on_remove )
277297
278298 self .status : DeviceStatus = DeviceStatus .CREATED
279299
280300 self ._endpoints : dict [int , Endpoint ] = {}
281301 for ep_id , endpoint in zigpy_device .endpoints .items ():
282302 if ep_id != 0 :
283- self ._endpoints [ep_id ] = Endpoint .new (endpoint , self )
303+ ep = Endpoint .new (endpoint , self )
304+ self ._endpoints [ep_id ] = ep
305+ self ._on_remove_callbacks .append (ep .on_remove )
284306
285307 def __repr__ (self ) -> str :
286308 """Return a string representation of the device."""
@@ -557,14 +579,9 @@ def zigbee_signature(self) -> dict[str, Any]:
557579 }
558580
559581 @property
560- def sw_version (self ) -> int | None :
582+ def firmware_version (self ) -> str | None :
561583 """Return the software version for this device."""
562- return self ._sw_build_id
563-
564- @sw_version .setter
565- def sw_version (self , sw_build_id : int ) -> None :
566- """Set the software version for this device."""
567- self ._sw_build_id = sw_build_id
584+ return self ._firmware_version
568585
569586 @property
570587 def platform_entities (self ) -> dict [tuple [Platform , str ], PlatformEntity ]:
@@ -587,9 +604,21 @@ def new(
587604 """Create new device."""
588605 return cls (zigpy_dev , gateway )
589606
590- def async_update_sw_build_id (self , sw_version : int ) -> None :
591- """Update device sw version."""
592- self ._sw_build_id = sw_version
607+ def async_update_firmware_version (self , firmware_version : str ) -> None :
608+ """Update device firmware version."""
609+ if firmware_version == self ._firmware_version :
610+ return
611+
612+ old_firmware_version = self ._firmware_version
613+ self ._firmware_version = firmware_version
614+
615+ self .emit (
616+ DeviceFirmwareInfoUpdatedEvent .event_type ,
617+ DeviceFirmwareInfoUpdatedEvent (
618+ old_firmware_version = old_firmware_version ,
619+ new_firmware_version = firmware_version ,
620+ ),
621+ )
593622
594623 async def _check_available (self , * _ : Any ) -> None :
595624 # don't flip the availability state of the coordinator
@@ -903,16 +932,32 @@ async def async_initialize(self, from_cache: bool = False) -> None:
903932 # At this point we can compute a primary entity
904933 self ._compute_primary_entity ()
905934
935+ # Sync the device's firmware version with the first platform entity
936+ for (platform , _unique_id ), entity in self .platform_entities .items ():
937+ if platform != Platform .UPDATE :
938+ continue
939+
940+ self ._firmware_version = entity .installed_version
941+
942+ def entity_update_listener (event : EntityStateChangedEvent ) -> None :
943+ """Listen to firmware update entity changes."""
944+ entity = self .get_platform_entity (event .platform , event .unique_id )
945+ self .async_update_firmware_version (entity .installed_version )
946+
947+ self ._on_remove_callbacks .append (
948+ entity .on_event (STATE_CHANGED , entity_update_listener )
949+ )
950+
951+ break
952+
906953 self .debug ("power source: %s" , self .power_source )
907954 self .status = DeviceStatus .INITIALIZED
908955 self .debug ("completed initialization" )
909956
910957 async def on_remove (self ) -> None :
911958 """Cancel tasks this device owns."""
912- self ._zdo_handler .on_remove ()
913-
914- for endpoint in self ._endpoints .values ():
915- endpoint .on_remove ()
959+ for callback in self ._on_remove_callbacks :
960+ callback ()
916961
917962 for platform_entity in self ._platform_entities .values ():
918963 await platform_entity .on_remove ()
0 commit comments