Skip to content

Commit fff11ff

Browse files
authored
Add _VALID_ATTRIBUTES to LocalDataCluster, fix Tuya datapoint mappings on LocalDataClusters (#3415)
* Add `_VALID_ATTRIBUTES` to `LocalDataCluster` This list should be populated with the attribute ids of attributes that will not be populated at first, but only later. All attribute reads on the `LocalDataCluster` for those specified attributes will return `None` with success status when no value is in the attribute cache. This allows ZHA entity creation for those entities (except some configuration entities that explicitly check for `None` attributes). If they are not in the valid attributes list, they will retain the previous behavior returning as "unsupported attribute". This prevents ZHA entity creation for those attributes. * Add test checking reading attributes on `LocalDataCluster` This adds a test which checks if the reading of the following works as expected on a `LocalDataCluster`: - invalid attribute reading returning unsupported - constant attribute reading working - valid attribute reading returning `None` with success status * Mark attributes from Tuya datapoint mappings on `LocalDataCluster`s as valid This allows entities in ZHA to be created, as the initial attribute reads will return `None` with success status, instead of "unsupported attribute".
1 parent 93af47a commit fff11ff

File tree

3 files changed

+77
-4
lines changed

3 files changed

+77
-4
lines changed

tests/test_quirks.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515
import zigpy.profiles
1616
import zigpy.quirks as zq
1717
from zigpy.quirks import CustomDevice
18+
from zigpy.quirks.v2 import QuirkBuilder
1819
import zigpy.types
20+
from zigpy.zcl import foundation
1921
import zigpy.zdo.types
2022

2123
import zhaquirks
@@ -841,3 +843,40 @@ def check_for_duplicate_cluster_ids(clusters) -> None:
841843
for ep_id, ep_data in quirk.replacement[ENDPOINTS].items(): # noqa: B007
842844
check_for_duplicate_cluster_ids(ep_data.get(INPUT_CLUSTERS, []))
843845
check_for_duplicate_cluster_ids(ep_data.get(OUTPUT_CLUSTERS, []))
846+
847+
848+
async def test_local_data_cluster(zigpy_device_from_v2_quirk) -> None:
849+
"""Ensure reading attributes from a LocalDataCluster works as expected."""
850+
851+
class TestLocalCluster(zhaquirks.LocalDataCluster):
852+
"""Test cluster."""
853+
854+
cluster_id = 0x1234
855+
_CONSTANT_ATTRIBUTES = {1: 10}
856+
_VALID_ATTRIBUTES = [2]
857+
858+
(
859+
QuirkBuilder("manufacturer-local-test", "model")
860+
.adds(TestLocalCluster)
861+
.add_to_registry()
862+
)
863+
device = zigpy_device_from_v2_quirk("manufacturer-local-test", "model")
864+
assert isinstance(device.endpoints[1].in_clusters[0x1234], TestLocalCluster)
865+
866+
# reading invalid attribute return unsupported attribute
867+
assert await device.endpoints[1].in_clusters[0x1234].read_attributes([0]) == (
868+
{},
869+
{0: foundation.Status.UNSUPPORTED_ATTRIBUTE},
870+
)
871+
872+
# reading constant attribute works
873+
assert await device.endpoints[1].in_clusters[0x1234].read_attributes([1]) == (
874+
{1: 10},
875+
{},
876+
)
877+
878+
# reading valid attribute returns None with success status
879+
assert await device.endpoints[1].in_clusters[0x1234].read_attributes([2]) == (
880+
{2: None},
881+
{},
882+
)

zhaquirks/__init__.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import pathlib
1010
import pkgutil
1111
import sys
12+
import typing
1213
from typing import Any
1314

1415
import zigpy.device
@@ -60,9 +61,15 @@ def __init__(self, *args, **kwargs):
6061

6162

6263
class LocalDataCluster(CustomCluster):
63-
"""Cluster meant to prevent remote calls."""
64+
"""Cluster meant to prevent remote calls.
6465
65-
_CONSTANT_ATTRIBUTES = {}
66+
Set _CONSTANT_ATTRIBUTES to provide constant values for attribute ids.
67+
Set _VALID_ATTRIBUTES to provide a list of valid attribute ids that will never be shown as unsupported.
68+
These are attributes that should be populated later.
69+
"""
70+
71+
_CONSTANT_ATTRIBUTES: dict[int, typing.Any] = {}
72+
_VALID_ATTRIBUTES: list[int] = []
6673

6774
async def bind(self):
6875
"""Prevent bind."""
@@ -94,7 +101,10 @@ async def read_attributes_raw(self, attributes, manufacturer=None, **kwargs):
94101
record.value.value = self._CONSTANT_ATTRIBUTES[record.attrid]
95102
else:
96103
record.value.value = self._attr_cache.get(record.attrid)
97-
if record.value.value is not None:
104+
if (
105+
record.value.value is not None
106+
or record.attrid in self._VALID_ATTRIBUTES
107+
):
98108
record.status = foundation.Status.SUCCESS
99109
return (records,)
100110

zhaquirks/tuya/__init__.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -864,7 +864,7 @@ class TuyaLocalCluster(LocalDataCluster):
864864
"""
865865

866866
def update_attribute(self, attr_name: str, value: Any) -> None:
867-
"""Update attribute by attribute name."""
867+
"""Update attribute by name and safeguard against unknown attributes."""
868868

869869
try:
870870
attr = self.attributes_by_name[attr_name]
@@ -1497,8 +1497,32 @@ class TuyaNewManufCluster(CustomCluster):
14971497
),
14981498
}
14991499

1500+
dp_to_attribute: dict[int, DPToAttributeMapping] = {}
15001501
data_point_handlers: dict[int, str] = {}
15011502

1503+
def __init__(self, *args, **kwargs):
1504+
"""Initialize the cluster and mark attributes as valid on LocalDataClusters."""
1505+
super().__init__(*args, **kwargs)
1506+
for dp_map in self.dp_to_attribute.values():
1507+
# get the endpoint that is being mapped to
1508+
endpoint = self.endpoint
1509+
if dp_map.endpoint_id:
1510+
endpoint = self.endpoint.device.endpoints.get(dp_map.endpoint_id)
1511+
1512+
# the endpoint to be mapped to might not actually exist within all quirks
1513+
if not endpoint:
1514+
continue
1515+
1516+
cluster = getattr(endpoint, dp_map.ep_attribute, None)
1517+
# the cluster to be mapped to might not actually exist within all quirks
1518+
if not cluster:
1519+
continue
1520+
1521+
# mark mapped to attribute as valid if existing and if on a LocalDataCluster
1522+
attr = cluster.attributes_by_name.get(dp_map.attribute_name)
1523+
if attr and isinstance(cluster, LocalDataCluster):
1524+
cluster._VALID_ATTRIBUTES.append(attr.id)
1525+
15021526
def handle_cluster_request(
15031527
self,
15041528
hdr: foundation.ZCLHeader,

0 commit comments

Comments
 (0)