Skip to content

Commit

Permalink
Bump ZHA to 0.0.32 (#124804)
Browse files Browse the repository at this point in the history
* Always prefer XY color mode in ZHA

Remove a few more HS remnants

* Use new ZHA OTA format

* Bump ZHA to 0.0.32

* Fix existing OTA unit tests

* Fix schema conversion test to account for new command parameters

* Update snapshot with new `zcl_type` kwarg

* Migrate existing entities to icon translations

* Remove "no longer compatible" test

* Test that the library release summary is correctly exposed to ZHA

* Revert "Always prefer XY color mode in ZHA"

This reverts commit 8fb7789.

* Test `release_notes`, not `release_summary`
  • Loading branch information
puddly authored Aug 30, 2024
1 parent c47b37a commit 6467c8d
Show file tree
Hide file tree
Showing 8 changed files with 116 additions and 87 deletions.
39 changes: 39 additions & 0 deletions homeassistant/components/zha/icons.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,18 @@
},
"presence_detection_timeout": {
"default": "mdi:timer-edit"
},
"exercise_trigger_time": {
"default": "mdi:clock"
},
"external_temperature_sensor": {
"default": "mdi:thermometer"
},
"load_room_mean": {
"default": "mdi:scale-balance"
},
"regulation_setpoint_offset": {
"default": "mdi:thermostat"
}
},
"select": {
Expand All @@ -94,6 +106,9 @@
},
"keypad_lockout": {
"default": "mdi:lock"
},
"exercise_day_of_week": {
"default": "mdi:wrench-clock"
}
},
"sensor": {
Expand Down Expand Up @@ -132,6 +147,15 @@
},
"hooks_state": {
"default": "mdi:hook"
},
"open_window_detected": {
"default": "mdi:window-open"
},
"load_estimate": {
"default": "mdi:scale-balance"
},
"preheat_time": {
"default": "mdi:radiator"
}
},
"switch": {
Expand All @@ -158,6 +182,21 @@
},
"hooks_locked": {
"default": "mdi:lock"
},
"external_window_sensor": {
"default": "mdi:window-open"
},
"use_internal_window_detection": {
"default": "mdi:window-open"
},
"prioritize_external_temperature_sensor": {
"default": "mdi:thermometer"
},
"heat_available": {
"default": "mdi:water-boiler"
},
"use_load_balancing": {
"default": "mdi:scale-balance"
}
}
},
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/zha/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"zha",
"universal_silabs_flasher"
],
"requirements": ["universal-silabs-flasher==0.0.22", "zha==0.0.31"],
"requirements": ["universal-silabs-flasher==0.0.22", "zha==0.0.32"],
"usb": [
{
"vid": "10C4",
Expand Down
11 changes: 10 additions & 1 deletion homeassistant/components/zha/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ class ZHAFirmwareUpdateEntity(
UpdateEntityFeature.INSTALL
| UpdateEntityFeature.PROGRESS
| UpdateEntityFeature.SPECIFIC_VERSION
| UpdateEntityFeature.RELEASE_NOTES
)

def __init__(self, entity_data: EntityData, **kwargs: Any) -> None:
Expand Down Expand Up @@ -143,6 +144,14 @@ def release_summary(self) -> str | None:
"""
return self.entity_data.entity.release_summary

async def async_release_notes(self) -> str | None:
"""Return full release notes.
This is suitable for a long changelog that does not fit in the release_summary
property. The returned string can contain markdown.
"""
return self.entity_data.entity.release_notes

@property
def release_url(self) -> str | None:
"""URL to the full release notes of the latest version available."""
Expand All @@ -155,7 +164,7 @@ async def async_install(
) -> None:
"""Install an update."""
try:
await self.entity_data.entity.async_install(version=version, backup=backup)
await self.entity_data.entity.async_install(version=version)
except ZHAException as exc:
raise HomeAssistantError(exc) from exc
finally:
Expand Down
2 changes: 1 addition & 1 deletion requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3012,7 +3012,7 @@ zeroconf==0.133.0
zeversolar==0.3.1

# homeassistant.components.zha
zha==0.0.31
zha==0.0.32

# homeassistant.components.zhong_hong
zhong-hong-hvac==1.0.12
Expand Down
2 changes: 1 addition & 1 deletion requirements_test_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2389,7 +2389,7 @@ zeroconf==0.133.0
zeversolar==0.3.1

# homeassistant.components.zha
zha==0.0.31
zha==0.0.32

# homeassistant.components.zwave_js
zwave-js-server-python==0.57.0
Expand Down
18 changes: 9 additions & 9 deletions tests/components/zha/snapshots/test_diagnostics.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -162,19 +162,19 @@
'0x0500': dict({
'attributes': dict({
'0x0000': dict({
'attribute': "ZCLAttributeDef(id=0x0000, name='zone_state', type=<enum 'ZoneState'>, access=<ZCLAttributeAccess.Read: 1>, mandatory=True, is_manufacturer_specific=False)",
'attribute': "ZCLAttributeDef(id=0x0000, name='zone_state', type=<enum 'ZoneState'>, zcl_type=<DataTypeId.enum8: 48>, access=<ZCLAttributeAccess.Read: 1>, mandatory=True, is_manufacturer_specific=False)",
'value': None,
}),
'0x0001': dict({
'attribute': "ZCLAttributeDef(id=0x0001, name='zone_type', type=<enum 'ZoneType'>, access=<ZCLAttributeAccess.Read: 1>, mandatory=True, is_manufacturer_specific=False)",
'attribute': "ZCLAttributeDef(id=0x0001, name='zone_type', type=<enum 'ZoneType'>, zcl_type=<DataTypeId.uint16: 33>, access=<ZCLAttributeAccess.Read: 1>, mandatory=True, is_manufacturer_specific=False)",
'value': None,
}),
'0x0002': dict({
'attribute': "ZCLAttributeDef(id=0x0002, name='zone_status', type=<flag 'ZoneStatus'>, access=<ZCLAttributeAccess.Read: 1>, mandatory=True, is_manufacturer_specific=False)",
'attribute': "ZCLAttributeDef(id=0x0002, name='zone_status', type=<flag 'ZoneStatus'>, zcl_type=<DataTypeId.map16: 25>, access=<ZCLAttributeAccess.Read: 1>, mandatory=True, is_manufacturer_specific=False)",
'value': None,
}),
'0x0010': dict({
'attribute': "ZCLAttributeDef(id=0x0010, name='cie_addr', type=<class 'zigpy.types.named.EUI64'>, access=<ZCLAttributeAccess.Read|Write: 3>, mandatory=True, is_manufacturer_specific=False)",
'attribute': "ZCLAttributeDef(id=0x0010, name='cie_addr', type=<class 'zigpy.types.named.EUI64'>, zcl_type=<DataTypeId.EUI64: 240>, access=<ZCLAttributeAccess.Read|Write: 3>, mandatory=True, is_manufacturer_specific=False)",
'value': list([
50,
79,
Expand All @@ -187,15 +187,15 @@
]),
}),
'0x0011': dict({
'attribute': "ZCLAttributeDef(id=0x0011, name='zone_id', type=<class 'zigpy.types.basic.uint8_t'>, access=<ZCLAttributeAccess.Read: 1>, mandatory=True, is_manufacturer_specific=False)",
'attribute': "ZCLAttributeDef(id=0x0011, name='zone_id', type=<class 'zigpy.types.basic.uint8_t'>, zcl_type=<DataTypeId.uint8: 32>, access=<ZCLAttributeAccess.Read: 1>, mandatory=True, is_manufacturer_specific=False)",
'value': None,
}),
'0x0012': dict({
'attribute': "ZCLAttributeDef(id=0x0012, name='num_zone_sensitivity_levels_supported', type=<class 'zigpy.types.basic.uint8_t'>, access=<ZCLAttributeAccess.Read: 1>, mandatory=False, is_manufacturer_specific=False)",
'attribute': "ZCLAttributeDef(id=0x0012, name='num_zone_sensitivity_levels_supported', type=<class 'zigpy.types.basic.uint8_t'>, zcl_type=<DataTypeId.uint8: 32>, access=<ZCLAttributeAccess.Read: 1>, mandatory=False, is_manufacturer_specific=False)",
'value': None,
}),
'0x0013': dict({
'attribute': "ZCLAttributeDef(id=0x0013, name='current_zone_sensitivity_level', type=<class 'zigpy.types.basic.uint8_t'>, access=<ZCLAttributeAccess.Read|Write: 3>, mandatory=False, is_manufacturer_specific=False)",
'attribute': "ZCLAttributeDef(id=0x0013, name='current_zone_sensitivity_level', type=<class 'zigpy.types.basic.uint8_t'>, zcl_type=<DataTypeId.uint8: 32>, access=<ZCLAttributeAccess.Read|Write: 3>, mandatory=False, is_manufacturer_specific=False)",
'value': None,
}),
}),
Expand All @@ -208,11 +208,11 @@
'0x0501': dict({
'attributes': dict({
'0xfffd': dict({
'attribute': "ZCLAttributeDef(id=0xFFFD, name='cluster_revision', type=<class 'zigpy.types.basic.uint16_t'>, access=<ZCLAttributeAccess.Read: 1>, mandatory=True, is_manufacturer_specific=False)",
'attribute': "ZCLAttributeDef(id=0xFFFD, name='cluster_revision', type=<class 'zigpy.types.basic.uint16_t'>, zcl_type=<DataTypeId.uint16: 33>, access=<ZCLAttributeAccess.Read: 1>, mandatory=True, is_manufacturer_specific=False)",
'value': None,
}),
'0xfffe': dict({
'attribute': "ZCLAttributeDef(id=0xFFFE, name='reporting_status', type=<enum 'AttributeReportingStatus'>, access=<ZCLAttributeAccess.Read: 1>, mandatory=False, is_manufacturer_specific=False)",
'attribute': "ZCLAttributeDef(id=0xFFFE, name='reporting_status', type=<enum 'AttributeReportingStatus'>, zcl_type=<DataTypeId.enum8: 48>, access=<ZCLAttributeAccess.Read: 1>, mandatory=False, is_manufacturer_specific=False)",
'value': None,
}),
}),
Expand Down
10 changes: 4 additions & 6 deletions tests/components/zha/test_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,16 +60,14 @@ async def test_zcl_schema_conversions(hass: HomeAssistant) -> None:
"required": True,
},
{
"type": "integer",
"valueMin": 0,
"valueMax": 255,
"type": "multi_select",
"options": ["Execute if off present"],
"name": "options_mask",
"optional": True,
},
{
"type": "integer",
"valueMin": 0,
"valueMax": 255,
"type": "multi_select",
"options": ["Execute if off"],
"name": "options_override",
"optional": True,
},
Expand Down
119 changes: 51 additions & 68 deletions tests/components/zha/test_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
from unittest.mock import AsyncMock, call, patch

import pytest
from zha.application.platforms.update import (
FirmwareUpdateEntity as ZhaFirmwareUpdateEntity,
)
from zigpy.exceptions import DeliveryError
from zigpy.ota import OtaImageWithMetadata
from zigpy.ota import OtaImagesResult, OtaImageWithMetadata
import zigpy.ota.image as firmware
from zigpy.ota.providers import BaseOtaImageMetadata
from zigpy.profiles import zha
Expand Down Expand Up @@ -43,6 +46,8 @@
from .common import find_entity_id, update_attribute_cache
from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE

from tests.typing import WebSocketGenerator


@pytest.fixture(autouse=True)
def update_platform_only():
Expand Down Expand Up @@ -119,8 +124,11 @@ async def setup_test_data(
),
)

cluster.endpoint.device.application.ota.get_ota_image = AsyncMock(
return_value=None if file_not_found else fw_image
cluster.endpoint.device.application.ota.get_ota_images = AsyncMock(
return_value=OtaImagesResult(
upgrades=() if file_not_found else (fw_image,),
downgrades=(),
)
)
zha_device_proxy: ZHADeviceProxy = gateway_proxy.get_device_proxy(zigpy_device.ieee)
zha_device_proxy.device.async_update_sw_build_id(installed_fw_version)
Expand Down Expand Up @@ -544,81 +552,56 @@ async def endpoint_reply(cluster_id, tsn, data, command_id):
)


async def test_firmware_update_no_longer_compatible(
async def test_update_release_notes(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
setup_zha,
zigpy_device_mock,
) -> None:
"""Test ZHA update platform - firmware update is no longer valid."""
"""Test ZHA update platform release notes."""
await setup_zha()
zha_device, cluster, fw_image, installed_fw_version = await setup_test_data(
hass, zigpy_device_mock
)

entity_id = find_entity_id(Platform.UPDATE, zha_device, hass)
assert entity_id is not None

assert hass.states.get(entity_id).state == STATE_UNKNOWN
gateway = get_zha_gateway(hass)
gateway_proxy: ZHAGatewayProxy = get_zha_gateway_proxy(hass)

# simulate an image available notification
await cluster._handle_query_next_image(
foundation.ZCLHeader.cluster(
tsn=0x12, command_id=general.Ota.ServerCommandDefs.query_next_image.id
),
general.QueryNextImageCommand(
fw_image.firmware.header.field_control,
zha_device.device.manufacturer_code,
fw_image.firmware.header.image_type,
installed_fw_version,
fw_image.firmware.header.header_version,
),
zigpy_device = zigpy_device_mock(
{
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,
SIG_EP_PROFILE: zha.PROFILE_ID,
}
},
node_descriptor=b"\x02@\x84_\x11\x7fd\x00\x00,d\x00\x00",
)

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.firmware.header.file_version:08x}"
)
gateway.get_or_create_device(zigpy_device)
await gateway.async_device_initialized(zigpy_device)
await hass.async_block_till_done(wait_background_tasks=True)

new_version = 0x99999999
zha_device: ZHADeviceProxy = gateway_proxy.get_device_proxy(zigpy_device.ieee)
zha_lib_entity = next(
e
for e in zha_device.device.platform_entities.values()
if isinstance(e, ZhaFirmwareUpdateEntity)
)
zha_lib_entity._attr_release_notes = "Some lengthy release notes"
zha_lib_entity.maybe_emit_state_changed_event()
await hass.async_block_till_done()

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):
zha_device.device.device.packet_received(
make_packet(
zha_device.device.device,
cluster,
general.Ota.ServerCommandDefs.query_next_image.name,
field_control=general.Ota.QueryNextImageCommand.FieldControl.HardwareVersion,
manufacturer_code=fw_image.firmware.header.manufacturer_id,
image_type=fw_image.firmware.header.image_type,
# The device reports that it is no longer compatible!
current_file_version=new_version,
hardware_version=1,
)
)
entity_id = find_entity_id(Platform.UPDATE, zha_device, hass)
assert entity_id is not None

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,
)
ws_client = await hass_ws_client(hass)
await ws_client.send_json(
{
"id": 1,
"type": "update/release_notes",
"entity_id": entity_id,
}
)

# We updated the currently installed firmware version, as it is no longer valid
state = hass.states.get(entity_id)
assert state.state == STATE_OFF
attrs = state.attributes
assert attrs[ATTR_INSTALLED_VERSION] == f"0x{new_version:08x}"
assert not attrs[ATTR_IN_PROGRESS]
assert attrs[ATTR_LATEST_VERSION] == f"0x{new_version:08x}"
result = await ws_client.receive_json()
assert result["success"] is True
assert result["result"] == "Some lengthy release notes"

0 comments on commit 6467c8d

Please sign in to comment.