Skip to content

Commit

Permalink
[Python] Update AttributeRead (#11575)
Browse files Browse the repository at this point in the history
  • Loading branch information
erjiaqing authored and pull[bot] committed Nov 1, 2023
1 parent a637a45 commit 3546104
Show file tree
Hide file tree
Showing 9 changed files with 622 additions and 323 deletions.
2 changes: 1 addition & 1 deletion src/controller/python/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ shared_library("ChipDeviceCtrl") {
"ChipDeviceController-StorageDelegate.cpp",
"ChipDeviceController-StorageDelegate.h",
"chip/clusters/CHIPClusters.cpp",
"chip/clusters/attribute.cpp",
"chip/clusters/command.cpp",
"chip/clusters/write.cpp",
"chip/discovery/NodeResolution.cpp",
"chip/interaction_model/Delegate.cpp",
"chip/interaction_model/Delegate.h",
Expand Down
71 changes: 44 additions & 27 deletions src/controller/python/chip/ChipDeviceCtrl.py
Original file line number Diff line number Diff line change
Expand Up @@ -378,37 +378,54 @@ def WriteAttribute(self, nodeid: int, attributes):
raise self._ChipStack.ErrorToException(res)
return future

def ReadAttribute(self, nodeid: int, attributes: typing.List[typing.Tuple[int, ClusterAttribute.AttributeReadRequest]]):
def ReadAttribute(self, nodeid: int, attributes: typing.List[typing.Union[
None, # Empty tuple, all wildcard
typing.Tuple[int], # Endpoint
# Wildcard endpoint, Cluster id present
typing.Tuple[typing.Type[ClusterObjects.Cluster]],
# Wildcard endpoint, Cluster + Attribute present
typing.Tuple[typing.Type[ClusterObjects.ClusterAttributeDescriptor]],
# Wildcard attribute id
typing.Tuple[int, typing.Type[ClusterObjects.Cluster]],
# Concrete path
typing.Tuple[int, typing.Type[ClusterObjects.ClusterAttributeDescriptor]]
]]):
eventLoop = asyncio.get_running_loop()
future = eventLoop.create_future()

device = self.GetConnectedDeviceSync(nodeid)
# TODO: Here, we translates multi attribute read into many individual attribute reads, this should be fixed by implementing Python's attribute read API.
res = []
for attr in attributes:
endpointId = attr[0]
attribute = attr[1]
clusterInfo = self._Cluster.GetClusterInfoById(
attribute.cluster_id)
if not clusterInfo:
raise UnknownCluster(attribute.cluster_id)
attributeInfo = clusterInfo.get("attributes", {}).get(
attribute.attribute_id, None)
if not attributeInfo:
raise UnknownAttribute(
clusterInfo["clusterName"], attribute.attribute_id)
self._Cluster.ReadAttribute(
device, clusterInfo["clusterName"], attributeInfo["attributeName"], endpointId, 0, False)
readRes = im.GetAttributeReadResponse(
im.DEFAULT_ATTRIBUTEREAD_APPID)
res.append(ClusterAttribute.AttributeReadResult(
Path=ClusterAttribute.AttributePath(
EndpointId=endpointId, ClusterId=attribute.cluster_id, AttributeId=attribute.attribute_id),
Status=readRes.status,
Data=(attribute.FromTagDictOrRawValue(
readRes.value) if readRes.value is not None else None),
))
future.set_result(res)
attrs = []
for v in attributes:
endpoint = None
cluster = None
attribute = None
if v == () or v == ('*'):
# Wildcard
pass
elif len(v) == 1:
if v[0] is int:
endpoint = v[0]
elif issubclass(v[0], ClusterObjects.Cluster):
cluster = v[0]
elif issubclass(v[0], ClusterObjects.ClusterAttributeDescriptor):
attribute = v[0]
else:
raise ValueError("Unsupported Attribute Path")
elif len(v) == 2:
# endpoint + (cluster) attribute / endpoint + cluster
endpoint = v[0]
if issubclass(v[1], ClusterObjects.Cluster):
cluster = v[1]
elif issubclass(v[1], ClusterAttribute.ClusterAttributeDescriptor):
attribute = v[1]
else:
raise ValueError("Unsupported Attribute Path")
attrs.append(ClusterAttribute.AttributePath(
EndpointId=endpoint, Cluster=cluster, Attribute=attribute))
res = self._ChipStack.Call(
lambda: ClusterAttribute.ReadAttributes(future, eventLoop, device, attrs))
if res != 0:
raise self._ChipStack.ErrorToException(res)
return future

def ZCLSend(self, cluster, command, nodeid, endpoint, groupid, args, blocking=False):
Expand Down
160 changes: 156 additions & 4 deletions src/controller/python/chip/clusters/Attribute.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,40 @@
from .ClusterObjects import ClusterAttributeDescriptor
import chip.exceptions
import chip.interaction_model
import chip.tlv

import inspect
import sys
import logging


@dataclass
class AttributePath:
EndpointId: int
ClusterId: int
AttributeId: int
EndpointId: int = None
ClusterId: int = None
AttributeId: int = None

def __init__(self, EndpointId: int = None, Cluster=None, Attribute=None, ClusterId=None, AttributeId=None):
self.EndpointId = EndpointId
if Cluster is not None:
# Wildcard read for a specific cluster
if (Attribute is not None) or (ClusterId is not None) or (AttributeId is not None):
raise Warning(
"Attribute, ClusterId and AttributeId is ignored when Cluster is specified")
self.ClusterId = Cluster.id
return
if Attribute is not None:
if (ClusterId is not None) or (AttributeId is not None):
raise Warning(
"ClusterId and AttributeId is ignored when Attribute is specified")
self.ClusterId = Attribute.cluster_id
self.AttributeId = Attribute.attribute_id
return
self.ClusterId = ClusterId
self.AttributeId = AttributeId

def __str__(self) -> str:
return f"{self.EndpointId}/{self.ClusterId}/{self.AttributeId}"


@dataclass
Expand Down Expand Up @@ -61,6 +88,72 @@ class AttributeReadResult(AttributeStatus):
Data: Any = None


_AttributeIndex = {}


def _BuildAttributeIndex():
''' Build internal attribute index for locating the corresponding cluster object by path in the future.
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.
This is acceptable during init, but unacceptable when the server returns lots of attributes at the same time.
'''
for clusterName, obj in inspect.getmembers(sys.modules['chip.clusters.Objects']):
if ('chip.clusters.Objects' in str(obj)) and inspect.isclass(obj):
for objName, subclass in inspect.getmembers(obj):
if inspect.isclass(subclass) and (('Attribute') in str(subclass)):
for attributeName, attribute in inspect.getmembers(subclass):
if inspect.isclass(attribute):
for name, field in inspect.getmembers(attribute):
if ('__dataclass_fields__' in name):
_AttributeIndex[str(AttributePath(ClusterId=field['cluster_id'].default, AttributeId=field['attribute_id'].default))] = eval(
'chip.clusters.Objects.' + clusterName + '.Attributes.' + attributeName)


class AsyncReadTransaction:
def __init__(self, future: Future, eventLoop):
self._event_loop = eventLoop
self._future = future
self._res = []

def _handleAttributeData(self, path: AttributePath, status: int, data: bytes):
try:
imStatus = status
try:
imStatus = chip.interaction_model.Status(status)
except:
pass
attributeType = _AttributeIndex.get(str(AttributePath(
ClusterId=path.ClusterId, AttributeId=path.AttributeId)), None)
attributeValue = None
if attributeType is None:
attributeValue = chip.tlv.TLVReader(data).get().get("Any", {})
else:
attributeValue = attributeType.FromTLV(data)
self._res.append(AttributeReadResult(
Path=path, Status=imStatus, Data=attributeValue))
except Exception as ex:
logging.exception(ex)

def handleAttributeData(self, path: AttributePath, status: int, data: bytes):
self._event_loop.call_soon_threadsafe(
self._handleAttributeData, path, status, data)

def _handleError(self, chipError: int):
self._future.set_exception(
chip.exceptions.ChipStackError(chipError))

def handleError(self, chipError: int):
self._event_loop.call_soon_threadsafe(
self._handleError, chipError
)

def _handleDone(self, asd):
if not self._future.done():
self._future.set_result(self._res)

def handleDone(self):
self._event_loop.call_soon_threadsafe(self._handleDone, "asdasa")


class AsyncWriteTransaction:
def __init__(self, future: Future, eventLoop):
self._event_loop = eventLoop
Expand Down Expand Up @@ -95,6 +188,32 @@ def handleDone(self):
self._event_loop.call_soon_threadsafe(self._handleDone)


_OnReadAttributeDataCallbackFunct = CFUNCTYPE(
None, py_object, c_uint16, c_uint32, c_uint32, c_uint16, c_char_p, c_size_t)
_OnReadErrorCallbackFunct = CFUNCTYPE(
None, py_object, c_uint32)
_OnReadDoneCallbackFunct = CFUNCTYPE(
None, py_object)


@_OnReadAttributeDataCallbackFunct
def _OnReadAttributeDataCallback(closure, endpoint: int, cluster: int, attribute: int, status, data, len):
dataBytes = ctypes.string_at(data, len)
closure.handleAttributeData(AttributePath(
EndpointId=endpoint, ClusterId=cluster, AttributeId=attribute), status, dataBytes[:])


@_OnReadErrorCallbackFunct
def _OnReadErrorCallback(closure, chiperror: int):
closure.handleError(chiperror)


@_OnReadDoneCallbackFunct
def _OnReadDoneCallback(closure):
closure.handleDone()
ctypes.pythonapi.Py_DecRef(ctypes.py_object(closure))


_OnWriteResponseCallbackFunct = CFUNCTYPE(
None, py_object, c_uint16, c_uint32, c_uint32, c_uint16)
_OnWriteErrorCallbackFunct = CFUNCTYPE(
Expand All @@ -105,7 +224,8 @@ def handleDone(self):

@_OnWriteResponseCallbackFunct
def _OnWriteResponseCallback(closure, endpoint: int, cluster: int, attribute: int, status):
closure.handleResponse(AttributePath(endpoint, cluster, attribute), status)
closure.handleResponse(AttributePath(
EndpointId=endpoint, ClusterId=cluster, AttributeId=attribute), status)


@_OnWriteErrorCallbackFunct
Expand Down Expand Up @@ -144,6 +264,31 @@ def WriteAttributes(future: Future, eventLoop, device, attributes: List[Attribut
return res


def ReadAttributes(future: Future, eventLoop, device, attributes: List[AttributePath]) -> int:
handle = chip.native.GetLibraryHandle()
transaction = AsyncReadTransaction(future, eventLoop)

readargs = []
for attr in attributes:
path = chip.interaction_model.AttributePathIBstruct.parse(
b'\xff' * chip.interaction_model.AttributePathIBstruct.sizeof())
if attr.EndpointId is not None:
path.EndpointId = attr.EndpointId
if attr.ClusterId is not None:
path.ClusterId = attr.ClusterId
if attr.AttributeId is not None:
path.AttributeId = attr.AttributeId
path = chip.interaction_model.AttributePathIBstruct.build(path)
readargs.append(ctypes.c_char_p(path))

ctypes.pythonapi.Py_IncRef(ctypes.py_object(transaction))
res = handle.pychip_ReadClient_ReadAttributes(
ctypes.py_object(transaction), device, ctypes.c_size_t(len(attributes)), *readargs)
if res != 0:
ctypes.pythonapi.Py_DecRef(ctypes.py_object(transaction))
return res


def Init():
handle = chip.native.GetLibraryHandle()

Expand All @@ -155,6 +300,13 @@ def Init():
handle.pychip_WriteClient_WriteAttributes.restype = c_uint32
setter.Set('pychip_WriteClient_InitCallbacks', None, [
_OnWriteResponseCallbackFunct, _OnWriteErrorCallbackFunct, _OnWriteDoneCallbackFunct])
handle.pychip_ReadClient_ReadAttributes.restype = c_uint32
setter.Set('pychip_ReadClient_InitCallbacks', None, [
_OnReadAttributeDataCallbackFunct, _OnReadErrorCallbackFunct, _OnReadDoneCallbackFunct])

handle.pychip_WriteClient_InitCallbacks(
_OnWriteResponseCallback, _OnWriteErrorCallback, _OnWriteDoneCallback)
handle.pychip_ReadClient_InitCallbacks(
_OnReadAttributeDataCallback, _OnReadErrorCallback, _OnReadDoneCallback)

_BuildAttributeIndex()
7 changes: 7 additions & 0 deletions src/controller/python/chip/clusters/ClusterObjects.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,13 @@ def command_id(self) -> int:
raise NotImplementedError()


class Cluster:
''' This class does nothing, but a convenient class that generated clusters can inherit from.
This gives the ability that the users can use issubclass(X, Cluster) to determine if the class represnents a Cluster.
'''
pass


class ClusterAttributeDescriptor:
'''
The ClusterAttributeDescriptor is used for holding an attribute's metadata like its cluster id, attribute id and its type.
Expand Down
Loading

0 comments on commit 3546104

Please sign in to comment.