2022-08-30 16:49:27 +00:00
|
|
|
"""Representation of Z-Wave updates."""
|
|
|
|
from __future__ import annotations
|
|
|
|
|
2022-09-06 13:40:20 +00:00
|
|
|
import asyncio
|
2022-08-30 16:49:27 +00:00
|
|
|
from collections.abc import Callable
|
2022-09-06 13:40:20 +00:00
|
|
|
from datetime import datetime, timedelta
|
2022-08-30 16:49:27 +00:00
|
|
|
from typing import Any
|
|
|
|
|
|
|
|
from awesomeversion import AwesomeVersion
|
|
|
|
from zwave_js_server.client import Client as ZwaveClient
|
|
|
|
from zwave_js_server.const import NodeStatus
|
2022-09-06 13:40:20 +00:00
|
|
|
from zwave_js_server.exceptions import BaseZwaveJSServerError, FailedZWaveCommand
|
2022-08-30 16:49:27 +00:00
|
|
|
from zwave_js_server.model.driver import Driver
|
|
|
|
from zwave_js_server.model.firmware import FirmwareUpdateInfo
|
|
|
|
from zwave_js_server.model.node import Node as ZwaveNode
|
|
|
|
|
|
|
|
from homeassistant.components.update import UpdateDeviceClass, UpdateEntity
|
|
|
|
from homeassistant.components.update.const import UpdateEntityFeature
|
|
|
|
from homeassistant.config_entries import ConfigEntry
|
|
|
|
from homeassistant.core import HomeAssistant, callback
|
|
|
|
from homeassistant.exceptions import HomeAssistantError
|
|
|
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
2022-09-05 10:15:14 +00:00
|
|
|
from homeassistant.helpers.entity import EntityCategory
|
2022-08-30 16:49:27 +00:00
|
|
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
2022-09-06 13:40:20 +00:00
|
|
|
from homeassistant.helpers.event import async_call_later
|
|
|
|
from homeassistant.helpers.start import async_at_start
|
2022-08-30 16:49:27 +00:00
|
|
|
|
|
|
|
from .const import API_KEY_FIRMWARE_UPDATE_SERVICE, DATA_CLIENT, DOMAIN, LOGGER
|
2022-09-05 10:15:14 +00:00
|
|
|
from .helpers import get_device_info, get_valueless_base_unique_id
|
2022-08-30 16:49:27 +00:00
|
|
|
|
|
|
|
PARALLEL_UPDATES = 1
|
|
|
|
|
|
|
|
|
|
|
|
async def async_setup_entry(
|
|
|
|
hass: HomeAssistant,
|
|
|
|
config_entry: ConfigEntry,
|
|
|
|
async_add_entities: AddEntitiesCallback,
|
|
|
|
) -> None:
|
|
|
|
"""Set up Z-Wave button from config entry."""
|
|
|
|
client: ZwaveClient = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT]
|
|
|
|
|
2022-09-06 13:40:20 +00:00
|
|
|
semaphore = asyncio.Semaphore(3)
|
|
|
|
|
2022-08-30 16:49:27 +00:00
|
|
|
@callback
|
|
|
|
def async_add_firmware_update_entity(node: ZwaveNode) -> None:
|
|
|
|
"""Add firmware update entity."""
|
|
|
|
driver = client.driver
|
|
|
|
assert driver is not None # Driver is ready before platforms are loaded.
|
2022-09-06 13:40:20 +00:00
|
|
|
async_add_entities([ZWaveNodeFirmwareUpdate(driver, node, semaphore)])
|
2022-08-30 16:49:27 +00:00
|
|
|
|
|
|
|
config_entry.async_on_unload(
|
|
|
|
async_dispatcher_connect(
|
|
|
|
hass,
|
|
|
|
f"{DOMAIN}_{config_entry.entry_id}_add_firmware_update_entity",
|
|
|
|
async_add_firmware_update_entity,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
class ZWaveNodeFirmwareUpdate(UpdateEntity):
|
|
|
|
"""Representation of a firmware update entity."""
|
|
|
|
|
|
|
|
_attr_entity_category = EntityCategory.CONFIG
|
|
|
|
_attr_device_class = UpdateDeviceClass.FIRMWARE
|
|
|
|
_attr_supported_features = (
|
|
|
|
UpdateEntityFeature.INSTALL | UpdateEntityFeature.RELEASE_NOTES
|
|
|
|
)
|
|
|
|
_attr_has_entity_name = True
|
2022-09-06 13:40:20 +00:00
|
|
|
_attr_should_poll = False
|
2022-08-30 16:49:27 +00:00
|
|
|
|
2022-09-06 13:40:20 +00:00
|
|
|
def __init__(
|
|
|
|
self, driver: Driver, node: ZwaveNode, semaphore: asyncio.Semaphore
|
|
|
|
) -> None:
|
2022-08-30 16:49:27 +00:00
|
|
|
"""Initialize a Z-Wave device firmware update entity."""
|
|
|
|
self.driver = driver
|
|
|
|
self.node = node
|
2022-09-06 13:40:20 +00:00
|
|
|
self.semaphore = semaphore
|
2022-08-30 16:49:27 +00:00
|
|
|
self._latest_version_firmware: FirmwareUpdateInfo | None = None
|
|
|
|
self._status_unsub: Callable[[], None] | None = None
|
2022-09-06 13:40:20 +00:00
|
|
|
self._poll_unsub: Callable[[], None] | None = None
|
2022-08-30 16:49:27 +00:00
|
|
|
|
|
|
|
# Entity class attributes
|
|
|
|
self._attr_name = "Firmware"
|
|
|
|
self._base_unique_id = get_valueless_base_unique_id(driver, node)
|
|
|
|
self._attr_unique_id = f"{self._base_unique_id}.firmware_update"
|
2022-09-06 13:40:20 +00:00
|
|
|
self._attr_installed_version = self._attr_latest_version = node.firmware_version
|
2022-08-30 16:49:27 +00:00
|
|
|
# device may not be precreated in main handler yet
|
2022-09-05 10:15:14 +00:00
|
|
|
self._attr_device_info = get_device_info(driver, node)
|
2022-08-30 16:49:27 +00:00
|
|
|
|
2022-09-06 13:40:20 +00:00
|
|
|
@callback
|
2022-09-03 20:53:21 +00:00
|
|
|
def _update_on_status_change(self, _: dict[str, Any]) -> None:
|
2022-08-30 16:49:27 +00:00
|
|
|
"""Update the entity when node is awake."""
|
|
|
|
self._status_unsub = None
|
2022-09-06 13:40:20 +00:00
|
|
|
self.hass.async_create_task(self._async_update())
|
2022-08-30 16:49:27 +00:00
|
|
|
|
2022-09-06 13:40:20 +00:00
|
|
|
async def _async_update(self, _: HomeAssistant | datetime | None = None) -> None:
|
2022-08-30 16:49:27 +00:00
|
|
|
"""Update the entity."""
|
2022-09-06 13:40:20 +00:00
|
|
|
self._poll_unsub = None
|
2022-09-03 20:53:21 +00:00
|
|
|
for status, event_name in (
|
|
|
|
(NodeStatus.ASLEEP, "wake up"),
|
|
|
|
(NodeStatus.DEAD, "alive"),
|
|
|
|
):
|
|
|
|
if self.node.status == status:
|
|
|
|
if not self._status_unsub:
|
|
|
|
self._status_unsub = self.node.once(
|
|
|
|
event_name, self._update_on_status_change
|
|
|
|
)
|
|
|
|
return
|
|
|
|
|
2022-09-06 13:40:20 +00:00
|
|
|
try:
|
|
|
|
async with self.semaphore:
|
|
|
|
available_firmware_updates = (
|
|
|
|
await self.driver.controller.async_get_available_firmware_updates(
|
|
|
|
self.node, API_KEY_FIRMWARE_UPDATE_SERVICE
|
|
|
|
)
|
|
|
|
)
|
|
|
|
except FailedZWaveCommand as err:
|
|
|
|
LOGGER.debug(
|
|
|
|
"Failed to get firmware updates for node %s: %s",
|
|
|
|
self.node.node_id,
|
|
|
|
err,
|
2022-08-31 02:02:13 +00:00
|
|
|
)
|
2022-08-30 16:49:27 +00:00
|
|
|
else:
|
2022-09-06 13:40:20 +00:00
|
|
|
if available_firmware_updates:
|
|
|
|
self._latest_version_firmware = latest_firmware = max(
|
|
|
|
available_firmware_updates,
|
|
|
|
key=lambda x: AwesomeVersion(x.version),
|
|
|
|
)
|
|
|
|
|
|
|
|
# If we have an available firmware update that is a higher version than
|
|
|
|
# what's on the node, we should advertise it, otherwise there is
|
|
|
|
# nothing to do.
|
|
|
|
new_version = latest_firmware.version
|
|
|
|
current_version = self.node.firmware_version
|
|
|
|
if AwesomeVersion(new_version) > AwesomeVersion(current_version):
|
|
|
|
self._attr_latest_version = new_version
|
|
|
|
self.async_write_ha_state()
|
|
|
|
finally:
|
|
|
|
self._poll_unsub = async_call_later(
|
|
|
|
self.hass, timedelta(days=1), self._async_update
|
|
|
|
)
|
2022-08-30 16:49:27 +00:00
|
|
|
|
|
|
|
async def async_release_notes(self) -> str | None:
|
|
|
|
"""Get release notes."""
|
|
|
|
if self._latest_version_firmware is None:
|
|
|
|
return None
|
|
|
|
return self._latest_version_firmware.changelog
|
|
|
|
|
|
|
|
async def async_install(
|
|
|
|
self, version: str | None, backup: bool, **kwargs: Any
|
|
|
|
) -> None:
|
|
|
|
"""Install an update."""
|
|
|
|
firmware = self._latest_version_firmware
|
|
|
|
assert firmware
|
|
|
|
try:
|
|
|
|
for file in firmware.files:
|
|
|
|
await self.driver.controller.async_begin_ota_firmware_update(
|
|
|
|
self.node, file
|
|
|
|
)
|
|
|
|
except BaseZwaveJSServerError as err:
|
|
|
|
raise HomeAssistantError(err) from err
|
|
|
|
else:
|
2022-09-06 13:40:20 +00:00
|
|
|
self._attr_installed_version = self._attr_latest_version = firmware.version
|
2022-08-31 02:02:13 +00:00
|
|
|
self._latest_version_firmware = None
|
2022-09-06 13:40:20 +00:00
|
|
|
self.async_write_ha_state()
|
2022-08-30 16:49:27 +00:00
|
|
|
|
|
|
|
async def async_poll_value(self, _: bool) -> None:
|
|
|
|
"""Poll a value."""
|
|
|
|
LOGGER.error(
|
|
|
|
"There is no value to refresh for this entity so the zwave_js.refresh_value "
|
|
|
|
"service won't work for it"
|
|
|
|
)
|
|
|
|
|
|
|
|
async def async_added_to_hass(self) -> None:
|
|
|
|
"""Call when entity is added."""
|
|
|
|
self.async_on_remove(
|
|
|
|
async_dispatcher_connect(
|
|
|
|
self.hass,
|
|
|
|
f"{DOMAIN}_{self.unique_id}_poll_value",
|
|
|
|
self.async_poll_value,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
self.async_on_remove(
|
|
|
|
async_dispatcher_connect(
|
|
|
|
self.hass,
|
|
|
|
f"{DOMAIN}_{self._base_unique_id}_remove_entity",
|
|
|
|
self.async_remove,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
2022-09-06 13:40:20 +00:00
|
|
|
self.async_on_remove(async_at_start(self.hass, self._async_update))
|
|
|
|
|
2022-08-30 16:49:27 +00:00
|
|
|
async def async_will_remove_from_hass(self) -> None:
|
|
|
|
"""Call when entity will be removed."""
|
|
|
|
if self._status_unsub:
|
|
|
|
self._status_unsub()
|
|
|
|
self._status_unsub = None
|
2022-09-06 13:40:20 +00:00
|
|
|
|
|
|
|
if self._poll_unsub:
|
|
|
|
self._poll_unsub()
|
|
|
|
self._poll_unsub = None
|