diff --git a/scripts/tests/chiptest/__init__.py b/scripts/tests/chiptest/__init__.py index 5baa27da80b54f..fbfc79cf39b212 100644 --- a/scripts/tests/chiptest/__init__.py +++ b/scripts/tests/chiptest/__init__.py @@ -128,13 +128,10 @@ def _GetSlowTests() -> Set[str]: def _GetInDevelopmentTests() -> Set[str]: """Tests that fail in YAML for some reason. - Goal is for this set to become empty. + Currently this is empty and returns an empty set, but this is kept around in case + there are tests that are a work in progress. """ - return { - # Needs group support in repl / The test is incorrect - see https://github.com/CHIP-Specifications/chip-test-plans/issues/2431 for details. - "Test_TC_SC_5_2.yaml", - "TestGroupMessaging.yaml", # Needs group support in repl - } + return set() def _AllYamlTests(): diff --git a/src/controller/python/chip/ChipDeviceCtrl.py b/src/controller/python/chip/ChipDeviceCtrl.py index f37316651effd7..14419e091ac006 100644 --- a/src/controller/python/chip/ChipDeviceCtrl.py +++ b/src/controller/python/chip/ChipDeviceCtrl.py @@ -939,6 +939,34 @@ async def WriteAttribute(self, nodeid: int, attributes: typing.List[typing.Tuple future, eventLoop, device.deviceProxy, attrs, timedRequestTimeoutMs=timedRequestTimeoutMs, interactionTimeoutMs=interactionTimeoutMs, busyWaitMs=busyWaitMs).raise_on_error() return await future + def WriteGroupAttribute(self, groupid: int, attributes: typing.List[typing.Tuple[ClusterObjects.ClusterAttributeDescriptor, int]], busyWaitMs: typing.Union[None, int] = None): + ''' + Write a list of attributes on a target group. + + groupid: Group ID to send write attribute to. + attributes: A list of tuples of type (cluster-object, data-version). The data-version can be omitted. + + E.g + (Clusters.UnitTesting.Attributes.XYZAttribute('hello'), 1) -- Group Write 'hello' with data version 1 + ''' + self.CheckIsActive() + + attrs = [] + invalid_endpoint = 0xFFFF + for v in attributes: + if len(v) == 2: + attrs.append(ClusterAttribute.AttributeWriteRequest( + invalid_endpoint, v[0], v[1], 1, v[0].value)) + else: + attrs.append(ClusterAttribute.AttributeWriteRequest( + invalid_endpoint, v[0], 0, 0, v[0].value)) + + ClusterAttribute.WriteGroupAttributes( + groupid, self.devCtrl, attrs, busyWaitMs=busyWaitMs).raise_on_error() + + # An empty list is the expected return for sending group write attribute. + return [] + def _parseAttributePathTuple(self, pathTuple: typing.Union[ None, # Empty tuple, all wildcard typing.Tuple[int], # Endpoint diff --git a/src/controller/python/chip/clusters/Attribute.py b/src/controller/python/chip/clusters/Attribute.py index 6a72469b4cf43e..240579071def38 100644 --- a/src/controller/python/chip/clusters/Attribute.py +++ b/src/controller/python/chip/clusters/Attribute.py @@ -949,6 +949,32 @@ def WriteAttributes(future: Future, eventLoop, device, attributes: List[Attribut return res +def WriteGroupAttributes(groupId: int, devCtrl: c_void_p, attributes: List[AttributeWriteRequest], busyWaitMs: Union[None, int] = None) -> PyChipError: + handle = chip.native.GetLibraryHandle() + + writeargs = [] + for attr in attributes: + path = chip.interaction_model.AttributePathIBstruct.parse( + b'\x00' * chip.interaction_model.AttributePathIBstruct.sizeof()) + path.EndpointId = attr.EndpointId + path.ClusterId = attr.Attribute.cluster_id + path.AttributeId = attr.Attribute.attribute_id + path.DataVersion = attr.DataVersion + path.HasDataVersion = attr.HasDataVersion + path = chip.interaction_model.AttributePathIBstruct.build(path) + tlv = attr.Attribute.ToTLV(None, attr.Data) + writeargs.append(ctypes.c_char_p(path)) + writeargs.append(ctypes.c_char_p(bytes(tlv))) + writeargs.append(ctypes.c_int(len(tlv))) + + return builtins.chipStack.Call( + lambda: handle.pychip_WriteClient_WriteGroupAttributes( + ctypes.c_uint16(groupId), devCtrl, + ctypes.c_uint16(0 if busyWaitMs is None else busyWaitMs), + ctypes.c_size_t(len(attributes)), *writeargs) + ) + + # This struct matches the PyReadAttributeParams in attribute.cpp, for passing various params together. _ReadParams = construct.Struct( "MinInterval" / construct.Int32ul, @@ -1081,6 +1107,7 @@ def Init(): setter = chip.native.NativeLibraryHandleMethodArguments(handle) handle.pychip_WriteClient_WriteAttributes.restype = PyChipError + handle.pychip_WriteClient_WriteGroupAttributes.restype = PyChipError setter.Set('pychip_WriteClient_InitCallbacks', None, [ _OnWriteResponseCallbackFunct, _OnWriteErrorCallbackFunct, _OnWriteDoneCallbackFunct]) handle.pychip_ReadClient_Read.restype = PyChipError diff --git a/src/controller/python/chip/clusters/attribute.cpp b/src/controller/python/chip/clusters/attribute.cpp index 212e784be4ff74..1ed6d7e1cb82c6 100644 --- a/src/controller/python/chip/clusters/attribute.cpp +++ b/src/controller/python/chip/clusters/attribute.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include @@ -247,6 +248,8 @@ struct __attribute__((packed)) PyReadAttributeParams // Encodes n attribute write requests, follows 3 * n arguments, in the (AttributeWritePath*=void *, uint8_t*, size_t) order. PyChipError pychip_WriteClient_WriteAttributes(void * appContext, DeviceProxy * device, uint16_t timedWriteTimeoutMs, uint16_t interactionTimeoutMs, uint16_t busyWaitMs, size_t n, ...); +PyChipError pychip_WriteClient_WriteGroupAttributes(chip::GroupId groupId, chip::Controller::DeviceCommissioner * devCtrl, + uint16_t busyWaitMs, size_t n, ...); PyChipError pychip_ReadClient_ReadAttributes(void * appContext, ReadClient ** pReadClient, ReadClientCallback ** pCallback, DeviceProxy * device, uint8_t * readParamsBuf, size_t n, size_t total, ...); } @@ -380,6 +383,64 @@ PyChipError pychip_WriteClient_WriteAttributes(void * appContext, DeviceProxy * return ToPyChipError(err); } +PyChipError pychip_WriteClient_WriteGroupAttributes(chip::GroupId groupId, chip::Controller::DeviceCommissioner * devCtrl, + uint16_t busyWaitMs, size_t n, ...) +{ + CHIP_ERROR err = CHIP_NO_ERROR; + + chip::Messaging::ExchangeManager * exchangeManager = chip::app::InteractionModelEngine::GetInstance()->GetExchangeManager(); + VerifyOrReturnError(exchangeManager != nullptr, ToPyChipError(CHIP_ERROR_INCORRECT_STATE)); + + std::unique_ptr client = std::make_unique( + app::InteractionModelEngine::GetInstance()->GetExchangeManager(), nullptr /* callback */, Optional::Missing()); + + va_list args; + va_start(args, n); + + { + for (size_t i = 0; i < n; i++) + { + void * path = va_arg(args, void *); + void * tlv = va_arg(args, void *); + int length = va_arg(args, int); + + python::AttributePath pathObj; + memcpy(&pathObj, path, sizeof(python::AttributePath)); + uint8_t * tlvBuffer = reinterpret_cast(tlv); + + TLV::TLVReader reader; + reader.Init(tlvBuffer, static_cast(length)); + reader.Next(); + Optional dataVersion; + if (pathObj.hasDataVersion == 1) + { + dataVersion.SetValue(pathObj.dataVersion); + } + // Using kInvalidEndpointId as that used when sending group write requests. + SuccessOrExit( + err = client->PutPreencodedAttribute( + chip::app::ConcreteDataAttributePath(kInvalidEndpointId, pathObj.clusterId, pathObj.attributeId, dataVersion), + reader)); + } + } + + { + auto fabricIndex = devCtrl->GetFabricIndex(); + + chip::Transport::OutgoingGroupSession session(groupId, fabricIndex); + SuccessOrExit(err = client->SendWriteRequest(chip::SessionHandle(session), System::Clock::kZero)); + } + + if (busyWaitMs) + { + usleep(busyWaitMs * 1000); + } + +exit: + va_end(args); + return ToPyChipError(err); +} + void pychip_ReadClient_Abort(ReadClient * apReadClient, ReadClientCallback * apCallback) { VerifyOrDie(apReadClient != nullptr); diff --git a/src/controller/python/chip/yaml/runner.py b/src/controller/python/chip/yaml/runner.py index 8780c2b5c40fcf..0e75b595e02061 100644 --- a/src/controller/python/chip/yaml/runner.py +++ b/src/controller/python/chip/yaml/runner.py @@ -547,8 +547,13 @@ def __init__(self, test_step, cluster: str, context: _ExecutionContext): self._endpoint = test_step.endpoint self._interation_timeout_ms = test_step.timed_interaction_timeout_ms self._node_id = test_step.node_id + self._group_id = test_step.group_id self._request_object = None + if self._node_id is None and self._group_id is None: + raise UnexpectedParsingError( + 'Both node_id and group_id are None, at least one needs to be provided') + attribute = context.data_model_lookup.get_attribute( self._cluster, self._attribute_name) if attribute is None: @@ -575,12 +580,21 @@ def __init__(self, test_step, cluster: str, context: _ExecutionContext): def run_action(self, dev_ctrl: ChipDeviceController) -> _ActionResult: try: - resp = asyncio.run( - dev_ctrl.WriteAttribute(self._node_id, [(self._endpoint, self._request_object)], - timedRequestTimeoutMs=self._interation_timeout_ms, - busyWaitMs=self._busy_wait_ms)) + if self._group_id: + resp = dev_ctrl.WriteGroupAttribute(self._group_id, [(self._request_object,)], + busyWaitMs=self._busy_wait_ms) + else: + resp = asyncio.run( + dev_ctrl.WriteAttribute(self._node_id, [(self._endpoint, self._request_object)], + timedRequestTimeoutMs=self._interation_timeout_ms, + busyWaitMs=self._busy_wait_ms)) except chip.interaction_model.InteractionModelError as error: return _ActionResult(status=_ActionStatus.ERROR, response=error) + + # Group writes are expected to have no response upon success. + if self._group_id and len(resp) == 0: + return _ActionResult(status=_ActionStatus.SUCCESS, response=None) + if len(resp) == 1 and isinstance(resp[0], AttributeStatus): if resp[0].Status == chip.interaction_model.Status.Success: return _ActionResult(status=_ActionStatus.SUCCESS, response=None)