diff --git a/src/controller/python/chip/ChipDeviceCtrl.py b/src/controller/python/chip/ChipDeviceCtrl.py index 6d01af635c9c00..d0c7128bc1cb62 100644 --- a/src/controller/python/chip/ChipDeviceCtrl.py +++ b/src/controller/python/chip/ChipDeviceCtrl.py @@ -556,7 +556,7 @@ async def WriteAttribute(self, nodeid: int, attributes: typing.List[typing.Tuple raise self._ChipStack.ErrorToException(res) return await future - async def ReadAttribute(self, nodeid: int, attributes: typing.List[typing.Union[ + def _parseAttributePathTuple(self, pathTuple: typing.Union[ None, # Empty tuple, all wildcard typing.Tuple[int], # Endpoint # Wildcard endpoint, Cluster id present @@ -567,9 +567,125 @@ async def ReadAttribute(self, nodeid: int, attributes: typing.List[typing.Union[ typing.Tuple[int, typing.Type[ClusterObjects.Cluster]], # Concrete path typing.Tuple[int, typing.Type[ClusterObjects.ClusterAttributeDescriptor]] - ]], dataVersionFilters: typing.List[typing.Tuple[int, typing.Type[ClusterObjects.Cluster], int]] = None, returnClusterObject: bool = False, reportInterval: typing.Tuple[int, int] = None, fabricFiltered: bool = True): + ]): + endpoint = None + cluster = None + attribute = None + + if pathTuple == ('*') or pathTuple == (): + # Wildcard + pass + elif type(pathTuple) is not tuple: + if type(pathTuple) is int: + endpoint = pathTuple + elif issubclass(pathTuple, ClusterObjects.Cluster): + cluster = pathTuple + elif issubclass(pathTuple, ClusterObjects.ClusterAttributeDescriptor): + attribute = pathTuple + else: + raise ValueError("Unsupported Attribute Path") + else: + # endpoint + (cluster) attribute / endpoint + cluster + endpoint = pathTuple[0] + if issubclass(pathTuple[1], ClusterObjects.Cluster): + cluster = pathTuple[1] + elif issubclass(pathTuple[1], ClusterAttribute.ClusterAttributeDescriptor): + attribute = pathTuple[1] + else: + raise ValueError("Unsupported Attribute Path") + return ClusterAttribute.AttributePath( + EndpointId=endpoint, Cluster=cluster, Attribute=attribute) + + def _parseDataVersionFilterTuple(self, pathTuple: typing.List[typing.Tuple[int, typing.Type[ClusterObjects.Cluster], int]]): + endpoint = None + cluster = None + + # endpoint + (cluster) attribute / endpoint + cluster + endpoint = pathTuple[0] + if issubclass(pathTuple[1], ClusterObjects.Cluster): + cluster = pathTuple[1] + else: + raise ValueError("Unsupported Cluster Path") + dataVersion = pathTuple[2] + return ClusterAttribute.DataVersionFilter( + EndpointId=endpoint, Cluster=cluster, DataVersion=dataVersion) + + def _parseEventPathTuple(self, pathTuple: typing.Union[ + None, # Empty tuple, all wildcard + typing.Tuple[str, int], # all wildcard with urgency set + typing.Tuple[int, int], # Endpoint, + # Wildcard endpoint, Cluster id present + typing.Tuple[typing.Type[ClusterObjects.Cluster], int], + # Wildcard endpoint, Cluster + Event present + typing.Tuple[typing.Type[ClusterObjects.ClusterEvent], int], + # Wildcard event id + typing.Tuple[int, typing.Type[ClusterObjects.Cluster], int], + # Concrete path + typing.Tuple[int, + typing.Type[ClusterObjects.ClusterEvent], int] + ]): + endpoint = None + cluster = None + event = None + urgent = False + if pathTuple in [('*'), ()]: + # Wildcard + pass + elif type(pathTuple) is not tuple: + print(type(pathTuple)) + if type(pathTuple) is int: + endpoint = pathTuple + elif issubclass(pathTuple, ClusterObjects.Cluster): + cluster = pathTuple + elif issubclass(pathTuple, ClusterObjects.ClusterEvent): + event = pathTuple + else: + raise ValueError("Unsupported Event Path") + else: + if pathTuple[0] == '*': + urgent = pathTuple[-1] + pass + else: + # endpoint + (cluster) event / endpoint + cluster + endpoint = pathTuple[0] + if issubclass(pathTuple[1], ClusterObjects.Cluster): + cluster = pathTuple[1] + elif issubclass(pathTuple[1], ClusterAttribute.ClusterEvent): + event = pathTuple[1] + else: + raise ValueError("Unsupported Attribute Path") + urgent = pathTuple[-1] + return ClusterAttribute.EventPath( + EndpointId=endpoint, Cluster=cluster, Event=event, Urgent=urgent) + + async def Read(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]] + ]] = None, dataVersionFilters: typing.List[typing.Tuple[int, typing.Type[ClusterObjects.Cluster], int]] = None, events: typing.List[typing.Union[ + None, # Empty tuple, all wildcard + typing.Tuple[str, int], # all wildcard with urgency set + typing.Tuple[int, int], # Endpoint, + # Wildcard endpoint, Cluster id present + typing.Tuple[typing.Type[ClusterObjects.Cluster], int], + # Wildcard endpoint, Cluster + Event present + typing.Tuple[typing.Type[ClusterObjects.ClusterEvent], int], + # Wildcard event id + typing.Tuple[int, typing.Type[ClusterObjects.Cluster], int], + # Concrete path + typing.Tuple[int, + typing.Type[ClusterObjects.ClusterEvent], int] + ]] = None, + returnClusterObject: bool = False, reportInterval: typing.Tuple[int, int] = None, fabricFiltered: bool = True): ''' - Read a list of attributes from a target node + Read a list of attributes and/or events from a target node nodeId: Target's Node ID attributes: A list of tuples of varying types depending on the type of read being requested: @@ -587,6 +703,16 @@ async def ReadAttribute(self, nodeid: int, attributes: typing.List[typing.Union[ ReadAttribute(1, [ Clusters.Basic ] ) -- case 5 above. ReadAttribute(1, [ (1, Clusters.Basic.Attributes.Location ] ) -- case 1 above. + dataVersionFilters: A list of tuples of (endpoint, cluster, data version). + + events: A list of tuples of varying types depending on the type of read being requested: + (endpoint, Clusters.ClusterA.EventA, urgent): Endpoint = specific, Cluster = specific, Event = specific, Urgent = True/False + (endpoint, Clusters.ClusterA, urgent): Endpoint = specific, Cluster = specific, Event = *, Urgent = True/False + (Clusters.ClusterA.EventA, urgent): Endpoint = *, Cluster = specific, Event = specific, Urgent = True/False + endpoint: Endpoint = specific, Cluster = *, Event = *, Urgent = True/False + Clusters.ClusterA: Endpoint = *, Cluster = specific, Event = *, Urgent = True/False + '*' or (): Endpoint = *, Cluster = *, Event = *, Urgent = True/False + returnClusterObject: This returns the data as consolidated cluster objects, with all attributes for a cluster inside a single cluster-wide cluster object. @@ -599,57 +725,62 @@ async def ReadAttribute(self, nodeid: int, attributes: typing.List[typing.Union[ future = eventLoop.create_future() device = self.GetConnectedDeviceSync(nodeid) - attrs = [] - filters = [] - for v in attributes: - endpoint = None - cluster = None - attribute = None - if v == ('*') or v == (): - # Wildcard - pass - elif type(v) is not tuple: - if type(v) is int: - endpoint = v - elif issubclass(v, ClusterObjects.Cluster): - cluster = v - elif issubclass(v, ClusterObjects.ClusterAttributeDescriptor): - attribute = v - else: - raise ValueError("Unsupported Attribute Path") - else: - # 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)) - if dataVersionFilters != None: - for v in dataVersionFilters: - endpoint = None - cluster = None - - # endpoint + (cluster) attribute / endpoint + cluster - endpoint = v[0] - if issubclass(v[1], ClusterObjects.Cluster): - cluster = v[1] - else: - raise ValueError("Unsupported Cluster Path") - dataVersion = v[2] - filters.append(ClusterAttribute.DataVersionFilter( - EndpointId=endpoint, Cluster=cluster, DataVersion=dataVersion)) - else: - filters = None + attributePaths = [self._parseAttributePathTuple( + v) for v in attributes] if attributes else None + clusterDataVersionFilters = [self._parseDataVersionFilterTuple( + v) for v in dataVersionFilters] if dataVersionFilters else None + eventPaths = [self._parseEventPathTuple( + v) for v in events] if events else None + res = self._ChipStack.Call( - lambda: ClusterAttribute.ReadAttributes(future, eventLoop, device, self, attrs, filters, returnClusterObject, ClusterAttribute.SubscriptionParameters(reportInterval[0], reportInterval[1]) if reportInterval else None, fabricFiltered=fabricFiltered)) + lambda: ClusterAttribute.Read(future=future, eventLoop=eventLoop, device=device, devCtrl=self, attributes=attributePaths, dataVersionFilters=clusterDataVersionFilters, events=eventPaths, returnClusterObject=returnClusterObject, subscriptionParameters=ClusterAttribute.SubscriptionParameters(reportInterval[0], reportInterval[1]) if reportInterval else None, fabricFiltered=fabricFiltered)) if res != 0: raise self._ChipStack.ErrorToException(res) return await future + async 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]] + ]], dataVersionFilters: typing.List[typing.Tuple[int, typing.Type[ClusterObjects.Cluster], int]] = None, returnClusterObject: bool = False, reportInterval: typing.Tuple[int, int] = None, fabricFiltered: bool = True): + ''' + Read a list of attributes from a target node, this is a wrapper of DeviceController.Read() + + nodeId: Target's Node ID + attributes: A list of tuples of varying types depending on the type of read being requested: + (endpoint, Clusters.ClusterA.AttributeA): Endpoint = specific, Cluster = specific, Attribute = specific + (endpoint, Clusters.ClusterA): Endpoint = specific, Cluster = specific, Attribute = * + (Clusters.ClusterA.AttributeA): Endpoint = *, Cluster = specific, Attribute = specific + endpoint: Endpoint = specific, Cluster = *, Attribute = * + Clusters.ClusterA: Endpoint = *, Cluster = specific, Attribute = * + '*' or (): Endpoint = *, Cluster = *, Attribute = * + + The cluster and attributes specified above are to be selected from the generated cluster objects. + + e.g. + ReadAttribute(1, [ 1 ] ) -- case 4 above. + ReadAttribute(1, [ Clusters.Basic ] ) -- case 5 above. + ReadAttribute(1, [ (1, Clusters.Basic.Attributes.Location ] ) -- case 1 above. + + returnClusterObject: This returns the data as consolidated cluster objects, with all attributes for a cluster inside + a single cluster-wide cluster object. + + reportInterval: A tuple of two int-s for (MinIntervalFloor, MaxIntervalCeiling). Used by establishing subscriptions. + When not provided, a read request will be sent. + ''' + res = await self.Read(nodeid, attributes=attributes, dataVersionFilters=dataVersionFilters, returnClusterObject=returnClusterObject, reportInterval=reportInterval, fabricFiltered=fabricFiltered) + if isinstance(res, ClusterAttribute.SubscriptionTransaction): + return res + else: + return res.attributes + async def ReadEvent(self, nodeid: int, events: typing.List[typing.Union[ None, # Empty tuple, all wildcard typing.Tuple[str, int], # all wildcard with urgency set @@ -664,7 +795,7 @@ async def ReadEvent(self, nodeid: int, events: typing.List[typing.Union[ typing.Tuple[int, typing.Type[ClusterObjects.ClusterEvent], int] ]], reportInterval: typing.Tuple[int, int] = None): ''' - Read a list of events from a target node + Read a list of events from a target node, this is a wrapper of DeviceController.Read() nodeId: Target's Node ID events: A list of tuples of varying types depending on the type of read being requested: @@ -685,53 +816,11 @@ async def ReadEvent(self, nodeid: int, events: typing.List[typing.Union[ reportInterval: A tuple of two int-s for (MinIntervalFloor, MaxIntervalCeiling). Used by establishing subscriptions. When not provided, a read request will be sent. ''' - self.CheckIsActive() - - eventLoop = asyncio.get_running_loop() - future = eventLoop.create_future() - - device = self.GetConnectedDeviceSync(nodeid) - eves = [] - for v in events: - endpoint = None - cluster = None - event = None - urgent = False - if v in [('*'), ()]: - # Wildcard - pass - elif type(v) is not tuple: - print(type(v)) - if type(v) is int: - endpoint = v - elif issubclass(v, ClusterObjects.Cluster): - cluster = v - elif issubclass(v, ClusterObjects.ClusterEvent): - event = v - else: - raise ValueError("Unsupported Event Path") - else: - if v[0] == '*': - urgent = v[-1] - pass - else: - # endpoint + (cluster) event / endpoint + cluster - endpoint = v[0] - if issubclass(v[1], ClusterObjects.Cluster): - cluster = v[1] - elif issubclass(v[1], ClusterAttribute.ClusterEvent): - event = v[1] - else: - raise ValueError("Unsupported Attribute Path") - urgent = v[-1] - eves.append(ClusterAttribute.EventPath( - EndpointId=endpoint, Cluster=cluster, Event=event, Urgent=urgent)) - res = self._ChipStack.Call( - lambda: ClusterAttribute.ReadEvents(future, eventLoop, device, self, eves, ClusterAttribute.SubscriptionParameters(reportInterval[0], reportInterval[1]) if reportInterval else None)) - if res != 0: - raise self._ChipStack.ErrorToException(res) - outcome = await future - return await future + res = await self.Read(nodeid=nodeid, events=events, reportInterval=reportInterval) + if isinstance(res, ClusterAttribute.SubscriptionTransaction): + return res + else: + return res.events def ZCLSend(self, cluster, command, nodeid, endpoint, groupid, args, blocking=False): self.CheckIsActive() diff --git a/src/controller/python/chip/clusters/Attribute.py b/src/controller/python/chip/clusters/Attribute.py index e2bbb695e14d37..0c2b8fc2b1f7cf 100644 --- a/src/controller/python/chip/clusters/Attribute.py +++ b/src/controller/python/chip/clusters/Attribute.py @@ -17,6 +17,7 @@ # Needed to use types in type hints before they are fully defined. from __future__ import annotations +from asyncio import events from asyncio.futures import Future import ctypes @@ -97,8 +98,8 @@ def __init__(self, EndpointId: int = None, Cluster=None, ClusterId=None, DataVer raise Warning( "Attribute, ClusterId and AttributeId is ignored when Cluster is specified") self.ClusterId = Cluster.id - return - self.ClusterId = ClusterId + else: + self.ClusterId = ClusterId self.DataVersion = DataVersion def __str__(self) -> str: @@ -566,19 +567,18 @@ def _BuildEventIndex(): 'chip.clusters.Objects.' + clusterName + '.Events.' + eventName) -class TransactionType(Enum): - READ_EVENTS = 1 - READ_ATTRIBUTES = 2 - - class AsyncReadTransaction: - def __init__(self, future: Future, eventLoop, devCtrl, transactionType: TransactionType, returnClusterObject: bool): + @dataclass + class ReadResponse: + attributes: AttributeCache = None + events: List[ClusterEvent] = None + + def __init__(self, future: Future, eventLoop, devCtrl, returnClusterObject: bool): self._event_loop = eventLoop self._future = future self._subscription_handler = None self._events = [] self._devCtrl = devCtrl - self._transactionType = transactionType self._cache = AttributeCache(returnClusterObject=returnClusterObject) self._changedPathSet = set() self._pReadClient = None @@ -692,10 +692,8 @@ def _handleReportEnd(self): def _handleDone(self): if not self._future.done(): - if (self._transactionType == TransactionType.READ_EVENTS): - self._future.set_result(self._events) - else: - self._future.set_result(self._cache.attributeCache) + self._future.set_result(AsyncReadTransaction.ReadResponse( + attributes=self._cache.attributeCache, events=self._events)) def handleDone(self): self._event_loop.call_soon_threadsafe(self._handleDone) @@ -862,27 +860,34 @@ def WriteAttributes(future: Future, eventLoop, device, attributes: List[Attribut ) -def ReadAttributes(future: Future, eventLoop, device, devCtrl, attributes: List[AttributePath], dataVersionFilters: List[DataVersionFilter] = None, returnClusterObject: bool = True, subscriptionParameters: SubscriptionParameters = None, fabricFiltered: bool = True) -> int: +def Read(future: Future, eventLoop, device, devCtrl, attributes: List[AttributePath] = None, dataVersionFilters: List[DataVersionFilter] = None, events: List[EventPath] = None, returnClusterObject: bool = True, subscriptionParameters: SubscriptionParameters = None, fabricFiltered: bool = True) -> int: + if (not attributes) and dataVersionFilters: + raise ValueError( + "Must provide valid attribute list when data version filters is not null") + if (not attributes) and (not events): + raise ValueError( + "Must read some something" + ) handle = chip.native.GetLibraryHandle() transaction = AsyncReadTransaction( - future, eventLoop, devCtrl, TransactionType.READ_ATTRIBUTES, returnClusterObject) + future, eventLoop, devCtrl, returnClusterObject) - dataVersionFilterLength = 0 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)) + + if attributes is not None: + 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)) if dataVersionFilters is not None: - dataVersionFilterLength = len(dataVersionFilters) for f in dataVersionFilters: filter = chip.interaction_model.DataVersionFilterIBstruct.parse( b'\xff' * chip.interaction_model.DataVersionFilterIBstruct.sizeof()) @@ -905,6 +910,23 @@ def ReadAttributes(future: Future, eventLoop, device, devCtrl, attributes: List[ filter) readargs.append(ctypes.c_char_p(filter)) + if events is not None: + for event in events: + path = chip.interaction_model.EventPathIBstruct.parse( + b'\xff' * chip.interaction_model.EventPathIBstruct.sizeof()) + if event.EndpointId is not None: + path.EndpointId = event.EndpointId + if event.ClusterId is not None: + path.ClusterId = event.ClusterId + if event.EventId is not None: + path.EventId = event.EventId + if event.Urgent is not None and subscriptionParameters is not None: + path.Urgent = event.Urgent + else: + path.Urgent = 0 + path = chip.interaction_model.EventPathIBstruct.build(path) + readargs.append(ctypes.c_char_p(path)) + ctypes.pythonapi.Py_IncRef(ctypes.py_object(transaction)) minInterval = 0 maxInterval = 0 @@ -921,14 +943,16 @@ def ReadAttributes(future: Future, eventLoop, device, devCtrl, attributes: List[ params.IsFabricFiltered = fabricFiltered params = _ReadParams.build(params) - res = handle.pychip_ReadClient_ReadAttributes( + res = handle.pychip_ReadClient_Read( ctypes.py_object(transaction), ctypes.byref(readClientObj), ctypes.byref(readCallbackObj), device, ctypes.c_char_p(params), - ctypes.c_size_t(len(attributes)), - ctypes.c_size_t(len(attributes) + dataVersionFilterLength), + ctypes.c_size_t(0 if attributes is None else len(attributes)), + ctypes.c_size_t( + 0 if dataVersionFilters is None else len(dataVersionFilters)), + ctypes.c_size_t(0 if events is None else len(events)), *readargs) transaction.SetClientObjPointers(readClientObj, readCallbackObj) @@ -938,54 +962,12 @@ def ReadAttributes(future: Future, eventLoop, device, devCtrl, attributes: List[ return res -def ReadEvents(future: Future, eventLoop, device, devCtrl, events: List[EventPath], subscriptionParameters: SubscriptionParameters = None) -> int: - handle = chip.native.GetLibraryHandle() - transaction = AsyncReadTransaction( - future, eventLoop, devCtrl, TransactionType.READ_EVENTS, False) - - readargs = [] - for event in events: - path = chip.interaction_model.EventPathIBstruct.parse( - b'\xff' * chip.interaction_model.EventPathIBstruct.sizeof()) - if event.EndpointId is not None: - path.EndpointId = event.EndpointId - if event.ClusterId is not None: - path.ClusterId = event.ClusterId - if event.EventId is not None: - path.EventId = event.EventId - if event.Urgent is not None and subscriptionParameters is not None: - path.Urgent = event.Urgent - else: - path.Urgent = 0 - path = chip.interaction_model.EventPathIBstruct.build(path) - readargs.append(ctypes.c_char_p(path)) - - readClientObj = ctypes.POINTER(c_void_p)() - readCallbackObj = ctypes.POINTER(c_void_p)() - - ctypes.pythonapi.Py_IncRef(ctypes.py_object(transaction)) - - params = _ReadParams.parse(b'\x00' * _ReadParams.sizeof()) - - if subscriptionParameters is not None: - params.MinInterval = subscriptionParameters.MinReportIntervalFloorSeconds - params.MaxInterval = subscriptionParameters.MaxReportIntervalCeilingSeconds - params.IsSubscription = True - params = _ReadParams.build(params) - - res = handle.pychip_ReadClient_ReadEvents( - ctypes.py_object(transaction), - ctypes.byref(readClientObj), - ctypes.byref(readCallbackObj), - device, - ctypes.c_char_p(params), - ctypes.c_size_t(len(events)), *readargs) +def ReadAttributes(future: Future, eventLoop, device, devCtrl, attributes: List[AttributePath], dataVersionFilters: List[DataVersionFilter] = None, returnClusterObject: bool = True, subscriptionParameters: SubscriptionParameters = None, fabricFiltered: bool = True) -> int: + return Read(future=future, eventLoop=eventLoop, device=device, devCtrl=devCtrl, attributes=attributes, dataVersionFilters=dataVersionFilters, events=None, returnClusterObject=returnClusterObject, subscriptionParameters=subscriptionParameters, fabricFiltered=fabricFiltered) - transaction.SetClientObjPointers(readClientObj, readCallbackObj) - if res != 0: - ctypes.pythonapi.Py_DecRef(ctypes.py_object(transaction)) - return res +def ReadEvents(future: Future, eventLoop, device, devCtrl, events: List[EventPath], returnClusterObject: bool = True, subscriptionParameters: SubscriptionParameters = None, fabricFiltered: bool = True) -> int: + return Read(future=future, eventLoop=eventLoop, device=device, devCtrl=devCtrl, attributes=None, dataVersionFilters=None, events=events, returnClusterObject=returnClusterObject, subscriptionParameters=subscriptionParameters, fabricFiltered=fabricFiltered) def Init(): @@ -999,7 +981,7 @@ 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 + handle.pychip_ReadClient_Read.restype = c_uint32 setter.Set('pychip_ReadClient_InitCallbacks', None, [ _OnReadAttributeDataCallbackFunct, _OnReadEventDataCallbackFunct, _OnSubscriptionEstablishedCallbackFunct, _OnReadErrorCallbackFunct, _OnReadDoneCallbackFunct, _OnReportBeginCallbackFunct, _OnReportEndCallbackFunct]) diff --git a/src/controller/python/chip/clusters/attribute.cpp b/src/controller/python/chip/clusters/attribute.cpp index 6e6f4f5d7681b6..d303275cf8e260 100644 --- a/src/controller/python/chip/clusters/attribute.cpp +++ b/src/controller/python/chip/clusters/attribute.cpp @@ -361,9 +361,9 @@ void pychip_ReadClient_Abort(ReadClient * apReadClient, ReadClientCallback * apC delete apCallback; } -chip::ChipError::StorageType pychip_ReadClient_ReadAttributes(void * appContext, ReadClient ** pReadClient, - ReadClientCallback ** pCallback, DeviceProxy * device, - uint8_t * readParamsBuf, size_t n, size_t total, ...) +chip::ChipError::StorageType pychip_ReadClient_Read(void * appContext, ReadClient ** pReadClient, ReadClientCallback ** pCallback, + DeviceProxy * device, uint8_t * readParamsBuf, size_t numAttributePaths, + size_t numDataversionFilters, size_t numEventPaths, ...) { CHIP_ERROR err = CHIP_NO_ERROR; PyReadAttributeParams pyParams = {}; @@ -372,34 +372,42 @@ chip::ChipError::StorageType pychip_ReadClient_ReadAttributes(void * appContext, std::unique_ptr callback = std::make_unique(appContext); - size_t m = total - n; va_list args; - va_start(args, total); + va_start(args, numEventPaths); - std::unique_ptr readPaths(new AttributePathParams[n]); - std::unique_ptr dataVersionFilters(new chip::app::DataVersionFilter[m]); + std::unique_ptr attributePaths(new AttributePathParams[numAttributePaths]); + std::unique_ptr dataVersionFilters(new chip::app::DataVersionFilter[numDataversionFilters]); + std::unique_ptr eventPaths(new EventPathParams[numEventPaths]); std::unique_ptr readClient; + for (size_t i = 0; i < numAttributePaths; i++) { - for (size_t i = 0; i < n; i++) - { - void * path = va_arg(args, void *); + void * path = va_arg(args, void *); - python::AttributePath pathObj; - memcpy(&pathObj, path, sizeof(python::AttributePath)); + python::AttributePath pathObj; + memcpy(&pathObj, path, sizeof(python::AttributePath)); - readPaths[i] = AttributePathParams(pathObj.endpointId, pathObj.clusterId, pathObj.attributeId); - } + attributePaths[i] = AttributePathParams(pathObj.endpointId, pathObj.clusterId, pathObj.attributeId); } - for (size_t j = 0; j < m; j++) + for (size_t i = 0; i < numDataversionFilters; i++) { void * filter = va_arg(args, void *); python::DataVersionFilter filterObj; memcpy(&filterObj, filter, sizeof(python::DataVersionFilter)); - dataVersionFilters[j] = chip::app::DataVersionFilter(filterObj.endpointId, filterObj.clusterId, filterObj.dataVersion); + dataVersionFilters[i] = chip::app::DataVersionFilter(filterObj.endpointId, filterObj.clusterId, filterObj.dataVersion); + } + + for (size_t i = 0; i < numEventPaths; i++) + { + void * path = va_arg(args, void *); + + python::EventPath pathObj; + memcpy(&pathObj, path, sizeof(python::EventPath)); + + eventPaths[i] = EventPathParams(pathObj.endpointId, pathObj.clusterId, pathObj.eventId, pathObj.urgentEvent == 1); } Optional session = device->GetSecureSession(); @@ -411,89 +419,30 @@ chip::ChipError::StorageType pychip_ReadClient_ReadAttributes(void * appContext, VerifyOrExit(readClient != nullptr, err = CHIP_ERROR_NO_MEMORY); { ReadPrepareParams params(session.Value()); - params.mpAttributePathParamsList = readPaths.get(); - params.mAttributePathParamsListSize = n; - if (m != 0) - { - params.mpDataVersionFilterList = dataVersionFilters.get(); - params.mDataVersionFilterListSize = m; - } - - params.mIsFabricFiltered = pyParams.isFabricFiltered; - - if (pyParams.isSubscription) + if (numAttributePaths != 0) { - params.mMinIntervalFloorSeconds = pyParams.minInterval; - params.mMaxIntervalCeilingSeconds = pyParams.maxInterval; - readPaths.release(); - err = readClient->SendAutoResubscribeRequest(std::move(params)); - SuccessOrExit(err); + params.mpAttributePathParamsList = attributePaths.get(); + params.mAttributePathParamsListSize = numAttributePaths; } - else + if (numDataversionFilters != 0) { - err = readClient->SendRequest(params); - SuccessOrExit(err); + params.mpDataVersionFilterList = dataVersionFilters.get(); + params.mDataVersionFilterListSize = numDataversionFilters; } - } - - *pReadClient = readClient.get(); - *pCallback = callback.get(); - - callback->AdoptReadClient(std::move(readClient)); - - callback.release(); - -exit: - va_end(args); - return err.AsInteger(); -} - -chip::ChipError::StorageType pychip_ReadClient_ReadEvents(void * appContext, ReadClient ** pReadClient, - ReadClientCallback ** pCallback, DeviceProxy * device, - - uint8_t * readParamsBuf, size_t n, ...) -{ - CHIP_ERROR err = CHIP_NO_ERROR; - PyReadAttributeParams pyParams = {}; - memcpy(&pyParams, readParamsBuf, sizeof(pyParams)); - - std::unique_ptr callback = std::make_unique(appContext); - - va_list args; - va_start(args, n); - - std::unique_ptr readPaths(new EventPathParams[n]); - std::unique_ptr readClient; - - { - for (size_t i = 0; i < n; i++) + if (numEventPaths != 0) { - void * path = va_arg(args, void *); - - python::EventPath pathObj; - memcpy(&pathObj, path, sizeof(python::EventPath)); - - readPaths[i] = EventPathParams(pathObj.endpointId, pathObj.clusterId, pathObj.eventId, pathObj.urgentEvent == 1); + params.mpEventPathParamsList = eventPaths.get(); + params.mEventPathParamsListSize = numEventPaths; } - } - - Optional session = device->GetSecureSession(); - VerifyOrExit(session.HasValue(), err = CHIP_ERROR_NOT_CONNECTED); - readClient = std::make_unique(InteractionModelEngine::GetInstance(), device->GetExchangeManager(), *callback.get(), - pyParams.isSubscription ? ReadClient::InteractionType::Subscribe - : ReadClient::InteractionType::Read); - - { - ReadPrepareParams params(session.Value()); - params.mpEventPathParamsList = readPaths.get(); - params.mEventPathParamsListSize = n; + params.mIsFabricFiltered = pyParams.isFabricFiltered; if (pyParams.isSubscription) { params.mMinIntervalFloorSeconds = pyParams.minInterval; params.mMaxIntervalCeilingSeconds = pyParams.maxInterval; - readPaths.release(); + attributePaths.release(); + eventPaths.release(); err = readClient->SendAutoResubscribeRequest(std::move(params)); SuccessOrExit(err); } diff --git a/src/controller/python/test/test_scripts/cluster_objects.py b/src/controller/python/test/test_scripts/cluster_objects.py index 8548a2e2bf0247..ede848ac05788a 100644 --- a/src/controller/python/test/test_scripts/cluster_objects.py +++ b/src/controller/python/test/test_scripts/cluster_objects.py @@ -272,24 +272,29 @@ async def TestReadAttributeRequests(cls, devCtrl): # raise AssertionError( # "Expect the fabric index matches the one current reading") - async def TriggerAndWaitForEvents(cls, devCtrl, req): + @classmethod + async def _TriggerEvent(cls, devCtrl): # We trigger sending an event a couple of times just to be safe. - res = await devCtrl.SendCommand(nodeid=NODE_ID, endpoint=1, payload=Clusters.TestCluster.Commands.TestEmitTestEventRequest()) - res = await devCtrl.SendCommand(nodeid=NODE_ID, endpoint=1, payload=Clusters.TestCluster.Commands.TestEmitTestEventRequest()) - res = await devCtrl.SendCommand(nodeid=NODE_ID, endpoint=1, payload=Clusters.TestCluster.Commands.TestEmitTestEventRequest()) - res = await devCtrl.SendCommand(nodeid=NODE_ID, endpoint=1, payload=Clusters.TestCluster.Commands.TestEmitTestFabricScopedEventRequest(arg1=0)) - res = await devCtrl.SendCommand(nodeid=NODE_ID, endpoint=1, payload=Clusters.TestCluster.Commands.TestEmitTestFabricScopedEventRequest(arg1=1)) - # Events may take some time to flush, so wait for about 10s or so to get some events. - for i in range(0, 10): - print("Reading out events..") - res = await devCtrl.ReadEvent(nodeid=NODE_ID, events=req) - if (len(res) != 0): - break - - time.sleep(1) - - if (len(res) == 0): - raise AssertionError("Got no events back") + await devCtrl.SendCommand(nodeid=NODE_ID, endpoint=1, payload=Clusters.TestCluster.Commands.TestEmitTestEventRequest()) + await devCtrl.SendCommand(nodeid=NODE_ID, endpoint=1, payload=Clusters.TestCluster.Commands.TestEmitTestEventRequest()) + await devCtrl.SendCommand(nodeid=NODE_ID, endpoint=1, payload=Clusters.TestCluster.Commands.TestEmitTestEventRequest()) + await devCtrl.SendCommand(nodeid=NODE_ID, endpoint=1, payload=Clusters.TestCluster.Commands.TestEmitTestFabricScopedEventRequest(arg1=0)) + await devCtrl.SendCommand(nodeid=NODE_ID, endpoint=1, payload=Clusters.TestCluster.Commands.TestEmitTestFabricScopedEventRequest(arg1=1)) + + @classmethod + async def _RetryForContent(cls, request, until, retryCount=10, intervalSeconds=1): + for i in range(retryCount): + logger.info(f"Attempt {i + 1}/{retryCount}") + res = await request() + if until(res): + return res + asyncio.sleep(1) + raise AssertionError("condition is not met") + + @classmethod + async def TriggerAndWaitForEvents(cls, devCtrl, req): + await cls._TriggerEvent(devCtrl) + await cls._RetryForContent(request=lambda: devCtrl.ReadEvent(nodeid=NODE_ID, events=req), until=lambda res: res != 0) @classmethod @base.test_case @@ -299,35 +304,35 @@ async def TestReadEventRequests(cls, devCtrl, expectEventsNum): (1, Clusters.TestCluster.Events.TestEvent, 0), ] - await cls.TriggerAndWaitForEvents(cls, devCtrl, req) + await cls.TriggerAndWaitForEvents(devCtrl, req) logger.info("2: Reading Ex Cx E*") req = [ (1, Clusters.TestCluster, 0), ] - await cls.TriggerAndWaitForEvents(cls, devCtrl, req) + await cls.TriggerAndWaitForEvents(devCtrl, req) logger.info("3: Reading Ex C* E*") req = [ 1 ] - await cls.TriggerAndWaitForEvents(cls, devCtrl, req) + await cls.TriggerAndWaitForEvents(devCtrl, req) logger.info("4: Reading E* C* E*") req = [ '*' ] - await cls.TriggerAndWaitForEvents(cls, devCtrl, req) + await cls.TriggerAndWaitForEvents(devCtrl, req) logger.info("5: Reading Ex Cx E* Urgency") req = [ (1, Clusters.TestCluster, 1), ] - await cls.TriggerAndWaitForEvents(cls, devCtrl, req) + await cls.TriggerAndWaitForEvents(devCtrl, req) # TODO: Add more wildcard test for IM events. @@ -402,6 +407,9 @@ async def TestReadWriteAttributeRequestsWithVersion(cls, devCtrl): VerifyDecodeSuccess(res) data_version = res[0][Clusters.Basic][DataVersion] + logger.info(res) + logger.info(data_version) + res = await devCtrl.WriteAttribute(nodeid=NODE_ID, attributes=[ (0, Clusters.Basic.Attributes.NodeLabel( @@ -467,6 +475,36 @@ async def TestReadWriteAttributeRequestsWithVersion(cls, devCtrl): f"Item {i} is not expected, expect {expectedRes[i]} got {res[i]}") raise AssertionError("Write returned unexpected result.") + @classmethod + @base.test_case + async def TestMixedReadAttributeAndEvents(cls, devCtrl): + def attributePathPossibilities(): + yield ('Ex Cx Ax', [ + (0, Clusters.Basic.Attributes.VendorName), + (0, Clusters.Basic.Attributes.ProductID), + (0, Clusters.Basic.Attributes.HardwareVersion), + ]) + yield ('Ex Cx A*', [(0, Clusters.Basic)]) + yield ('E* Cx A*', [Clusters.Descriptor.Attributes.ServerList]) + yield ('E* A* A*', ['*']) + + def eventPathPossibilities(): + yield ('Ex Cx Ex', [(1, Clusters.TestCluster.Events.TestEvent, 0)]) + yield ('Ex Cx E*', [(1, Clusters.TestCluster, 0)]) + yield ('Ex C* E*', [1]) + yield ('E* C* E*', ['*']) + yield ('Ex Cx E* Urgent', [(1, Clusters.TestCluster, 1)]) + + testCount = 0 + + for attributes in attributePathPossibilities(): + for events in eventPathPossibilities(): + logging.info( + f"{testCount}: Reading mixed Attributes({attributes[0]}) Events({events[0]})") + await cls._TriggerEvent(devCtrl) + res = await cls._RetryForContent(request=lambda: devCtrl.Read(nodeid=NODE_ID, attributes=attributes[1], events=events[1]), until=lambda res: res != 0) + VerifyDecodeSuccess(res.attributes) + @classmethod async def RunTest(cls, devCtrl): try: @@ -478,6 +516,7 @@ async def RunTest(cls, devCtrl): await cls.TestReadWriteAttributeRequestsWithVersion(devCtrl) await cls.TestReadAttributeRequests(devCtrl) await cls.TestSubscribeAttribute(devCtrl) + await cls.TestMixedReadAttributeAndEvents(devCtrl) # Note: Write will change some attribute values, always put it after read tests await cls.TestWriteRequest(devCtrl) await cls.TestTimedRequest(devCtrl) diff --git a/src/test_driver/linux-cirque/MobileDeviceTest.py b/src/test_driver/linux-cirque/MobileDeviceTest.py index 0c988b0398971c..1b47f255ec452e 100755 --- a/src/test_driver/linux-cirque/MobileDeviceTest.py +++ b/src/test_driver/linux-cirque/MobileDeviceTest.py @@ -93,7 +93,7 @@ def run_controller_test(self): self.execute_device_cmd(req_device_id, "pip3 install {}".format(os.path.join( CHIP_REPO, "out/debug/linux_x64_gcc/controller/python/chip-0.0-cp37-abi3-linux_x86_64.whl"))) - command = "gdb -return-child-result -q -ex run -ex bt --args python3 {} -t 150 -a {} --paa-trust-store-path {}".format( + command = "gdb -return-child-result -q -ex run -ex bt --args python3 {} -t 240 -a {} --paa-trust-store-path {}".format( os.path.join( CHIP_REPO, "src/controller/python/test/test_scripts/mobile-device-test.py"), ethernet_ip, os.path.join(CHIP_REPO, MATTER_DEVELOPMENT_PAA_ROOT_CERTS))