mirror of
https://github.com/home-assistant/core.git
synced 2025-08-01 09:38:21 +00:00
Add Z-Wave controller firmware updates (#149623)
This commit is contained in:
parent
8e9e304608
commit
bb6bcfdd01
@ -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,
|
||||||
|
@ -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
Loading…
x
Reference in New Issue
Block a user