Skip to content

Commit 2526498

Browse files
erjiaqingpull[bot]
authored andcommitted
[Python] Update AttributeRead (#11575)
1 parent 3fa5f3d commit 2526498

File tree

9 files changed

+622
-323
lines changed

9 files changed

+622
-323
lines changed

src/controller/python/BUILD.gn

+1-1
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ shared_library("ChipDeviceCtrl") {
4545
"ChipDeviceController-StorageDelegate.cpp",
4646
"ChipDeviceController-StorageDelegate.h",
4747
"chip/clusters/CHIPClusters.cpp",
48+
"chip/clusters/attribute.cpp",
4849
"chip/clusters/command.cpp",
49-
"chip/clusters/write.cpp",
5050
"chip/discovery/NodeResolution.cpp",
5151
"chip/interaction_model/Delegate.cpp",
5252
"chip/interaction_model/Delegate.h",

src/controller/python/chip/ChipDeviceCtrl.py

+44-27
Original file line numberDiff line numberDiff line change
@@ -378,37 +378,54 @@ def WriteAttribute(self, nodeid: int, attributes):
378378
raise self._ChipStack.ErrorToException(res)
379379
return future
380380

381-
def ReadAttribute(self, nodeid: int, attributes: typing.List[typing.Tuple[int, ClusterAttribute.AttributeReadRequest]]):
381+
def ReadAttribute(self, nodeid: int, attributes: typing.List[typing.Union[
382+
None, # Empty tuple, all wildcard
383+
typing.Tuple[int], # Endpoint
384+
# Wildcard endpoint, Cluster id present
385+
typing.Tuple[typing.Type[ClusterObjects.Cluster]],
386+
# Wildcard endpoint, Cluster + Attribute present
387+
typing.Tuple[typing.Type[ClusterObjects.ClusterAttributeDescriptor]],
388+
# Wildcard attribute id
389+
typing.Tuple[int, typing.Type[ClusterObjects.Cluster]],
390+
# Concrete path
391+
typing.Tuple[int, typing.Type[ClusterObjects.ClusterAttributeDescriptor]]
392+
]]):
382393
eventLoop = asyncio.get_running_loop()
383394
future = eventLoop.create_future()
384395

385396
device = self.GetConnectedDeviceSync(nodeid)
386-
# TODO: Here, we translates multi attribute read into many individual attribute reads, this should be fixed by implementing Python's attribute read API.
387-
res = []
388-
for attr in attributes:
389-
endpointId = attr[0]
390-
attribute = attr[1]
391-
clusterInfo = self._Cluster.GetClusterInfoById(
392-
attribute.cluster_id)
393-
if not clusterInfo:
394-
raise UnknownCluster(attribute.cluster_id)
395-
attributeInfo = clusterInfo.get("attributes", {}).get(
396-
attribute.attribute_id, None)
397-
if not attributeInfo:
398-
raise UnknownAttribute(
399-
clusterInfo["clusterName"], attribute.attribute_id)
400-
self._Cluster.ReadAttribute(
401-
device, clusterInfo["clusterName"], attributeInfo["attributeName"], endpointId, 0, False)
402-
readRes = im.GetAttributeReadResponse(
403-
im.DEFAULT_ATTRIBUTEREAD_APPID)
404-
res.append(ClusterAttribute.AttributeReadResult(
405-
Path=ClusterAttribute.AttributePath(
406-
EndpointId=endpointId, ClusterId=attribute.cluster_id, AttributeId=attribute.attribute_id),
407-
Status=readRes.status,
408-
Data=(attribute.FromTagDictOrRawValue(
409-
readRes.value) if readRes.value is not None else None),
410-
))
411-
future.set_result(res)
397+
attrs = []
398+
for v in attributes:
399+
endpoint = None
400+
cluster = None
401+
attribute = None
402+
if v == () or v == ('*'):
403+
# Wildcard
404+
pass
405+
elif len(v) == 1:
406+
if v[0] is int:
407+
endpoint = v[0]
408+
elif issubclass(v[0], ClusterObjects.Cluster):
409+
cluster = v[0]
410+
elif issubclass(v[0], ClusterObjects.ClusterAttributeDescriptor):
411+
attribute = v[0]
412+
else:
413+
raise ValueError("Unsupported Attribute Path")
414+
elif len(v) == 2:
415+
# endpoint + (cluster) attribute / endpoint + cluster
416+
endpoint = v[0]
417+
if issubclass(v[1], ClusterObjects.Cluster):
418+
cluster = v[1]
419+
elif issubclass(v[1], ClusterAttribute.ClusterAttributeDescriptor):
420+
attribute = v[1]
421+
else:
422+
raise ValueError("Unsupported Attribute Path")
423+
attrs.append(ClusterAttribute.AttributePath(
424+
EndpointId=endpoint, Cluster=cluster, Attribute=attribute))
425+
res = self._ChipStack.Call(
426+
lambda: ClusterAttribute.ReadAttributes(future, eventLoop, device, attrs))
427+
if res != 0:
428+
raise self._ChipStack.ErrorToException(res)
412429
return future
413430

414431
def ZCLSend(self, cluster, command, nodeid, endpoint, groupid, args, blocking=False):

src/controller/python/chip/clusters/Attribute.py

+156-4
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,40 @@
2424
from .ClusterObjects import ClusterAttributeDescriptor
2525
import chip.exceptions
2626
import chip.interaction_model
27+
import chip.tlv
28+
29+
import inspect
30+
import sys
31+
import logging
2732

2833

2934
@dataclass
3035
class AttributePath:
31-
EndpointId: int
32-
ClusterId: int
33-
AttributeId: int
36+
EndpointId: int = None
37+
ClusterId: int = None
38+
AttributeId: int = None
39+
40+
def __init__(self, EndpointId: int = None, Cluster=None, Attribute=None, ClusterId=None, AttributeId=None):
41+
self.EndpointId = EndpointId
42+
if Cluster is not None:
43+
# Wildcard read for a specific cluster
44+
if (Attribute is not None) or (ClusterId is not None) or (AttributeId is not None):
45+
raise Warning(
46+
"Attribute, ClusterId and AttributeId is ignored when Cluster is specified")
47+
self.ClusterId = Cluster.id
48+
return
49+
if Attribute is not None:
50+
if (ClusterId is not None) or (AttributeId is not None):
51+
raise Warning(
52+
"ClusterId and AttributeId is ignored when Attribute is specified")
53+
self.ClusterId = Attribute.cluster_id
54+
self.AttributeId = Attribute.attribute_id
55+
return
56+
self.ClusterId = ClusterId
57+
self.AttributeId = AttributeId
58+
59+
def __str__(self) -> str:
60+
return f"{self.EndpointId}/{self.ClusterId}/{self.AttributeId}"
3461

3562

3663
@dataclass
@@ -61,6 +88,72 @@ class AttributeReadResult(AttributeStatus):
6188
Data: Any = None
6289

6390

91+
_AttributeIndex = {}
92+
93+
94+
def _BuildAttributeIndex():
95+
''' Build internal attribute index for locating the corresponding cluster object by path in the future.
96+
We do this because this operation will take a long time when there are lots of attributes, it takes about 300ms for a single query.
97+
This is acceptable during init, but unacceptable when the server returns lots of attributes at the same time.
98+
'''
99+
for clusterName, obj in inspect.getmembers(sys.modules['chip.clusters.Objects']):
100+
if ('chip.clusters.Objects' in str(obj)) and inspect.isclass(obj):
101+
for objName, subclass in inspect.getmembers(obj):
102+
if inspect.isclass(subclass) and (('Attribute') in str(subclass)):
103+
for attributeName, attribute in inspect.getmembers(subclass):
104+
if inspect.isclass(attribute):
105+
for name, field in inspect.getmembers(attribute):
106+
if ('__dataclass_fields__' in name):
107+
_AttributeIndex[str(AttributePath(ClusterId=field['cluster_id'].default, AttributeId=field['attribute_id'].default))] = eval(
108+
'chip.clusters.Objects.' + clusterName + '.Attributes.' + attributeName)
109+
110+
111+
class AsyncReadTransaction:
112+
def __init__(self, future: Future, eventLoop):
113+
self._event_loop = eventLoop
114+
self._future = future
115+
self._res = []
116+
117+
def _handleAttributeData(self, path: AttributePath, status: int, data: bytes):
118+
try:
119+
imStatus = status
120+
try:
121+
imStatus = chip.interaction_model.Status(status)
122+
except:
123+
pass
124+
attributeType = _AttributeIndex.get(str(AttributePath(
125+
ClusterId=path.ClusterId, AttributeId=path.AttributeId)), None)
126+
attributeValue = None
127+
if attributeType is None:
128+
attributeValue = chip.tlv.TLVReader(data).get().get("Any", {})
129+
else:
130+
attributeValue = attributeType.FromTLV(data)
131+
self._res.append(AttributeReadResult(
132+
Path=path, Status=imStatus, Data=attributeValue))
133+
except Exception as ex:
134+
logging.exception(ex)
135+
136+
def handleAttributeData(self, path: AttributePath, status: int, data: bytes):
137+
self._event_loop.call_soon_threadsafe(
138+
self._handleAttributeData, path, status, data)
139+
140+
def _handleError(self, chipError: int):
141+
self._future.set_exception(
142+
chip.exceptions.ChipStackError(chipError))
143+
144+
def handleError(self, chipError: int):
145+
self._event_loop.call_soon_threadsafe(
146+
self._handleError, chipError
147+
)
148+
149+
def _handleDone(self, asd):
150+
if not self._future.done():
151+
self._future.set_result(self._res)
152+
153+
def handleDone(self):
154+
self._event_loop.call_soon_threadsafe(self._handleDone, "asdasa")
155+
156+
64157
class AsyncWriteTransaction:
65158
def __init__(self, future: Future, eventLoop):
66159
self._event_loop = eventLoop
@@ -95,6 +188,32 @@ def handleDone(self):
95188
self._event_loop.call_soon_threadsafe(self._handleDone)
96189

97190

191+
_OnReadAttributeDataCallbackFunct = CFUNCTYPE(
192+
None, py_object, c_uint16, c_uint32, c_uint32, c_uint16, c_char_p, c_size_t)
193+
_OnReadErrorCallbackFunct = CFUNCTYPE(
194+
None, py_object, c_uint32)
195+
_OnReadDoneCallbackFunct = CFUNCTYPE(
196+
None, py_object)
197+
198+
199+
@_OnReadAttributeDataCallbackFunct
200+
def _OnReadAttributeDataCallback(closure, endpoint: int, cluster: int, attribute: int, status, data, len):
201+
dataBytes = ctypes.string_at(data, len)
202+
closure.handleAttributeData(AttributePath(
203+
EndpointId=endpoint, ClusterId=cluster, AttributeId=attribute), status, dataBytes[:])
204+
205+
206+
@_OnReadErrorCallbackFunct
207+
def _OnReadErrorCallback(closure, chiperror: int):
208+
closure.handleError(chiperror)
209+
210+
211+
@_OnReadDoneCallbackFunct
212+
def _OnReadDoneCallback(closure):
213+
closure.handleDone()
214+
ctypes.pythonapi.Py_DecRef(ctypes.py_object(closure))
215+
216+
98217
_OnWriteResponseCallbackFunct = CFUNCTYPE(
99218
None, py_object, c_uint16, c_uint32, c_uint32, c_uint16)
100219
_OnWriteErrorCallbackFunct = CFUNCTYPE(
@@ -105,7 +224,8 @@ def handleDone(self):
105224

106225
@_OnWriteResponseCallbackFunct
107226
def _OnWriteResponseCallback(closure, endpoint: int, cluster: int, attribute: int, status):
108-
closure.handleResponse(AttributePath(endpoint, cluster, attribute), status)
227+
closure.handleResponse(AttributePath(
228+
EndpointId=endpoint, ClusterId=cluster, AttributeId=attribute), status)
109229

110230

111231
@_OnWriteErrorCallbackFunct
@@ -144,6 +264,31 @@ def WriteAttributes(future: Future, eventLoop, device, attributes: List[Attribut
144264
return res
145265

146266

267+
def ReadAttributes(future: Future, eventLoop, device, attributes: List[AttributePath]) -> int:
268+
handle = chip.native.GetLibraryHandle()
269+
transaction = AsyncReadTransaction(future, eventLoop)
270+
271+
readargs = []
272+
for attr in attributes:
273+
path = chip.interaction_model.AttributePathIBstruct.parse(
274+
b'\xff' * chip.interaction_model.AttributePathIBstruct.sizeof())
275+
if attr.EndpointId is not None:
276+
path.EndpointId = attr.EndpointId
277+
if attr.ClusterId is not None:
278+
path.ClusterId = attr.ClusterId
279+
if attr.AttributeId is not None:
280+
path.AttributeId = attr.AttributeId
281+
path = chip.interaction_model.AttributePathIBstruct.build(path)
282+
readargs.append(ctypes.c_char_p(path))
283+
284+
ctypes.pythonapi.Py_IncRef(ctypes.py_object(transaction))
285+
res = handle.pychip_ReadClient_ReadAttributes(
286+
ctypes.py_object(transaction), device, ctypes.c_size_t(len(attributes)), *readargs)
287+
if res != 0:
288+
ctypes.pythonapi.Py_DecRef(ctypes.py_object(transaction))
289+
return res
290+
291+
147292
def Init():
148293
handle = chip.native.GetLibraryHandle()
149294

@@ -155,6 +300,13 @@ def Init():
155300
handle.pychip_WriteClient_WriteAttributes.restype = c_uint32
156301
setter.Set('pychip_WriteClient_InitCallbacks', None, [
157302
_OnWriteResponseCallbackFunct, _OnWriteErrorCallbackFunct, _OnWriteDoneCallbackFunct])
303+
handle.pychip_ReadClient_ReadAttributes.restype = c_uint32
304+
setter.Set('pychip_ReadClient_InitCallbacks', None, [
305+
_OnReadAttributeDataCallbackFunct, _OnReadErrorCallbackFunct, _OnReadDoneCallbackFunct])
158306

159307
handle.pychip_WriteClient_InitCallbacks(
160308
_OnWriteResponseCallback, _OnWriteErrorCallback, _OnWriteDoneCallback)
309+
handle.pychip_ReadClient_InitCallbacks(
310+
_OnReadAttributeDataCallback, _OnReadErrorCallback, _OnReadDoneCallback)
311+
312+
_BuildAttributeIndex()

src/controller/python/chip/clusters/ClusterObjects.py

+7
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,13 @@ def command_id(self) -> int:
149149
raise NotImplementedError()
150150

151151

152+
class Cluster:
153+
''' This class does nothing, but a convenient class that generated clusters can inherit from.
154+
This gives the ability that the users can use issubclass(X, Cluster) to determine if the class represnents a Cluster.
155+
'''
156+
pass
157+
158+
152159
class ClusterAttributeDescriptor:
153160
'''
154161
The ClusterAttributeDescriptor is used for holding an attribute's metadata like its cluster id, attribute id and its type.

0 commit comments

Comments
 (0)