diff --git a/src/controller/python/BUILD.gn b/src/controller/python/BUILD.gn index 82bc6732175a0a..da9b080e4fcd6d 100644 --- a/src/controller/python/BUILD.gn +++ b/src/controller/python/BUILD.gn @@ -51,6 +51,8 @@ shared_library("ChipDeviceCtrl") { sources += [ "chip/native/CommonStackInit.cpp" ] + defines = [] + if (chip_controller) { sources += [ "ChipCommissionableNodeController-ScriptBinding.cpp", @@ -76,6 +78,8 @@ shared_library("ChipDeviceCtrl") { "chip/native/PyChipError.cpp", "chip/utils/DeviceProxyUtils.cpp", ] + defines += [ "CHIP_CONFIG_MAX_GROUPS_PER_FABRIC=50" ] + defines += [ "CHIP_CONFIG_MAX_GROUP_KEYS_PER_FABRIC=50" ] } else { sources += [ "chip/server/Options.cpp", diff --git a/src/controller/python/ChipDeviceController-ScriptBinding.cpp b/src/controller/python/ChipDeviceController-ScriptBinding.cpp index e9cf070756f207..eaced943177314 100644 --- a/src/controller/python/ChipDeviceController-ScriptBinding.cpp +++ b/src/controller/python/ChipDeviceController-ScriptBinding.cpp @@ -238,6 +238,7 @@ PyChipError pychip_DeviceController_StackInit(Controller::Python::StorageAdapter sGroupDataProvider.SetStorageDelegate(storageAdapter); sGroupDataProvider.SetSessionKeystore(factoryParams.sessionKeystore); PyReturnErrorOnFailure(ToPyChipError(sGroupDataProvider.Init())); + Credentials::SetGroupDataProvider(&sGroupDataProvider); factoryParams.groupDataProvider = &sGroupDataProvider; PyReturnErrorOnFailure(ToPyChipError(sPersistentStorageOpCertStore.Init(storageAdapter))); diff --git a/src/controller/python/OpCredsBinding.cpp b/src/controller/python/OpCredsBinding.cpp index 3b12f8febd859f..c7f3e1dc20497c 100644 --- a/src/controller/python/OpCredsBinding.cpp +++ b/src/controller/python/OpCredsBinding.cpp @@ -465,6 +465,21 @@ PyChipError pychip_OpCreds_AllocateController(OpCredsContext * context, chip::Co return ToPyChipError(CHIP_NO_ERROR); } +PyChipError pychip_OpCreds_InitGroupTestingData(chip::Controller::DeviceCommissioner * devCtrl) +{ + VerifyOrReturnError(devCtrl != nullptr, ToPyChipError(CHIP_ERROR_INVALID_ARGUMENT)); + + uint8_t compressedFabricId[sizeof(uint64_t)] = { 0 }; + chip::MutableByteSpan compressedFabricIdSpan(compressedFabricId); + + CHIP_ERROR err = devCtrl->GetCompressedFabricIdBytes(compressedFabricIdSpan); + VerifyOrReturnError(err == CHIP_NO_ERROR, ToPyChipError(err)); + + err = chip::GroupTesting::InitData(&sGroupDataProvider, devCtrl->GetFabricIndex(), compressedFabricIdSpan); + + return ToPyChipError(err); +} + PyChipError pychip_OpCreds_SetMaximallyLargeCertsUsed(OpCredsContext * context, bool enabled) { VerifyOrReturnError(context != nullptr && context->mAdapter != nullptr, ToPyChipError(CHIP_ERROR_INCORRECT_STATE)); diff --git a/src/controller/python/chip/ChipDeviceCtrl.py b/src/controller/python/chip/ChipDeviceCtrl.py index 96e86adb090177..f37316651effd7 100644 --- a/src/controller/python/chip/ChipDeviceCtrl.py +++ b/src/controller/python/chip/ChipDeviceCtrl.py @@ -894,6 +894,18 @@ async def SendCommand(self, nodeid: int, endpoint: int, payload: ClusterObjects. ), payload, timedRequestTimeoutMs=timedRequestTimeoutMs, interactionTimeoutMs=interactionTimeoutMs, busyWaitMs=busyWaitMs).raise_on_error() return await future + def SendGroupCommand(self, groupid: int, payload: ClusterObjects.ClusterCommand, busyWaitMs: typing.Union[None, int] = None): + ''' + Send a group cluster-object encapsulated command to a group_id and get returned a future that can be awaited upon to get confirmation command was sent. + ''' + self.CheckIsActive() + + ClusterCommand.SendGroupCommand( + groupid, self.devCtrl, payload, busyWaitMs=busyWaitMs).raise_on_error() + + # None is the expected return for sending group commands. + return None + async def WriteAttribute(self, nodeid: int, attributes: typing.List[typing.Tuple[int, ClusterObjects.ClusterAttributeDescriptor, int]], timedRequestTimeoutMs: typing.Union[None, int] = None, interactionTimeoutMs: typing.Union[None, int] = None, busyWaitMs: typing.Union[None, int] = None): ''' Write a list of attributes on a target node. @@ -1289,6 +1301,15 @@ def IssueNOCChain(self, csr: Clusters.OperationalCredentials.Commands.CSRRespons self.devCtrl, py_object(self), csr.NOCSRElements, len(csr.NOCSRElements), nodeId) ) + def InitGroupTestingData(self): + """Populates the Device Controller's GroupDataProvider with known test group info and keys.""" + self.CheckIsActive() + + self._ChipStack.Call( + lambda: self._dmLib.pychip_OpCreds_InitGroupTestingData( + self.devCtrl) + ).raise_on_error() + # ----- Private Members ----- def _InitLib(self): if self._dmLib is None: @@ -1455,6 +1476,10 @@ def _InitLib(self): ] self._dmLib.pychip_DeviceController_IssueNOCChain.restype = PyChipError + self._dmLib.pychip_OpCreds_InitGroupTestingData.argtypes = [ + c_void_p] + self._dmLib.pychip_OpCreds_InitGroupTestingData.restype = PyChipError + self._dmLib.pychip_DeviceController_SetIssueNOCChainCallbackPythonCallback.argtypes = [ _IssueNOCChainCallbackPythonCallbackFunct] self._dmLib.pychip_DeviceController_SetIssueNOCChainCallbackPythonCallback.restype = None diff --git a/src/controller/python/chip/clusters/Command.py b/src/controller/python/chip/clusters/Command.py index 056688f5b572e3..df2037192e24c3 100644 --- a/src/controller/python/chip/clusters/Command.py +++ b/src/controller/python/chip/clusters/Command.py @@ -175,6 +175,22 @@ def SendCommand(future: Future, eventLoop, responseType: Type, device, commandPa )) +def SendGroupCommand(groupId: int, devCtrl: c_void_p, payload: ClusterCommand, busyWaitMs: Union[None, int] = None) -> PyChipError: + ''' Send a cluster-object encapsulated group command to a device and does the following: + - None (on a successful response containing no data) + - Raises an exception if any errors are encountered. + ''' + handle = chip.native.GetLibraryHandle() + + payloadTLV = payload.ToTLV() + return builtins.chipStack.Call( + lambda: handle.pychip_CommandSender_SendGroupCommand( + c_uint16(groupId), devCtrl, + payload.cluster_id, payload.command_id, payloadTLV, len(payloadTLV), + ctypes.c_uint16(0 if busyWaitMs is None else busyWaitMs), + )) + + def Init(): handle = chip.native.GetLibraryHandle() @@ -185,6 +201,8 @@ def Init(): setter.Set('pychip_CommandSender_SendCommand', PyChipError, [py_object, c_void_p, c_uint16, c_uint32, c_uint32, c_char_p, c_size_t, c_uint16]) + setter.Set('pychip_CommandSender_SendGroupCommand', + PyChipError, [c_uint16, c_void_p, c_uint32, c_uint32, c_char_p, c_size_t, c_uint16]) setter.Set('pychip_CommandSender_InitCallbacks', None, [ _OnCommandSenderResponseCallbackFunct, _OnCommandSenderErrorCallbackFunct, _OnCommandSenderDoneCallbackFunct]) diff --git a/src/controller/python/chip/clusters/command.cpp b/src/controller/python/chip/clusters/command.cpp index 169b925913c07a..0c812c49b83f72 100644 --- a/src/controller/python/chip/clusters/command.cpp +++ b/src/controller/python/chip/clusters/command.cpp @@ -37,6 +37,10 @@ PyChipError pychip_CommandSender_SendCommand(void * appContext, DeviceProxy * de chip::EndpointId endpointId, chip::ClusterId clusterId, chip::CommandId commandId, const uint8_t * payload, size_t length, uint16_t interactionTimeoutMs, uint16_t busyWaitMs); + +PyChipError pychip_CommandSender_SendGroupCommand(chip::GroupId groupId, chip::Controller::DeviceCommissioner * devCtrl, + chip::ClusterId clusterId, chip::CommandId commandId, const uint8_t * payload, + size_t length, uint16_t busyWaitMs); } namespace chip { @@ -168,6 +172,48 @@ PyChipError pychip_CommandSender_SendCommand(void * appContext, DeviceProxy * de usleep(busyWaitMs * 1000); } +exit: + return ToPyChipError(err); +} + +PyChipError pychip_CommandSender_SendGroupCommand(chip::GroupId groupId, chip::Controller::DeviceCommissioner * devCtrl, + chip::ClusterId clusterId, chip::CommandId commandId, const uint8_t * payload, + size_t length, uint16_t busyWaitMs) +{ + 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 sender = std::make_unique(nullptr /* callback */, exchangeManager); + + app::CommandPathParams cmdParams = { groupId, clusterId, commandId, (app::CommandPathFlags::kGroupIdValid) }; + + SuccessOrExit(err = sender->PrepareCommand(cmdParams, false)); + + { + auto writer = sender->GetCommandDataIBTLVWriter(); + TLV::TLVReader reader; + VerifyOrExit(writer != nullptr, err = CHIP_ERROR_INCORRECT_STATE); + reader.Init(payload, length); + reader.Next(); + SuccessOrExit(writer->CopyContainer(TLV::ContextTag(to_underlying(CommandDataIB::Tag::kFields)), reader)); + } + + SuccessOrExit(err = sender->FinishCommand(Optional::Missing())); + + { + auto fabricIndex = devCtrl->GetFabricIndex(); + + chip::Transport::OutgoingGroupSession session(groupId, fabricIndex); + SuccessOrExit(err = sender->SendGroupCommandRequest(chip::SessionHandle(session))); + } + + if (busyWaitMs) + { + usleep(busyWaitMs * 1000); + } + exit: return ToPyChipError(err); } diff --git a/src/controller/python/chip/yaml/runner.py b/src/controller/python/chip/yaml/runner.py index b3559a1e431956..8780c2b5c40fcf 100644 --- a/src/controller/python/chip/yaml/runner.py +++ b/src/controller/python/chip/yaml/runner.py @@ -156,6 +156,11 @@ def __init__(self, test_step, cluster: str, context: _ExecutionContext): self._expected_response_object = None self._endpoint = test_step.endpoint self._node_id = test_step.node_id + self._group_id = test_step.group_id + + 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') command = context.data_model_lookup.get_command(self._cluster, self._command_name) @@ -182,10 +187,15 @@ def __init__(self, test_step, cluster: str, context: _ExecutionContext): def run_action(self, dev_ctrl: ChipDeviceController) -> _ActionResult: try: - resp = asyncio.run(dev_ctrl.SendCommand( - 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.SendGroupCommand( + self._group_id, self._request_object, + busyWaitMs=self._busy_wait_ms) + else: + resp = asyncio.run(dev_ctrl.SendCommand( + 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) @@ -736,6 +746,7 @@ def __init__(self, test_spec_definition, certificate_authority_manager, alpha_de self._certificate_authority_manager = certificate_authority_manager self._dev_ctrls = {} + alpha_dev_ctrl.InitGroupTestingData() self._dev_ctrls['alpha'] = alpha_dev_ctrl def _invoke_action_factory(self, test_step, cluster: str): @@ -1014,6 +1025,7 @@ def _get_dev_ctrl(self, action: BaseAction): fabric = certificate_authority.NewFabricAdmin(vendorId=0xFFF1, fabricId=fabric_id) dev_ctrl = fabric.NewController() + dev_ctrl.InitGroupTestingData() self._dev_ctrls[action.identity] = dev_ctrl else: dev_ctrl = self._dev_ctrls['alpha']