diff --git a/tests/test_ota_image.py b/tests/test_ota_image.py index d378e9c8f..ab6e67ae0 100644 --- a/tests/test_ota_image.py +++ b/tests/test_ota_image.py @@ -314,6 +314,11 @@ def test_get_image_block_offset_too_large(raw_header, raw_sub_element): img.get_image_block(offset, size) +def test_cached_image_wrapping(image): + cached_img = CachedImage(image) + assert cached_img.header is image.header + + def wrap_ikea(data): header = bytearray(100) header[0:4] = b"NGIS" @@ -400,6 +405,21 @@ def test_parse_ota_hue_invalid(): firmware.parse_ota_image(header.replace(manufacturer_id=12).serialize() + rest) -def test_cached_image_wrapping(image): - cached_img = CachedImage(image) - assert cached_img.header is image.header +def test_legrand_container_unwrapping(image): + # Unwrapped size prefix and 1 + 16 byte suffix + data = ( + t.uint32_t(len(image.serialize())).serialize() + + image.serialize() + + b"\x01" + + b"abcdabcdabcdabcd" + ) + + with pytest.raises(ValueError): + firmware.parse_ota_image(data[:-1]) + + with pytest.raises(ValueError): + firmware.parse_ota_image(b"\xFF" + data[1:]) + + img, rest = firmware.parse_ota_image(data) + assert not rest + assert img == image diff --git a/tests/test_struct.py b/tests/test_struct.py index 778cc98ce..8784e44bc 100644 --- a/tests/test_struct.py +++ b/tests/test_struct.py @@ -774,3 +774,15 @@ class TestStruct(t.Struct): s1 = TestStruct(foo=1, bar=2, baz="asd") assert repr(s1) == "TestStruct(foo=2, bar=bar, baz=baz)" + + +def test_skip_missing(): + class TestStruct(t.Struct): + foo: t.uint8_t + bar: t.uint16_t + + assert TestStruct(foo=1).as_dict() == {"foo": 1, "bar": None} + assert TestStruct(foo=1).as_dict(skip_missing=True) == {"foo": 1} + + assert TestStruct(foo=1).as_tuple() == (1, None) + assert TestStruct(foo=1).as_tuple(skip_missing=True) == (1,) diff --git a/zigpy/__init__.py b/zigpy/__init__.py index 4e4c8c6f9..2fa1947b5 100644 --- a/zigpy/__init__.py +++ b/zigpy/__init__.py @@ -1,5 +1,5 @@ MAJOR_VERSION = 0 -MINOR_VERSION = 44 -PATCH_VERSION = "1" +MINOR_VERSION = 45 +PATCH_VERSION = "0.dev0" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" diff --git a/zigpy/ota/OTA_URLs.md b/zigpy/ota/OTA_URLs.md new file mode 100644 index 000000000..d4887cb93 --- /dev/null +++ b/zigpy/ota/OTA_URLs.md @@ -0,0 +1,87 @@ +# Zigbee OTA source provider sources for these and others + +Collection of external Zigbee OTA firmware images from official and unofficial OTA provider sources. + +### Koenkk zigbee-OTA repository + +Koenkk zigbee-OTA repository host third-party OTA firmware images and external URLs for many third-party Zigbee OTA firmware images. + +https://github.com/Koenkk/zigbee-OTA/tree/master/images + +https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/index.json + +### Dresden Elektronik + +Dresden Elektronik Zigbee OTA firmware images are made publicly available by Dresden Elektronik (first-party) at the following URL: + +https://github.com/dresden-elektronik/deconz-rest-plugin/wiki/OTA-Image-Types---Firmware-versions + +Dresden Elektronik also provide third-party OTA firmware images and external URLs for many third-party Zigbee OTA firmware images. + +### EUROTRONICS + +EUROTRONICS Zigbee OTA firmware images are made publicly available by EUROTRONIC Technology (first-party) at the following URL: + +https://github.com/EUROTRONIC-Technology/Spirit-ZigBee/releases/download/ + +### IKEA Trådfri + +IKEA Tradfi Zigbee OTA firmware images are made publicly available by IKEA (first-party) at the following URL: + +Download-URL: + +http://fw.ota.homesmart.ikea.net/feed/version_info.json + +Download-URL (Test/Beta-Version): + +http://fw.test.ota.homesmart.ikea.net/feed/version_info.json + +Release changelogs + +https://ww8.ikea.com/ikeahomesmart/releasenotes/releasenotes.html + +### LEDVANCE/Sylvania and OSRAM Lightify + +LEDVANCE/Sylvania and OSRAM Lightify Zigbee OTA firmware images are made publicly available by LEDVANCE (first-party) at the following URL: + +https://update.ledvance.com/firmware-overview + +https://api.update.ledvance.com/v1/zigbee/firmwares/download + +https://consumer.sylvania.com/our-products/smart/sylvania-smart-zigbee-products-menu/index.jsp + +### Legrand/Netatmo + +Legrand/Netatmo Zigbee OTA firmware images are made publicly available by Legrand (first-party) at the following URL: + +https://developer.legrand.com/documentation/operating-manual/ https://developer.legrand.com/documentation/firmwares-download/ + +### LiXee + +LiXee Zigbee OTA firmware images are made publicly available by Fairecasoimeme / ZiGate (first-party) at the following URL: + +https://github.com/fairecasoimeme/Zlinky_TIC/releases + +### SALUS/Computime + +SALUS/Computime Hue Zigbee OTA firmware images are made publicly available by SALUS (first-party) at the following URL: + +https://eu.salusconnect.io/demo/default/status/firmware + +### Sengled + +Sengled Zigbee OTA firmware images are made publicly available by Dresden Elektronik (third-party) at the following URL: + +https://github.com/dresden-elektronik/deconz-rest-plugin/wiki/OTA-Image-Types---Firmware-versions#sengled + +### Philips Hue (Signify) + +Philips Hue (Signify) Zigbee OTA firmware images are made publicly available by Dresden Elektronik (third-party) at the following URL: + +https://github.com/dresden-elektronik/deconz-rest-plugin/wiki/OTA-Image-Types---Firmware-versions#philips-hue + +### Ubisys + +Ubisys Zigbee OTA firmware images are made publicly available by Ubisys (first-party) at the following URL: + +https://www.ubisys.de/en/support/firmware/ diff --git a/zigpy/ota/image.py b/zigpy/ota/image.py index f3da5ab25..b76512255 100644 --- a/zigpy/ota/image.py +++ b/zigpy/ota/image.py @@ -223,8 +223,12 @@ def parse_ota_image(data: bytes) -> tuple[BaseOTAImage, bytes]: Attempts to extract any known OTA image type from data. Does not validate firmware. """ - # IKEA container needs to be unwrapped - if data.startswith(b"NGIS"): + if len(data) > 4 and int.from_bytes(data[0:4], "little") + 21 == len(data): + # Legrand OTA images are prefixed with their unwrapped size and include a 1 + 16 + # byte suffix + return OTAImage.deserialize(data[4:-17]) + elif data.startswith(b"NGIS"): + # IKEA container needs to be unwrapped if len(data) <= 24: raise ValueError( f"Data too short to contain IKEA container header: {len(data)}" diff --git a/zigpy/types/struct.py b/zigpy/types/struct.py index 989a5104c..01f457f11 100644 --- a/zigpy/types/struct.py +++ b/zigpy/types/struct.py @@ -184,11 +184,21 @@ def assigned_fields(self, *, strict=False) -> list[tuple[StructField, typing.Any return assigned_fields - def as_dict(self) -> dict[str, typing.Any]: - return {f.name: getattr(self, f.name) for f in self.fields} + def as_dict(self, *, skip_missing: bool = False) -> dict[str, typing.Any]: + d = {} - def as_tuple(self) -> tuple: - return tuple(getattr(self, f.name) for f in self.fields) + for f in self.fields: + value = getattr(self, f.name) + + if value is None and skip_missing: + continue + + d[f.name] = value + + return d + + def as_tuple(self, *, skip_missing: bool = False) -> tuple: + return tuple(self.as_dict(skip_missing=skip_missing).values()) def serialize(self) -> bytes: chunks = [] diff --git a/zigpy/zcl/__init__.py b/zigpy/zcl/__init__.py index e5b443920..1ee1bafd8 100644 --- a/zigpy/zcl/__init__.py +++ b/zigpy/zcl/__init__.py @@ -207,7 +207,7 @@ def from_id( cluster.cluster_id = cluster_id return cluster - LOGGER.warning("Unknown cluster 0x%04X", cluster_id) + LOGGER.debug("Unknown cluster 0x%04X", cluster_id) cluster = cls(endpoint, is_server) cluster.cluster_id = cluster_id diff --git a/zigpy/zcl/clusters/closures.py b/zigpy/zcl/clusters/closures.py index 3c1adf789..c490e6183 100644 --- a/zigpy/zcl/clusters/closures.py +++ b/zigpy/zcl/clusters/closures.py @@ -336,7 +336,7 @@ class EventType(t.enum8): "set_holiday_schedule", { "holiday_schedule_id": t.uint8_t, - "loca_start_time": t.LocalTime, + "local_start_time": t.LocalTime, "local_end_time": t.LocalTime, "operating_mode_during_holiday": OperatingMode, }, diff --git a/zigpy/zcl/clusters/general.py b/zigpy/zcl/clusters/general.py index 809463c07..5e765afaf 100644 --- a/zigpy/zcl/clusters/general.py +++ b/zigpy/zcl/clusters/general.py @@ -1380,7 +1380,7 @@ class ImageNotifyPayloadType(t.enum8): "file_offset": t.uint32_t, "maximum_data_size": t.uint8_t, "request_node_addr?": t.EUI64, - "minumum_block_period?": t.uint16_t, + "minimum_block_period?": t.uint16_t, }, False, ), diff --git a/zigpy/zcl/clusters/homeautomation.py b/zigpy/zcl/clusters/homeautomation.py index 5f3a4e026..44f4430fe 100644 --- a/zigpy/zcl/clusters/homeautomation.py +++ b/zigpy/zcl/clusters/homeautomation.py @@ -58,7 +58,7 @@ class ApplianceEventAlerts(Cluster): 0x00: ZCLCommandDef("get_alerts", {}, False) } client_commands: dict[int, ZCLCommandDef] = { - 0x00: ZCLCommandDef("get_alarts_response", {}, True), + 0x00: ZCLCommandDef("get_alerts_response", {}, True), 0x01: ZCLCommandDef("alerts_notification", {}, False), 0x02: ZCLCommandDef("event_notification", {}, False), } diff --git a/zigpy/zcl/clusters/lighting.py b/zigpy/zcl/clusters/lighting.py index a8c5dac73..4eac72f00 100644 --- a/zigpy/zcl/clusters/lighting.py +++ b/zigpy/zcl/clusters/lighting.py @@ -148,7 +148,7 @@ class ColorLoopDirection(t.enum8): 0x02: ZCLCommandDef( "step_hue", { - "setp_mode": StepMode, + "step_mode": StepMode, "step_size": t.uint8_t, "transition_time": t.uint8_t, "options_mask?": t.bitmap8,