Bump ZHA to 0.0.32 (#124804)

* 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 8fb7789ea8.

* Test `release_notes`, not `release_summary`
pull/124926/head
puddly 2024-08-30 08:48:09 -04:00 committed by GitHub
parent c47b37af4f
commit 6467c8d611
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 117 additions and 88 deletions

View File

@ -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": {
@ -94,6 +106,9 @@
},
"keypad_lockout": {
"default": "mdi:lock"
},
"exercise_day_of_week": {
"default": "mdi:wrench-clock"
}
},
"sensor": {
@ -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": {
@ -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"
}
}
},

View File

@ -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",

View File

@ -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:
@ -143,6 +144,14 @@ class ZHAFirmwareUpdateEntity(
"""
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."""
@ -155,7 +164,7 @@ class ZHAFirmwareUpdateEntity(
) -> 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:

View File

@ -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

View File

@ -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

View File

@ -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,
@ -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,
}),
}),
@ -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,
}),
}),

View File

@ -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,
},

View File

@ -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
@ -43,6 +46,8 @@ from homeassistant.setup import async_setup_component
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():
@ -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)
@ -544,81 +552,56 @@ async def test_firmware_update_raises(
)
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
gateway = get_zha_gateway(hass)
gateway_proxy: ZHAGatewayProxy = get_zha_gateway_proxy(hass)
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",
)
gateway.get_or_create_device(zigpy_device)
await gateway.async_device_initialized(zigpy_device)
await hass.async_block_till_done(wait_background_tasks=True)
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()
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
# 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,
),
)
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}"
)
new_version = 0x99999999
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,
)
)
cluster.endpoint.reply = AsyncMock(side_effect=endpoint_reply)
with pytest.raises(HomeAssistantError):
await hass.services.async_call(
UPDATE_DOMAIN,
SERVICE_INSTALL,
ws_client = await hass_ws_client(hass)
await ws_client.send_json(
{
ATTR_ENTITY_ID: entity_id,
},
blocking=True,
"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"