Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bump ZHA to 0.0.32 #124804

Merged
merged 11 commits into from
Aug 30, 2024
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 @@ -3001,7 +3001,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 @@ -2378,7 +2378,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"
Loading