From 3115af10416cb87bb9f53b2c758b93edbf4f8bb8 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Tue, 30 Jan 2024 22:34:48 -0500 Subject: [PATCH] Add update platform to ZHA (bumps zigpy to 0.61.0) (#107612) * stub out zha update entity * update matcher * updates based on assumptions / conversation * hook into current installed version * post rebase cleanup * incorporate zigpy changes * fix async_setup_entry * fix sw_version * make ota work with config diagnostic match * fix version format * sync up with latest Zigpy changes * fix name attribute * disable ota providers for tests * update device list * review comment * add current_file_version to Ota ZCL_INIT_ATTRS * updates to update and start tests * get installed version from restore data * better version handling * remove done todo notes * reorganize test * move image notify to cluster handler * add test for manual update check * firmware update success test * coverage * use zigpy defs * clean up after rebase * bump Zigpy * cleanup from review comments * fix busted F string * fix empty error * move inside check * guard zigbee network from bulk check for updates --- .../zha/core/cluster_handlers/general.py | 49 +- homeassistant/components/zha/core/const.py | 2 + homeassistant/components/zha/core/device.py | 10 + .../components/zha/core/discovery.py | 11 +- homeassistant/components/zha/manifest.json | 2 +- homeassistant/components/zha/update.py | 235 +++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/zha/common.py | 1 - tests/components/zha/conftest.py | 8 + tests/components/zha/test_cluster_handlers.py | 16 +- tests/components/zha/test_update.py | 587 ++++++++++++++++++ tests/components/zha/zha_devices_list.py | 385 ++++++++++++ 13 files changed, 1291 insertions(+), 19 deletions(-) create mode 100644 homeassistant/components/zha/update.py create mode 100644 tests/components/zha/test_update.py diff --git a/homeassistant/components/zha/core/cluster_handlers/general.py b/homeassistant/components/zha/core/cluster_handlers/general.py index 3cb450cc270..045e6a8f593 100644 --- a/homeassistant/components/zha/core/cluster_handlers/general.py +++ b/homeassistant/components/zha/core/cluster_handlers/general.py @@ -56,6 +56,7 @@ from ..const import ( SIGNAL_MOVE_LEVEL, SIGNAL_SET_LEVEL, SIGNAL_UPDATE_DEVICE, + UNKNOWN as ZHA_UNKNOWN, ) from . import ( AttrReportConfig, @@ -523,12 +524,47 @@ class OnOffConfigurationClusterHandler(ClusterHandler): @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(Ota.cluster_id) -@registries.CLIENT_CLUSTER_HANDLER_REGISTRY.register(Ota.cluster_id) -class OtaClientClusterHandler(ClientClusterHandler): +class OtaClusterHandler(ClusterHandler): """OTA cluster handler.""" BIND: bool = False + # Some devices have this cluster in the wrong collection (e.g. Third Reality) + ZCL_INIT_ATTRS = { + Ota.AttributeDefs.current_file_version.name: True, + } + + @property + def current_file_version(self) -> str: + """Return cached value of current_file_version attribute.""" + current_file_version = self.cluster.get( + Ota.AttributeDefs.current_file_version.name + ) + if current_file_version is not None: + return f"0x{int(current_file_version):08x}" + return ZHA_UNKNOWN + + +@registries.CLIENT_CLUSTER_HANDLER_REGISTRY.register(Ota.cluster_id) +class OtaClientClusterHandler(ClientClusterHandler): + """OTA client cluster handler.""" + + BIND: bool = False + + ZCL_INIT_ATTRS = { + Ota.AttributeDefs.current_file_version.name: True, + } + + @property + def current_file_version(self) -> str: + """Return cached value of current_file_version attribute.""" + current_file_version = self.cluster.get( + Ota.AttributeDefs.current_file_version.name + ) + if current_file_version is not None: + return f"0x{int(current_file_version):08x}" + return ZHA_UNKNOWN + @callback def cluster_command( self, tsn: int, command_id: int, args: list[Any] | None @@ -540,10 +576,17 @@ class OtaClientClusterHandler(ClientClusterHandler): cmd_name = command_id signal_id = self._endpoint.unique_id.split("-")[0] - if cmd_name == "query_next_image": + if cmd_name == Ota.ServerCommandDefs.query_next_image.name: assert args self.async_send_signal(SIGNAL_UPDATE_DEVICE.format(signal_id), args[3]) + async def async_check_for_update(self): + """Check for firmware availability by issuing an image notify command.""" + await self.cluster.image_notify( + payload_type=(self.cluster.ImageNotifyCommand.PayloadType.QueryJitter), + query_jitter=100, + ) + @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(Partition.cluster_id) class PartitionClusterHandler(ClusterHandler): diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index ecbd347a621..cb0aa466046 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -89,6 +89,7 @@ CLUSTER_HANDLER_LEVEL = ATTR_LEVEL CLUSTER_HANDLER_MULTISTATE_INPUT = "multistate_input" CLUSTER_HANDLER_OCCUPANCY = "occupancy" CLUSTER_HANDLER_ON_OFF = "on_off" +CLUSTER_HANDLER_OTA = "ota" CLUSTER_HANDLER_POWER_CONFIGURATION = "power" CLUSTER_HANDLER_PRESSURE = "pressure" CLUSTER_HANDLER_SHADE = "shade" @@ -120,6 +121,7 @@ PLATFORMS = ( Platform.SENSOR, Platform.SIREN, Platform.SWITCH, + Platform.UPDATE, ) CONF_ALARM_MASTER_CODE = "alarm_master_code" diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index a678dbea89a..dd5a39115ae 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -26,6 +26,7 @@ from homeassistant.const import ATTR_COMMAND, ATTR_DEVICE_ID, ATTR_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.device_registry import DeviceEntry from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -406,6 +407,15 @@ class ZHADevice(LogMixin): ATTR_MODEL: self.model, } + @property + def sw_version(self) -> str | None: + """Return the software version for this device.""" + device_registry = dr.async_get(self.hass) + reg_device: DeviceEntry | None = device_registry.async_get(self.device_id) + if reg_device is None: + return None + return reg_device.sw_version + @classmethod def new( cls, diff --git a/homeassistant/components/zha/core/discovery.py b/homeassistant/components/zha/core/discovery.py index 1944f632e9a..1fed2caab60 100644 --- a/homeassistant/components/zha/core/discovery.py +++ b/homeassistant/components/zha/core/discovery.py @@ -6,6 +6,8 @@ from collections.abc import Callable import logging from typing import TYPE_CHECKING, cast +from zigpy.zcl.clusters.general import Ota + from homeassistant.const import CONF_TYPE, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er @@ -32,6 +34,7 @@ from .. import ( # noqa: F401 sensor, siren, switch, + update, ) from . import const as zha_const, registries as zha_regs @@ -233,10 +236,16 @@ class ProbeEndpoint: cmpt_by_dev_type = zha_regs.DEVICE_CLASS[ep_profile_id].get(ep_device_type) if config_diagnostic_entities: + cluster_handlers = list(endpoint.all_cluster_handlers.values()) + ota_handler_id = f"{endpoint.id}:0x{Ota.cluster_id:04x}" + if ota_handler_id in endpoint.client_cluster_handlers: + cluster_handlers.append( + endpoint.client_cluster_handlers[ota_handler_id] + ) matches, claimed = zha_regs.ZHA_ENTITIES.get_config_diagnostic_entity( endpoint.device.manufacturer, endpoint.device.model, - list(endpoint.all_cluster_handlers.values()), + cluster_handlers, endpoint.device.quirk_id, ) else: diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index ead1087b8c8..9e09e20819f 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -26,7 +26,7 @@ "pyserial-asyncio==0.6", "zha-quirks==0.0.110", "zigpy-deconz==0.22.4", - "zigpy==0.60.7", + "zigpy==0.61.0", "zigpy-xbee==0.20.1", "zigpy-zigate==0.12.0", "zigpy-znp==0.12.1", diff --git a/homeassistant/components/zha/update.py b/homeassistant/components/zha/update.py new file mode 100644 index 00000000000..93912fc68db --- /dev/null +++ b/homeassistant/components/zha/update.py @@ -0,0 +1,235 @@ +"""Representation of ZHA updates.""" +from __future__ import annotations + +from dataclasses import dataclass +import functools +from typing import TYPE_CHECKING, Any + +from zigpy.ota.image import BaseOTAImage +from zigpy.types import uint16_t +from zigpy.zcl.foundation import Status + +from homeassistant.components.update import ( + ATTR_INSTALLED_VERSION, + ATTR_LATEST_VERSION, + UpdateDeviceClass, + UpdateEntity, + UpdateEntityFeature, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EntityCategory, Platform +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.restore_state import ExtraStoredData + +from .core import discovery +from .core.const import CLUSTER_HANDLER_OTA, SIGNAL_ADD_ENTITIES, UNKNOWN +from .core.helpers import get_zha_data +from .core.registries import ZHA_ENTITIES +from .entity import ZhaEntity + +if TYPE_CHECKING: + from .core.cluster_handlers import ClusterHandler + from .core.device import ZHADevice + +CONFIG_DIAGNOSTIC_MATCH = functools.partial( + ZHA_ENTITIES.config_diagnostic_match, Platform.UPDATE +) + +# don't let homeassistant check for updates button hammer the zigbee network +PARALLEL_UPDATES = 1 + + +@dataclass +class ZHAFirmwareUpdateExtraStoredData(ExtraStoredData): + """Extra stored data for ZHA firmware update entity.""" + + image_type: uint16_t | None + + def as_dict(self) -> dict[str, Any]: + """Return a dict representation of the extra data.""" + return {"image_type": self.image_type} + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Zigbee Home Automation update from config entry.""" + zha_data = get_zha_data(hass) + entities_to_create = zha_data.platforms[Platform.UPDATE] + + unsub = async_dispatcher_connect( + hass, + SIGNAL_ADD_ENTITIES, + functools.partial( + discovery.async_add_entities, async_add_entities, entities_to_create + ), + ) + config_entry.async_on_unload(unsub) + + +@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_OTA) +class ZHAFirmwareUpdateEntity(ZhaEntity, UpdateEntity): + """Representation of a ZHA firmware update entity.""" + + _unique_id_suffix = "firmware_update" + _attr_entity_category = EntityCategory.CONFIG + _attr_device_class = UpdateDeviceClass.FIRMWARE + _attr_supported_features = ( + UpdateEntityFeature.INSTALL + | UpdateEntityFeature.PROGRESS + | UpdateEntityFeature.SPECIFIC_VERSION + ) + + def __init__( + self, + unique_id: str, + zha_device: ZHADevice, + channels: list[ClusterHandler], + **kwargs: Any, + ) -> None: + """Initialize the ZHA update entity.""" + super().__init__(unique_id, zha_device, channels, **kwargs) + self._ota_cluster_handler: ClusterHandler = self.cluster_handlers[ + CLUSTER_HANDLER_OTA + ] + self._attr_installed_version: str = self.determine_installed_version() + self._image_type: uint16_t | None = None + self._latest_version_firmware: BaseOTAImage | None = None + self._result = None + + @callback + def determine_installed_version(self) -> str: + """Determine the currently installed firmware version.""" + currently_installed_version = self._ota_cluster_handler.current_file_version + version_from_dr = self.zha_device.sw_version + if currently_installed_version == UNKNOWN and version_from_dr: + currently_installed_version = version_from_dr + return currently_installed_version + + @property + def extra_restore_state_data(self) -> ZHAFirmwareUpdateExtraStoredData: + """Return ZHA firmware update specific state data to be restored.""" + return ZHAFirmwareUpdateExtraStoredData(self._image_type) + + @callback + def device_ota_update_available(self, image: BaseOTAImage) -> None: + """Handle ota update available signal from Zigpy.""" + self._latest_version_firmware = image + self._attr_latest_version = f"0x{image.header.file_version:08x}" + self._image_type = image.header.image_type + self.async_write_ha_state() + + @callback + def _update_progress(self, current: int, total: int, progress: float) -> None: + """Update install progress on event.""" + assert self._latest_version_firmware + self._attr_in_progress = int(progress) + self.async_write_ha_state() + + @callback + def _reset_progress(self, write_state: bool = True) -> None: + """Reset update install progress.""" + self._result = None + self._attr_in_progress = False + if write_state: + self.async_write_ha_state() + + async def async_update(self) -> None: + """Handle the update entity service call to manually check for available firmware updates.""" + await super().async_update() + # check for updates in the HA settings menu can invoke this so we need to check if the device + # is mains powered so we don't get a ton of errors in the logs from sleepy devices. + if self.zha_device.available and self.zha_device.is_mains_powered: + await self._ota_cluster_handler.async_check_for_update() + + async def async_install( + self, version: str | None, backup: bool, **kwargs: Any + ) -> None: + """Install an update.""" + firmware = self._latest_version_firmware + assert firmware + self._reset_progress(False) + self._attr_in_progress = True + self.async_write_ha_state() + + try: + self._result = await self.zha_device.device.update_firmware( + self._latest_version_firmware, + self._update_progress, + ) + except Exception as ex: + self._reset_progress() + raise HomeAssistantError(ex) from ex + + assert self._result is not None + + # If the update was not successful, we should throw an error to let the user know + if self._result != Status.SUCCESS: + # save result since reset_progress will clear it + results = self._result + self._reset_progress() + raise HomeAssistantError(f"Update was not successful - result: {results}") + + # If we get here, all files were installed successfully + self._attr_installed_version = ( + self._attr_latest_version + ) = f"0x{firmware.header.file_version:08x}" + self._latest_version_firmware = None + self._reset_progress() + + async def async_added_to_hass(self) -> None: + """Call when entity is added.""" + await super().async_added_to_hass() + last_state = await self.async_get_last_state() + # If we have a complete previous state, use that to set the installed version + if ( + last_state + and self._attr_installed_version == UNKNOWN + and (installed_version := last_state.attributes.get(ATTR_INSTALLED_VERSION)) + ): + self._attr_installed_version = installed_version + # If we have a complete previous state, use that to set the latest version + if ( + last_state + and (latest_version := last_state.attributes.get(ATTR_LATEST_VERSION)) + is not None + and latest_version != UNKNOWN + ): + self._attr_latest_version = latest_version + # If we have no state or latest version to restore, or the latest version is + # the same as the installed version, we can set the latest + # version to installed so that the entity starts as off. + elif ( + not last_state + or not latest_version + or latest_version == self._attr_installed_version + ): + self._attr_latest_version = self._attr_installed_version + + if self._attr_latest_version != self._attr_installed_version and ( + extra_data := await self.async_get_last_extra_data() + ): + self._image_type = extra_data.as_dict()["image_type"] + if self._image_type: + self._latest_version_firmware = ( + await self.zha_device.device.application.ota.get_ota_image( + self.zha_device.manufacturer_code, self._image_type + ) + ) + # if we can't locate an image but we have a latest version that differs + # we should set the latest version to the installed version to avoid + # confusion and errors + if not self._latest_version_firmware: + self._attr_latest_version = self._attr_installed_version + + self.zha_device.device.add_listener(self) + + async def async_will_remove_from_hass(self) -> None: + """Call when entity will be removed.""" + await super().async_will_remove_from_hass() + self._reset_progress(False) diff --git a/requirements_all.txt b/requirements_all.txt index c31be96d2d3..89c4ef5d6c2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2930,7 +2930,7 @@ zigpy-zigate==0.12.0 zigpy-znp==0.12.1 # homeassistant.components.zha -zigpy==0.60.7 +zigpy==0.61.0 # homeassistant.components.zoneminder zm-py==0.5.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ef7bab8f77b..c4e1d822f5f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2241,7 +2241,7 @@ zigpy-zigate==0.12.0 zigpy-znp==0.12.1 # homeassistant.components.zha -zigpy==0.60.7 +zigpy==0.61.0 # homeassistant.components.zwave_js zwave-js-server-python==0.55.3 diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index 44155d741b7..d679ac5cb03 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -54,7 +54,6 @@ def patch_cluster(cluster): cluster.configure_reporting_multiple = AsyncMock( return_value=zcl_f.ConfigureReportingResponse.deserialize(b"\x00")[0] ) - cluster.deserialize = Mock() cluster.handle_cluster_request = Mock() cluster.read_attributes = AsyncMock(wraps=cluster.read_attributes) cluster.read_attributes_raw = AsyncMock(side_effect=_read_attribute_raw) diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py index 4303c156e4b..1627ced5cbb 100644 --- a/tests/components/zha/conftest.py +++ b/tests/components/zha/conftest.py @@ -154,6 +154,14 @@ async def zigpy_app_controller(): zigpy.config.CONF_STARTUP_ENERGY_SCAN: False, zigpy.config.CONF_NWK_BACKUP_ENABLED: False, zigpy.config.CONF_TOPO_SCAN_ENABLED: False, + zigpy.config.CONF_OTA: { + zigpy.config.CONF_OTA_IKEA: False, + zigpy.config.CONF_OTA_INOVELLI: False, + zigpy.config.CONF_OTA_LEDVANCE: False, + zigpy.config.CONF_OTA_SALUS: False, + zigpy.config.CONF_OTA_SONOFF: False, + zigpy.config.CONF_OTA_THIRDREALITY: False, + }, } ) diff --git a/tests/components/zha/test_cluster_handlers.py b/tests/components/zha/test_cluster_handlers.py index b248244e243..7c17d79fe0e 100644 --- a/tests/components/zha/test_cluster_handlers.py +++ b/tests/components/zha/test_cluster_handlers.py @@ -990,15 +990,9 @@ async def test_cluster_handler_naming() -> None: assert issubclass(client_cluster_handler, cluster_handlers.ClientClusterHandler) assert client_cluster_handler.__name__.endswith("ClientClusterHandler") - server_cluster_handlers = [] for cluster_handler_dict in registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.values(): - # remove this filter in the update platform PR - server_cluster_handlers += [ - cluster_handler - for cluster_handler in cluster_handler_dict.values() - if cluster_handler.__name__ != "OtaClientClusterHandler" - ] - - for cluster_handler in server_cluster_handlers: - assert not issubclass(cluster_handler, cluster_handlers.ClientClusterHandler) - assert cluster_handler.__name__.endswith("ClusterHandler") + for cluster_handler in cluster_handler_dict.values(): + assert not issubclass( + cluster_handler, cluster_handlers.ClientClusterHandler + ) + assert cluster_handler.__name__.endswith("ClusterHandler") diff --git a/tests/components/zha/test_update.py b/tests/components/zha/test_update.py new file mode 100644 index 00000000000..c1424ca1730 --- /dev/null +++ b/tests/components/zha/test_update.py @@ -0,0 +1,587 @@ +"""Test ZHA firmware updates.""" +from unittest.mock import AsyncMock, MagicMock, call, patch + +import pytest +from zigpy.exceptions import DeliveryError +from zigpy.ota import CachedImage +import zigpy.ota.image as firmware +import zigpy.profiles.zha as zha +import zigpy.types as t +import zigpy.zcl.clusters.general as general +import zigpy.zcl.foundation as foundation + +from homeassistant.components.homeassistant import ( + DOMAIN as HA_DOMAIN, + SERVICE_UPDATE_ENTITY, +) +from homeassistant.components.update import ( + ATTR_IN_PROGRESS, + ATTR_INSTALLED_VERSION, + ATTR_LATEST_VERSION, + DOMAIN as UPDATE_DOMAIN, + SERVICE_INSTALL, +) +from homeassistant.components.update.const import ATTR_SKIPPED_VERSION +from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, Platform +from homeassistant.core import HomeAssistant, State +from homeassistant.exceptions import HomeAssistantError +from homeassistant.setup import async_setup_component + +from .common import async_enable_traffic, find_entity_id, update_attribute_cache +from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_TYPE + +from tests.common import mock_restore_cache_with_extra_data + + +@pytest.fixture(autouse=True) +def update_platform_only(): + """Only set up the update and required base platforms to speed up tests.""" + with patch( + "homeassistant.components.zha.PLATFORMS", + ( + Platform.UPDATE, + Platform.SENSOR, + Platform.SELECT, + Platform.SWITCH, + ), + ): + yield + + +@pytest.fixture +def zigpy_device(zigpy_device_mock): + """Device tracker zigpy device.""" + endpoints = { + 1: { + SIG_EP_INPUT: [general.Basic.cluster_id, general.OnOff.cluster_id], + SIG_EP_OUTPUT: [general.Ota.cluster_id], + SIG_EP_TYPE: zha.DeviceType.ON_OFF_SWITCH, + } + } + return zigpy_device_mock( + endpoints, node_descriptor=b"\x02@\x84_\x11\x7fd\x00\x00,d\x00\x00" + ) + + +async def setup_test_data( + zha_device_joined_restored, + zigpy_device, + skip_attribute_plugs=False, + file_not_found=False, +): + """Set up test data for the tests.""" + fw_version = 0x12345678 + installed_fw_version = fw_version - 10 + cluster = zigpy_device.endpoints[1].out_clusters[general.Ota.cluster_id] + if not skip_attribute_plugs: + cluster.PLUGGED_ATTR_READS = { + general.Ota.AttributeDefs.current_file_version.name: installed_fw_version + } + update_attribute_cache(cluster) + + # set up firmware image + fw_image = firmware.OTAImage() + fw_image.subelements = [firmware.SubElement(tag_id=0x0000, data=b"fw_image")] + fw_header = firmware.OTAImageHeader( + file_version=fw_version, + image_type=0x90, + manufacturer_id=zigpy_device.manufacturer_id, + upgrade_file_id=firmware.OTAImageHeader.MAGIC_VALUE, + header_version=256, + header_length=56, + field_control=0, + stack_version=2, + header_string="This is a test header!", + image_size=56 + 2 + 4 + 8, + ) + fw_image.header = fw_header + fw_image.should_update = MagicMock(return_value=True) + cached_image = CachedImage(fw_image) + + cluster.endpoint.device.application.ota.get_ota_image = AsyncMock( + return_value=None if file_not_found else cached_image + ) + + zha_device = await zha_device_joined_restored(zigpy_device) + zha_device.async_update_sw_build_id(installed_fw_version) + + return zha_device, cluster, fw_image, installed_fw_version + + +async def test_firmware_update_notification_from_zigpy( + hass: HomeAssistant, zha_device_joined_restored, zigpy_device +) -> None: + """Test ZHA update platform - firmware update notification.""" + zha_device, cluster, fw_image, installed_fw_version = await setup_test_data( + zha_device_joined_restored, zigpy_device + ) + + entity_id = find_entity_id(Platform.UPDATE, zha_device, hass) + assert entity_id is not None + + # allow traffic to flow through the gateway and device + await async_enable_traffic(hass, [zha_device]) + + assert hass.states.get(entity_id).state == STATE_OFF + + # simulate an image available notification + await cluster._handle_query_next_image( + fw_image.header.field_control, + zha_device.manufacturer_code, + fw_image.header.image_type, + installed_fw_version, + fw_image.header.header_version, + tsn=15, + ) + + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_ON + attrs = state.attributes + assert attrs[ATTR_INSTALLED_VERSION] == f"0x{installed_fw_version:08x}" + assert not attrs[ATTR_IN_PROGRESS] + assert attrs[ATTR_LATEST_VERSION] == f"0x{fw_image.header.file_version:08x}" + + +async def test_firmware_update_notification_from_service_call( + hass: HomeAssistant, zha_device_joined_restored, zigpy_device +) -> None: + """Test ZHA update platform - firmware update manual check.""" + zha_device, cluster, fw_image, installed_fw_version = await setup_test_data( + zha_device_joined_restored, zigpy_device + ) + + entity_id = find_entity_id(Platform.UPDATE, zha_device, hass) + assert entity_id is not None + + # allow traffic to flow through the gateway and device + await async_enable_traffic(hass, [zha_device]) + + assert hass.states.get(entity_id).state == STATE_OFF + + async def _async_image_notify_side_effect(*args, **kwargs): + await cluster._handle_query_next_image( + fw_image.header.field_control, + zha_device.manufacturer_code, + fw_image.header.image_type, + installed_fw_version, + fw_image.header.header_version, + tsn=15, + ) + + await async_setup_component(hass, HA_DOMAIN, {}) + cluster.image_notify = AsyncMock(side_effect=_async_image_notify_side_effect) + await hass.services.async_call( + HA_DOMAIN, + SERVICE_UPDATE_ENTITY, + service_data={ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + assert cluster.image_notify.await_count == 1 + assert cluster.image_notify.call_args_list[0] == call( + payload_type=cluster.ImageNotifyCommand.PayloadType.QueryJitter, + query_jitter=100, + ) + + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_ON + attrs = state.attributes + assert attrs[ATTR_INSTALLED_VERSION] == f"0x{installed_fw_version:08x}" + assert not attrs[ATTR_IN_PROGRESS] + assert attrs[ATTR_LATEST_VERSION] == f"0x{fw_image.header.file_version:08x}" + + +def make_packet(zigpy_device, cluster, cmd_name: str, **kwargs): + """Make a zigpy packet.""" + req_hdr, req_cmd = cluster._create_request( + general=False, + command_id=cluster.commands_by_name[cmd_name].id, + schema=cluster.commands_by_name[cmd_name].schema, + disable_default_response=False, + direction=foundation.Direction.Server_to_Client, + args=(), + kwargs=kwargs, + ) + + ota_packet = t.ZigbeePacket( + src=t.AddrModeAddress(addr_mode=t.AddrMode.NWK, address=zigpy_device.nwk), + src_ep=1, + dst=t.AddrModeAddress(addr_mode=t.AddrMode.NWK, address=0x0000), + dst_ep=1, + tsn=req_hdr.tsn, + profile_id=260, + cluster_id=cluster.cluster_id, + data=t.SerializableBytes(req_hdr.serialize() + req_cmd.serialize()), + lqi=255, + rssi=-30, + ) + + return ota_packet + + +async def test_firmware_update_success( + hass: HomeAssistant, zha_device_joined_restored, zigpy_device +) -> None: + """Test ZHA update platform - firmware update success.""" + zha_device, cluster, fw_image, installed_fw_version = await setup_test_data( + zha_device_joined_restored, zigpy_device + ) + + entity_id = find_entity_id(Platform.UPDATE, zha_device, hass) + assert entity_id is not None + + # allow traffic to flow through the gateway and device + await async_enable_traffic(hass, [zha_device]) + + assert hass.states.get(entity_id).state == STATE_OFF + + # simulate an image available notification + await cluster._handle_query_next_image( + fw_image.header.field_control, + zha_device.manufacturer_code, + fw_image.header.image_type, + installed_fw_version, + fw_image.header.header_version, + tsn=15, + ) + + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_ON + attrs = state.attributes + assert attrs[ATTR_INSTALLED_VERSION] == f"0x{installed_fw_version:08x}" + assert not attrs[ATTR_IN_PROGRESS] + assert attrs[ATTR_LATEST_VERSION] == f"0x{fw_image.header.file_version:08x}" + + async def endpoint_reply(cluster_id, tsn, data, command_id): + if cluster_id == general.Ota.cluster_id: + hdr, cmd = cluster.deserialize(data) + if isinstance(cmd, general.Ota.ImageNotifyCommand): + zigpy_device.packet_received( + make_packet( + zigpy_device, + cluster, + general.Ota.ServerCommandDefs.query_next_image.name, + field_control=general.Ota.QueryNextImageCommand.FieldControl.HardwareVersion, + manufacturer_code=fw_image.header.manufacturer_id, + image_type=fw_image.header.image_type, + current_file_version=fw_image.header.file_version - 10, + hardware_version=1, + ) + ) + elif isinstance( + cmd, general.Ota.ClientCommandDefs.query_next_image_response.schema + ): + assert cmd.status == foundation.Status.SUCCESS + assert cmd.manufacturer_code == fw_image.header.manufacturer_id + assert cmd.image_type == fw_image.header.image_type + assert cmd.file_version == fw_image.header.file_version + assert cmd.image_size == fw_image.header.image_size + zigpy_device.packet_received( + make_packet( + zigpy_device, + cluster, + general.Ota.ServerCommandDefs.image_block.name, + field_control=general.Ota.ImageBlockCommand.FieldControl.RequestNodeAddr, + manufacturer_code=fw_image.header.manufacturer_id, + image_type=fw_image.header.image_type, + file_version=fw_image.header.file_version, + file_offset=0, + maximum_data_size=40, + request_node_addr=zigpy_device.ieee, + ) + ) + elif isinstance( + cmd, general.Ota.ClientCommandDefs.image_block_response.schema + ): + if cmd.file_offset == 0: + assert cmd.status == foundation.Status.SUCCESS + assert cmd.manufacturer_code == fw_image.header.manufacturer_id + assert cmd.image_type == fw_image.header.image_type + assert cmd.file_version == fw_image.header.file_version + assert cmd.file_offset == 0 + assert cmd.image_data == fw_image.serialize()[0:40] + zigpy_device.packet_received( + make_packet( + zigpy_device, + cluster, + general.Ota.ServerCommandDefs.image_block.name, + field_control=general.Ota.ImageBlockCommand.FieldControl.RequestNodeAddr, + manufacturer_code=fw_image.header.manufacturer_id, + image_type=fw_image.header.image_type, + file_version=fw_image.header.file_version, + file_offset=40, + maximum_data_size=40, + request_node_addr=zigpy_device.ieee, + ) + ) + elif cmd.file_offset == 40: + assert cmd.status == foundation.Status.SUCCESS + assert cmd.manufacturer_code == fw_image.header.manufacturer_id + assert cmd.image_type == fw_image.header.image_type + assert cmd.file_version == fw_image.header.file_version + assert cmd.file_offset == 40 + assert cmd.image_data == fw_image.serialize()[40:70] + + # make sure the state machine gets progress reports + state = hass.states.get(entity_id) + assert state.state == STATE_ON + attrs = state.attributes + assert ( + attrs[ATTR_INSTALLED_VERSION] == f"0x{installed_fw_version:08x}" + ) + assert attrs[ATTR_IN_PROGRESS] == 57 + assert ( + attrs[ATTR_LATEST_VERSION] + == f"0x{fw_image.header.file_version:08x}" + ) + + zigpy_device.packet_received( + make_packet( + zigpy_device, + cluster, + general.Ota.ServerCommandDefs.upgrade_end.name, + status=foundation.Status.SUCCESS, + manufacturer_code=fw_image.header.manufacturer_id, + image_type=fw_image.header.image_type, + file_version=fw_image.header.file_version, + ) + ) + + elif isinstance( + cmd, general.Ota.ClientCommandDefs.upgrade_end_response.schema + ): + assert cmd.manufacturer_code == fw_image.header.manufacturer_id + assert cmd.image_type == fw_image.header.image_type + assert cmd.file_version == fw_image.header.file_version + assert cmd.current_time == 0 + assert cmd.upgrade_time == 0 + + cluster.endpoint.reply = AsyncMock(side_effect=endpoint_reply) + await hass.services.async_call( + UPDATE_DOMAIN, + SERVICE_INSTALL, + { + ATTR_ENTITY_ID: entity_id, + }, + blocking=True, + ) + + state = hass.states.get(entity_id) + assert state.state == STATE_OFF + attrs = state.attributes + assert attrs[ATTR_INSTALLED_VERSION] == f"0x{fw_image.header.file_version:08x}" + assert not attrs[ATTR_IN_PROGRESS] + assert attrs[ATTR_LATEST_VERSION] == attrs[ATTR_INSTALLED_VERSION] + + +async def test_firmware_update_raises( + hass: HomeAssistant, zha_device_joined_restored, zigpy_device +) -> None: + """Test ZHA update platform - firmware update raises.""" + zha_device, cluster, fw_image, installed_fw_version = await setup_test_data( + zha_device_joined_restored, zigpy_device + ) + + entity_id = find_entity_id(Platform.UPDATE, zha_device, hass) + assert entity_id is not None + + # allow traffic to flow through the gateway and device + await async_enable_traffic(hass, [zha_device]) + + assert hass.states.get(entity_id).state == STATE_OFF + + # simulate an image available notification + await cluster._handle_query_next_image( + fw_image.header.field_control, + zha_device.manufacturer_code, + fw_image.header.image_type, + installed_fw_version, + fw_image.header.header_version, + tsn=15, + ) + + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_ON + attrs = state.attributes + assert attrs[ATTR_INSTALLED_VERSION] == f"0x{installed_fw_version:08x}" + assert not attrs[ATTR_IN_PROGRESS] + assert attrs[ATTR_LATEST_VERSION] == f"0x{fw_image.header.file_version:08x}" + + async def endpoint_reply(cluster_id, tsn, data, command_id): + if cluster_id == general.Ota.cluster_id: + hdr, cmd = cluster.deserialize(data) + if isinstance(cmd, general.Ota.ImageNotifyCommand): + zigpy_device.packet_received( + make_packet( + zigpy_device, + cluster, + general.Ota.ServerCommandDefs.query_next_image.name, + field_control=general.Ota.QueryNextImageCommand.FieldControl.HardwareVersion, + manufacturer_code=fw_image.header.manufacturer_id, + image_type=fw_image.header.image_type, + current_file_version=fw_image.header.file_version - 10, + hardware_version=1, + ) + ) + elif isinstance( + cmd, general.Ota.ClientCommandDefs.query_next_image_response.schema + ): + assert cmd.status == foundation.Status.SUCCESS + assert cmd.manufacturer_code == fw_image.header.manufacturer_id + assert cmd.image_type == fw_image.header.image_type + assert cmd.file_version == fw_image.header.file_version + assert cmd.image_size == fw_image.header.image_size + raise DeliveryError("failed to deliver") + + cluster.endpoint.reply = AsyncMock(side_effect=endpoint_reply) + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + UPDATE_DOMAIN, + SERVICE_INSTALL, + { + ATTR_ENTITY_ID: entity_id, + }, + blocking=True, + ) + + with patch( + "zigpy.device.Device.update_firmware", + AsyncMock(side_effect=DeliveryError("failed to deliver")), + ), pytest.raises(HomeAssistantError): + await hass.services.async_call( + UPDATE_DOMAIN, + SERVICE_INSTALL, + { + ATTR_ENTITY_ID: entity_id, + }, + blocking=True, + ) + + +async def test_firmware_update_restore_data( + hass: HomeAssistant, zha_device_joined_restored, zigpy_device +) -> None: + """Test ZHA update platform - restore data.""" + fw_version = 0x12345678 + installed_fw_version = fw_version - 10 + mock_restore_cache_with_extra_data( + hass, + [ + ( + State( + "update.fakemanufacturer_fakemodel_firmware", + STATE_ON, + { + ATTR_INSTALLED_VERSION: f"0x{installed_fw_version:08x}", + ATTR_LATEST_VERSION: f"0x{fw_version:08x}", + ATTR_SKIPPED_VERSION: None, + }, + ), + {"image_type": 0x90}, + ) + ], + ) + zha_device, cluster, fw_image, installed_fw_version = await setup_test_data( + zha_device_joined_restored, zigpy_device + ) + + entity_id = find_entity_id(Platform.UPDATE, zha_device, hass) + assert entity_id is not None + + # allow traffic to flow through the gateway and device + await async_enable_traffic(hass, [zha_device]) + + state = hass.states.get(entity_id) + assert state.state == STATE_ON + attrs = state.attributes + assert attrs[ATTR_INSTALLED_VERSION] == f"0x{installed_fw_version:08x}" + assert not attrs[ATTR_IN_PROGRESS] + assert attrs[ATTR_LATEST_VERSION] == f"0x{fw_image.header.file_version:08x}" + + +async def test_firmware_update_restore_file_not_found( + hass: HomeAssistant, zha_device_joined_restored, zigpy_device +) -> None: + """Test ZHA update platform - restore data - file not found.""" + fw_version = 0x12345678 + installed_fw_version = fw_version - 10 + mock_restore_cache_with_extra_data( + hass, + [ + ( + State( + "update.fakemanufacturer_fakemodel_firmware", + STATE_ON, + { + ATTR_INSTALLED_VERSION: f"0x{installed_fw_version:08x}", + ATTR_LATEST_VERSION: f"0x{fw_version:08x}", + ATTR_SKIPPED_VERSION: None, + }, + ), + {"image_type": 0x90}, + ) + ], + ) + zha_device, cluster, fw_image, installed_fw_version = await setup_test_data( + zha_device_joined_restored, zigpy_device, file_not_found=True + ) + + entity_id = find_entity_id(Platform.UPDATE, zha_device, hass) + assert entity_id is not None + + # allow traffic to flow through the gateway and device + await async_enable_traffic(hass, [zha_device]) + + state = hass.states.get(entity_id) + assert state.state == STATE_OFF + attrs = state.attributes + assert attrs[ATTR_INSTALLED_VERSION] == f"0x{installed_fw_version:08x}" + assert not attrs[ATTR_IN_PROGRESS] + assert attrs[ATTR_LATEST_VERSION] == f"0x{installed_fw_version:08x}" + + +async def test_firmware_update_restore_version_from_state_machine( + hass: HomeAssistant, zha_device_joined_restored, zigpy_device +) -> None: + """Test ZHA update platform - restore data - file not found.""" + fw_version = 0x12345678 + installed_fw_version = fw_version - 10 + mock_restore_cache_with_extra_data( + hass, + [ + ( + State( + "update.fakemanufacturer_fakemodel_firmware", + STATE_ON, + { + ATTR_INSTALLED_VERSION: f"0x{installed_fw_version:08x}", + ATTR_LATEST_VERSION: f"0x{fw_version:08x}", + ATTR_SKIPPED_VERSION: None, + }, + ), + {"image_type": 0x90}, + ) + ], + ) + zha_device, cluster, fw_image, installed_fw_version = await setup_test_data( + zha_device_joined_restored, + zigpy_device, + skip_attribute_plugs=True, + file_not_found=True, + ) + + entity_id = find_entity_id(Platform.UPDATE, zha_device, hass) + assert entity_id is not None + + # allow traffic to flow through the gateway and device + await async_enable_traffic(hass, [zha_device]) + + state = hass.states.get(entity_id) + assert state.state == STATE_OFF + attrs = state.attributes + assert attrs[ATTR_INSTALLED_VERSION] == f"0x{installed_fw_version:08x}" + assert not attrs[ATTR_IN_PROGRESS] + assert attrs[ATTR_LATEST_VERSION] == f"0x{installed_fw_version:08x}" diff --git a/tests/components/zha/zha_devices_list.py b/tests/components/zha/zha_devices_list.py index a45ffce9e47..9a9535178d2 100644 --- a/tests/components/zha/zha_devices_list.py +++ b/tests/components/zha/zha_devices_list.py @@ -127,6 +127,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "LQISensor", DEV_SIG_ENT_MAP_ID: "sensor.bosch_isw_zpr1_wp13_lqi", }, + ("update", "00:11:22:33:44:55:66:77-5-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.bosch_isw_zpr1_wp13_firmware", + }, }, }, { @@ -165,6 +170,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "LQISensor", DEV_SIG_ENT_MAP_ID: "sensor.centralite_3130_lqi", }, + ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.centralite_3130_firmware", + }, }, }, { @@ -248,6 +258,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "LQISensor", DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_lqi", }, + ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.centralite_3210_l_firmware", + }, }, }, { @@ -296,6 +311,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Humidity", DEV_SIG_ENT_MAP_ID: "sensor.centralite_3310_s_humidity", }, + ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.centralite_3310_s_firmware", + }, }, }, { @@ -351,6 +371,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "LQISensor", DEV_SIG_ENT_MAP_ID: "sensor.centralite_3315_s_lqi", }, + ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.centralite_3315_s_firmware", + }, }, }, { @@ -406,6 +431,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "LQISensor", DEV_SIG_ENT_MAP_ID: "sensor.centralite_3320_l_lqi", }, + ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.centralite_3320_l_firmware", + }, }, }, { @@ -461,6 +491,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "LQISensor", DEV_SIG_ENT_MAP_ID: "sensor.centralite_3326_l_lqi", }, + ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.centralite_3326_l_firmware", + }, }, }, { @@ -523,6 +558,11 @@ DEVICES = [ "binary_sensor.centralite_motion_sensor_a_occupancy" ), }, + ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.centralite_motion_sensor_a_firmware", + }, }, }, { @@ -593,6 +633,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "LQISensor", DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_psmp5_00_00_02_02tc_lqi", }, + ("update", "00:11:22:33:44:55:66:77-4-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.climaxtechnology_psmp5_00_00_02_02tc_firmware", + }, }, }, { @@ -798,6 +843,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "LQISensor", DEV_SIG_ENT_MAP_ID: "sensor.heiman_smokesensor_em_lqi", }, + ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.heiman_smokesensor_em_firmware", + }, }, }, { @@ -836,6 +886,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "LQISensor", DEV_SIG_ENT_MAP_ID: "sensor.heiman_co_v16_lqi", }, + ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.heiman_co_v16_firmware", + }, }, }, { @@ -899,6 +954,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "LQISensor", DEV_SIG_ENT_MAP_ID: "sensor.heiman_warningdevice_lqi", }, + ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.heiman_warningdevice_firmware", + }, }, }, { @@ -952,6 +1012,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "LQISensor", DEV_SIG_ENT_MAP_ID: "sensor.hivehome_com_mot003_lqi", }, + ("update", "00:11:22:33:44:55:66:77-6-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.hivehome_com_mot003_firmware", + }, }, }, { @@ -1005,6 +1070,11 @@ DEVICES = [ "sensor.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_lqi" ), }, + ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_firmware", + }, }, }, { @@ -1051,6 +1121,11 @@ DEVICES = [ "sensor.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_lqi" ), }, + ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_firmware", + }, }, }, { @@ -1097,6 +1172,11 @@ DEVICES = [ "sensor.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_lqi" ), }, + ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_firmware", + }, }, }, { @@ -1143,6 +1223,11 @@ DEVICES = [ "sensor.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_lqi" ), }, + ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_firmware", + }, }, }, { @@ -1189,6 +1274,11 @@ DEVICES = [ "sensor.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_lqi" ), }, + ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_firmware", + }, }, }, { @@ -1231,6 +1321,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "LQISensor", DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_control_outlet_lqi", }, + ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.ikea_of_sweden_tradfri_control_outlet_firmware", + }, }, }, { @@ -1280,6 +1375,11 @@ DEVICES = [ "binary_sensor.ikea_of_sweden_tradfri_motion_sensor_motion" ), }, + ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.ikea_of_sweden_tradfri_motion_sensor_firmware", + }, }, }, { @@ -1322,6 +1422,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "LQISensor", DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_on_off_switch_lqi", }, + ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.ikea_of_sweden_tradfri_on_off_switch_firmware", + }, }, }, { @@ -1364,6 +1469,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "LQISensor", DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_remote_control_lqi", }, + ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.ikea_of_sweden_tradfri_remote_control_firmware", + }, }, }, { @@ -1408,6 +1518,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "LQISensor", DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_signal_repeater_lqi", }, + ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.ikea_of_sweden_tradfri_signal_repeater_firmware", + }, }, }, { @@ -1452,6 +1567,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "LQISensor", DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_wireless_dimmer_lqi", }, + ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.ikea_of_sweden_tradfri_wireless_dimmer_firmware", + }, }, }, { @@ -1512,6 +1632,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "LQISensor", DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45852_lqi", }, + ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.jasco_products_45852_firmware", + }, }, }, { @@ -1572,6 +1697,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "LQISensor", DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45856_lqi", }, + ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.jasco_products_45856_firmware", + }, }, }, { @@ -1632,6 +1762,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "LQISensor", DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45857_lqi", }, + ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.jasco_products_45857_firmware", + }, }, }, { @@ -1685,6 +1820,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "LQISensor", DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_610_mp_1_3_lqi", }, + ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.keen_home_inc_sv02_610_mp_1_3_firmware", + }, }, }, { @@ -1738,6 +1878,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "LQISensor", DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_2_lqi", }, + ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.keen_home_inc_sv02_612_mp_1_2_firmware", + }, }, }, { @@ -1791,6 +1936,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "LQISensor", DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_3_lqi", }, + ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.keen_home_inc_sv02_612_mp_1_3_firmware", + }, }, }, { @@ -1836,6 +1986,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "KofFan", DEV_SIG_ENT_MAP_ID: "fan.king_of_fans_inc_hbuniversalcfremote_fan", }, + ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.king_of_fans_inc_hbuniversalcfremote_firmware", + }, }, }, { @@ -1874,6 +2029,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "LQISensor", DEV_SIG_ENT_MAP_ID: "sensor.lds_zbt_cctswitch_d0001_lqi", }, + ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.lds_zbt_cctswitch_d0001_firmware", + }, }, }, { @@ -1912,6 +2072,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "LQISensor", DEV_SIG_ENT_MAP_ID: "sensor.ledvance_a19_rgbw_lqi", }, + ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.ledvance_a19_rgbw_firmware", + }, }, }, { @@ -1950,6 +2115,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "LQISensor", DEV_SIG_ENT_MAP_ID: "sensor.ledvance_flex_rgbw_lqi", }, + ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.ledvance_flex_rgbw_firmware", + }, }, }, { @@ -1988,6 +2158,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "LQISensor", DEV_SIG_ENT_MAP_ID: "sensor.ledvance_plug_lqi", }, + ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.ledvance_plug_firmware", + }, }, }, { @@ -2026,6 +2201,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "LQISensor", DEV_SIG_ENT_MAP_ID: "sensor.ledvance_rt_rgbw_lqi", }, + ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.ledvance_rt_rgbw_firmware", + }, }, }, { @@ -2110,6 +2290,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_summation_delivered", }, + ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.lumi_lumi_plug_maus01_firmware", + }, }, }, { @@ -2210,6 +2395,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Light", DEV_SIG_ENT_MAP_ID: "light.lumi_lumi_relay_c2acn01_light_2", }, + ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.lumi_lumi_relay_c2acn01_firmware", + }, }, }, { @@ -2262,6 +2452,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "LQISensor", DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b186acn01_lqi", }, + ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.lumi_lumi_remote_b186acn01_firmware", + }, }, }, { @@ -2314,6 +2509,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "LQISensor", DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b286acn01_lqi", }, + ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.lumi_lumi_remote_b286acn01_firmware", + }, }, }, { @@ -2780,6 +2980,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "LQISensor", DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_86sw1_lqi", }, + ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.lumi_lumi_sensor_86sw1_firmware", + }, }, }, { @@ -2832,6 +3037,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "LQISensor", DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_cube_aqgl01_lqi", }, + ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.lumi_lumi_sensor_cube_aqgl01_firmware", + }, }, }, { @@ -2894,6 +3104,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Humidity", DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_ht_humidity", }, + ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.lumi_lumi_sensor_ht_firmware", + }, }, }, { @@ -2937,6 +3152,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Opening", DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_sensor_magnet_opening", }, + ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.lumi_lumi_sensor_magnet_firmware", + }, }, }, { @@ -3042,6 +3262,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "LQISensor", DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_motion_aq2_lqi", }, + ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.lumi_lumi_sensor_motion_aq2_firmware", + }, }, }, { @@ -3092,6 +3317,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "LQISensor", DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_smoke_lqi", }, + ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.lumi_lumi_sensor_smoke_firmware", + }, }, }, { @@ -3130,6 +3360,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "LQISensor", DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_lqi", }, + ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.lumi_lumi_sensor_switch_firmware", + }, }, }, { @@ -3246,6 +3481,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "LQISensor", DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_wleak_aq1_lqi", }, + ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.lumi_lumi_sensor_wleak_aq1_firmware", + }, }, }, { @@ -3316,6 +3556,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "DeviceTemperature", DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_vibration_aq1_device_temperature", }, + ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.lumi_lumi_vibration_aq1_firmware", + }, }, }, { @@ -3534,6 +3779,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "LQISensor", DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_a19_rgbw_lqi", }, + ("update", "00:11:22:33:44:55:66:77-3-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.osram_lightify_a19_rgbw_firmware", + }, }, }, { @@ -3572,6 +3822,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "LQISensor", DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_dimming_switch_lqi", }, + ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.osram_lightify_dimming_switch_firmware", + }, }, }, { @@ -3610,6 +3865,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "LQISensor", DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_flex_rgbw_lqi", }, + ("update", "00:11:22:33:44:55:66:77-3-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.osram_lightify_flex_rgbw_firmware", + }, }, }, { @@ -3684,6 +3944,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "LQISensor", DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_lqi", }, + ("update", "00:11:22:33:44:55:66:77-3-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.osram_lightify_rt_tunable_white_firmware", + }, }, }, { @@ -3722,6 +3987,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "LQISensor", DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_lqi", }, + ("update", "00:11:22:33:44:55:66:77-3-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.osram_plug_01_firmware", + }, }, }, { @@ -3816,6 +4086,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "LQISensor", DEV_SIG_ENT_MAP_ID: "sensor.osram_switch_4x_lightify_lqi", }, + ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.osram_switch_4x_lightify_firmware", + }, }, }, { @@ -3866,6 +4141,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Battery", DEV_SIG_ENT_MAP_ID: "sensor.philips_rwl020_battery", }, + ("update", "00:11:22:33:44:55:66:77-2-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.philips_rwl020_firmware", + }, }, }, { @@ -3914,6 +4194,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "LQISensor", DEV_SIG_ENT_MAP_ID: "sensor.samjin_button_lqi", }, + ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.samjin_button_firmware", + }, }, }, { @@ -3967,6 +4252,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "LQISensor", DEV_SIG_ENT_MAP_ID: "sensor.samjin_multi_lqi", }, + ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.samjin_multi_firmware", + }, }, }, { @@ -4015,6 +4305,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "LQISensor", DEV_SIG_ENT_MAP_ID: "sensor.samjin_water_lqi", }, + ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.samjin_water_firmware", + }, }, }, { @@ -4083,6 +4378,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Switch", DEV_SIG_ENT_MAP_ID: "switch.securifi_ltd_unk_model_switch", }, + ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.securifi_ltd_unk_model_firmware", + }, }, }, { @@ -4131,6 +4431,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "LQISensor", DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_dws04n_sf_lqi", }, + ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.sercomm_corp_sz_dws04n_sf_firmware", + }, }, }, { @@ -4221,6 +4526,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "LQISensor", DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_lqi", }, + ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.sercomm_corp_sz_esw01_firmware", + }, }, }, { @@ -4274,6 +4584,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "LQISensor", DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_pir04_lqi", }, + ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.sercomm_corp_sz_pir04_firmware", + }, }, }, { @@ -4344,6 +4659,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Switch", DEV_SIG_ENT_MAP_ID: "switch.sinope_technologies_rm3250zb_switch", }, + ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.sinope_technologies_rm3250zb_firmware", + }, }, }, { @@ -4444,6 +4764,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "SetpointChangeSource", DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_setpoint_change_source", }, + ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.sinope_technologies_th1123zb_firmware", + }, }, }, { @@ -4544,6 +4869,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "SetpointChangeSource", DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_setpoint_change_source", }, + ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.sinope_technologies_th1124zb_firmware", + }, }, }, { @@ -4617,6 +4947,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Switch", DEV_SIG_ENT_MAP_ID: "switch.smartthings_outletv4_switch", }, + ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.smartthings_outletv4_firmware", + }, }, }, { @@ -4660,6 +4995,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "LQISensor", DEV_SIG_ENT_MAP_ID: "sensor.smartthings_tagv4_lqi", }, + ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.smartthings_tagv4_firmware", + }, }, }, { @@ -4698,6 +5038,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Switch", DEV_SIG_ENT_MAP_ID: "switch.third_reality_inc_3rss007z_switch", }, + ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.third_reality_inc_3rss007z_firmware", + }, }, }, { @@ -4741,6 +5086,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Switch", DEV_SIG_ENT_MAP_ID: "switch.third_reality_inc_3rss008z_switch", }, + ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.third_reality_inc_3rss008z_firmware", + }, }, }, { @@ -4789,6 +5139,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "LQISensor", DEV_SIG_ENT_MAP_ID: "sensor.visonic_mct_340_e_lqi", }, + ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.visonic_mct_340_e_firmware", + }, }, }, { @@ -4847,6 +5202,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "SetpointChangeSource", DEV_SIG_ENT_MAP_ID: "sensor.zen_within_zen_01_setpoint_change_source", }, + ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.zen_within_zen_01_firmware", + }, }, }, { @@ -4916,6 +5276,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Light", DEV_SIG_ENT_MAP_ID: "light.tyzb01_ns1ndbww_ts0004_light_4", }, + ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.tyzb01_ns1ndbww_ts0004_firmware", + }, }, }, { @@ -5012,6 +5377,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "LQISensor", DEV_SIG_ENT_MAP_ID: "sensor.sengled_e11_g13_lqi", }, + ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.sengled_e11_g13_firmware", + }, }, }, { @@ -5065,6 +5435,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "LQISensor", DEV_SIG_ENT_MAP_ID: "sensor.sengled_e12_n14_lqi", }, + ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.sengled_e12_n14_firmware", + }, }, }, { @@ -5118,6 +5493,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "LQISensor", DEV_SIG_ENT_MAP_ID: "sensor.sengled_z01_a19nae26_lqi", }, + ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.sengled_z01_a19nae26_firmware", + }, }, }, { @@ -5584,6 +5964,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "HueV1MotionSensitivity", DEV_SIG_ENT_MAP_ID: "select.philips_sml001_motion_sensitivity", }, + ("update", "00:11:22:33:44:55:66:77-2-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.philips_sml001_firmware", + }, }, }, ]