Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions tests/test_switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import asyncio
import logging
from typing import Final
from unittest.mock import call, patch

import pytest
Expand Down Expand Up @@ -34,6 +35,7 @@
get_group_entity,
group_entity_availability_test,
join_zigpy_device,
patch_cluster_for_testing,
send_attributes_report,
update_attribute_cache,
zigpy_device_from_json,
Expand All @@ -43,6 +45,11 @@
from zha.application.platforms import GroupEntity, PlatformEntity
from zha.exceptions import ZHAException
from zha.quirks import QUIRK_REGISTRY_ENTRY_ATTR, DeviceRegistry
from zha.zigbee.cluster_config import (
AggregatedAttrConfig,
AggregatedClusterConfig,
initialize_cluster_configs,
)
from zha.zigbee.device import Device
from zha.zigbee.group import Group, GroupMemberReference

Expand Down Expand Up @@ -904,3 +911,71 @@ async def test_binary_output_cluster(zha_gateway: Gateway) -> None:
manufacturer=UNDEFINED,
)
]


class MissingOnOffAttributesCluster(CustomCluster, general.OnOff):
"""OnOff cluster without the standard attribute definitions."""

class AttributeDefs(zcl_f.BaseAttributeDefs):
"""Attribute definitions not inheriting from OnOff.AttributeDefs."""

window_detection_temperature: Final = zcl_f.ZCLAttributeDef(
id=0x6000, type=t.int16s, is_manufacturer_specific=True
)


async def test_switch_missing_standard_attribute_definitions(
zha_gateway: Gateway,
) -> None:
"""A quirk-replaced OnOff cluster does not abort device discovery."""
registry = DeviceRegistry()
zigpy_device = create_mock_zigpy_device(
zha_gateway,
ZIGPY_DEVICE,
manufacturer="Test_Manufacturer",
model="Test_Model",
)

(
QuirkBuilder(zigpy_device.manufacturer, zigpy_device.model)
.replaces(MissingOnOffAttributesCluster)
.add_to_registry(registry)
)

zigpy_device = registry.resolve(zigpy_device)
assert getattr(zigpy_device, QUIRK_REGISTRY_ENTRY_ATTR, None) is not None
assert isinstance(zigpy_device.endpoints[1].on_off, MissingOnOffAttributesCluster)
patch_cluster_for_testing(zigpy_device.endpoints[1].on_off)

zha_device = await join_zigpy_device(zha_gateway, zigpy_device)

with pytest.raises(KeyError):
get_entity(zha_device, platform=Platform.SWITCH)


async def test_missing_definition_does_not_suppress_valid_startup_read(
zha_gateway: Gateway,
) -> None:
"""An undefined config attribute does not suppress a valid startup read."""
zigpy_device = create_mock_zigpy_device(
zha_gateway,
ZIGPY_DEVICE,
)
cluster = zigpy_device.endpoints[1].on_off
configs = {
(1, general.OnOff.cluster_id, True): AggregatedClusterConfig(
cluster=cluster,
attributes={
general.OnOff.AttributeDefs.on_off.name: AggregatedAttrConfig(
read_on_startup=True
),
"missing_attribute": AggregatedAttrConfig(read_on_startup=True),
},
)
}

await initialize_cluster_configs(configs, from_cache=False)
assert cluster.read_attributes_raw.call_count == 1
assert cluster.read_attributes_raw.call_args.args[0] == [
general.OnOff.AttributeDefs.on_off.id
]
13 changes: 9 additions & 4 deletions zha/application/platforms/sensor/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -919,8 +919,9 @@ async def async_update(self) -> None:
continue
if not entity.enabled:
continue
if entity._attribute_name and self._cluster.is_attribute_unsupported(
entity._attribute_name
if entity._attribute_name and (
entity._attribute_name not in self._cluster.attributes_by_name
or self._cluster.is_attribute_unsupported(entity._attribute_name)
):
continue
cfg = entity._server_cluster_config.get(self._cluster_id)
Expand All @@ -929,10 +930,14 @@ async def async_update(self) -> None:
for attr_def, attr_cfg in cfg.attributes.items():
if attr_cfg.reporting is None:
continue
if self._cluster.is_attribute_unsupported(attr_def):
attr_name = attr_def if isinstance(attr_def, str) else attr_def.name
if (
attr_name not in self._cluster.attributes_by_name
or self._cluster.is_attribute_unsupported(attr_name)
):
continue

attrs.add(attr_def if isinstance(attr_def, str) else attr_def.name)
attrs.add(attr_name)

if not attrs:
return
Expand Down
9 changes: 6 additions & 3 deletions zha/application/platforms/switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,10 @@ def on_add(self) -> None:
)

def _is_supported(self) -> bool:
if self._cluster.is_attribute_unsupported(self._attribute_name):
if (
self._attribute_name not in self._cluster.attributes_by_name
or self._cluster.is_attribute_unsupported(self._attribute_name)
):
_LOGGER.debug(
"%s is not supported - skipping %s entity creation",
self._attribute_name,
Expand Down Expand Up @@ -1041,8 +1044,8 @@ def _is_supported(self) -> bool:

# this entity needs a second attribute to function
if (
self._cluster.is_attribute_unsupported(window_covering_mode_attr)
or window_covering_mode_attr not in self._cluster.attributes_by_name
window_covering_mode_attr not in self._cluster.attributes_by_name
or self._cluster.is_attribute_unsupported(window_covering_mode_attr)
or self._cluster.get(window_covering_mode_attr) is None
):
_LOGGER.debug(
Expand Down
34 changes: 24 additions & 10 deletions zha/zigbee/cluster_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,15 @@ async def configure_cluster_configs(
for attr_name, attr_config in agg.attributes.items():
if attr_config.reporting is None:
continue
if attr_name not in agg.cluster.attributes_by_name:
_LOGGER.debug(
"[%s] Attribute %s has no definition on cluster %s,"
" skipping reporting configuration",
agg.cluster.endpoint.device.ieee,
attr_name,
agg.cluster.ep_attribute,
)
continue
attr_def = agg.cluster.find_attribute(attr_name)
reporting_attrs[attr_def] = attr_config.reporting

Expand Down Expand Up @@ -248,16 +257,21 @@ async def initialize_cluster_configs(
) -> None:
"""Read initial attribute values from aggregated configs."""
for agg in configs.values():
cached_attrs = [
attr_name
for attr_name, attr_config in agg.attributes.items()
if not attr_config.read_on_startup
]
fresh_attrs = [
attr_name
for attr_name, attr_config in agg.attributes.items()
if attr_config.read_on_startup
]
cached_attrs: list[str] = []
fresh_attrs: list[str] = []
for attr_name, attr_config in agg.attributes.items():
if attr_name not in agg.cluster.attributes_by_name:
_LOGGER.debug(
"[%s] Attribute %s has no definition on cluster %s,"
" skipping initialization",
agg.cluster.endpoint.device.ieee,
attr_name,
agg.cluster.ep_attribute,
)
continue

attrs = fresh_attrs if attr_config.read_on_startup else cached_attrs
attrs.append(attr_name)

if cached_attrs:
try:
Expand Down
Loading