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 8fb7789ea8ddb6ed2a287aed5010374c0452f6c9.

* Test `release_notes`, not `release_summary`
This commit is contained in:
puddly 2024-08-30 08:48:09 -04:00 committed by Bram Kragten
parent 3c0480596d
commit 26f3305743
8 changed files with 117 additions and 88 deletions

View File

@ -86,6 +86,18 @@
}, },
"presence_detection_timeout": { "presence_detection_timeout": {
"default": "mdi:timer-edit" "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": { "select": {
@ -94,6 +106,9 @@
}, },
"keypad_lockout": { "keypad_lockout": {
"default": "mdi:lock" "default": "mdi:lock"
},
"exercise_day_of_week": {
"default": "mdi:wrench-clock"
} }
}, },
"sensor": { "sensor": {
@ -132,6 +147,15 @@
}, },
"hooks_state": { "hooks_state": {
"default": "mdi:hook" "default": "mdi:hook"
},
"open_window_detected": {
"default": "mdi:window-open"
},
"load_estimate": {
"default": "mdi:scale-balance"
},
"preheat_time": {
"default": "mdi:radiator"
} }
}, },
"switch": { "switch": {
@ -158,6 +182,21 @@
}, },
"hooks_locked": { "hooks_locked": {
"default": "mdi:lock" "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", "zha",
"universal_silabs_flasher" "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": [ "usb": [
{ {
"vid": "10C4", "vid": "10C4",

View File

@ -95,6 +95,7 @@ class ZHAFirmwareUpdateEntity(
UpdateEntityFeature.INSTALL UpdateEntityFeature.INSTALL
| UpdateEntityFeature.PROGRESS | UpdateEntityFeature.PROGRESS
| UpdateEntityFeature.SPECIFIC_VERSION | UpdateEntityFeature.SPECIFIC_VERSION
| UpdateEntityFeature.RELEASE_NOTES
) )
def __init__(self, entity_data: EntityData, **kwargs: Any) -> None: def __init__(self, entity_data: EntityData, **kwargs: Any) -> None:
@ -143,6 +144,14 @@ class ZHAFirmwareUpdateEntity(
""" """
return self.entity_data.entity.release_summary 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 @property
def release_url(self) -> str | None: def release_url(self) -> str | None:
"""URL to the full release notes of the latest version available.""" """URL to the full release notes of the latest version available."""
@ -155,7 +164,7 @@ class ZHAFirmwareUpdateEntity(
) -> None: ) -> None:
"""Install an update.""" """Install an update."""
try: 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: except ZHAException as exc:
raise HomeAssistantError(exc) from exc raise HomeAssistantError(exc) from exc
finally: finally:

View File

@ -3006,7 +3006,7 @@ zeroconf==0.133.0
zeversolar==0.3.1 zeversolar==0.3.1
# homeassistant.components.zha # homeassistant.components.zha
zha==0.0.31 zha==0.0.32
# homeassistant.components.zhong_hong # homeassistant.components.zhong_hong
zhong-hong-hvac==1.0.12 zhong-hong-hvac==1.0.12

View File

@ -2383,7 +2383,7 @@ zeroconf==0.133.0
zeversolar==0.3.1 zeversolar==0.3.1
# homeassistant.components.zha # homeassistant.components.zha
zha==0.0.31 zha==0.0.32
# homeassistant.components.zwave_js # homeassistant.components.zwave_js
zwave-js-server-python==0.57.0 zwave-js-server-python==0.57.0

View File

@ -162,19 +162,19 @@
'0x0500': dict({ '0x0500': dict({
'attributes': dict({ 'attributes': dict({
'0x0000': 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, 'value': None,
}), }),
'0x0001': dict({ '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, 'value': None,
}), }),
'0x0002': dict({ '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, 'value': None,
}), }),
'0x0010': dict({ '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([ 'value': list([
50, 50,
79, 79,
@ -187,15 +187,15 @@
]), ]),
}), }),
'0x0011': dict({ '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, 'value': None,
}), }),
'0x0012': dict({ '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, 'value': None,
}), }),
'0x0013': dict({ '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, 'value': None,
}), }),
}), }),
@ -208,11 +208,11 @@
'0x0501': dict({ '0x0501': dict({
'attributes': dict({ 'attributes': dict({
'0xfffd': 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, 'value': None,
}), }),
'0xfffe': dict({ '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, 'value': None,
}), }),
}), }),

View File

@ -60,16 +60,14 @@ async def test_zcl_schema_conversions(hass: HomeAssistant) -> None:
"required": True, "required": True,
}, },
{ {
"type": "integer", "type": "multi_select",
"valueMin": 0, "options": ["Execute if off present"],
"valueMax": 255,
"name": "options_mask", "name": "options_mask",
"optional": True, "optional": True,
}, },
{ {
"type": "integer", "type": "multi_select",
"valueMin": 0, "options": ["Execute if off"],
"valueMax": 255,
"name": "options_override", "name": "options_override",
"optional": True, "optional": True,
}, },

View File

@ -3,8 +3,11 @@
from unittest.mock import AsyncMock, call, patch from unittest.mock import AsyncMock, call, patch
import pytest import pytest
from zha.application.platforms.update import (
FirmwareUpdateEntity as ZhaFirmwareUpdateEntity,
)
from zigpy.exceptions import DeliveryError from zigpy.exceptions import DeliveryError
from zigpy.ota import OtaImageWithMetadata from zigpy.ota import OtaImagesResult, OtaImageWithMetadata
import zigpy.ota.image as firmware import zigpy.ota.image as firmware
from zigpy.ota.providers import BaseOtaImageMetadata from zigpy.ota.providers import BaseOtaImageMetadata
from zigpy.profiles import zha 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 .common import find_entity_id, update_attribute_cache
from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE
from tests.typing import WebSocketGenerator
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def update_platform_only(): def update_platform_only():
@ -119,8 +124,11 @@ async def setup_test_data(
), ),
) )
cluster.endpoint.device.application.ota.get_ota_image = AsyncMock( cluster.endpoint.device.application.ota.get_ota_images = AsyncMock(
return_value=None if file_not_found else fw_image 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: ZHADeviceProxy = gateway_proxy.get_device_proxy(zigpy_device.ieee)
zha_device_proxy.device.async_update_sw_build_id(installed_fw_version) 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: HomeAssistant,
hass_ws_client: WebSocketGenerator,
setup_zha, setup_zha,
zigpy_device_mock, zigpy_device_mock,
) -> None: ) -> None:
"""Test ZHA update platform - firmware update is no longer valid.""" """Test ZHA update platform release notes."""
await setup_zha() 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) entity_id = find_entity_id(Platform.UPDATE, zha_device, hass)
assert entity_id is not None assert entity_id is not None
assert hass.states.get(entity_id).state == STATE_UNKNOWN ws_client = await hass_ws_client(hass)
await ws_client.send_json(
# 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,
{ {
ATTR_ENTITY_ID: entity_id, "id": 1,
}, "type": "update/release_notes",
blocking=True, "entity_id": entity_id,
}
) )
# We updated the currently installed firmware version, as it is no longer valid result = await ws_client.receive_json()
state = hass.states.get(entity_id) assert result["success"] is True
assert state.state == STATE_OFF assert result["result"] == "Some lengthy release notes"
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}"