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

[Python] Update AttributeRead command for wildcard read #11575

Merged
merged 1 commit into from
Nov 12, 2021
Merged
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
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']):
erjiaqing marked this conversation as resolved.
Show resolved Hide resolved
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