Add Z-Wave controller firmware updates (#149623)

This commit is contained in:
Martin Hjelmare 2025-07-30 11:07:41 +02:00 committed by GitHub
parent 8e9e304608
commit bb6bcfdd01
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 580 additions and 270 deletions

View File

@ -147,6 +147,7 @@ CONFIG_SCHEMA = vol.Schema(
}, },
extra=vol.ALLOW_EXTRA, extra=vol.ALLOW_EXTRA,
) )
MIN_CONTROLLER_FIRMWARE_SDK_VERSION = AwesomeVersion("6.50.0")
PLATFORMS = [ PLATFORMS = [
Platform.BINARY_SENSOR, Platform.BINARY_SENSOR,
@ -799,11 +800,19 @@ class NodeEvents:
node.on("notification", self.async_on_notification) node.on("notification", self.async_on_notification)
) )
# Create a firmware update entity for each non-controller device that # Create a firmware update entity for each device that
# supports firmware updates # supports firmware updates
if not node.is_controller_node and any( controller = self.controller_events.driver_events.driver.controller
cc.id == CommandClass.FIRMWARE_UPDATE_MD.value if (
for cc in node.command_classes not (is_controller_node := node.is_controller_node)
and any(
cc.id == CommandClass.FIRMWARE_UPDATE_MD.value
for cc in node.command_classes
)
) or (
is_controller_node
and (sdk_version := controller.sdk_version) is not None
and sdk_version >= MIN_CONTROLLER_FIRMWARE_SDK_VERSION
): ):
async_dispatcher_send( async_dispatcher_send(
self.hass, self.hass,

View File

@ -4,26 +4,28 @@ from __future__ import annotations
import asyncio import asyncio
from collections import Counter from collections import Counter
from collections.abc import Callable from collections.abc import Awaitable, Callable
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Any, Final from typing import Any, Final, cast
from awesomeversion import AwesomeVersion from awesomeversion import AwesomeVersion
from zwave_js_server.const import NodeStatus from zwave_js_server.const import NodeStatus
from zwave_js_server.exceptions import BaseZwaveJSServerError, FailedZWaveCommand from zwave_js_server.exceptions import BaseZwaveJSServerError, FailedZWaveCommand
from zwave_js_server.model.driver import Driver from zwave_js_server.model.driver import Driver
from zwave_js_server.model.node import Node as ZwaveNode from zwave_js_server.model.firmware import (
from zwave_js_server.model.node.firmware import ( FirmwareUpdateInfo,
NodeFirmwareUpdateInfo, FirmwareUpdateProgress,
NodeFirmwareUpdateProgress, FirmwareUpdateResult,
NodeFirmwareUpdateResult,
) )
from zwave_js_server.model.node import Node as ZwaveNode
from zwave_js_server.model.node.firmware import NodeFirmwareUpdateInfo
from homeassistant.components.update import ( from homeassistant.components.update import (
ATTR_LATEST_VERSION, ATTR_LATEST_VERSION,
UpdateDeviceClass, UpdateDeviceClass,
UpdateEntity, UpdateEntity,
UpdateEntityDescription,
UpdateEntityFeature, UpdateEntityFeature,
) )
from homeassistant.const import EntityCategory from homeassistant.const import EntityCategory
@ -45,11 +47,54 @@ UPDATE_DELAY_INTERVAL = 5 # In minutes
ATTR_LATEST_VERSION_FIRMWARE = "latest_version_firmware" ATTR_LATEST_VERSION_FIRMWARE = "latest_version_firmware"
@dataclass(frozen=True, kw_only=True)
class ZWaveUpdateEntityDescription(UpdateEntityDescription):
"""Class describing Z-Wave update entity."""
install_method: Callable[
[ZWaveFirmwareUpdateEntity, FirmwareUpdateInfo],
Awaitable[FirmwareUpdateResult],
]
progress_method: Callable[[ZWaveFirmwareUpdateEntity], Callable[[], None]]
finished_method: Callable[[ZWaveFirmwareUpdateEntity], Callable[[], None]]
CONTROLLER_UPDATE_ENTITY_DESCRIPTION = ZWaveUpdateEntityDescription(
key="controller_firmware_update",
install_method=(
lambda entity, firmware_update_info: entity.driver.async_firmware_update_otw(
update_info=firmware_update_info
)
),
progress_method=lambda entity: entity.driver.on(
"firmware update progress", entity.update_progress
),
finished_method=lambda entity: entity.driver.on(
"firmware update finished", entity.update_finished
),
)
NODE_UPDATE_ENTITY_DESCRIPTION = ZWaveUpdateEntityDescription(
key="node_firmware_update",
install_method=(
lambda entity,
firmware_update_info: entity.driver.controller.async_firmware_update_ota(
entity.node, cast(NodeFirmwareUpdateInfo, firmware_update_info)
)
),
progress_method=lambda entity: entity.node.on(
"firmware update progress", entity.update_progress
),
finished_method=lambda entity: entity.node.on(
"firmware update finished", entity.update_finished
),
)
@dataclass @dataclass
class ZWaveNodeFirmwareUpdateExtraStoredData(ExtraStoredData): class ZWaveFirmwareUpdateExtraStoredData(ExtraStoredData):
"""Extra stored data for Z-Wave node firmware update entity.""" """Extra stored data for Z-Wave node firmware update entity."""
latest_version_firmware: NodeFirmwareUpdateInfo | None latest_version_firmware: FirmwareUpdateInfo | None
def as_dict(self) -> dict[str, Any]: def as_dict(self) -> dict[str, Any]:
"""Return a dict representation of the extra data.""" """Return a dict representation of the extra data."""
@ -60,7 +105,7 @@ class ZWaveNodeFirmwareUpdateExtraStoredData(ExtraStoredData):
} }
@classmethod @classmethod
def from_dict(cls, data: dict[str, Any]) -> ZWaveNodeFirmwareUpdateExtraStoredData: def from_dict(cls, data: dict[str, Any]) -> ZWaveFirmwareUpdateExtraStoredData:
"""Initialize the extra data from a dict.""" """Initialize the extra data from a dict."""
# If there was no firmware info stored, or if it's stale info, we don't restore # If there was no firmware info stored, or if it's stale info, we don't restore
# anything. # anything.
@ -70,7 +115,7 @@ class ZWaveNodeFirmwareUpdateExtraStoredData(ExtraStoredData):
): ):
return cls(None) return cls(None)
return cls(NodeFirmwareUpdateInfo.from_dict(firmware_dict)) return cls(FirmwareUpdateInfo.from_dict(firmware_dict))
async def async_setup_entry( async def async_setup_entry(
@ -92,7 +137,23 @@ async def async_setup_entry(
delay = timedelta(minutes=(cnt[UPDATE_DELAY_STRING] * UPDATE_DELAY_INTERVAL)) delay = timedelta(minutes=(cnt[UPDATE_DELAY_STRING] * UPDATE_DELAY_INTERVAL))
driver = client.driver driver = client.driver
assert driver is not None # Driver is ready before platforms are loaded. assert driver is not None # Driver is ready before platforms are loaded.
async_add_entities([ZWaveNodeFirmwareUpdate(driver, node, delay)]) if node.is_controller_node:
# If the node is a controller, we create a controller firmware update entity
entity = ZWaveFirmwareUpdateEntity(
driver,
node,
delay=delay,
entity_description=CONTROLLER_UPDATE_ENTITY_DESCRIPTION,
)
else:
# If the node is not a controller, we create a node firmware update entity
entity = ZWaveFirmwareUpdateEntity(
driver,
node,
delay=delay,
entity_description=NODE_UPDATE_ENTITY_DESCRIPTION,
)
async_add_entities([entity])
config_entry.async_on_unload( config_entry.async_on_unload(
async_dispatcher_connect( async_dispatcher_connect(
@ -103,9 +164,12 @@ async def async_setup_entry(
) )
class ZWaveNodeFirmwareUpdate(UpdateEntity): class ZWaveFirmwareUpdateEntity(UpdateEntity):
"""Representation of a firmware update entity.""" """Representation of a firmware update entity."""
driver: Driver
entity_description: ZWaveUpdateEntityDescription
node: ZwaveNode
_attr_entity_category = EntityCategory.CONFIG _attr_entity_category = EntityCategory.CONFIG
_attr_device_class = UpdateDeviceClass.FIRMWARE _attr_device_class = UpdateDeviceClass.FIRMWARE
_attr_supported_features = ( _attr_supported_features = (
@ -116,17 +180,24 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity):
_attr_has_entity_name = True _attr_has_entity_name = True
_attr_should_poll = False _attr_should_poll = False
def __init__(self, driver: Driver, node: ZwaveNode, delay: timedelta) -> None: def __init__(
self,
driver: Driver,
node: ZwaveNode,
delay: timedelta,
entity_description: ZWaveUpdateEntityDescription,
) -> None:
"""Initialize a Z-Wave device firmware update entity.""" """Initialize a Z-Wave device firmware update entity."""
self.driver = driver self.driver = driver
self.entity_description = entity_description
self.node = node self.node = node
self._latest_version_firmware: NodeFirmwareUpdateInfo | None = None self._latest_version_firmware: FirmwareUpdateInfo | None = None
self._status_unsub: Callable[[], None] | None = None self._status_unsub: Callable[[], None] | None = None
self._poll_unsub: Callable[[], None] | None = None self._poll_unsub: Callable[[], None] | None = None
self._progress_unsub: Callable[[], None] | None = None self._progress_unsub: Callable[[], None] | None = None
self._finished_unsub: Callable[[], None] | None = None self._finished_unsub: Callable[[], None] | None = None
self._finished_event = asyncio.Event() self._finished_event = asyncio.Event()
self._result: NodeFirmwareUpdateResult | None = None self._result: FirmwareUpdateResult | None = None
self._delay: Final[timedelta] = delay self._delay: Final[timedelta] = delay
# Entity class attributes # Entity class attributes
@ -138,9 +209,9 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity):
self._attr_device_info = get_device_info(driver, node) self._attr_device_info = get_device_info(driver, node)
@property @property
def extra_restore_state_data(self) -> ZWaveNodeFirmwareUpdateExtraStoredData: def extra_restore_state_data(self) -> ZWaveFirmwareUpdateExtraStoredData:
"""Return ZWave Node Firmware Update specific state data to be restored.""" """Return ZWave Node Firmware Update specific state data to be restored."""
return ZWaveNodeFirmwareUpdateExtraStoredData(self._latest_version_firmware) return ZWaveFirmwareUpdateExtraStoredData(self._latest_version_firmware)
@callback @callback
def _update_on_status_change(self, _: dict[str, Any]) -> None: def _update_on_status_change(self, _: dict[str, Any]) -> None:
@ -149,9 +220,9 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity):
self.hass.async_create_task(self._async_update()) self.hass.async_create_task(self._async_update())
@callback @callback
def _update_progress(self, event: dict[str, Any]) -> None: def update_progress(self, event: dict[str, Any]) -> None:
"""Update install progress on event.""" """Update install progress on event."""
progress: NodeFirmwareUpdateProgress = event["firmware_update_progress"] progress: FirmwareUpdateProgress = event["firmware_update_progress"]
if not self._latest_version_firmware: if not self._latest_version_firmware:
return return
self._attr_in_progress = True self._attr_in_progress = True
@ -159,9 +230,9 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity):
self.async_write_ha_state() self.async_write_ha_state()
@callback @callback
def _update_finished(self, event: dict[str, Any]) -> None: def update_finished(self, event: dict[str, Any]) -> None:
"""Update install progress on event.""" """Update install progress on event."""
result: NodeFirmwareUpdateResult = event["firmware_update_finished"] result: FirmwareUpdateResult = event["firmware_update_finished"]
self._result = result self._result = result
self._finished_event.set() self._finished_event.set()
@ -266,15 +337,11 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity):
self._attr_update_percentage = None self._attr_update_percentage = None
self.async_write_ha_state() self.async_write_ha_state()
self._progress_unsub = self.node.on( self._progress_unsub = self.entity_description.progress_method(self)
"firmware update progress", self._update_progress self._finished_unsub = self.entity_description.finished_method(self)
)
self._finished_unsub = self.node.on(
"firmware update finished", self._update_finished
)
try: try:
await self.driver.controller.async_firmware_update_ota(self.node, firmware) await self.entity_description.install_method(self, firmware)
except BaseZwaveJSServerError as err: except BaseZwaveJSServerError as err:
self._unsub_firmware_events_and_reset_progress() self._unsub_firmware_events_and_reset_progress()
raise HomeAssistantError(err) from err raise HomeAssistantError(err) from err
@ -342,8 +409,7 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity):
is not None is not None
and (extra_data := await self.async_get_last_extra_data()) and (extra_data := await self.async_get_last_extra_data())
and ( and (
latest_version_firmware latest_version_firmware := ZWaveFirmwareUpdateExtraStoredData.from_dict(
:= ZWaveNodeFirmwareUpdateExtraStoredData.from_dict(
extra_data.as_dict() extra_data.as_dict()
).latest_version_firmware ).latest_version_firmware
) )

File diff suppressed because it is too large Load Diff