From eec94fe1d9c49fc459e7c8fbfb3fa08bb1bbe603 Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Wed, 5 Jun 2024 23:13:04 +0200 Subject: [PATCH 1/2] Update Python controller bindings with latest patches Add patches from upstream master branch which improve the Python controller bindings. Notable this makes use of more asyncio calls in the SDK stack, updates some defines around logging, drops unnecessary and obsolete logging code, and removes quite some unnecessary code in general. --- 0001-Support-custom-platform-tag.patch | 9 +- ...se-data-as-platform-storage-location.patch | 9 +- 0003-Linux-Increase-number-of-endpoints.patch | 4 +- ...nt-async-friendly-GetConnectedDevice.patch | 6 +- ...-Enable-node-ID-logging-in-exchanges.patch | 4 +- ...-Attribute-DataCallback-for-Arm64-Ap.patch | 9 +- ...h => 0007-Add-raw-attribute-callback.patch | 7 +- ...liminate-ZCLSubscribeAttribute-33337.patch | 594 ++++ ...inate-ZCLReadAttribute-ZCLSend-33428.patch | 589 ++++ ...iringDelegate-for-each-DeviceControl.patch | 671 +++++ ...thon-Call-SDK-asyncio-friendly-32764.patch | 359 +++ ...ke-AttributePath-more-pythonic-33571.patch | 308 ++ 0013-Python-Drop-chip-device-ctrl-33488.patch | 2532 +++++++++++++++++ ...ove-obsolete-callback-handling-33665.patch | 36 +- ...utomation-level-to-log-defines-33670.patch | 94 + ...ove-obsolete-logging-callbacks-33718.patch | 256 ++ 0017-Python-Drop-network-lock-33720.patch | 63 + 17 files changed, 5510 insertions(+), 40 deletions(-) rename 0004-Python-Implement-async-friendly-GetConnectedDevice.patch => 0004-Implement-async-friendly-GetConnectedDevice.patch (97%) rename 0001-Python-Fix-OnRead-Event-Attribute-DataCallback-for-A.patch => 0006-Fix-OnRead-Event-Attribute-DataCallback-for-Arm64-Ap.patch (92%) rename 0006-Python-Add-raw-attribute-callback.patch => 0007-Add-raw-attribute-callback.patch (95%) create mode 100644 0008-Python-Eliminate-ZCLSubscribeAttribute-33337.patch create mode 100644 0009-Python-Eliminate-ZCLReadAttribute-ZCLSend-33428.patch create mode 100644 0010-Python-Create-pairingDelegate-for-each-DeviceControl.patch create mode 100644 0011-Python-Call-SDK-asyncio-friendly-32764.patch create mode 100644 0012-Python-Make-AttributePath-more-pythonic-33571.patch create mode 100644 0013-Python-Drop-chip-device-ctrl-33488.patch rename 0007-Python-Remove-obsolete-callback-handling.patch => 0014-Python-Remove-obsolete-callback-handling-33665.patch (68%) create mode 100644 0015-Python-Add-automation-level-to-log-defines-33670.patch create mode 100644 0016-Python-Remove-obsolete-logging-callbacks-33718.patch create mode 100644 0017-Python-Drop-network-lock-33720.patch diff --git a/0001-Support-custom-platform-tag.patch b/0001-Support-custom-platform-tag.patch index 1f3d3e7..c54addb 100644 --- a/0001-Support-custom-platform-tag.patch +++ b/0001-Support-custom-platform-tag.patch @@ -1,5 +1,4 @@ -From d06671d9031156ce3b29e06d7edb344d8f6ee0d6 Mon Sep 17 00:00:00 2001 -Message-ID: +From bcd84a21c67a681fcaba43118d9d9bb7ed618b55 Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Tue, 22 Nov 2022 10:51:17 +0100 Subject: [PATCH] Support custom platform tag @@ -9,7 +8,7 @@ Subject: [PATCH] Support custom platform tag 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/controller/python/BUILD.gn b/src/controller/python/BUILD.gn -index 4b84c63d5f..b01dafbc30 100644 +index 5fc2212098..caa33c3a40 100644 --- a/src/controller/python/BUILD.gn +++ b/src/controller/python/BUILD.gn @@ -35,6 +35,15 @@ declare_args() { @@ -28,7 +27,7 @@ index 4b84c63d5f..b01dafbc30 100644 } shared_library("ChipDeviceCtrl") { -@@ -340,16 +349,7 @@ chip_python_wheel_action("chip-core") { +@@ -344,16 +353,7 @@ chip_python_wheel_action("chip-core") { cpu_tag = current_cpu } @@ -47,5 +46,5 @@ index 4b84c63d5f..b01dafbc30 100644 tags = "cp37-abi3-" + py_platform_tag -- -2.42.0 +2.45.2 diff --git a/0002-Use-data-as-platform-storage-location.patch b/0002-Use-data-as-platform-storage-location.patch index 24dfcb7..82f975b 100644 --- a/0002-Use-data-as-platform-storage-location.patch +++ b/0002-Use-data-as-platform-storage-location.patch @@ -1,7 +1,4 @@ -From 88ba0f8233f97a9bb773341da7c996deee9675fe Mon Sep 17 00:00:00 2001 -Message-ID: <88ba0f8233f97a9bb773341da7c996deee9675fe.1698087175.git.stefan@agner.ch> -In-Reply-To: -References: +From 5c1316070604e8e7ade9b518ad717c63276dd06a Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Fri, 27 May 2022 16:38:14 +0200 Subject: [PATCH] Use /data as platform storage location @@ -11,7 +8,7 @@ Subject: [PATCH] Use /data as platform storage location 1 file changed, 6 insertions(+) diff --git a/src/platform/Linux/BUILD.gn b/src/platform/Linux/BUILD.gn -index a2cfa6b39c..f6fd74ab0c 100644 +index d73a2dcb0f..97c397994e 100644 --- a/src/platform/Linux/BUILD.gn +++ b/src/platform/Linux/BUILD.gn @@ -38,6 +38,12 @@ if (chip_mdns == "platform") { @@ -28,5 +25,5 @@ index a2cfa6b39c..f6fd74ab0c 100644 "../DeviceSafeQueue.cpp", "../DeviceSafeQueue.h", -- -2.42.0 +2.45.2 diff --git a/0003-Linux-Increase-number-of-endpoints.patch b/0003-Linux-Increase-number-of-endpoints.patch index 292e041..deffa66 100644 --- a/0003-Linux-Increase-number-of-endpoints.patch +++ b/0003-Linux-Increase-number-of-endpoints.patch @@ -1,4 +1,4 @@ -From 47ca82473af8d77eb89732884542c719ccdd9312 Mon Sep 17 00:00:00 2001 +From 3bcfcd1d91050868d0153bf7692db3ac844ccc94 Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Thu, 29 Feb 2024 19:07:15 +0100 Subject: [PATCH] Linux: Increase number of endpoints @@ -36,5 +36,5 @@ index 3aab9a7b9b..02e664eddc 100644 // On linux platform, we have sys/socket.h, so HAVE_SO_BINDTODEVICE should be set to 1 -- -2.44.0 +2.45.2 diff --git a/0004-Python-Implement-async-friendly-GetConnectedDevice.patch b/0004-Implement-async-friendly-GetConnectedDevice.patch similarity index 97% rename from 0004-Python-Implement-async-friendly-GetConnectedDevice.patch rename to 0004-Implement-async-friendly-GetConnectedDevice.patch index 3759f7a..770a6fd 100644 --- a/0004-Python-Implement-async-friendly-GetConnectedDevice.patch +++ b/0004-Implement-async-friendly-GetConnectedDevice.patch @@ -1,7 +1,7 @@ -From f9fc067ad51d3989a2045f19fc5641971ce1ee20 Mon Sep 17 00:00:00 2001 +From 7ce75765081aeebef5f3adc397dff4db256348eb Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Wed, 27 Mar 2024 22:13:19 +0100 -Subject: [PATCH] [Python] Implement async friendly GetConnectedDevice +Subject: [PATCH] Implement async friendly GetConnectedDevice Currently GetConnectedDeviceSync() is blocking e.g. when a new session needs to be created. This is not asyncio friendly as it blocks the @@ -129,5 +129,5 @@ index 369260787d..b3d0aa2d7f 100644 v) for v in attributes] if attributes else None clusterDataVersionFilters = [self._parseDataVersionFilterTuple( -- -2.44.0 +2.45.2 diff --git a/0005-Enable-node-ID-logging-in-exchanges.patch b/0005-Enable-node-ID-logging-in-exchanges.patch index 2301737..06669c0 100644 --- a/0005-Enable-node-ID-logging-in-exchanges.patch +++ b/0005-Enable-node-ID-logging-in-exchanges.patch @@ -1,4 +1,4 @@ -From 3c551b7a706428d2fde77df0bc93d81bb37a2022 Mon Sep 17 00:00:00 2001 +From c1536db9448763f2e45b3aeef34f30cbfb41889e Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Thu, 18 Apr 2024 21:46:59 +0200 Subject: [PATCH] Enable node ID logging in exchanges @@ -22,5 +22,5 @@ index 558e0ee08e..5a3f5facb7 100644 /** -- -2.44.0 +2.45.2 diff --git a/0001-Python-Fix-OnRead-Event-Attribute-DataCallback-for-A.patch b/0006-Fix-OnRead-Event-Attribute-DataCallback-for-Arm64-Ap.patch similarity index 92% rename from 0001-Python-Fix-OnRead-Event-Attribute-DataCallback-for-A.patch rename to 0006-Fix-OnRead-Event-Attribute-DataCallback-for-Arm64-Ap.patch index 619a8ae..0db0b18 100644 --- a/0001-Python-Fix-OnRead-Event-Attribute-DataCallback-for-A.patch +++ b/0006-Fix-OnRead-Event-Attribute-DataCallback-for-Arm64-Ap.patch @@ -1,9 +1,8 @@ -From dce7020f3a9e542e6afbc59f131c77210df5f7db Mon Sep 17 00:00:00 2001 -Message-ID: +From 113143a945bbe5663b78b59b05d6b00670d79292 Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Thu, 25 Apr 2024 15:19:17 +0200 -Subject: [PATCH] [Python] Fix OnRead[Event|Attribute]DataCallback for Arm64 - Apple Patform devices +Subject: [PATCH] Fix OnRead[Event|Attribute]DataCallback for Arm64 Apple + Patform devices On M1/Arm64 macOS systems, the OnReadEventDataCallback often returned an invalid status, e.g.: @@ -50,5 +49,5 @@ index e31f3431b8..b73b4a49b4 100644 // When the apData is nullptr, means we did not receive a valid event data from server, status will be some error // status. -- -2.44.0 +2.45.2 diff --git a/0006-Python-Add-raw-attribute-callback.patch b/0007-Add-raw-attribute-callback.patch similarity index 95% rename from 0006-Python-Add-raw-attribute-callback.patch rename to 0007-Add-raw-attribute-callback.patch index 44c8b68..1931922 100644 --- a/0006-Python-Add-raw-attribute-callback.patch +++ b/0007-Add-raw-attribute-callback.patch @@ -1,8 +1,7 @@ -From 9bc05af1e1ef2ec93336dc0eecba16b6802b6fb1 Mon Sep 17 00:00:00 2001 -Message-ID: <9bc05af1e1ef2ec93336dc0eecba16b6802b6fb1.1716466458.git.stefan@agner.ch> +From 4fe4daf7f165dc007ab6092bee2adef67650d129 Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Thu, 23 May 2024 12:48:54 +0200 -Subject: [PATCH] [Python] Add raw attribute callback +Subject: [PATCH] Add raw attribute callback Add new subscription callback which uses raw AttributePath as paths of changed attributes. This allows to subscribe to custom clusters, @@ -102,5 +101,5 @@ index 9e46eed469..ce522bf452 100644 # Clear it out once we've notified of all changes in this transaction. self._changedPathSet = set() -- -2.45.1 +2.45.2 diff --git a/0008-Python-Eliminate-ZCLSubscribeAttribute-33337.patch b/0008-Python-Eliminate-ZCLSubscribeAttribute-33337.patch new file mode 100644 index 0000000..81e86fe --- /dev/null +++ b/0008-Python-Eliminate-ZCLSubscribeAttribute-33337.patch @@ -0,0 +1,594 @@ +From ba5d91afca90d186fb78de54f5d71705747b097a Mon Sep 17 00:00:00 2001 +From: Stefan Agner +Date: Mon, 13 May 2024 15:16:56 +0200 +Subject: [PATCH] [Python] Eliminate ZCLSubscribeAttribute (#33337) + +* Use asyncio sleep to unblock asyncio event loop + +* Avoid fixed sleep in TestCaseEviction + +Use asyncio Event and wait_for to wait for the change and continue +immediately when received. + +* Make TestSubscription an async test + +The current test implementation starves the asyncio event loop by +synchronously waiting for the threading.Condition. This prevents +making the SubscriptionTransaction fully leveraging the async +paradigm. + +It probably would be possible to mix asyncio.sleep() and threading, +but instead embrace the async pradigm for this test. + +* Make TestSubscriptionResumption an async test + +The current test implementation starves the asyncio event loop by +synchronously waiting for the threading.Condition. This prevents +making the SubscriptionTransaction fully leveraging the async +paradigm. + +It probably would be possible to mix asyncio.sleep() and threading, +but instead embrace the async pradigm for this test. + +* Make TestSubscriptionResumptionCapacityStep1 an async test + +Eliminate use of ZCLSubscribeAttribute and embrace asyncio. + +* Make TestSubscriptionResumptionCapacityStep2 an async test + +Eliminate use of ZCLSubscribeAttribute and embrace asyncio. + +* Remove ZCLSubscribeAttribute from subscription_resumption_timeout_test + +Use ReadAttribute with asyncio in subscription_resumption_timeout_test +as well. + +* Rewrite TestWriteBasicAttributes to drop ZCLRead/WriteAttribute + +* Improve wait for end of update task in TestSubscription +--- + .../python/test/test_scripts/base.py | 219 +++++++++--------- + .../test/test_scripts/mobile-device-test.py | 9 +- + ...cription_resumption_capacity_test_ctrl1.py | 5 +- + ...cription_resumption_capacity_test_ctrl2.py | 6 +- + .../subscription_resumption_test.py | 5 +- + .../subscription_resumption_timeout_test.py | 10 +- + 6 files changed, 128 insertions(+), 126 deletions(-) + +diff --git a/src/controller/python/test/test_scripts/base.py b/src/controller/python/test/test_scripts/base.py +index ed091858d2..5edb78f8e1 100644 +--- a/src/controller/python/test/test_scripts/base.py ++++ b/src/controller/python/test/test_scripts/base.py +@@ -697,13 +697,13 @@ class BaseTestHelper: + # on the sub we established previously. Since it was just marked defunct, it should return back to being + # active and a report should get delivered. + # +- sawValueChange = False ++ sawValueChangeEvent = asyncio.Event() ++ loop = asyncio.get_running_loop() + + def OnValueChange(path: Attribute.TypedAttributePath, transaction: Attribute.SubscriptionTransaction) -> None: +- nonlocal sawValueChange + self.logger.info("Saw value change!") + if (path.AttributeType == Clusters.UnitTesting.Attributes.Int8u and path.Path.EndpointId == 1): +- sawValueChange = True ++ loop.call_soon_threadsafe(sawValueChangeEvent.set) + + self.logger.info("Testing CASE defunct logic") + +@@ -720,14 +720,15 @@ class BaseTestHelper: + # was received. + # + await self.devCtrl2.WriteAttribute(nodeid, [(1, Clusters.UnitTesting.Attributes.Int8u(4))]) +- time.sleep(2) + +- sub.Shutdown() +- +- if sawValueChange is False: ++ try: ++ await asyncio.wait_for(sawValueChangeEvent.wait(), 2) ++ except TimeoutError: + self.logger.error( + "Didn't see value change in time, likely because sub got terminated due to unexpected session eviction!") + return False ++ finally: ++ sub.Shutdown() + + # + # In this test, we're going to setup a subscription on fabric1 through devCtl, then, constantly keep +@@ -739,7 +740,7 @@ class BaseTestHelper: + # + self.logger.info("Testing fabric-isolated CASE eviction") + +- sawValueChange = False ++ sawValueChangeEvent.clear() + sub = await self.devCtrl.ReadAttribute(nodeid, [(Clusters.UnitTesting.Attributes.Int8u)], reportInterval=(0, 1)) + sub.SetAttributeUpdateCallback(OnValueChange) + +@@ -752,20 +753,21 @@ class BaseTestHelper: + # was received. + # + await self.devCtrl2.WriteAttribute(nodeid, [(1, Clusters.UnitTesting.Attributes.Int8u(4))]) +- time.sleep(2) +- +- sub.Shutdown() + +- if sawValueChange is False: ++ try: ++ await asyncio.wait_for(sawValueChangeEvent.wait(), 2) ++ except TimeoutError: + self.logger.error("Didn't see value change in time, likely because sub got terminated due to other fabric (fabric1)") + return False ++ finally: ++ sub.Shutdown() + + # + # Do the same test again, but reversing the roles of fabric1 and fabric2. + # + self.logger.info("Testing fabric-isolated CASE eviction (reverse)") + +- sawValueChange = False ++ sawValueChangeEvent.clear() + sub = await self.devCtrl2.ReadAttribute(nodeid, [(Clusters.UnitTesting.Attributes.Int8u)], reportInterval=(0, 1)) + sub.SetAttributeUpdateCallback(OnValueChange) + +@@ -774,13 +776,13 @@ class BaseTestHelper: + await self.devCtrl.ReadAttribute(nodeid, [(Clusters.BasicInformation.Attributes.ClusterRevision)]) + + await self.devCtrl.WriteAttribute(nodeid, [(1, Clusters.UnitTesting.Attributes.Int8u(4))]) +- time.sleep(2) +- +- sub.Shutdown() +- +- if sawValueChange is False: ++ try: ++ await asyncio.wait_for(sawValueChangeEvent.wait(), 2) ++ except TimeoutError: + self.logger.error("Didn't see value change in time, likely because sub got terminated due to other fabric (fabric2)") + return False ++ finally: ++ sub.Shutdown() + + return True + +@@ -1198,121 +1200,114 @@ class BaseTestHelper: + return False + return True + +- def TestWriteBasicAttributes(self, nodeid: int, endpoint: int, group: int): ++ async def TestWriteBasicAttributes(self, nodeid: int, endpoint: int): + @ dataclass + class AttributeWriteRequest: +- cluster: str +- attribute: str ++ cluster: Clusters.ClusterObjects.Cluster ++ attribute: Clusters.ClusterObjects.ClusterAttributeDescriptor + value: Any + expected_status: IM.Status = IM.Status.Success + + requests = [ +- AttributeWriteRequest("BasicInformation", "NodeLabel", "Test"), +- AttributeWriteRequest("BasicInformation", "Location", ++ AttributeWriteRequest(Clusters.BasicInformation, Clusters.BasicInformation.Attributes.NodeLabel, "Test"), ++ AttributeWriteRequest(Clusters.BasicInformation, Clusters.BasicInformation.Attributes.Location, + "a pretty loooooooooooooog string", IM.Status.ConstraintError), + ] +- failed_zcl = [] ++ failed_attribute_write = [] + for req in requests: + try: + try: +- self.devCtrl.ZCLWriteAttribute(cluster=req.cluster, +- attribute=req.attribute, +- nodeid=nodeid, +- endpoint=endpoint, +- groupid=group, +- value=req.value) ++ await self.devCtrl.WriteAttribute(nodeid, [(endpoint, req.attribute, 0)]) + if req.expected_status != IM.Status.Success: + raise AssertionError( +- f"Write attribute {req.cluster}.{req.attribute} expects failure but got success response") ++ f"Write attribute {req.attribute.__qualname__} expects failure but got success response") + except Exception as ex: + if req.expected_status != IM.Status.Success: + continue + else: + raise ex +- res = self.devCtrl.ZCLReadAttribute( +- cluster=req.cluster, attribute=req.attribute, nodeid=nodeid, endpoint=endpoint, groupid=group) +- TestResult(f"Read attribute {req.cluster}.{req.attribute}", res).assertValueEqual( +- req.value) ++ ++ res = await self.devCtrl.ReadAttribute(nodeid, [(endpoint, req.attribute)]) ++ val = res[endpoint][req.cluster][req.attribute] ++ if val != req.value: ++ raise Exception( ++ f"Read attribute {req.attribute.__qualname__}: expected value {req.value}, got {val}") + except Exception as ex: +- failed_zcl.append(str(ex)) +- if failed_zcl: +- self.logger.exception(f"Following attributes failed: {failed_zcl}") ++ failed_attribute_write.append(str(ex)) ++ if failed_attribute_write: ++ self.logger.exception(f"Following attributes failed: {failed_attribute_write}") + return False + return True + +- def TestSubscription(self, nodeid: int, endpoint: int): ++ async def TestSubscription(self, nodeid: int, endpoint: int): + desiredPath = None + receivedUpdate = 0 +- updateLock = threading.Lock() +- updateCv = threading.Condition(updateLock) ++ updateEvent = asyncio.Event() ++ loop = asyncio.get_running_loop() + + def OnValueChange(path: Attribute.TypedAttributePath, transaction: Attribute.SubscriptionTransaction) -> None: +- nonlocal desiredPath, updateCv, updateLock, receivedUpdate ++ nonlocal desiredPath, updateEvent, receivedUpdate + if path.Path != desiredPath: + return + + data = transaction.GetAttribute(path) + logger.info( + f"Received report from server: path: {path.Path}, value: {data}") +- with updateLock: +- receivedUpdate += 1 +- updateCv.notify_all() +- +- class _conductAttributeChange(threading.Thread): +- def __init__(self, devCtrl: ChipDeviceCtrl.ChipDeviceController, nodeid: int, endpoint: int): +- super(_conductAttributeChange, self).__init__() +- self.nodeid = nodeid +- self.endpoint = endpoint +- self.devCtrl = devCtrl +- +- def run(self): +- for i in range(5): +- time.sleep(3) +- self.devCtrl.ZCLSend( +- "OnOff", "Toggle", self.nodeid, self.endpoint, 0, {}) ++ receivedUpdate += 1 ++ loop.call_soon_threadsafe(updateEvent.set) ++ ++ async def _conductAttributeChange(devCtrl: ChipDeviceCtrl.ChipDeviceController, nodeid: int, endpoint: int): ++ for i in range(5): ++ await asyncio.sleep(3) ++ await self.devCtrl.SendCommand(nodeid, endpoint, Clusters.OnOff.Commands.Toggle()) + + try: + desiredPath = Clusters.Attribute.AttributePath( + EndpointId=1, ClusterId=6, AttributeId=0) + # OnOff Cluster, OnOff Attribute +- subscription = self.devCtrl.ZCLSubscribeAttribute( +- "OnOff", "OnOff", nodeid, endpoint, 1, 10) ++ subscription = await self.devCtrl.ReadAttribute(nodeid, [(endpoint, Clusters.OnOff.Attributes.OnOff)], None, False, reportInterval=(1, 10), ++ keepSubscriptions=False, autoResubscribe=True) + subscription.SetAttributeUpdateCallback(OnValueChange) +- changeThread = _conductAttributeChange( +- self.devCtrl, nodeid, endpoint) + # Reset the number of subscriptions received as subscribing causes a callback. +- changeThread.start() +- with updateCv: +- while receivedUpdate < 5: +- # We should observe 5 attribute changes +- # The changing thread will change the value after 3 seconds. If we're waiting more than 10, assume something +- # is really wrong and bail out here with some information. +- if not updateCv.wait(10.0): +- self.logger.error( +- "Failed to receive subscription update") +- break +- +- # thread changes 5 times, and sleeps for 3 seconds in between. +- # Add an additional 3 seconds of slack. Timeout is in seconds. +- changeThread.join(18.0) ++ taskAttributeChange = loop.create_task(_conductAttributeChange(self.devCtrl, nodeid, endpoint)) + +- # +- # Clean-up by shutting down the sub. Otherwise, we're going to get callbacks through +- # OnValueChange on what will soon become an invalid +- # execution context above. +- # +- subscription.Shutdown() ++ while receivedUpdate < 5: ++ # We should observe 5 attribute changes ++ # The changing thread will change the value after 3 seconds. If we're waiting more than 10, assume something ++ # is really wrong and bail out here with some information. ++ try: ++ await asyncio.wait_for(updateEvent.wait(), 10) ++ updateEvent.clear() ++ except TimeoutError: ++ self.logger.error( ++ "Failed to receive subscription update") ++ break + +- if changeThread.is_alive(): +- # Thread join timed out +- self.logger.error("Failed to join change thread") +- return False ++ # At this point the task should really have done the three attribute, ++ # otherwise something is wrong. Wait for just 1s in case of a race ++ # condition between the last attribute update and the callback. ++ try: ++ await asyncio.wait_for(taskAttributeChange, 1) ++ except asyncio.TimeoutError: ++ # If attribute change task did not finish something is wrong. Cancel ++ # the task. ++ taskAttributeChange.cancel() ++ # This will throw a asyncio.CancelledError and makes sure the test ++ # is declared failed. ++ await taskAttributeChange + + return True if receivedUpdate == 5 else False + + except Exception as ex: + self.logger.exception(f"Failed to finish API test: {ex}") + return False ++ finally: ++ # ++ # Clean-up by shutting down the sub. Otherwise, we're going to get callbacks through ++ # OnValueChange on what will soon become an invalid ++ # execution context above. ++ # ++ subscription.Shutdown() + + return True + +@@ -1346,7 +1341,7 @@ class BaseTestHelper: + + return status == IM.Status.UnsupportedAccess + +- def TestSubscriptionResumption(self, nodeid: int, endpoint: int, remote_ip: str, ssh_port: int, remote_server_app: str): ++ async def TestSubscriptionResumption(self, nodeid: int, endpoint: int, remote_ip: str, ssh_port: int, remote_server_app: str): + ''' + This test validates that the device can resume the subscriptions after restarting. + It is executed in Linux Cirque tests and the steps of this test are: +@@ -1355,42 +1350,40 @@ class BaseTestHelper: + 3. Validate that the controller can receive a report from the remote server app + ''' + desiredPath = None +- receivedUpdate = False +- updateLock = threading.Lock() +- updateCv = threading.Condition(updateLock) ++ updateEvent = asyncio.Event() ++ loop = asyncio.get_running_loop() + + def OnValueReport(path: Attribute.TypedAttributePath, transaction: Attribute.SubscriptionTransaction) -> None: +- nonlocal desiredPath, updateCv, updateLock, receivedUpdate ++ nonlocal desiredPath, updateEvent, receivedUpdate + if path.Path != desiredPath: + return + + data = transaction.GetAttribute(path) + logger.info( + f"Received report from server: path: {path.Path}, value: {data}") +- with updateLock: +- receivedUpdate = True +- updateCv.notify_all() ++ loop.call_soon_threadsafe(updateEvent.set) + + try: + desiredPath = Clusters.Attribute.AttributePath( + EndpointId=0, ClusterId=0x28, AttributeId=5) + # BasicInformation Cluster, NodeLabel Attribute +- subscription = self.devCtrl.ZCLSubscribeAttribute( +- "BasicInformation", "NodeLabel", nodeid, endpoint, 1, 50, keepSubscriptions=True, autoResubscribe=False) ++ subscription = await self.devCtrl.ReadAttribute(nodeid, [(endpoint, Clusters.BasicInformation.Attributes.NodeLabel)], None, False, reportInterval=(1, 50), ++ keepSubscriptions=True, autoResubscribe=False) + subscription.SetAttributeUpdateCallback(OnValueReport) + +- self.logger.info("Restart remote deivce") ++ self.logger.info("Restart remote device") + restartRemoteThread = restartRemoteDevice( + remote_ip, ssh_port, "root", "admin", remote_server_app, "--thread --discriminator 3840") + restartRemoteThread.start() + # After device restarts, the attribute will be set dirty so the subscription can receive + # the update +- with updateCv: +- while receivedUpdate is False: +- if not updateCv.wait(10.0): +- self.logger.error( +- "Failed to receive subscription resumption report") +- break ++ receivedUpdate = False ++ try: ++ await asyncio.wait_for(updateEvent.wait(), 10) ++ receivedUpdate = True ++ except TimeoutError: ++ self.logger.error( ++ "Failed to receive subscription resumption report") + + restartRemoteThread.join(10.0) + +@@ -1437,25 +1430,26 @@ class BaseTestHelper: + controller 1 in container 1 while the Step2 is executed in controller 2 in container 2 + ''' + +- def TestSubscriptionResumptionCapacityStep1(self, nodeid: int, endpoint: int, passcode: int, subscription_capacity: int): ++ async def TestSubscriptionResumptionCapacityStep1(self, nodeid: int, endpoint: int, passcode: int, subscription_capacity: int): + try: + # BasicInformation Cluster, NodeLabel Attribute + for i in range(subscription_capacity): +- self.devCtrl.ZCLSubscribeAttribute( +- "BasicInformation", "NodeLabel", nodeid, endpoint, 1, 50, keepSubscriptions=True, autoResubscribe=False) ++ await self.devCtrl.ReadAttribute(nodeid, [(endpoint, Clusters.BasicInformation.Attributes.NodeLabel)], None, ++ False, reportInterval=(1, 50), ++ keepSubscriptions=True, autoResubscribe=False) + + logger.info("Send OpenCommissioningWindow command on fist controller") + discriminator = 3840 + salt = secrets.token_bytes(16) + iterations = 2000 + verifier = GenerateVerifier(passcode, salt, iterations) +- asyncio.run(self.devCtrl.SendCommand( ++ await self.devCtrl.SendCommand( + nodeid, 0, Clusters.AdministratorCommissioning.Commands.OpenCommissioningWindow( + commissioningTimeout=180, + PAKEPasscodeVerifier=verifier, + discriminator=discriminator, + iterations=iterations, +- salt=salt), timedRequestTimeoutMs=10000)) ++ salt=salt), timedRequestTimeoutMs=10000) + return True + + except Exception as ex: +@@ -1464,8 +1458,8 @@ class BaseTestHelper: + + return True + +- def TestSubscriptionResumptionCapacityStep2(self, nodeid: int, endpoint: int, remote_ip: str, ssh_port: int, +- remote_server_app: str, subscription_capacity: int): ++ async def TestSubscriptionResumptionCapacityStep2(self, nodeid: int, endpoint: int, remote_ip: str, ssh_port: int, ++ remote_server_app: str, subscription_capacity: int): + try: + self.logger.info("Restart remote deivce") + extra_agrs = f"--thread --discriminator 3840 --subscription-capacity {subscription_capacity}" +@@ -1479,8 +1473,9 @@ class BaseTestHelper: + self.logger.info("Send a new subscription request from the second controller") + # Close previous session so that the second controller will res-establish the session with the remote device + self.devCtrl.CloseSession(nodeid) +- self.devCtrl.ZCLSubscribeAttribute( +- "BasicInformation", "NodeLabel", nodeid, endpoint, 1, 50, keepSubscriptions=True, autoResubscribe=False) ++ await self.devCtrl.ReadAttribute(nodeid, [(endpoint, Clusters.BasicInformation.Attributes.NodeLabel)], None, ++ False, reportInterval=(1, 50), ++ keepSubscriptions=True, autoResubscribe=False) + + if restartRemoteThread.is_alive(): + # Thread join timed out +diff --git a/src/controller/python/test/test_scripts/mobile-device-test.py b/src/controller/python/test/test_scripts/mobile-device-test.py +index 9ceaa35d24..33ae713fe0 100755 +--- a/src/controller/python/test/test_scripts/mobile-device-test.py ++++ b/src/controller/python/test/test_scripts/mobile-device-test.py +@@ -129,9 +129,8 @@ def TestDatamodel(test: BaseTestHelper, device_nodeid: int): + "Failed to test Read Basic Attributes") + + logger.info("Testing attribute writing") +- FailIfNot(test.TestWriteBasicAttributes(nodeid=device_nodeid, +- endpoint=ENDPOINT_ID, +- group=GROUP_ID), ++ FailIfNot(asyncio.run(test.TestWriteBasicAttributes(nodeid=device_nodeid, ++ endpoint=ENDPOINT_ID)), + "Failed to test Write Basic Attributes") + + logger.info("Testing attribute reading basic again") +@@ -141,11 +140,11 @@ def TestDatamodel(test: BaseTestHelper, device_nodeid: int): + "Failed to test Read Basic Attributes") + + logger.info("Testing subscription") +- FailIfNot(test.TestSubscription(nodeid=device_nodeid, endpoint=LIGHTING_ENDPOINT_ID), ++ FailIfNot(asyncio.run(test.TestSubscription(nodeid=device_nodeid, endpoint=LIGHTING_ENDPOINT_ID)), + "Failed to subscribe attributes.") + + logger.info("Testing another subscription that kills previous subscriptions") +- FailIfNot(test.TestSubscription(nodeid=device_nodeid, endpoint=LIGHTING_ENDPOINT_ID), ++ FailIfNot(asyncio.run(test.TestSubscription(nodeid=device_nodeid, endpoint=LIGHTING_ENDPOINT_ID)), + "Failed to subscribe attributes.") + + logger.info("Testing re-subscription") +diff --git a/src/controller/python/test/test_scripts/subscription_resumption_capacity_test_ctrl1.py b/src/controller/python/test/test_scripts/subscription_resumption_capacity_test_ctrl1.py +index 19065b8a35..e02564e293 100755 +--- a/src/controller/python/test/test_scripts/subscription_resumption_capacity_test_ctrl1.py ++++ b/src/controller/python/test/test_scripts/subscription_resumption_capacity_test_ctrl1.py +@@ -19,6 +19,7 @@ + + # Commissioning test. + ++import asyncio + import os + import sys + from optparse import OptionParser +@@ -113,8 +114,8 @@ def main(): + "Failed on on-network commissioing") + + FailIfNot( +- test.TestSubscriptionResumptionCapacityStep1( +- options.nodeid, TEST_ENDPOINT_ID, options.setuppin, options.subscriptionCapacity), ++ asyncio.run(test.TestSubscriptionResumptionCapacityStep1( ++ options.nodeid, TEST_ENDPOINT_ID, options.setuppin, options.subscriptionCapacity)), + "Failed on step 1 of testing subscription resumption capacity") + + timeoutTicker.stop() +diff --git a/src/controller/python/test/test_scripts/subscription_resumption_capacity_test_ctrl2.py b/src/controller/python/test/test_scripts/subscription_resumption_capacity_test_ctrl2.py +index 2f3058afcd..ac449a9f54 100755 +--- a/src/controller/python/test/test_scripts/subscription_resumption_capacity_test_ctrl2.py ++++ b/src/controller/python/test/test_scripts/subscription_resumption_capacity_test_ctrl2.py +@@ -19,6 +19,7 @@ + + # Commissioning test. + ++import asyncio + import os + import sys + from optparse import OptionParser +@@ -125,8 +126,9 @@ def main(): + "Failed on on-network commissioing") + + FailIfNot( +- test.TestSubscriptionResumptionCapacityStep2(options.nodeid, TEST_ENDPOINT_ID, options.deviceAddress, +- TEST_SSH_PORT, options.remoteServerApp, options.subscriptionCapacity), ++ asyncio.run( ++ test.TestSubscriptionResumptionCapacityStep2(options.nodeid, TEST_ENDPOINT_ID, options.deviceAddress, ++ TEST_SSH_PORT, options.remoteServerApp, options.subscriptionCapacity)), + "Failed on testing subscription resumption capacity") + + timeoutTicker.stop() +diff --git a/src/controller/python/test/test_scripts/subscription_resumption_test.py b/src/controller/python/test/test_scripts/subscription_resumption_test.py +index 8b2000fb07..79edf6a289 100755 +--- a/src/controller/python/test/test_scripts/subscription_resumption_test.py ++++ b/src/controller/python/test/test_scripts/subscription_resumption_test.py +@@ -19,6 +19,7 @@ + + # Commissioning test. + ++import asyncio + import os + import sys + from optparse import OptionParser +@@ -115,8 +116,8 @@ def main(): + "Failed on on-network commissioing") + + FailIfNot( +- test.TestSubscriptionResumption(options.nodeid, TEST_ENDPOINT_ID, options.deviceAddress, +- TEST_SSH_PORT, options.remoteServerApp), "Failed to resume subscription") ++ asyncio.run(test.TestSubscriptionResumption(options.nodeid, TEST_ENDPOINT_ID, options.deviceAddress, ++ TEST_SSH_PORT, options.remoteServerApp)), "Failed to resume subscription") + + timeoutTicker.stop() + +diff --git a/src/controller/python/test/test_scripts/subscription_resumption_timeout_test.py b/src/controller/python/test/test_scripts/subscription_resumption_timeout_test.py +index 1f6411f636..4932e5b4cc 100755 +--- a/src/controller/python/test/test_scripts/subscription_resumption_timeout_test.py ++++ b/src/controller/python/test/test_scripts/subscription_resumption_timeout_test.py +@@ -19,11 +19,13 @@ + + # Commissioning test. + ++import asyncio + import os + import sys + from optparse import OptionParser + + from base import BaseTestHelper, FailIfNot, TestFail, TestTimeout, logger ++from chip import clusters as Clusters + + TEST_DISCRIMINATOR = 3840 + TEST_SETUPPIN = 20202021 +@@ -101,10 +103,12 @@ def main(): + + FailIfNot( + test.TestOnNetworkCommissioning(options.discriminator, options.setuppin, options.nodeid, options.deviceAddress), +- "Failed on on-network commissioing") ++ "Failed on on-network commissioning") ++ + try: +- test.devCtrl.ZCLSubscribeAttribute("BasicInformation", "NodeLabel", options.nodeid, TEST_ENDPOINT_ID, 1, 2, +- keepSubscriptions=True, autoResubscribe=False) ++ asyncio.run(test.devCtrl.ReadAttribute(options.nodeid, ++ [(TEST_ENDPOINT_ID, Clusters.BasicInformation.Attributes.NodeLabel)], ++ None, False, reportInterval=(1, 2), keepSubscriptions=True, autoResubscribe=False)) + except Exception as ex: + TestFail(f"Failed to subscribe attribute: {ex}") + +-- +2.45.2 + diff --git a/0009-Python-Eliminate-ZCLReadAttribute-ZCLSend-33428.patch b/0009-Python-Eliminate-ZCLReadAttribute-ZCLSend-33428.patch new file mode 100644 index 0000000..dbbe138 --- /dev/null +++ b/0009-Python-Eliminate-ZCLReadAttribute-ZCLSend-33428.patch @@ -0,0 +1,589 @@ +From 393dbaae77c7bf26a869b8498ce5e731ff0b9651 Mon Sep 17 00:00:00 2001 +From: Stefan Agner +Date: Thu, 16 May 2024 09:13:00 +0200 +Subject: [PATCH] [Python] Eliminate ZCLReadAttribute/ZCLSend (#33428) + +* Convert TestLevelControlCluster to asyncio + +Remove ZCLReadAttribute and ZCLSend API use from the level control +test TestLevelControlCluster and convert to asyncio. + +* Convert TestReadBasicAttributes to asyncio + +Remove ZCLReadAttribute API use from basic information cluster test +and convert to use asyncio. + +* Use SendCommand directly in send_zcl_command + +Avoid using ZCLSend API instead use SendCommand directly in the +send_zcl_command helper function. + +* Convert TestFailsafe to use asyncio/SendCommand + +Remove ZCLSend API usage and call SendCommand directly. Also convert +the test to a test using asyncio. + +* Convert TestOnOffCluster to use asyncio/SendCommand + +Remove ZCLSend API usage and call SendCommand directly. Also convert +the test to a test using asyncio. + +* Drop TestResult helper class + +The class is no longer required. Test results are tested directly. + +* Fix send_zcl_command argument formatting + +* Catch exception more specifically + +* Fix TestWriteBasicAttributes for all cases + +It seems TestWriteBasicAttributes did not correctly write +the attributes. The broad exception handling seems to have hidden +this issue even. Make sure the attributes with the correct value +get written, and check for unexpected and expected IM errors +in the per-attribute results specifically. + +* Fix TestFailsafe by catching correct exception + +* Drop unused import +--- + .../python/test/test_scripts/base.py | 189 ++++++++---------- + .../commissioning_failure_test.py | 6 +- + .../test/test_scripts/commissioning_test.py | 6 +- + .../test/test_scripts/failsafe_tests.py | 3 +- + .../test/test_scripts/mobile-device-test.py | 30 ++- + .../test_scripts/split_commissioning_test.py | 11 +- + .../mbed/integration_tests/common/utils.py | 41 ++-- + 7 files changed, 134 insertions(+), 152 deletions(-) + +diff --git a/src/controller/python/test/test_scripts/base.py b/src/controller/python/test/test_scripts/base.py +index 5edb78f8e1..c9b1881dd7 100644 +--- a/src/controller/python/test/test_scripts/base.py ++++ b/src/controller/python/test/test_scripts/base.py +@@ -178,29 +178,6 @@ class TestTimeout(threading.Thread): + TestFail("Timeout", doCrash=True) + + +-class TestResult: +- def __init__(self, operationName, result): +- self.operationName = operationName +- self.result = result +- +- def assertStatusEqual(self, expected): +- if self.result is None: +- raise Exception(f"{self.operationName}: no result got") +- if self.result.status != expected: +- raise Exception( +- f"{self.operationName}: expected status {expected}, got {self.result.status}") +- return self +- +- def assertValueEqual(self, expected): +- self.assertStatusEqual(0) +- if self.result is None: +- raise Exception(f"{self.operationName}: no result got") +- if self.result.value != expected: +- raise Exception( +- f"{self.operationName}: expected value {expected}, got {self.result.value}") +- return self +- +- + class BaseTestHelper: + def __init__(self, nodeid: int, paaTrustStorePath: str, testCommissioner: bool = False, + keypair: p256keypair.P256Keypair = None): +@@ -368,15 +345,16 @@ class BaseTestHelper: + def TestUsedTestCommissioner(self): + return self.devCtrl.GetTestCommissionerUsed() + +- def TestFailsafe(self, nodeid: int): ++ async def TestFailsafe(self, nodeid: int): + self.logger.info("Testing arm failsafe") + + self.logger.info("Setting failsafe on CASE connection") +- err, resp = self.devCtrl.ZCLSend("GeneralCommissioning", "ArmFailSafe", nodeid, +- 0, 0, dict(expiryLengthSeconds=60, breadcrumb=1), blocking=True) +- if err != 0: ++ try: ++ resp = await self.devCtrl.SendCommand(nodeid, 0, ++ Clusters.GeneralCommissioning.Commands.ArmFailSafe(expiryLengthSeconds=60, breadcrumb=1)) ++ except IM.InteractionModelError as ex: + self.logger.error( +- "Failed to send arm failsafe command error is {} with im response{}".format(err, resp)) ++ "Failed to send arm failsafe command error is {}".format(ex.status)) + return False + + if resp.errorCode is not Clusters.GeneralCommissioning.Enums.CommissioningErrorEnum.kOk: +@@ -387,17 +365,17 @@ class BaseTestHelper: + self.logger.info( + "Attempting to open basic commissioning window - this should fail since the failsafe is armed") + try: +- asyncio.run(self.devCtrl.SendCommand( ++ await self.devCtrl.SendCommand( + nodeid, + 0, + Clusters.AdministratorCommissioning.Commands.OpenBasicCommissioningWindow(180), + timedRequestTimeoutMs=10000 +- )) ++ ) + # we actually want the exception here because we want to see a failure, so return False here + self.logger.error( + 'Incorrectly succeeded in opening basic commissioning window') + return False +- except Exception: ++ except IM.InteractionModelError: + pass + + # TODO: +@@ -413,39 +391,39 @@ class BaseTestHelper: + self.logger.info( + "Attempting to open enhanced commissioning window - this should fail since the failsafe is armed") + try: +- asyncio.run(self.devCtrl.SendCommand( ++ await self.devCtrl.SendCommand( + nodeid, 0, Clusters.AdministratorCommissioning.Commands.OpenCommissioningWindow( + commissioningTimeout=180, + PAKEPasscodeVerifier=verifier, + discriminator=discriminator, + iterations=iterations, +- salt=salt), timedRequestTimeoutMs=10000)) ++ salt=salt), timedRequestTimeoutMs=10000) + + # we actually want the exception here because we want to see a failure, so return False here + self.logger.error( + 'Incorrectly succeeded in opening enhanced commissioning window') + return False +- except Exception: ++ except IM.InteractionModelError: + pass + + self.logger.info("Disarming failsafe on CASE connection") +- err, resp = self.devCtrl.ZCLSend("GeneralCommissioning", "ArmFailSafe", nodeid, +- 0, 0, dict(expiryLengthSeconds=0, breadcrumb=1), blocking=True) +- if err != 0: ++ try: ++ resp = await self.devCtrl.SendCommand(nodeid, 0, ++ Clusters.GeneralCommissioning.Commands.ArmFailSafe(expiryLengthSeconds=0, breadcrumb=1)) ++ except IM.InteractionModelError as ex: + self.logger.error( +- "Failed to send arm failsafe command error is {} with im response{}".format(err, resp)) ++ "Failed to send arm failsafe command error is {}".format(ex.status)) + return False + + self.logger.info( + "Opening Commissioning Window - this should succeed since the failsafe was just disarmed") + try: +- asyncio.run( +- self.devCtrl.SendCommand( +- nodeid, +- 0, +- Clusters.AdministratorCommissioning.Commands.OpenBasicCommissioningWindow(180), +- timedRequestTimeoutMs=10000 +- )) ++ await self.devCtrl.SendCommand( ++ nodeid, ++ 0, ++ Clusters.AdministratorCommissioning.Commands.OpenBasicCommissioningWindow(180), ++ timedRequestTimeoutMs=10000 ++ ) + except Exception: + self.logger.error( + 'Failed to open commissioning window after disarming failsafe') +@@ -453,11 +431,12 @@ class BaseTestHelper: + + self.logger.info( + "Attempting to arm failsafe over CASE - this should fail since the commissioning window is open") +- err, resp = self.devCtrl.ZCLSend("GeneralCommissioning", "ArmFailSafe", nodeid, +- 0, 0, dict(expiryLengthSeconds=60, breadcrumb=1), blocking=True) +- if err != 0: ++ try: ++ resp = await self.devCtrl.SendCommand(nodeid, 0, ++ Clusters.GeneralCommissioning.Commands.ArmFailSafe(expiryLengthSeconds=60, breadcrumb=1)) ++ except IM.InteractionModelError as ex: + self.logger.error( +- "Failed to send arm failsafe command error is {} with im response{}".format(err, resp)) ++ "Failed to send arm failsafe command error is {}".format(ex.status)) + return False + if resp.errorCode is Clusters.GeneralCommissioning.Enums.CommissioningErrorEnum.kBusyWithOtherAdmin: + return True +@@ -1094,50 +1073,48 @@ class BaseTestHelper: + self.devCtrl.SetThreadOperationalDataset(bytes.fromhex(dataset)) + return True + +- def TestOnOffCluster(self, nodeid: int, endpoint: int, group: int): ++ async def TestOnOffCluster(self, nodeid: int, endpoint: int): + self.logger.info( + "Sending On/Off commands to device {} endpoint {}".format(nodeid, endpoint)) +- err, resp = self.devCtrl.ZCLSend("OnOff", "On", nodeid, +- endpoint, group, {}, blocking=True) +- if err != 0: ++ ++ try: ++ await self.devCtrl.SendCommand(nodeid, endpoint, ++ Clusters.OnOff.Commands.On()) ++ except IM.InteractionModelError as ex: + self.logger.error( +- "failed to send OnOff.On: error is {} with im response{}".format(err, resp)) ++ "failed to send OnOff.On: error is {}".format(ex.status)) + return False +- err, resp = self.devCtrl.ZCLSend("OnOff", "Off", nodeid, +- endpoint, group, {}, blocking=True) +- if err != 0: ++ ++ try: ++ await self.devCtrl.SendCommand(nodeid, endpoint, ++ Clusters.OnOff.Commands.Off()) ++ except IM.InteractionModelError as ex: + self.logger.error( +- "failed to send OnOff.Off: error is {} with im response {}".format(err, resp)) ++ "failed to send OnOff.Off: error is {}".format(ex.status)) + return False + return True + +- def TestLevelControlCluster(self, nodeid: int, endpoint: int, group: int): ++ async def TestLevelControlCluster(self, nodeid: int, endpoint: int): + self.logger.info( + f"Sending MoveToLevel command to device {nodeid} endpoint {endpoint}") +- try: +- commonArgs = dict(transitionTime=0, optionsMask=1, optionsOverride=1) + ++ commonArgs = dict(transitionTime=0, optionsMask=1, optionsOverride=1) ++ ++ async def _moveClusterLevel(setLevel): ++ await self.devCtrl.SendCommand(nodeid, ++ endpoint, ++ Clusters.LevelControl.Commands.MoveToLevel(**commonArgs, level=setLevel)) ++ res = await self.devCtrl.ReadAttribute(nodeid, [(endpoint, Clusters.LevelControl.Attributes.CurrentLevel)]) ++ readVal = res[endpoint][Clusters.LevelControl][Clusters.LevelControl.Attributes.CurrentLevel] ++ if readVal != setLevel: ++ raise Exception(f"Read attribute LevelControl.CurrentLevel: expected value {setLevel}, got {readVal}") ++ ++ try: + # Move to 1 +- self.devCtrl.ZCLSend("LevelControl", "MoveToLevel", nodeid, +- endpoint, group, dict(**commonArgs, level=1), blocking=True) +- res = self.devCtrl.ZCLReadAttribute(cluster="LevelControl", +- attribute="CurrentLevel", +- nodeid=nodeid, +- endpoint=endpoint, +- groupid=group) +- TestResult("Read attribute LevelControl.CurrentLevel", +- res).assertValueEqual(1) ++ await _moveClusterLevel(1) + + # Move to 254 +- self.devCtrl.ZCLSend("LevelControl", "MoveToLevel", nodeid, +- endpoint, group, dict(**commonArgs, level=254), blocking=True) +- res = self.devCtrl.ZCLReadAttribute(cluster="LevelControl", +- attribute="CurrentLevel", +- nodeid=nodeid, +- endpoint=endpoint, +- groupid=group) +- TestResult("Read attribute LevelControl.CurrentLevel", +- res).assertValueEqual(254) ++ await _moveClusterLevel(254) + + return True + except Exception as ex: +@@ -1170,29 +1147,27 @@ class BaseTestHelper: + self.logger.exception("Failed to resolve. {}".format(ex)) + return False + +- def TestReadBasicAttributes(self, nodeid: int, endpoint: int, group: int): ++ async def TestReadBasicAttributes(self, nodeid: int, endpoint: int): ++ attrs = Clusters.BasicInformation.Attributes + basic_cluster_attrs = { +- "VendorName": "TEST_VENDOR", +- "VendorID": 0xFFF1, +- "ProductName": "TEST_PRODUCT", +- "ProductID": 0x8001, +- "NodeLabel": "Test", +- "Location": "XX", +- "HardwareVersion": 0, +- "HardwareVersionString": "TEST_VERSION", +- "SoftwareVersion": 1, +- "SoftwareVersionString": "1.0", ++ attrs.VendorName: "TEST_VENDOR", ++ attrs.VendorID: 0xFFF1, ++ attrs.ProductName: "TEST_PRODUCT", ++ attrs.ProductID: 0x8001, ++ attrs.NodeLabel: "Test", ++ attrs.Location: "XX", ++ attrs.HardwareVersion: 0, ++ attrs.HardwareVersionString: "TEST_VERSION", ++ attrs.SoftwareVersion: 1, ++ attrs.SoftwareVersionString: "1.0", + } + failed_zcl = {} + for basic_attr, expected_value in basic_cluster_attrs.items(): + try: +- res = self.devCtrl.ZCLReadAttribute(cluster="BasicInformation", +- attribute=basic_attr, +- nodeid=nodeid, +- endpoint=endpoint, +- groupid=group) +- TestResult(f"Read attribute {basic_attr}", res).assertValueEqual( +- expected_value) ++ res = await self.devCtrl.ReadAttribute(nodeid, [(endpoint, basic_attr)]) ++ readVal = res[endpoint][Clusters.BasicInformation][basic_attr] ++ if readVal != expected_value: ++ raise Exception(f"Read attribute: expected value {expected_value}, got {readVal}") + except Exception as ex: + failed_zcl[basic_attr] = str(ex) + if failed_zcl: +@@ -1216,16 +1191,16 @@ class BaseTestHelper: + failed_attribute_write = [] + for req in requests: + try: +- try: +- await self.devCtrl.WriteAttribute(nodeid, [(endpoint, req.attribute, 0)]) +- if req.expected_status != IM.Status.Success: +- raise AssertionError( +- f"Write attribute {req.attribute.__qualname__} expects failure but got success response") +- except Exception as ex: +- if req.expected_status != IM.Status.Success: +- continue +- else: +- raise ex ++ # Errors tested here is in the per-attribute result list (type AttributeStatus) ++ write_res = await self.devCtrl.WriteAttribute(nodeid, [(endpoint, req.attribute(req.value))]) ++ status = write_res[0].Status ++ if req.expected_status != status: ++ raise AssertionError( ++ f"Write attribute {req.attribute.__qualname__} expects {req.expected_status} but got {status}") ++ ++ # Only execute read tests where write is successful. ++ if req.expected_status != IM.Status.Success: ++ continue + + res = await self.devCtrl.ReadAttribute(nodeid, [(endpoint, req.attribute)]) + val = res[endpoint][req.cluster][req.attribute] +diff --git a/src/controller/python/test/test_scripts/commissioning_failure_test.py b/src/controller/python/test/test_scripts/commissioning_failure_test.py +index 4681dd108d..a535c8b184 100755 +--- a/src/controller/python/test/test_scripts/commissioning_failure_test.py ++++ b/src/controller/python/test/test_scripts/commissioning_failure_test.py +@@ -19,6 +19,7 @@ + + # Commissioning test. + ++import asyncio + import os + import sys + from optparse import OptionParser +@@ -121,9 +122,8 @@ def main(): + FailIfNot(test.TestCommissionFailure(1, 0), "Failed to commission device") + + logger.info("Testing on off cluster") +- FailIfNot(test.TestOnOffCluster(nodeid=1, +- endpoint=LIGHTING_ENDPOINT_ID, +- group=GROUP_ID), "Failed to test on off cluster") ++ FailIfNot(asyncio.run(test.TestOnOffCluster(nodeid=1, ++ endpoint=LIGHTING_ENDPOINT_ID)), "Failed to test on off cluster") + + timeoutTicker.stop() + +diff --git a/src/controller/python/test/test_scripts/commissioning_test.py b/src/controller/python/test/test_scripts/commissioning_test.py +index b6adc0f477..4a7f15d6c3 100755 +--- a/src/controller/python/test/test_scripts/commissioning_test.py ++++ b/src/controller/python/test/test_scripts/commissioning_test.py +@@ -19,6 +19,7 @@ + + # Commissioning test. + ++import asyncio + import os + import sys + from optparse import OptionParser +@@ -146,9 +147,8 @@ def main(): + TestFail("Must provide device address or setup payload to commissioning the device") + + logger.info("Testing on off cluster") +- FailIfNot(test.TestOnOffCluster(nodeid=options.nodeid, +- endpoint=LIGHTING_ENDPOINT_ID, +- group=GROUP_ID), "Failed to test on off cluster") ++ FailIfNot(asyncio.run(test.TestOnOffCluster(nodeid=options.nodeid, ++ endpoint=LIGHTING_ENDPOINT_ID)), "Failed to test on off cluster") + + FailIfNot(test.TestUsedTestCommissioner(), + "Test commissioner check failed") +diff --git a/src/controller/python/test/test_scripts/failsafe_tests.py b/src/controller/python/test/test_scripts/failsafe_tests.py +index 4b3838430c..d1a2034e73 100755 +--- a/src/controller/python/test/test_scripts/failsafe_tests.py ++++ b/src/controller/python/test/test_scripts/failsafe_tests.py +@@ -19,6 +19,7 @@ + + # Commissioning test. + ++import asyncio + import os + import sys + from optparse import OptionParser +@@ -99,7 +100,7 @@ def main(): + nodeid=1), + "Failed to finish key exchange") + +- FailIfNot(test.TestFailsafe(nodeid=1), "Failed failsafe test") ++ FailIfNot(asyncio.run(test.TestFailsafe(nodeid=1)), "Failed failsafe test") + + timeoutTicker.stop() + +diff --git a/src/controller/python/test/test_scripts/mobile-device-test.py b/src/controller/python/test/test_scripts/mobile-device-test.py +index 33ae713fe0..8f6f534dce 100755 +--- a/src/controller/python/test/test_scripts/mobile-device-test.py ++++ b/src/controller/python/test/test_scripts/mobile-device-test.py +@@ -102,20 +102,17 @@ def TestDatamodel(test: BaseTestHelper, device_nodeid: int): + logger.info("Testing datamodel functions") + + logger.info("Testing on off cluster") +- FailIfNot(test.TestOnOffCluster(nodeid=device_nodeid, +- endpoint=LIGHTING_ENDPOINT_ID, +- group=GROUP_ID), "Failed to test on off cluster") ++ FailIfNot(asyncio.run(test.TestOnOffCluster(nodeid=device_nodeid, ++ endpoint=LIGHTING_ENDPOINT_ID)), "Failed to test on off cluster") + + logger.info("Testing level control cluster") +- FailIfNot(test.TestLevelControlCluster(nodeid=device_nodeid, +- endpoint=LIGHTING_ENDPOINT_ID, +- group=GROUP_ID), ++ FailIfNot(asyncio.run(test.TestLevelControlCluster(nodeid=device_nodeid, ++ endpoint=LIGHTING_ENDPOINT_ID)), + "Failed to test level control cluster") + + logger.info("Testing sending commands to non exist endpoint") +- FailIfNot(not test.TestOnOffCluster(nodeid=device_nodeid, +- endpoint=233, +- group=GROUP_ID), "Failed to test on off cluster on non-exist endpoint") ++ FailIfNot(not asyncio.run(test.TestOnOffCluster(nodeid=device_nodeid, ++ endpoint=233)), "Failed to test on off cluster on non-exist endpoint") + + # Test experimental Python cluster objects API + logger.info("Testing cluster objects API") +@@ -123,9 +120,8 @@ def TestDatamodel(test: BaseTestHelper, device_nodeid: int): + "Failed when testing Python Cluster Object APIs") + + logger.info("Testing attribute reading") +- FailIfNot(test.TestReadBasicAttributes(nodeid=device_nodeid, +- endpoint=ENDPOINT_ID, +- group=GROUP_ID), ++ FailIfNot(asyncio.run(test.TestReadBasicAttributes(nodeid=device_nodeid, ++ endpoint=ENDPOINT_ID)), + "Failed to test Read Basic Attributes") + + logger.info("Testing attribute writing") +@@ -134,9 +130,8 @@ def TestDatamodel(test: BaseTestHelper, device_nodeid: int): + "Failed to test Write Basic Attributes") + + logger.info("Testing attribute reading basic again") +- FailIfNot(test.TestReadBasicAttributes(nodeid=1, +- endpoint=ENDPOINT_ID, +- group=GROUP_ID), ++ FailIfNot(asyncio.run(test.TestReadBasicAttributes(nodeid=1, ++ endpoint=ENDPOINT_ID)), + "Failed to test Read Basic Attributes") + + logger.info("Testing subscription") +@@ -152,9 +147,8 @@ def TestDatamodel(test: BaseTestHelper, device_nodeid: int): + "Failed to validated re-subscription") + + logger.info("Testing on off cluster over resolved connection") +- FailIfNot(test.TestOnOffCluster(nodeid=device_nodeid, +- endpoint=LIGHTING_ENDPOINT_ID, +- group=GROUP_ID), "Failed to test on off cluster") ++ FailIfNot(asyncio.run(test.TestOnOffCluster(nodeid=device_nodeid, ++ endpoint=LIGHTING_ENDPOINT_ID)), "Failed to test on off cluster") + + logger.info("Testing writing/reading fabric sensitive data") + asyncio.run(test.TestFabricSensitive(nodeid=device_nodeid)) +diff --git a/src/controller/python/test/test_scripts/split_commissioning_test.py b/src/controller/python/test/test_scripts/split_commissioning_test.py +index 47fedb3aad..9233d58b90 100755 +--- a/src/controller/python/test/test_scripts/split_commissioning_test.py ++++ b/src/controller/python/test/test_scripts/split_commissioning_test.py +@@ -19,6 +19,7 @@ + + # Commissioning test. + ++import asyncio + import os + import sys + from optparse import OptionParser +@@ -118,14 +119,12 @@ def main(): + "Failed to commission device 2") + + logger.info("Testing on off cluster on device 1") +- FailIfNot(test.TestOnOffCluster(nodeid=1, +- endpoint=LIGHTING_ENDPOINT_ID, +- group=GROUP_ID), "Failed to test on off cluster on device 1") ++ FailIfNot(asyncio.run(test.TestOnOffCluster(nodeid=1, ++ endpoint=LIGHTING_ENDPOINT_ID)), "Failed to test on off cluster on device 1") + + logger.info("Testing on off cluster on device 2") +- FailIfNot(test.TestOnOffCluster(nodeid=2, +- endpoint=LIGHTING_ENDPOINT_ID, +- group=GROUP_ID), "Failed to test on off cluster on device 2") ++ FailIfNot(asyncio.run(test.TestOnOffCluster(nodeid=2, ++ endpoint=LIGHTING_ENDPOINT_ID)), "Failed to test on off cluster on device 2") + + timeoutTicker.stop() + +diff --git a/src/test_driver/mbed/integration_tests/common/utils.py b/src/test_driver/mbed/integration_tests/common/utils.py +index 036b612d7b..2b1db4da30 100644 +--- a/src/test_driver/mbed/integration_tests/common/utils.py ++++ b/src/test_driver/mbed/integration_tests/common/utils.py +@@ -14,6 +14,7 @@ + # limitations under the License. + + ++import asyncio + import logging + import platform + import random +@@ -114,21 +115,33 @@ def send_zcl_command(devCtrl, line): + if len(args) < 5: + raise exceptions.InvalidArgumentCount(5, len(args)) + +- if args[0] not in all_commands: +- raise exceptions.UnknownCluster(args[0]) +- command = all_commands.get(args[0]).get(args[1], None) ++ cluster = args[0] ++ command = args[1] ++ if cluster not in all_commands: ++ raise exceptions.UnknownCluster(cluster) ++ commandObj = all_commands.get(cluster).get(command, None) + # When command takes no arguments, (not command) is True +- if command is None: +- raise exceptions.UnknownCommand(args[0], args[1]) +- err, res = devCtrl.ZCLSend(args[0], args[1], int( +- args[2]), int(args[3]), int(args[4]), FormatZCLArguments(args[5:], command), blocking=True) +- if err != 0: +- log.error("Failed to send ZCL command [{}] {}.".format(err, res)) +- elif res is not None: +- log.info("Success, received command response:") +- log.info(res) +- else: +- log.info("Success, no command response.") ++ if commandObj is None: ++ raise exceptions.UnknownCommand(cluster, command) ++ ++ try: ++ req = commandObj(**FormatZCLArguments(args[5:], commandObj)) ++ except BaseException: ++ raise exceptions.UnknownCommand(cluster, command) ++ ++ nodeid = int(args[2]) ++ endpoint = int(args[3]) ++ try: ++ res = asyncio.run(devCtrl.SendCommand(nodeid, endpoint, req)) ++ logging.debug(f"CommandResponse {res}") ++ if res is not None: ++ log.info("Success, received command response:") ++ log.info(res) ++ else: ++ log.info("Success, no command response.") ++ except exceptions.InteractionModelError as ex: ++ return (int(ex.status), None) ++ log.error("Failed to send ZCL command [{}] {}.".format(int(ex.status), None)) + except exceptions.ChipStackException as ex: + log.error("An exception occurred during processing ZCL command:") + log.error(str(ex)) +-- +2.45.2 + diff --git a/0010-Python-Create-pairingDelegate-for-each-DeviceControl.patch b/0010-Python-Create-pairingDelegate-for-each-DeviceControl.patch new file mode 100644 index 0000000..973f96b --- /dev/null +++ b/0010-Python-Create-pairingDelegate-for-each-DeviceControl.patch @@ -0,0 +1,671 @@ +From 25180ab3236c2af49f6003109e1a2e8769c3ba4b Mon Sep 17 00:00:00 2001 +From: "tianfeng.yang" <130436698+tianfeng-yang@users.noreply.github.com> +Date: Wed, 17 Apr 2024 21:53:13 +0800 +Subject: [PATCH] [Python] Create pairingDelegate for each DeviceController + (#32369) + +* Create pairingDelegate for each DeviceController + +* restore the original pairingcomplete callback logic +--- + .../ChipDeviceController-ScriptBinding.cpp | 97 +++++++++++-------- + src/controller/python/OpCredsBinding.cpp | 40 +++++--- + src/controller/python/chip/ChipDeviceCtrl.py | 69 ++++++++----- + 3 files changed, 128 insertions(+), 78 deletions(-) + +diff --git a/src/controller/python/ChipDeviceController-ScriptBinding.cpp b/src/controller/python/ChipDeviceController-ScriptBinding.cpp +index d78c9da8ed..a55d3865bd 100644 +--- a/src/controller/python/ChipDeviceController-ScriptBinding.cpp ++++ b/src/controller/python/ChipDeviceController-ScriptBinding.cpp +@@ -105,7 +105,6 @@ chip::Controller::CommissioningParameters sCommissioningParameters; + + } // namespace + +-chip::Controller::ScriptDevicePairingDelegate sPairingDelegate; + chip::Controller::ScriptPairingDeviceDiscoveryDelegate sPairingDeviceDiscoveryDelegate; + chip::Credentials::GroupDataProviderImpl sGroupDataProvider; + chip::Credentials::PersistentStorageOpCertStore sPersistentStorageOpCertStore; +@@ -121,9 +120,8 @@ extern "C" { + PyChipError pychip_DeviceController_StackInit(Controller::Python::StorageAdapter * storageAdapter, bool enableServerInteractions); + PyChipError pychip_DeviceController_StackShutdown(); + +-PyChipError pychip_DeviceController_NewDeviceController(chip::Controller::DeviceCommissioner ** outDevCtrl, +- chip::NodeId localDeviceId, bool useTestCommissioner); +-PyChipError pychip_DeviceController_DeleteDeviceController(chip::Controller::DeviceCommissioner * devCtrl); ++PyChipError pychip_DeviceController_DeleteDeviceController(chip::Controller::DeviceCommissioner * devCtrl, ++ chip::Controller::ScriptDevicePairingDelegate * pairingDelegate); + PyChipError pychip_DeviceController_GetAddressAndPort(chip::Controller::DeviceCommissioner * devCtrl, chip::NodeId nodeId, + char * outAddress, uint64_t maxAddressLen, uint16_t * outPort); + PyChipError pychip_DeviceController_GetCompressedFabricId(chip::Controller::DeviceCommissioner * devCtrl, uint64_t * outFabricId); +@@ -168,15 +166,17 @@ PyChipError pychip_DeviceController_DiscoverCommissionableNodesDeviceType(chip:: + uint16_t device_type); + PyChipError pychip_DeviceController_DiscoverCommissionableNodesCommissioningEnabled(chip::Controller::DeviceCommissioner * devCtrl); + +-PyChipError pychip_DeviceController_OnNetworkCommission(chip::Controller::DeviceCommissioner * devCtrl, uint64_t nodeId, +- uint32_t setupPasscode, const uint8_t filterType, const char * filterParam, +- uint32_t discoveryTimeoutMsec); ++PyChipError pychip_DeviceController_OnNetworkCommission(chip::Controller::DeviceCommissioner * devCtrl, ++ chip::Controller::ScriptDevicePairingDelegate * pairingDelegate, ++ uint64_t nodeId, uint32_t setupPasscode, const uint8_t filterType, ++ const char * filterParam, uint32_t discoveryTimeoutMsec); + + PyChipError pychip_DeviceController_PostTaskOnChipThread(ChipThreadTaskRunnerFunct callback, void * pythonContext); + +-PyChipError pychip_DeviceController_OpenCommissioningWindow(chip::Controller::DeviceCommissioner * devCtrl, chip::NodeId nodeid, +- uint16_t timeout, uint32_t iteration, uint16_t discriminator, +- uint8_t optionInt); ++PyChipError pychip_DeviceController_OpenCommissioningWindow(chip::Controller::DeviceCommissioner * devCtrl, ++ chip::Controller::ScriptDevicePairingDelegate * pairingDelegate, ++ chip::NodeId nodeid, uint16_t timeout, uint32_t iteration, ++ uint16_t discriminator, uint8_t optionInt); + + void pychip_DeviceController_PrintDiscoveredDevices(chip::Controller::DeviceCommissioner * devCtrl); + bool pychip_DeviceController_GetIPForDiscoveredDevice(chip::Controller::DeviceCommissioner * devCtrl, int idx, char * addrStr, +@@ -184,19 +184,28 @@ bool pychip_DeviceController_GetIPForDiscoveredDevice(chip::Controller::DeviceCo + + // Pairing Delegate + PyChipError +-pychip_ScriptDevicePairingDelegate_SetKeyExchangeCallback(chip::Controller::DeviceCommissioner * devCtrl, ++pychip_ScriptDevicePairingDelegate_SetKeyExchangeCallback(chip::Controller::ScriptDevicePairingDelegate * pairingDelegate, + chip::Controller::DevicePairingDelegate_OnPairingCompleteFunct callback); + + PyChipError pychip_ScriptDevicePairingDelegate_SetCommissioningCompleteCallback( +- chip::Controller::DeviceCommissioner * devCtrl, chip::Controller::DevicePairingDelegate_OnCommissioningCompleteFunct callback); ++ chip::Controller::ScriptDevicePairingDelegate * pairingDelegate, ++ chip::Controller::DevicePairingDelegate_OnCommissioningCompleteFunct callback); + + PyChipError pychip_ScriptDevicePairingDelegate_SetCommissioningStatusUpdateCallback( +- chip::Controller::DeviceCommissioner * devCtrl, ++ chip::Controller::ScriptDevicePairingDelegate * pairingDelegate, + chip::Controller::DevicePairingDelegate_OnCommissioningStatusUpdateFunct callback); ++ + PyChipError +-pychip_ScriptDevicePairingDelegate_SetFabricCheckCallback(chip::Controller::DevicePairingDelegate_OnFabricCheckFunct callback); ++pychip_ScriptDevicePairingDelegate_SetFabricCheckCallback(chip::Controller::ScriptDevicePairingDelegate * pairingDelegate, ++ chip::Controller::DevicePairingDelegate_OnFabricCheckFunct callback); ++ + PyChipError pychip_ScriptDevicePairingDelegate_SetOpenWindowCompleteCallback( +- chip::Controller::DeviceCommissioner * devCtrl, chip::Controller::DevicePairingDelegate_OnWindowOpenCompleteFunct callback); ++ chip::Controller::ScriptDevicePairingDelegate * pairingDelegate, ++ chip::Controller::DevicePairingDelegate_OnWindowOpenCompleteFunct callback); ++ ++PyChipError ++pychip_ScriptDevicePairingDelegate_SetExpectingPairingComplete(chip::Controller::ScriptDevicePairingDelegate * pairingDelegate, ++ bool value); + + // BLE + PyChipError pychip_DeviceCommissioner_CloseBleConnection(chip::Controller::DeviceCommissioner * devCtrl); +@@ -354,7 +363,6 @@ const char * pychip_DeviceController_StatusReportToString(uint32_t profileId, ui + PyChipError pychip_DeviceController_ConnectBLE(chip::Controller::DeviceCommissioner * devCtrl, uint16_t discriminator, + uint32_t setupPINCode, chip::NodeId nodeid) + { +- sPairingDelegate.SetExpectingPairingComplete(true); + return ToPyChipError(devCtrl->PairDevice(nodeid, + chip::RendezvousParameters() + .SetPeerAddress(Transport::PeerAddress(Transport::Type::kBle)) +@@ -378,14 +386,12 @@ PyChipError pychip_DeviceController_ConnectIP(chip::Controller::DeviceCommission + addr.SetTransportType(chip::Transport::Type::kUdp).SetIPAddress(peerAddr).SetInterface(ifaceOutput); + params.SetPeerAddress(addr).SetDiscriminator(0); + +- sPairingDelegate.SetExpectingPairingComplete(true); + return ToPyChipError(devCtrl->PairDevice(nodeid, params, sCommissioningParameters)); + } + + PyChipError pychip_DeviceController_ConnectWithCode(chip::Controller::DeviceCommissioner * devCtrl, const char * onboardingPayload, + chip::NodeId nodeid, uint8_t discoveryType) + { +- sPairingDelegate.SetExpectingPairingComplete(true); + return ToPyChipError(devCtrl->PairDevice(nodeid, onboardingPayload, sCommissioningParameters, + static_cast(discoveryType))); + } +@@ -430,9 +436,10 @@ PyChipError pychip_DeviceController_UnpairDevice(chip::Controller::DeviceCommiss + return ToPyChipError(err); + } + +-PyChipError pychip_DeviceController_OnNetworkCommission(chip::Controller::DeviceCommissioner * devCtrl, uint64_t nodeId, +- uint32_t setupPasscode, const uint8_t filterType, const char * filterParam, +- uint32_t discoveryTimeoutMsec) ++PyChipError pychip_DeviceController_OnNetworkCommission(chip::Controller::DeviceCommissioner * devCtrl, ++ chip::Controller::ScriptDevicePairingDelegate * pairingDelegate, ++ uint64_t nodeId, uint32_t setupPasscode, const uint8_t filterType, ++ const char * filterParam, uint32_t discoveryTimeoutMsec) + { + Dnssd::DiscoveryFilter filter(static_cast(filterType)); + switch (static_cast(filterType)) +@@ -467,9 +474,8 @@ PyChipError pychip_DeviceController_OnNetworkCommission(chip::Controller::Device + return ToPyChipError(CHIP_ERROR_INVALID_ARGUMENT); + } + +- sPairingDelegate.SetExpectingPairingComplete(true); +- CHIP_ERROR err = sPairingDeviceDiscoveryDelegate.Init(nodeId, setupPasscode, sCommissioningParameters, &sPairingDelegate, +- devCtrl, discoveryTimeoutMsec); ++ CHIP_ERROR err = sPairingDeviceDiscoveryDelegate.Init(nodeId, setupPasscode, sCommissioningParameters, pairingDelegate, devCtrl, ++ discoveryTimeoutMsec); + VerifyOrReturnError(err == CHIP_NO_ERROR, ToPyChipError(err)); + return ToPyChipError(devCtrl->DiscoverCommissionableNodes(filter)); + } +@@ -587,7 +593,6 @@ PyChipError pychip_DeviceController_EstablishPASESessionIP(chip::Controller::Dev + addr.SetPort(port); + } + params.SetPeerAddress(addr).SetDiscriminator(0); +- sPairingDelegate.SetExpectingPairingComplete(true); + return ToPyChipError(devCtrl->EstablishPASEConnection(nodeid, params)); + } + +@@ -598,14 +603,12 @@ PyChipError pychip_DeviceController_EstablishPASESessionBLE(chip::Controller::De + RendezvousParameters params = chip::RendezvousParameters().SetSetupPINCode(setupPINCode); + addr.SetTransportType(chip::Transport::Type::kBle); + params.SetPeerAddress(addr).SetDiscriminator(discriminator); +- sPairingDelegate.SetExpectingPairingComplete(true); + return ToPyChipError(devCtrl->EstablishPASEConnection(nodeid, params)); + } + + PyChipError pychip_DeviceController_EstablishPASESession(chip::Controller::DeviceCommissioner * devCtrl, const char * setUpCode, + chip::NodeId nodeid) + { +- sPairingDelegate.SetExpectingPairingComplete(true); + return ToPyChipError(devCtrl->EstablishPASEConnection(nodeid, setUpCode)); + } + +@@ -656,15 +659,17 @@ PyChipError pychip_DeviceController_DiscoverCommissionableNodesCommissioningEnab + } + + PyChipError pychip_ScriptDevicePairingDelegate_SetOpenWindowCompleteCallback( +- chip::Controller::DeviceCommissioner * devCtrl, chip::Controller::DevicePairingDelegate_OnWindowOpenCompleteFunct callback) ++ chip::Controller::ScriptDevicePairingDelegate * pairingDelegate, ++ chip::Controller::DevicePairingDelegate_OnWindowOpenCompleteFunct callback) + { +- sPairingDelegate.SetCommissioningWindowOpenCallback(callback); ++ pairingDelegate->SetCommissioningWindowOpenCallback(callback); + return ToPyChipError(CHIP_NO_ERROR); + } + +-PyChipError pychip_DeviceController_OpenCommissioningWindow(chip::Controller::DeviceCommissioner * devCtrl, chip::NodeId nodeid, +- uint16_t timeout, uint32_t iteration, uint16_t discriminator, +- uint8_t optionInt) ++PyChipError pychip_DeviceController_OpenCommissioningWindow(chip::Controller::DeviceCommissioner * devCtrl, ++ chip::Controller::ScriptDevicePairingDelegate * pairingDelegate, ++ chip::NodeId nodeid, uint16_t timeout, uint32_t iteration, ++ uint16_t discriminator, uint8_t optionInt) + { + const auto option = static_cast(optionInt); + if (option == Controller::CommissioningWindowOpener::CommissioningWindowOption::kOriginalSetupCode) +@@ -680,7 +685,7 @@ PyChipError pychip_DeviceController_OpenCommissioningWindow(chip::Controller::De + Platform::New(static_cast(devCtrl)); + PyChipError err = ToPyChipError(opener->OpenCommissioningWindow(nodeid, System::Clock::Seconds16(timeout), iteration, + discriminator, NullOptional, NullOptional, +- sPairingDelegate.GetOpenWindowCallback(opener), payload)); ++ pairingDelegate->GetOpenWindowCallback(opener), payload)); + return err; + } + +@@ -688,32 +693,42 @@ PyChipError pychip_DeviceController_OpenCommissioningWindow(chip::Controller::De + } + + PyChipError +-pychip_ScriptDevicePairingDelegate_SetKeyExchangeCallback(chip::Controller::DeviceCommissioner * devCtrl, ++pychip_ScriptDevicePairingDelegate_SetKeyExchangeCallback(chip::Controller::ScriptDevicePairingDelegate * pairingDelegate, + chip::Controller::DevicePairingDelegate_OnPairingCompleteFunct callback) + { +- sPairingDelegate.SetKeyExchangeCallback(callback); ++ pairingDelegate->SetKeyExchangeCallback(callback); + return ToPyChipError(CHIP_NO_ERROR); + } + + PyChipError pychip_ScriptDevicePairingDelegate_SetCommissioningCompleteCallback( +- chip::Controller::DeviceCommissioner * devCtrl, chip::Controller::DevicePairingDelegate_OnCommissioningCompleteFunct callback) ++ chip::Controller::ScriptDevicePairingDelegate * pairingDelegate, ++ chip::Controller::DevicePairingDelegate_OnCommissioningCompleteFunct callback) + { +- sPairingDelegate.SetCommissioningCompleteCallback(callback); ++ pairingDelegate->SetCommissioningCompleteCallback(callback); + return ToPyChipError(CHIP_NO_ERROR); + } + + PyChipError pychip_ScriptDevicePairingDelegate_SetCommissioningStatusUpdateCallback( +- chip::Controller::DeviceCommissioner * devCtrl, ++ chip::Controller::ScriptDevicePairingDelegate * pairingDelegate, + chip::Controller::DevicePairingDelegate_OnCommissioningStatusUpdateFunct callback) + { +- sPairingDelegate.SetCommissioningStatusUpdateCallback(callback); ++ pairingDelegate->SetCommissioningStatusUpdateCallback(callback); ++ return ToPyChipError(CHIP_NO_ERROR); ++} ++ ++PyChipError ++pychip_ScriptDevicePairingDelegate_SetFabricCheckCallback(chip::Controller::ScriptDevicePairingDelegate * pairingDelegate, ++ chip::Controller::DevicePairingDelegate_OnFabricCheckFunct callback) ++{ ++ pairingDelegate->SetFabricCheckCallback(callback); + return ToPyChipError(CHIP_NO_ERROR); + } + + PyChipError +-pychip_ScriptDevicePairingDelegate_SetFabricCheckCallback(chip::Controller::DevicePairingDelegate_OnFabricCheckFunct callback) ++pychip_ScriptDevicePairingDelegate_SetExpectingPairingComplete(chip::Controller::ScriptDevicePairingDelegate * pairingDelegate, ++ bool value) + { +- sPairingDelegate.SetFabricCheckCallback(callback); ++ pairingDelegate->SetExpectingPairingComplete(value); + return ToPyChipError(CHIP_NO_ERROR); + } + +diff --git a/src/controller/python/OpCredsBinding.cpp b/src/controller/python/OpCredsBinding.cpp +index 79995d133b..66a6c84184 100644 +--- a/src/controller/python/OpCredsBinding.cpp ++++ b/src/controller/python/OpCredsBinding.cpp +@@ -402,12 +402,11 @@ void pychip_OnCommissioningStatusUpdate(chip::PeerId peerId, chip::Controller::C + * TODO(#25214): Need clean up API + * + */ +-PyChipError pychip_OpCreds_AllocateControllerForPythonCommissioningFLow(chip::Controller::DeviceCommissioner ** outDevCtrl, +- chip::python::pychip_P256Keypair * operationalKey, +- uint8_t * noc, uint32_t nocLen, uint8_t * icac, +- uint32_t icacLen, uint8_t * rcac, uint32_t rcacLen, +- const uint8_t * ipk, uint32_t ipkLen, +- chip::VendorId adminVendorId, bool enableServerInteractions) ++PyChipError pychip_OpCreds_AllocateControllerForPythonCommissioningFLow( ++ chip::Controller::DeviceCommissioner ** outDevCtrl, chip::Controller::ScriptDevicePairingDelegate ** outPairingDelegate, ++ chip::python::pychip_P256Keypair * operationalKey, uint8_t * noc, uint32_t nocLen, uint8_t * icac, uint32_t icacLen, ++ uint8_t * rcac, uint32_t rcacLen, const uint8_t * ipk, uint32_t ipkLen, chip::VendorId adminVendorId, ++ bool enableServerInteractions) + { + ReturnErrorCodeIf(nocLen > Controller::kMaxCHIPDERCertLength, ToPyChipError(CHIP_ERROR_NO_MEMORY)); + ReturnErrorCodeIf(icacLen > Controller::kMaxCHIPDERCertLength, ToPyChipError(CHIP_ERROR_NO_MEMORY)); +@@ -415,11 +414,13 @@ PyChipError pychip_OpCreds_AllocateControllerForPythonCommissioningFLow(chip::Co + + ChipLogDetail(Controller, "Creating New Device Controller"); + ++ auto pairingDelegate = std::make_unique(); ++ VerifyOrReturnError(pairingDelegate != nullptr, ToPyChipError(CHIP_ERROR_NO_MEMORY)); + auto devCtrl = std::make_unique(); + VerifyOrReturnError(devCtrl != nullptr, ToPyChipError(CHIP_ERROR_NO_MEMORY)); + + Controller::SetupParams initParams; +- initParams.pairingDelegate = &sPairingDelegate; ++ initParams.pairingDelegate = pairingDelegate.get(); + initParams.operationalCredentialsDelegate = &sPlaceholderOperationalCredentialsIssuer; + initParams.operationalKeypair = operationalKey; + initParams.controllerRCAC = ByteSpan(rcac, rcacLen); +@@ -450,13 +451,15 @@ PyChipError pychip_OpCreds_AllocateControllerForPythonCommissioningFLow(chip::Co + chip::Credentials::SetSingleIpkEpochKey(&sGroupDataProvider, devCtrl->GetFabricIndex(), fabricIpk, compressedFabricIdSpan); + VerifyOrReturnError(err == CHIP_NO_ERROR, ToPyChipError(err)); + +- *outDevCtrl = devCtrl.release(); ++ *outDevCtrl = devCtrl.release(); ++ *outPairingDelegate = pairingDelegate.release(); + + return ToPyChipError(CHIP_NO_ERROR); + } + + // TODO(#25214): Need clean up API + PyChipError pychip_OpCreds_AllocateController(OpCredsContext * context, chip::Controller::DeviceCommissioner ** outDevCtrl, ++ chip::Controller::ScriptDevicePairingDelegate ** outPairingDelegate, + FabricId fabricId, chip::NodeId nodeId, chip::VendorId adminVendorId, + const char * paaTrustStorePath, bool useTestCommissioner, + bool enableServerInteractions, CASEAuthTag * caseAuthTags, uint32_t caseAuthTagLen, +@@ -468,6 +471,8 @@ PyChipError pychip_OpCreds_AllocateController(OpCredsContext * context, chip::Co + + VerifyOrReturnError(context != nullptr, ToPyChipError(CHIP_ERROR_INVALID_ARGUMENT)); + ++ auto pairingDelegate = std::make_unique(); ++ VerifyOrReturnError(pairingDelegate != nullptr, ToPyChipError(CHIP_ERROR_NO_MEMORY)); + auto devCtrl = std::make_unique(); + VerifyOrReturnError(devCtrl != nullptr, ToPyChipError(CHIP_ERROR_NO_MEMORY)); + +@@ -524,7 +529,7 @@ PyChipError pychip_OpCreds_AllocateController(OpCredsContext * context, chip::Co + VerifyOrReturnError(err == CHIP_NO_ERROR, ToPyChipError(err)); + + Controller::SetupParams initParams; +- initParams.pairingDelegate = &sPairingDelegate; ++ initParams.pairingDelegate = pairingDelegate.get(); + initParams.operationalCredentialsDelegate = context->mAdapter.get(); + initParams.operationalKeypair = controllerKeyPair; + initParams.controllerRCAC = rcacSpan; +@@ -538,9 +543,9 @@ PyChipError pychip_OpCreds_AllocateController(OpCredsContext * context, chip::Co + if (useTestCommissioner) + { + initParams.defaultCommissioner = &sTestCommissioner; +- sPairingDelegate.SetCommissioningSuccessCallback(pychip_OnCommissioningSuccess); +- sPairingDelegate.SetCommissioningFailureCallback(pychip_OnCommissioningFailure); +- sPairingDelegate.SetCommissioningStatusUpdateCallback(pychip_OnCommissioningStatusUpdate); ++ pairingDelegate->SetCommissioningSuccessCallback(pychip_OnCommissioningSuccess); ++ pairingDelegate->SetCommissioningFailureCallback(pychip_OnCommissioningFailure); ++ pairingDelegate->SetCommissioningStatusUpdateCallback(pychip_OnCommissioningStatusUpdate); + } + + err = Controller::DeviceControllerFactory::GetInstance().SetupCommissioner(initParams, *devCtrl); +@@ -562,7 +567,8 @@ PyChipError pychip_OpCreds_AllocateController(OpCredsContext * context, chip::Co + chip::Credentials::SetSingleIpkEpochKey(&sGroupDataProvider, devCtrl->GetFabricIndex(), defaultIpk, compressedFabricIdSpan); + VerifyOrReturnError(err == CHIP_NO_ERROR, ToPyChipError(err)); + +- *outDevCtrl = devCtrl.release(); ++ *outDevCtrl = devCtrl.release(); ++ *outPairingDelegate = pairingDelegate.release(); + + return ToPyChipError(CHIP_NO_ERROR); + } +@@ -596,7 +602,8 @@ void pychip_OpCreds_FreeDelegate(OpCredsContext * context) + Platform::Delete(context); + } + +-PyChipError pychip_DeviceController_DeleteDeviceController(chip::Controller::DeviceCommissioner * devCtrl) ++PyChipError pychip_DeviceController_DeleteDeviceController(chip::Controller::DeviceCommissioner * devCtrl, ++ chip::Controller::ScriptDevicePairingDelegate * pairingDelegate) + { + if (devCtrl != nullptr) + { +@@ -604,6 +611,11 @@ PyChipError pychip_DeviceController_DeleteDeviceController(chip::Controller::Dev + delete devCtrl; + } + ++ if (pairingDelegate != nullptr) ++ { ++ delete pairingDelegate; ++ } ++ + return ToPyChipError(CHIP_NO_ERROR); + } + +diff --git a/src/controller/python/chip/ChipDeviceCtrl.py b/src/controller/python/chip/ChipDeviceCtrl.py +index b3d0aa2d7f..9cbd7a32d2 100644 +--- a/src/controller/python/chip/ChipDeviceCtrl.py ++++ b/src/controller/python/chip/ChipDeviceCtrl.py +@@ -253,8 +253,10 @@ class ChipDeviceControllerBase(): + + self._InitLib() + ++ pairingDelegate = c_void_p(None) + devCtrl = c_void_p(None) + ++ self.pairingDelegate = pairingDelegate + self.devCtrl = devCtrl + self.name = name + self.fabricCheckNodeId = -1 +@@ -263,7 +265,7 @@ class ChipDeviceControllerBase(): + self._Cluster = ChipClusters(builtins.chipStack) + self._Cluster.InitLib(self._dmLib) + +- def _set_dev_ctrl(self, devCtrl): ++ def _set_dev_ctrl(self, devCtrl, pairingDelegate): + def HandleCommissioningComplete(nodeid, err): + if err.is_success: + logging.info("Commissioning complete") +@@ -321,25 +323,26 @@ class ChipDeviceControllerBase(): + if not err.is_success: + HandleCommissioningComplete(0, err) + ++ self.pairingDelegate = pairingDelegate + self.devCtrl = devCtrl + + self.cbHandlePASEEstablishmentCompleteFunct = _DevicePairingDelegate_OnPairingCompleteFunct( + HandlePASEEstablishmentComplete) + self._dmLib.pychip_ScriptDevicePairingDelegate_SetKeyExchangeCallback( +- self.devCtrl, self.cbHandlePASEEstablishmentCompleteFunct) ++ self.pairingDelegate, self.cbHandlePASEEstablishmentCompleteFunct) + + self.cbHandleCommissioningCompleteFunct = _DevicePairingDelegate_OnCommissioningCompleteFunct( + HandleCommissioningComplete) + self._dmLib.pychip_ScriptDevicePairingDelegate_SetCommissioningCompleteCallback( +- self.devCtrl, self.cbHandleCommissioningCompleteFunct) ++ self.pairingDelegate, self.cbHandleCommissioningCompleteFunct) + + self.cbHandleFabricCheckFunct = _DevicePairingDelegate_OnFabricCheckFunct(HandleFabricCheck) +- self._dmLib.pychip_ScriptDevicePairingDelegate_SetFabricCheckCallback(self.cbHandleFabricCheckFunct) ++ self._dmLib.pychip_ScriptDevicePairingDelegate_SetFabricCheckCallback(self.pairingDelegate, self.cbHandleFabricCheckFunct) + + self.cbHandleOpenWindowCompleteFunct = _DevicePairingDelegate_OnOpenWindowCompleteFunct( + HandleOpenWindowComplete) + self._dmLib.pychip_ScriptDevicePairingDelegate_SetOpenWindowCompleteCallback( +- self.devCtrl, self.cbHandleOpenWindowCompleteFunct) ++ self.pairingDelegate, self.cbHandleOpenWindowCompleteFunct) + + self.cbHandleDeviceUnpairCompleteFunct = _DeviceUnpairingCompleteFunct(HandleUnpairDeviceComplete) + +@@ -355,6 +358,11 @@ class ChipDeviceControllerBase(): + + ChipDeviceController.activeList.add(self) + ++ def _enablePairingCompeleteCallback(self, value: bool): ++ self._ChipStack.Call( ++ lambda: self._dmLib.pychip_ScriptDevicePairingDelegate_SetExpectingPairingComplete(self.pairingDelegate, value) ++ ).raise_on_error() ++ + @property + def fabricAdmin(self) -> FabricAdmin.FabricAdmin: + return self._fabricAdmin +@@ -389,8 +397,9 @@ class ChipDeviceControllerBase(): + if self.devCtrl is not None: + self._ChipStack.Call( + lambda: self._dmLib.pychip_DeviceController_DeleteDeviceController( +- self.devCtrl) ++ self.devCtrl, self.pairingDelegate) + ).raise_on_error() ++ self.pairingDelegate = None + self.devCtrl = None + + ChipDeviceController.activeList.remove(self) +@@ -437,6 +446,7 @@ class ChipDeviceControllerBase(): + self._ChipStack.commissioningCompleteEvent.clear() + + self.state = DCState.COMMISSIONING ++ self._enablePairingCompeleteCallback(True) + self._ChipStack.CallAsync( + lambda: self._dmLib.pychip_DeviceController_ConnectBLE( + self.devCtrl, discriminator, setupPinCode, nodeid) +@@ -487,6 +497,7 @@ class ChipDeviceControllerBase(): + self.CheckIsActive() + + self.state = DCState.RENDEZVOUS_ONGOING ++ self._enablePairingCompeleteCallback(True) + return self._ChipStack.CallAsync( + lambda: self._dmLib.pychip_DeviceController_EstablishPASESessionBLE( + self.devCtrl, setupPinCode, discriminator, nodeid) +@@ -496,6 +507,7 @@ class ChipDeviceControllerBase(): + self.CheckIsActive() + + self.state = DCState.RENDEZVOUS_ONGOING ++ self._enablePairingCompeleteCallback(True) + return self._ChipStack.CallAsync( + lambda: self._dmLib.pychip_DeviceController_EstablishPASESessionIP( + self.devCtrl, ipaddr.encode("utf-8"), setupPinCode, nodeid, port) +@@ -505,6 +517,7 @@ class ChipDeviceControllerBase(): + self.CheckIsActive() + + self.state = DCState.RENDEZVOUS_ONGOING ++ self._enablePairingCompeleteCallback(True) + return self._ChipStack.CallAsync( + lambda: self._dmLib.pychip_DeviceController_EstablishPASESession( + self.devCtrl, setUpCode.encode("utf-8"), nodeid) +@@ -726,7 +739,7 @@ class ChipDeviceControllerBase(): + self.CheckIsActive() + self._ChipStack.CallAsync( + lambda: self._dmLib.pychip_DeviceController_OpenCommissioningWindow( +- self.devCtrl, nodeid, timeout, iteration, discriminator, option) ++ self.devCtrl, self.pairingDelegate, nodeid, timeout, iteration, discriminator, option) + ).raise_on_error() + self._ChipStack.callbackRes.raise_on_error() + return self._ChipStack.openCommissioningWindowPincode[nodeid] +@@ -1565,16 +1578,13 @@ class ChipDeviceControllerBase(): + self._dmLib = CDLL(self._ChipStack.LocateChipDLL()) + + self._dmLib.pychip_DeviceController_DeleteDeviceController.argtypes = [ +- c_void_p] ++ c_void_p, c_void_p] + self._dmLib.pychip_DeviceController_DeleteDeviceController.restype = PyChipError + + self._dmLib.pychip_DeviceController_ConnectBLE.argtypes = [ + c_void_p, c_uint16, c_uint32, c_uint64] + self._dmLib.pychip_DeviceController_ConnectBLE.restype = PyChipError + +- self._dmLib.pychip_DeviceController_ConnectIP.argtypes = [ +- c_void_p, c_char_p, c_uint32, c_uint64] +- + self._dmLib.pychip_DeviceController_SetThreadOperationalDataset.argtypes = [ + c_char_p, c_uint32] + self._dmLib.pychip_DeviceController_SetThreadOperationalDataset.restype = PyChipError +@@ -1610,7 +1620,7 @@ class ChipDeviceControllerBase(): + self._dmLib.pychip_DeviceController_Commission.restype = PyChipError + + self._dmLib.pychip_DeviceController_OnNetworkCommission.argtypes = [ +- c_void_p, c_uint64, c_uint32, c_uint8, c_char_p, c_uint32] ++ c_void_p, c_void_p, c_uint64, c_uint32, c_uint8, c_char_p, c_uint32] + self._dmLib.pychip_DeviceController_OnNetworkCommission.restype = PyChipError + + self._dmLib.pychip_DeviceController_DiscoverCommissionableNodes.argtypes = [ +@@ -1648,6 +1658,7 @@ class ChipDeviceControllerBase(): + self._dmLib.pychip_DeviceController_EstablishPASESessionBLE.argtypes = [ + c_void_p, c_uint32, c_uint16, c_uint64] + self._dmLib.pychip_DeviceController_EstablishPASESessionBLE.restype = PyChipError ++ + self._dmLib.pychip_DeviceController_EstablishPASESession.argtypes = [ + c_void_p, c_char_p, c_uint64] + self._dmLib.pychip_DeviceController_EstablishPASESession.restype = PyChipError +@@ -1655,10 +1666,12 @@ class ChipDeviceControllerBase(): + self._dmLib.pychip_DeviceController_DiscoverAllCommissionableNodes.argtypes = [ + c_void_p] + self._dmLib.pychip_DeviceController_DiscoverAllCommissionableNodes.restype = PyChipError ++ + self._dmLib.pychip_DeviceController_PrintDiscoveredDevices.argtypes = [ + c_void_p] + self._dmLib.pychip_DeviceController_PrintDiscoveredDevices.argtypes = [ + c_void_p, _ChipDeviceController_IterateDiscoveredCommissionableNodesFunct] ++ + self._dmLib.pychip_DeviceController_HasDiscoveredCommissionableNode.argtypes = [c_void_p] + self._dmLib.pychip_DeviceController_HasDiscoveredCommissionableNode.restype = c_bool + +@@ -1703,9 +1716,13 @@ class ChipDeviceControllerBase(): + self._dmLib.pychip_ScriptDevicePairingDelegate_SetCommissioningStatusUpdateCallback.restype = PyChipError + + self._dmLib.pychip_ScriptDevicePairingDelegate_SetFabricCheckCallback.argtypes = [ +- _DevicePairingDelegate_OnFabricCheckFunct] ++ c_void_p, _DevicePairingDelegate_OnFabricCheckFunct] + self._dmLib.pychip_ScriptDevicePairingDelegate_SetFabricCheckCallback.restype = PyChipError + ++ self._dmLib.pychip_ScriptDevicePairingDelegate_SetExpectingPairingComplete.argtypes = [ ++ c_void_p, c_bool] ++ self._dmLib.pychip_ScriptDevicePairingDelegate_SetExpectingPairingComplete.restype = PyChipError ++ + self._dmLib.pychip_GetConnectedDeviceByNodeId.argtypes = [ + c_void_p, c_uint64, py_object, _DeviceAvailableCallbackFunct] + self._dmLib.pychip_GetConnectedDeviceByNodeId.restype = PyChipError +@@ -1733,8 +1750,9 @@ class ChipDeviceControllerBase(): + self._dmLib.pychip_DeviceController_GetCompressedFabricId.restype = PyChipError + + self._dmLib.pychip_DeviceController_OpenCommissioningWindow.argtypes = [ +- c_void_p, c_uint64, c_uint16, c_uint32, c_uint16, c_uint8] ++ c_void_p, c_void_p, c_uint64, c_uint16, c_uint32, c_uint16, c_uint8] + self._dmLib.pychip_DeviceController_OpenCommissioningWindow.restype = PyChipError ++ + self._dmLib.pychip_TestCommissionerUsed.argtypes = [] + self._dmLib.pychip_TestCommissionerUsed.restype = c_bool + +@@ -1750,6 +1768,7 @@ class ChipDeviceControllerBase(): + self._dmLib.pychip_SetTestCommissionerSimulateFailureOnStage.argtypes = [ + c_uint8] + self._dmLib.pychip_SetTestCommissionerSimulateFailureOnStage.restype = c_bool ++ + self._dmLib.pychip_SetTestCommissionerSimulateFailureOnReport.argtypes = [ + c_uint8] + self._dmLib.pychip_SetTestCommissionerSimulateFailureOnReport.restype = c_bool +@@ -1762,8 +1781,7 @@ class ChipDeviceControllerBase(): + self._dmLib.pychip_GetCompletionError.restype = PyChipError + + self._dmLib.pychip_DeviceController_IssueNOCChain.argtypes = [ +- c_void_p, py_object, c_char_p, c_size_t, c_uint64 +- ] ++ c_void_p, py_object, c_char_p, c_size_t, c_uint64] + self._dmLib.pychip_DeviceController_IssueNOCChain.restype = PyChipError + + self._dmLib.pychip_OpCreds_InitGroupTestingData.argtypes = [ +@@ -1784,11 +1802,11 @@ class ChipDeviceControllerBase(): + self._dmLib.pychip_DeviceController_GetLogFilter = c_uint8 + + self._dmLib.pychip_OpCreds_AllocateController.argtypes = [c_void_p, POINTER( +- c_void_p), c_uint64, c_uint64, c_uint16, c_char_p, c_bool, c_bool, POINTER(c_uint32), c_uint32, c_void_p] ++ c_void_p), POINTER(c_void_p), c_uint64, c_uint64, c_uint16, c_char_p, c_bool, c_bool, POINTER(c_uint32), c_uint32, c_void_p] + self._dmLib.pychip_OpCreds_AllocateController.restype = PyChipError + + self._dmLib.pychip_OpCreds_AllocateControllerForPythonCommissioningFLow.argtypes = [ +- POINTER(c_void_p), c_void_p, POINTER(c_char), c_uint32, POINTER(c_char), c_uint32, POINTER(c_char), c_uint32, POINTER(c_char), c_uint32, c_uint16, c_bool] ++ POINTER(c_void_p), POINTER(c_void_p), c_void_p, POINTER(c_char), c_uint32, POINTER(c_char), c_uint32, POINTER(c_char), c_uint32, POINTER(c_char), c_uint32, c_uint16, c_bool] + self._dmLib.pychip_OpCreds_AllocateControllerForPythonCommissioningFLow.restype = PyChipError + + self._dmLib.pychip_DeviceController_SetIpk.argtypes = [c_void_p, POINTER(c_char), c_size_t] +@@ -1810,6 +1828,7 @@ class ChipDeviceController(ChipDeviceControllerBase): + + self._dmLib.pychip_DeviceController_SetIssueNOCChainCallbackPythonCallback(_IssueNOCChainCallbackPythonCallback) + ++ pairingDelegate = c_void_p(None) + devCtrl = c_void_p(None) + + c_catTags = (c_uint32 * len(catTags))() +@@ -1821,7 +1840,7 @@ class ChipDeviceController(ChipDeviceControllerBase): + self._externalKeyPair = keypair + self._ChipStack.Call( + lambda: self._dmLib.pychip_OpCreds_AllocateController(c_void_p( +- opCredsContext), pointer(devCtrl), fabricId, nodeId, adminVendorId, c_char_p(None if len(paaTrustStorePath) == 0 else str.encode(paaTrustStorePath)), useTestCommissioner, self._ChipStack.enableServerInteractions, c_catTags, len(catTags), None if keypair is None else keypair.native_object) ++ opCredsContext), pointer(devCtrl), pointer(pairingDelegate), fabricId, nodeId, adminVendorId, c_char_p(None if len(paaTrustStorePath) == 0 else str.encode(paaTrustStorePath)), useTestCommissioner, self._ChipStack.enableServerInteractions, c_catTags, len(catTags), None if keypair is None else keypair.native_object) + ).raise_on_error() + + self._fabricAdmin = fabricAdmin +@@ -1829,7 +1848,7 @@ class ChipDeviceController(ChipDeviceControllerBase): + self._nodeId = nodeId + self._caIndex = fabricAdmin.caIndex + +- self._set_dev_ctrl(devCtrl=devCtrl) ++ self._set_dev_ctrl(devCtrl=devCtrl, pairingDelegate=pairingDelegate) + + self._finish_init() + +@@ -1975,9 +1994,10 @@ class ChipDeviceController(ChipDeviceControllerBase): + + self._ChipStack.commissioningCompleteEvent.clear() + ++ self._enablePairingCompeleteCallback(True) + self._ChipStack.CallAsync( + lambda: self._dmLib.pychip_DeviceController_OnNetworkCommission( +- self.devCtrl, nodeId, setupPinCode, int(filterType), str(filter).encode("utf-8") + b"\x00" if filter is not None else None, discoveryTimeoutMsec) ++ self.devCtrl, self.pairingDelegate, nodeId, setupPinCode, int(filterType), str(filter).encode("utf-8") + b"\x00" if filter is not None else None, discoveryTimeoutMsec) + ) + if not self._ChipStack.commissioningCompleteEvent.isSet(): + # Error 50 is a timeout +@@ -1998,6 +2018,7 @@ class ChipDeviceController(ChipDeviceControllerBase): + + self._ChipStack.commissioningCompleteEvent.clear() + ++ self._enablePairingCompeleteCallback(True) + self._ChipStack.CallAsync( + lambda: self._dmLib.pychip_DeviceController_ConnectWithCode( + self.devCtrl, setupPayload, nodeid, discoveryType.value) +@@ -2017,6 +2038,7 @@ class ChipDeviceController(ChipDeviceControllerBase): + + self._ChipStack.commissioningCompleteEvent.clear() + ++ self._enablePairingCompeleteCallback(True) + self._ChipStack.CallAsync( + lambda: self._dmLib.pychip_DeviceController_ConnectIP( + self.devCtrl, ipaddr.encode("utf-8"), setupPinCode, nodeid) +@@ -2061,6 +2083,7 @@ class BareChipDeviceController(ChipDeviceControllerBase): + ''' + super().__init__(name or f"ctrl(v/{adminVendorId})") + ++ pairingDelegate = c_void_p(None) + devCtrl = c_void_p(None) + + # Device should hold a reference to the key to avoid it being GC-ed. +@@ -2069,9 +2092,9 @@ class BareChipDeviceController(ChipDeviceControllerBase): + + self._ChipStack.Call( + lambda: self._dmLib.pychip_OpCreds_AllocateControllerForPythonCommissioningFLow( +- c_void_p(devCtrl), nativeKey, noc, len(noc), icac, len(icac) if icac else 0, rcac, len(rcac), ipk, len(ipk) if ipk else 0, adminVendorId, self._ChipStack.enableServerInteractions) ++ c_void_p(devCtrl), c_void_p(pairingDelegate), nativeKey, noc, len(noc), icac, len(icac) if icac else 0, rcac, len(rcac), ipk, len(ipk) if ipk else 0, adminVendorId, self._ChipStack.enableServerInteractions) + ).raise_on_error() + +- self._set_dev_ctrl(devCtrl) ++ self._set_dev_ctrl(devCtrl, pairingDelegate) + + self._finish_init() +-- +2.45.2 + diff --git a/0011-Python-Call-SDK-asyncio-friendly-32764.patch b/0011-Python-Call-SDK-asyncio-friendly-32764.patch new file mode 100644 index 0000000..d70ef52 --- /dev/null +++ b/0011-Python-Call-SDK-asyncio-friendly-32764.patch @@ -0,0 +1,359 @@ +From 56b56ee08006d935cb86dd3fb38e96d79c78bb45 Mon Sep 17 00:00:00 2001 +From: Stefan Agner +Date: Thu, 16 May 2024 09:35:25 +0200 +Subject: [PATCH] [Python] Call SDK asyncio friendly (#32764) + +* [Python] Rename CallAsync to CallAsyncWithCallback + +CallAsync continuously calls a callback function during the wait +for the call. Rename the function to reflect that fact. + +This frees up CallAsync for an asyncio friendly implementation. + +* [Python] Implement asyncio variant of CallAsync + +Call Matter SDK in a asyncio friendly way. During posting of the task +onto the CHIP mainloop, it makes sure that the asyncio loop is not +blocked. + +* [Python] Use CallAsync where appropriate + +* Rename AsyncSimpleCallableHandle to AsyncioCallableHandle + +* Rename CallAsyncWithCallback to CallAsyncWithCompleteCallback + +Also add a comment that the function needs to be released by registering +a callback and setting the complete event. + +* Add comments about lock +--- + src/controller/python/chip/ChipDeviceCtrl.py | 43 ++++++++------- + src/controller/python/chip/ChipStack.py | 53 ++++++++++++++++++- + .../python/chip/clusters/Command.py | 16 +++--- + 3 files changed, 83 insertions(+), 29 deletions(-) + +diff --git a/src/controller/python/chip/ChipDeviceCtrl.py b/src/controller/python/chip/ChipDeviceCtrl.py +index 9cbd7a32d2..3282ffd191 100644 +--- a/src/controller/python/chip/ChipDeviceCtrl.py ++++ b/src/controller/python/chip/ChipDeviceCtrl.py +@@ -186,7 +186,7 @@ class DeviceProxyWrapper(): + def __del__(self): + if (self._dmLib is not None and hasattr(builtins, 'chipStack') and builtins.chipStack is not None): + # This destructor is called from any threading context, including on the Matter threading context. +- # So, we cannot call chipStack.Call or chipStack.CallAsync which waits for the posted work to ++ # So, we cannot call chipStack.Call or chipStack.CallAsyncWithCompleteCallback which waits for the posted work to + # actually be executed. Instead, we just post/schedule the work and move on. + builtins.chipStack.PostTaskOnChipThread(lambda: self._dmLib.pychip_FreeOperationalDeviceProxy(self._deviceProxy)) + +@@ -447,7 +447,7 @@ class ChipDeviceControllerBase(): + + self.state = DCState.COMMISSIONING + self._enablePairingCompeleteCallback(True) +- self._ChipStack.CallAsync( ++ self._ChipStack.CallAsyncWithCompleteCallback( + lambda: self._dmLib.pychip_DeviceController_ConnectBLE( + self.devCtrl, discriminator, setupPinCode, nodeid) + ).raise_on_error() +@@ -459,7 +459,7 @@ class ChipDeviceControllerBase(): + def UnpairDevice(self, nodeid: int): + self.CheckIsActive() + +- return self._ChipStack.CallAsync( ++ return self._ChipStack.CallAsyncWithCompleteCallback( + lambda: self._dmLib.pychip_DeviceController_UnpairDevice( + self.devCtrl, nodeid, self.cbHandleDeviceUnpairCompleteFunct) + ).raise_on_error() +@@ -498,7 +498,7 @@ class ChipDeviceControllerBase(): + + self.state = DCState.RENDEZVOUS_ONGOING + self._enablePairingCompeleteCallback(True) +- return self._ChipStack.CallAsync( ++ return self._ChipStack.CallAsyncWithCompleteCallback( + lambda: self._dmLib.pychip_DeviceController_EstablishPASESessionBLE( + self.devCtrl, setupPinCode, discriminator, nodeid) + ) +@@ -508,7 +508,7 @@ class ChipDeviceControllerBase(): + + self.state = DCState.RENDEZVOUS_ONGOING + self._enablePairingCompeleteCallback(True) +- return self._ChipStack.CallAsync( ++ return self._ChipStack.CallAsyncWithCompleteCallback( + lambda: self._dmLib.pychip_DeviceController_EstablishPASESessionIP( + self.devCtrl, ipaddr.encode("utf-8"), setupPinCode, nodeid, port) + ) +@@ -518,7 +518,7 @@ class ChipDeviceControllerBase(): + + self.state = DCState.RENDEZVOUS_ONGOING + self._enablePairingCompeleteCallback(True) +- return self._ChipStack.CallAsync( ++ return self._ChipStack.CallAsyncWithCompleteCallback( + lambda: self._dmLib.pychip_DeviceController_EstablishPASESession( + self.devCtrl, setUpCode.encode("utf-8"), nodeid) + ) +@@ -737,7 +737,7 @@ class ChipDeviceControllerBase(): + Returns CommissioningParameters + ''' + self.CheckIsActive() +- self._ChipStack.CallAsync( ++ self._ChipStack.CallAsyncWithCompleteCallback( + lambda: self._dmLib.pychip_DeviceController_OpenCommissioningWindow( + self.devCtrl, self.pairingDelegate, nodeid, timeout, iteration, discriminator, option) + ).raise_on_error() +@@ -842,7 +842,7 @@ class ChipDeviceControllerBase(): + + if allowPASE: + returnDevice = c_void_p(None) +- res = self._ChipStack.Call(lambda: self._dmLib.pychip_GetDeviceBeingCommissioned( ++ res = await self._ChipStack.CallAsync(lambda: self._dmLib.pychip_GetDeviceBeingCommissioned( + self.devCtrl, nodeid, byref(returnDevice)), timeoutMs) + if res.is_success: + logging.info('Using PASE connection') +@@ -872,11 +872,12 @@ class ChipDeviceControllerBase(): + + closure = DeviceAvailableClosure(eventLoop, future) + ctypes.pythonapi.Py_IncRef(ctypes.py_object(closure)) +- self._ChipStack.Call(lambda: self._dmLib.pychip_GetConnectedDeviceByNodeId( ++ res = await self._ChipStack.CallAsync(lambda: self._dmLib.pychip_GetConnectedDeviceByNodeId( + self.devCtrl, nodeid, ctypes.py_object(closure), _DeviceAvailableCallback), +- timeoutMs).raise_on_error() ++ timeoutMs) ++ res.raise_on_error() + +- # The callback might have been received synchronously (during self._ChipStack.Call()). ++ # The callback might have been received synchronously (during self._ChipStack.CallAsync()). + # In that case the Future has already been set it will return immediately + if (timeoutMs): + timeout = float(timeoutMs) / 1000 +@@ -1004,13 +1005,14 @@ class ChipDeviceControllerBase(): + future = eventLoop.create_future() + + device = await self.GetConnectedDevice(nodeid, timeoutMs=interactionTimeoutMs) +- ClusterCommand.SendCommand( ++ res = await ClusterCommand.SendCommand( + future, eventLoop, responseType, device.deviceProxy, ClusterCommand.CommandPath( + EndpointId=endpoint, + ClusterId=payload.cluster_id, + CommandId=payload.command_id, + ), payload, timedRequestTimeoutMs=timedRequestTimeoutMs, +- interactionTimeoutMs=interactionTimeoutMs, busyWaitMs=busyWaitMs, suppressResponse=suppressResponse).raise_on_error() ++ interactionTimeoutMs=interactionTimeoutMs, busyWaitMs=busyWaitMs, suppressResponse=suppressResponse) ++ res.raise_on_error() + return await future + + async def SendBatchCommands(self, nodeid: int, commands: typing.List[ClusterCommand.InvokeRequestInfo], +@@ -1046,10 +1048,11 @@ class ChipDeviceControllerBase(): + + device = await self.GetConnectedDevice(nodeid, timeoutMs=interactionTimeoutMs) + +- ClusterCommand.SendBatchCommands( ++ res = await ClusterCommand.SendBatchCommands( + future, eventLoop, device.deviceProxy, commands, + timedRequestTimeoutMs=timedRequestTimeoutMs, +- interactionTimeoutMs=interactionTimeoutMs, busyWaitMs=busyWaitMs, suppressResponse=suppressResponse).raise_on_error() ++ interactionTimeoutMs=interactionTimeoutMs, busyWaitMs=busyWaitMs, suppressResponse=suppressResponse) ++ res.raise_on_error() + return await future + + def SendGroupCommand(self, groupid: int, payload: ClusterObjects.ClusterCommand, busyWaitMs: typing.Union[None, int] = None): +@@ -1879,7 +1882,7 @@ class ChipDeviceController(ChipDeviceControllerBase): + self._ChipStack.commissioningCompleteEvent.clear() + self.state = DCState.COMMISSIONING + +- self._ChipStack.CallAsync( ++ self._ChipStack.CallAsyncWithCompleteCallback( + lambda: self._dmLib.pychip_DeviceController_Commission( + self.devCtrl, nodeid) + ) +@@ -1995,7 +1998,7 @@ class ChipDeviceController(ChipDeviceControllerBase): + self._ChipStack.commissioningCompleteEvent.clear() + + self._enablePairingCompeleteCallback(True) +- self._ChipStack.CallAsync( ++ self._ChipStack.CallAsyncWithCompleteCallback( + lambda: self._dmLib.pychip_DeviceController_OnNetworkCommission( + self.devCtrl, self.pairingDelegate, nodeId, setupPinCode, int(filterType), str(filter).encode("utf-8") + b"\x00" if filter is not None else None, discoveryTimeoutMsec) + ) +@@ -2019,7 +2022,7 @@ class ChipDeviceController(ChipDeviceControllerBase): + self._ChipStack.commissioningCompleteEvent.clear() + + self._enablePairingCompeleteCallback(True) +- self._ChipStack.CallAsync( ++ self._ChipStack.CallAsyncWithCompleteCallback( + lambda: self._dmLib.pychip_DeviceController_ConnectWithCode( + self.devCtrl, setupPayload, nodeid, discoveryType.value) + ) +@@ -2039,7 +2042,7 @@ class ChipDeviceController(ChipDeviceControllerBase): + self._ChipStack.commissioningCompleteEvent.clear() + + self._enablePairingCompeleteCallback(True) +- self._ChipStack.CallAsync( ++ self._ChipStack.CallAsyncWithCompleteCallback( + lambda: self._dmLib.pychip_DeviceController_ConnectIP( + self.devCtrl, ipaddr.encode("utf-8"), setupPinCode, nodeid) + ) +@@ -2053,7 +2056,7 @@ class ChipDeviceController(ChipDeviceControllerBase): + The NOC chain will be provided in TLV cert format.""" + self.CheckIsActive() + +- return self._ChipStack.CallAsync( ++ return self._ChipStack.CallAsyncWithCompleteCallback( + lambda: self._dmLib.pychip_DeviceController_IssueNOCChain( + self.devCtrl, py_object(self), csr.NOCSRElements, len(csr.NOCSRElements), nodeId) + ) +diff --git a/src/controller/python/chip/ChipStack.py b/src/controller/python/chip/ChipStack.py +index 6df7e41de4..35f9e24ef4 100644 +--- a/src/controller/python/chip/ChipStack.py ++++ b/src/controller/python/chip/ChipStack.py +@@ -26,6 +26,7 @@ + + from __future__ import absolute_import, print_function + ++import asyncio + import builtins + import logging + import os +@@ -164,6 +165,35 @@ class AsyncCallableHandle: + return self._res + + ++class AsyncioCallableHandle: ++ """Class which handles Matter SDK Calls asyncio friendly""" ++ ++ def __init__(self, callback): ++ self._callback = callback ++ self._loop = asyncio.get_event_loop() ++ self._future = self._loop.create_future() ++ self._result = None ++ self._exception = None ++ ++ @property ++ def future(self): ++ return self._future ++ ++ def _done(self): ++ if self._exception: ++ self._future.set_exception(self._exception) ++ else: ++ self._future.set_result(self._result) ++ ++ def __call__(self): ++ try: ++ self._result = self._callback() ++ except Exception as ex: ++ self._exception = ex ++ self._loop.call_soon_threadsafe(self._done) ++ pythonapi.Py_DecRef(py_object(self)) ++ ++ + _CompleteFunct = CFUNCTYPE(None, c_void_p, c_void_p) + _ErrorFunct = CFUNCTYPE(None, c_void_p, c_void_p, + c_ulong, POINTER(DeviceStatusStruct)) +@@ -178,6 +208,7 @@ class ChipStack(object): + bluetoothAdapter=None, enableServerInteractions=True): + builtins.enableDebugMode = False + ++ # TODO: Probably no longer necessary, see https://github.com/project-chip/connectedhomeip/issues/33321. + self.networkLock = Lock() + self.completeEvent = Event() + self.commissioningCompleteEvent = Event() +@@ -318,6 +349,7 @@ class ChipStack(object): + logFunct = 0 + if not isinstance(logFunct, _LogMessageFunct): + logFunct = _LogMessageFunct(logFunct) ++ # TODO: Lock probably no longer necessary, see https://github.com/project-chip/connectedhomeip/issues/33321. + with self.networkLock: + # NOTE: ChipStack must hold a reference to the CFUNCTYPE object while it is + # set. Otherwise it may get garbage collected, and logging calls from the +@@ -360,6 +392,7 @@ class ChipStack(object): + # throw error if op in progress + self.callbackRes = None + self.completeEvent.clear() ++ # TODO: Lock probably no longer necessary, see https://github.com/project-chip/connectedhomeip/issues/33321. + with self.networkLock: + res = self.PostTaskOnChipThread(callFunct).Wait(timeoutMs) + self.completeEvent.set() +@@ -367,14 +400,32 @@ class ChipStack(object): + return self.callbackRes + return res + +- def CallAsync(self, callFunct): ++ async def CallAsync(self, callFunct, timeoutMs: int = None): ++ '''Run a Python function on CHIP stack, and wait for the response. ++ This function will post a task on CHIP mainloop and waits for the call response in a asyncio friendly manner. ++ ''' ++ callObj = AsyncioCallableHandle(callFunct) ++ pythonapi.Py_IncRef(py_object(callObj)) ++ ++ res = self._ChipStackLib.pychip_DeviceController_PostTaskOnChipThread( ++ self.cbHandleChipThreadRun, py_object(callObj)) ++ ++ if not res.is_success: ++ pythonapi.Py_DecRef(py_object(callObj)) ++ raise res.to_exception() ++ ++ return await asyncio.wait_for(callObj.future, timeoutMs / 1000 if timeoutMs else None) ++ ++ def CallAsyncWithCompleteCallback(self, callFunct): + '''Run a Python function on CHIP stack, and wait for the application specific response. + This function is a wrapper of PostTaskOnChipThread, which includes some handling of application specific logics. + Calling this function on CHIP on CHIP mainloop thread will cause deadlock. ++ Make sure to register the necessary callbacks which release the function by setting the completeEvent. + ''' + # throw error if op in progress + self.callbackRes = None + self.completeEvent.clear() ++ # TODO: Lock probably no longer necessary, see https://github.com/project-chip/connectedhomeip/issues/33321. + with self.networkLock: + res = self.PostTaskOnChipThread(callFunct).Wait() + +diff --git a/src/controller/python/chip/clusters/Command.py b/src/controller/python/chip/clusters/Command.py +index 89aae537c9..6ef25cb211 100644 +--- a/src/controller/python/chip/clusters/Command.py ++++ b/src/controller/python/chip/clusters/Command.py +@@ -291,9 +291,9 @@ def TestOnlySendCommandTimedRequestFlagWithNoTimedInvoke(future: Future, eventLo + )) + + +-def SendCommand(future: Future, eventLoop, responseType: Type, device, commandPath: CommandPath, payload: ClusterCommand, +- timedRequestTimeoutMs: Union[None, int] = None, interactionTimeoutMs: Union[None, int] = None, busyWaitMs: Union[None, int] = None, +- suppressResponse: Union[None, bool] = None) -> PyChipError: ++async def SendCommand(future: Future, eventLoop, responseType: Type, device, commandPath: CommandPath, payload: ClusterCommand, ++ timedRequestTimeoutMs: Union[None, int] = None, interactionTimeoutMs: Union[None, int] = None, ++ busyWaitMs: Union[None, int] = None, suppressResponse: Union[None, bool] = None) -> PyChipError: + ''' Send a cluster-object encapsulated command to a device and does the following: + - On receipt of a successful data response, returns the cluster-object equivalent through the provided future. + - None (on a successful response containing no data) +@@ -316,7 +316,7 @@ def SendCommand(future: Future, eventLoop, responseType: Type, device, commandPa + + payloadTLV = payload.ToTLV() + ctypes.pythonapi.Py_IncRef(ctypes.py_object(transaction)) +- return builtins.chipStack.Call( ++ return await builtins.chipStack.CallAsync( + lambda: handle.pychip_CommandSender_SendCommand( + ctypes.py_object(transaction), device, + c_uint16(0 if timedRequestTimeoutMs is None else timedRequestTimeoutMs), commandPath.EndpointId, +@@ -353,9 +353,9 @@ def _BuildPyInvokeRequestData(commands: List[InvokeRequestInfo], timedRequestTim + return pyBatchCommandsData + + +-def SendBatchCommands(future: Future, eventLoop, device, commands: List[InvokeRequestInfo], +- timedRequestTimeoutMs: Optional[int] = None, interactionTimeoutMs: Optional[int] = None, busyWaitMs: Optional[int] = None, +- suppressResponse: Optional[bool] = None) -> PyChipError: ++async def SendBatchCommands(future: Future, eventLoop, device, commands: List[InvokeRequestInfo], ++ timedRequestTimeoutMs: Optional[int] = None, interactionTimeoutMs: Optional[int] = None, ++ busyWaitMs: Optional[int] = None, suppressResponse: Optional[bool] = None) -> PyChipError: + ''' Initiates an InvokeInteraction with the batch commands provided. + + Arguments: +@@ -388,7 +388,7 @@ def SendBatchCommands(future: Future, eventLoop, device, commands: List[InvokeRe + transaction = AsyncBatchCommandsTransaction(future, eventLoop, responseTypes) + ctypes.pythonapi.Py_IncRef(ctypes.py_object(transaction)) + +- return builtins.chipStack.Call( ++ return await builtins.chipStack.CallAsync( + lambda: handle.pychip_CommandSender_SendBatchCommands( + py_object(transaction), device, + c_uint16(0 if timedRequestTimeoutMs is None else timedRequestTimeoutMs), +-- +2.45.2 + diff --git a/0012-Python-Make-AttributePath-more-pythonic-33571.patch b/0012-Python-Make-AttributePath-more-pythonic-33571.patch new file mode 100644 index 0000000..151e6ea --- /dev/null +++ b/0012-Python-Make-AttributePath-more-pythonic-33571.patch @@ -0,0 +1,308 @@ +From 518fdbac13ace67cfbd4482286320f4c45ab1b05 Mon Sep 17 00:00:00 2001 +From: Stefan Agner +Date: Fri, 24 May 2024 10:39:41 +0200 +Subject: [PATCH] [Python] Make AttributePath more pythonic (#33571) + +* [Python] Make AttributePath more pythonic + +Use dataclass default initializer to initialize AttributePath. Use +static method to initialize from Cluster or Attribute. + +Also hash the integer fields directly, this is more efficient than +formatting a string first. + +* Drop AttributePathWithListIndex + +Drop AttributePathWithListIndex as it is unused. + +* Make DataVersionFilter/EventPath pythonic as well + +Use frozen data classes and static initializers similar to +AttributePath. + +* Fix _parseEventPathTuple + +* Fix _parseDataVersionFilterTuple +--- + src/controller/python/chip/ChipDeviceCtrl.py | 47 +++------ + .../python/chip/clusters/Attribute.py | 97 ++++++------------- + .../test/test_scripts/cluster_objects.py | 2 +- + 3 files changed, 48 insertions(+), 98 deletions(-) + +diff --git a/src/controller/python/chip/ChipDeviceCtrl.py b/src/controller/python/chip/ChipDeviceCtrl.py +index 3282ffd191..4d14a42f18 100644 +--- a/src/controller/python/chip/ChipDeviceCtrl.py ++++ b/src/controller/python/chip/ChipDeviceCtrl.py +@@ -1154,33 +1154,26 @@ class ChipDeviceControllerBase(): + # Concrete path + typing.Tuple[int, typing.Type[ClusterObjects.ClusterAttributeDescriptor]] + ]): +- endpoint = None +- cluster = None +- attribute = None +- + if pathTuple == ('*') or pathTuple == (): + # Wildcard +- pass ++ return ClusterAttribute.AttributePath() + elif not isinstance(pathTuple, tuple): + if isinstance(pathTuple, int): +- endpoint = pathTuple ++ return ClusterAttribute.AttributePath(EndpointId=pathTuple) + elif issubclass(pathTuple, ClusterObjects.Cluster): +- cluster = pathTuple ++ return ClusterAttribute.AttributePath.from_cluster(EndpointId=None, Cluster=pathTuple) + elif issubclass(pathTuple, ClusterObjects.ClusterAttributeDescriptor): +- attribute = pathTuple ++ return ClusterAttribute.AttributePath.from_attribute(EndpointId=None, 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] ++ return ClusterAttribute.AttributePath.from_cluster(EndpointId=pathTuple[0], Cluster=pathTuple[1]) + elif issubclass(pathTuple[1], ClusterAttribute.ClusterAttributeDescriptor): +- attribute = pathTuple[1] ++ return ClusterAttribute.AttributePath.from_attribute(EndpointId=pathTuple[0], 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 +@@ -1193,7 +1186,7 @@ class ChipDeviceControllerBase(): + else: + raise ValueError("Unsupported Cluster Path") + dataVersion = pathTuple[2] +- return ClusterAttribute.DataVersionFilter( ++ return ClusterAttribute.DataVersionFilter.from_cluster( + EndpointId=endpoint, Cluster=cluster, DataVersion=dataVersion) + + def _parseEventPathTuple(self, pathTuple: typing.Union[ +@@ -1210,39 +1203,31 @@ class ChipDeviceControllerBase(): + typing.Tuple[int, + typing.Type[ClusterObjects.ClusterEvent], int] + ]): +- endpoint = None +- cluster = None +- event = None +- urgent = False + if pathTuple in [('*'), ()]: + # Wildcard +- pass ++ return ClusterAttribute.EventPath() + elif not isinstance(pathTuple, tuple): + logging.debug(type(pathTuple)) + if isinstance(pathTuple, int): +- endpoint = pathTuple ++ return ClusterAttribute.EventPath(EndpointId=pathTuple) + elif issubclass(pathTuple, ClusterObjects.Cluster): +- cluster = pathTuple ++ return ClusterAttribute.EventPath.from_cluster(EndpointId=None, Cluster=pathTuple) + elif issubclass(pathTuple, ClusterObjects.ClusterEvent): +- event = pathTuple ++ return ClusterAttribute.EventPath.from_event(EndpointId=None, Event=pathTuple) + else: + raise ValueError("Unsupported Event Path") + else: + if pathTuple[0] == '*': +- urgent = pathTuple[-1] +- pass ++ return ClusterAttribute.EventPath(Urgent=pathTuple[-1]) + else: ++ urgent = bool(pathTuple[-1]) if len(pathTuple) > 2 else False + # endpoint + (cluster) event / endpoint + cluster +- endpoint = pathTuple[0] + if issubclass(pathTuple[1], ClusterObjects.Cluster): +- cluster = pathTuple[1] ++ return ClusterAttribute.EventPath.from_cluster(EndpointId=pathTuple[0], Cluster=pathTuple[1], Urgent=urgent) + elif issubclass(pathTuple[1], ClusterAttribute.ClusterEvent): +- event = pathTuple[1] ++ return ClusterAttribute.EventPath.from_event(EndpointId=pathTuple[0], Event=pathTuple[1], Urgent=urgent) + else: + raise ValueError("Unsupported Attribute Path") +- urgent = bool(pathTuple[-1]) if len(pathTuple) > 2 else False +- 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 +@@ -1514,7 +1499,7 @@ class ChipDeviceControllerBase(): + + result = asyncio.run(self.ReadAttribute( + nodeid, [(endpoint, attributeType)])) +- path = ClusterAttribute.AttributePath( ++ path = ClusterAttribute.AttributePath.from_attribute( + EndpointId=endpoint, Attribute=attributeType) + return im.AttributeReadResult(path=im.AttributePath(nodeId=nodeid, endpointId=path.EndpointId, clusterId=path.ClusterId, attributeId=path.AttributeId), + status=0, value=result[endpoint][clusterType][attributeType], dataVersion=result[endpoint][clusterType][ClusterAttribute.DataVersion]) +diff --git a/src/controller/python/chip/clusters/Attribute.py b/src/controller/python/chip/clusters/Attribute.py +index ce522bf452..51389e19a1 100644 +--- a/src/controller/python/chip/clusters/Attribute.py ++++ b/src/controller/python/chip/clusters/Attribute.py +@@ -54,62 +54,43 @@ class EventPriority(Enum): + CRITICAL = 2 + + +-@dataclass ++@dataclass(frozen=True) + class AttributePath: + 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 ++ @staticmethod ++ def from_cluster(EndpointId: int, Cluster: Cluster) -> AttributePath: ++ if Cluster is None: ++ raise ValueError("Cluster cannot be None") ++ return AttributePath(EndpointId=EndpointId, ClusterId=Cluster.id) ++ ++ @staticmethod ++ def from_attribute(EndpointId: int, Attribute: ClusterAttributeDescriptor) -> AttributePath: ++ if Attribute is None: ++ raise ValueError("Attribute cannot be None") ++ return AttributePath(EndpointId=EndpointId, ClusterId=Attribute.cluster_id, AttributeId=Attribute.attribute_id) + + def __str__(self) -> str: + return f"{self.EndpointId}/{self.ClusterId}/{self.AttributeId}" + +- def __hash__(self): +- return str(self).__hash__() + +- +-@dataclass ++@dataclass(frozen=True) + class DataVersionFilter: + EndpointId: int = None + ClusterId: int = None + DataVersion: int = None + +- def __init__(self, EndpointId: int = None, Cluster=None, ClusterId=None, DataVersion=None): +- self.EndpointId = EndpointId +- if Cluster is not None: +- # Wildcard read for a specific cluster +- if (ClusterId is not None): +- raise Warning( +- "Attribute, ClusterId and AttributeId is ignored when Cluster is specified") +- self.ClusterId = Cluster.id +- else: +- self.ClusterId = ClusterId +- self.DataVersion = DataVersion ++ @staticmethod ++ def from_cluster(EndpointId: int, Cluster: Cluster, DataVersion: int = None) -> AttributePath: ++ if Cluster is None: ++ raise ValueError("Cluster cannot be None") ++ return DataVersionFilter(EndpointId=EndpointId, ClusterId=Cluster.id, DataVersion=DataVersion) + + def __str__(self) -> str: + return f"{self.EndpointId}/{self.ClusterId}/{self.DataVersion}" + +- def __hash__(self): +- return str(self).__hash__() +- + + @dataclass + class TypedAttributePath: +@@ -165,44 +146,28 @@ class TypedAttributePath: + self.AttributeId = self.AttributeType.attribute_id + + +-@dataclass ++@dataclass(frozen=True) + class EventPath: + EndpointId: int = None + ClusterId: int = None + EventId: int = None + Urgent: int = None + +- def __init__(self, EndpointId: int = None, Cluster=None, Event=None, ClusterId=None, EventId=None, Urgent=None): +- self.EndpointId = EndpointId +- self.Urgent = Urgent +- if Cluster is not None: +- # Wildcard read for a specific cluster +- if (Event is not None) or (ClusterId is not None) or (EventId is not None): +- raise Warning( +- "Event, ClusterId and AttributeId is ignored when Cluster is specified") +- self.ClusterId = Cluster.id +- return +- if Event is not None: +- if (ClusterId is not None) or (EventId is not None): +- raise Warning( +- "ClusterId and EventId is ignored when Event is specified") +- self.ClusterId = Event.cluster_id +- self.EventId = Event.event_id +- return +- self.ClusterId = ClusterId +- self.EventId = EventId ++ @staticmethod ++ def from_cluster(EndpointId: int, Cluster: Cluster, EventId: int = None, Urgent: int = None) -> "EventPath": ++ if Cluster is None: ++ raise ValueError("Cluster cannot be None") ++ return EventPath(EndpointId=EndpointId, ClusterId=Cluster.id, EventId=EventId, Urgent=Urgent) ++ ++ @staticmethod ++ def from_event(EndpointId: int, Event: ClusterEvent, Urgent: int = None) -> "EventPath": ++ if Event is None: ++ raise ValueError("Event cannot be None") ++ return EventPath(EndpointId=EndpointId, ClusterId=Event.cluster_id, EventId=Event.event_id, Urgent=Urgent) + + def __str__(self) -> str: + return f"{self.EndpointId}/{self.ClusterId}/{self.EventId}/{self.Urgent}" + +- def __hash__(self): +- return str(self).__hash__() +- +- +-@dataclass +-class AttributePathWithListIndex(AttributePath): +- ListIndex: int = None +- + + @dataclass + class EventHeader: +@@ -711,7 +676,7 @@ class AsyncReadTransaction: + def GetAllEventValues(self): + return self._events + +- def handleAttributeData(self, path: AttributePathWithListIndex, dataVersion: int, status: int, data: bytes): ++ def handleAttributeData(self, path: AttributePath, dataVersion: int, status: int, data: bytes): + try: + imStatus = chip.interaction_model.Status(status) + +diff --git a/src/controller/python/test/test_scripts/cluster_objects.py b/src/controller/python/test/test_scripts/cluster_objects.py +index 53516c1dc7..37f6819cbe 100644 +--- a/src/controller/python/test/test_scripts/cluster_objects.py ++++ b/src/controller/python/test/test_scripts/cluster_objects.py +@@ -164,7 +164,7 @@ class ClusterObjectTests: + ] + ) + expectedRes = [ +- AttributeStatus(Path=AttributePath( ++ AttributeStatus(Path=AttributePath.from_attribute( + EndpointId=1, + Attribute=Clusters.UnitTesting.Attributes.ListLongOctetString), Status=chip.interaction_model.Status.Success), + ] +-- +2.45.2 + diff --git a/0013-Python-Drop-chip-device-ctrl-33488.patch b/0013-Python-Drop-chip-device-ctrl-33488.patch new file mode 100644 index 0000000..f42cda9 --- /dev/null +++ b/0013-Python-Drop-chip-device-ctrl-33488.patch @@ -0,0 +1,2532 @@ +From 9be9b5e5f8b7988c71b178ffc24c76590c8f0f88 Mon Sep 17 00:00:00 2001 +From: Stefan Agner +Date: Fri, 24 May 2024 16:19:35 +0200 +Subject: [PATCH] [Python] Drop chip-device-ctrl (#33488) + +* Drop chip-device-ctrl and ZCL* APIs + +Drop the deprecated chip-device-ctrl and remove the ZCL* API from the +Python CHIP controller. + +* Update docs to reflect chip-repl commands + +Update the Python CHIP controller docs to reflect the CHIP REPL +instead of the now removed chip-device-ctrl tool. + +* Remove unused imports + +* Replace chip-device-ctrl with chip-repl + +* Update/reword main QUICK_START and READMEs + +* Fix wrong/buggy cross-reference + +* Remove common chip-device-ctrl example + +Remove the outdated chip-device-ctrl example and refer to the Python +controller REPL documentation. + +* Add --ble-adapter support to CHIP REPL and update docs + +Add support to select the Bluetooth adapter using the common +--ble-adapter command line argument. + +Update the advanced docs for the Python Controller. + +* Address review feedback + +* Trim list of commands/add link to official API docs +--- + docs/QUICK_START.md | 2 +- + .../python_chip_controller_advanced_usage.md | 226 ++-- + .../guides/python_chip_controller_building.md | 499 ++----- + .../lighting-app/infineon/cyw30739/README.md | 16 +- + examples/lighting-app/python/README.md | 15 +- + examples/lock-app/infineon/cyw30739/README.md | 16 +- + scripts/tools/linux_ip_namespace_setup.sh | 2 +- + src/controller/README.md | 2 +- + src/controller/python/BUILD.gn | 5 +- + src/controller/python/README.md | 14 +- + src/controller/python/chip-device-ctrl.py | 1202 ----------------- + src/controller/python/chip/ChipDeviceCtrl.py | 82 +- + src/controller/python/chip/ChipReplStartup.py | 4 +- + .../pycontroller/build-chip-wheel.py | 1 - + 14 files changed, 260 insertions(+), 1826 deletions(-) + delete mode 100755 src/controller/python/chip-device-ctrl.py + +diff --git a/docs/QUICK_START.md b/docs/QUICK_START.md +index 97f0f76b3b..10833e0c0c 100644 +--- a/docs/QUICK_START.md ++++ b/docs/QUICK_START.md +@@ -10,7 +10,7 @@ and platforms. + |
Controller / Admin
|
Node
| Description | + | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | + | [**chip-tool**](https://github.com/project-chip/connectedhomeip/blob/master/examples/chip-tool/README.md) (Linux / Mac)
Includes docs for all the cluster commands supported
| **all-clusters-app**
  • [M5Stack](https://github.com/project-chip/connectedhomeip/blob/master/examples/all-clusters-app/esp32/README.md) (ESP)
  • [Linux](https://github.com/project-chip/connectedhomeip/tree/master/examples/all-clusters-app/linux) simulation | Use the command line tool on a laptop to pair with and control an embedded Wi-Fi platform. This demo supports the “all-clusters-app”, so it provides the basic onoff light test and more. | +-| [**chip-device-ctrl.py**](https://github.com/project-chip/connectedhomeip/blob/master/src/controller/python/README.md) | **all-clusters-app**
  • [M5Stack](https://github.com/project-chip/connectedhomeip/blob/master/examples/all-clusters-app/esp32/README.md) (ESP)
  • [Linux](https://github.com/project-chip/connectedhomeip/tree/master/examples/all-clusters-app/linux) simulation | Same as above, but uses the pychip tool as Controller Node. | ++| [**chip-repl**](https://github.com/project-chip/connectedhomeip/blob/master/src/controller/python/README.md) | **all-clusters-app**
  • [M5Stack](https://github.com/project-chip/connectedhomeip/blob/master/examples/all-clusters-app/esp32/README.md) (ESP)
  • [Linux](https://github.com/project-chip/connectedhomeip/tree/master/examples/all-clusters-app/linux) simulation | Same as above, but uses the Python CHIP REPL as Controller Node. | + + ## Thread Nodes + +diff --git a/docs/guides/python_chip_controller_advanced_usage.md b/docs/guides/python_chip_controller_advanced_usage.md +index c3d3f55ddc..2eee5472fd 100644 +--- a/docs/guides/python_chip_controller_advanced_usage.md ++++ b/docs/guides/python_chip_controller_advanced_usage.md +@@ -7,8 +7,9 @@ tool or Matter accessories on Linux. + +
    + +-- [Bluetooth LE virtualization on Linux](#bluetooth-le-virtualization-on-linux) +-- [Debugging with gdb](#debugging-with-gdb) ++- [Using Python CHIP Controller advanced features](#using-python-chip-controller-advanced-features) ++ - [Bluetooth LE virtualization on Linux](#bluetooth-le-virtualization-on-linux) ++ - [Debugging with gdb](#debugging-with-gdb) + +
    + +@@ -62,38 +63,38 @@ interfaces working as Bluetooth LE central and peripheral, respectively. + TX bytes:3488 acl:95 sco:0 commands:110 errors:0 + ``` + +-4. Run the Python CHIP Controller with Bluetooth LE adapter defined from a ++4. Run the Python CHIP Controller REPL with Bluetooth LE adapter defined from a + command line: + +- For example, add `--bluetooth-adapter=hci2` to use the virtual interface +- `hci2` listed above. ++ For example, add `--ble-adapter=2` to use the virtual interface `hci2` ++ listed above. + + ``` +- chip-device-ctrl --bluetooth-adapter=hci2 ++ chip-repl --ble-adapter=2 + ``` + +
    + + ## Debugging with gdb + +-You can run the chip-device-ctrl under GDB for debugging, however, since the +-Matter core support library is a dynamic library, you cannot read the symbols +-unless it is fully loaded. ++You can run the chip-repl under GDB for debugging, however, since the Matter SDK ++library is a dynamic library, you cannot read the symbols unless it is fully ++loaded. + + The following block is a example debug session using GDB: + + ``` + # GDB cannot run scripts directly +-# so you need to run Python3 with the path of device controller +-# Here, we use the feature from bash to get the path of chip-device-ctrl without typing it. +-$ gdb --args python3 `which chip-device-ctrl` +-GNU gdb (Ubuntu 10.1-2ubuntu2) 10.1.90.20210411-git +-Copyright (C) 2021 Free Software Foundation, Inc. ++# so you need to run Python3 with the path of device controller REPL ++# Here, we use the feature from bash to get the path of chip-repl without typing it. ++$ gdb --args python3 `which chip-repl` ++GNU gdb (GDB) 14.2 ++Copyright (C) 2023 Free Software Foundation, Inc. + License GPLv3+: GNU GPL version 3 or later + This is free software: you are free to change and redistribute it. + There is NO WARRANTY, to the extent permitted by law. + Type "show copying" and "show warranty" for details. +-This GDB was configured as "aarch64-linux-gnu". ++This GDB was configured as "x86_64-pc-linux-gnu". + Type "show configuration" for configuration details. + For bug reporting instructions, please see: + . +@@ -103,6 +104,12 @@ Find the GDB manual and other documentation resources online at: + For help, type "help". + Type "apropos word" to search for commands related to "word"... + Reading symbols from python3... ++ ++This GDB supports auto-downloading debuginfo from the following URLs: ++ ++Enable debuginfod for this session? (y or [n]) n ++Debuginfod has been disabled. ++To make this setting permanent, add 'set debuginfod enabled off' to .gdbinit. + (No debugging symbols found in python3) + (gdb) + ``` +@@ -119,38 +126,68 @@ library, let run the Matter device controller first. + + ``` + (gdb) run +-Starting program: /usr/bin/python3 /home/ubuntu/.local/bin/chip-device-ctrl ++Starting program: /home/sag/projects/project-chip/connectedhomeip/out/venv/bin/python3 /home/sag/projects/project-chip/connectedhomeip/out/venv/bin/chip-repl + [Thread debugging using libthread_db enabled] +-Using host libthread_db library "/lib/aarch64-linux-gnu/libthread_db.so.1". +-CHIP:DIS: Init admin pairing table with server storage. +-CHIP:IN: local node id is 0x000000000001b669 +-CHIP:DL: MDNS failed to join multicast group on wpan0 for address type IPv4: Inet Error 1016 (0x000003F8): Address not found +-CHIP:ZCL: Using ZAP configuration... +-CHIP:ZCL: deactivate report event +-CHIP:CTL: Getting operational keys +-CHIP:CTL: Generating operational certificate for the controller +-CHIP:CTL: Getting root certificate for the controller from the issuer +-CHIP:CTL: Generating credentials +-CHIP:CTL: Loaded credentials successfully +-CHIP:DL: Platform main loop started. +-Chip Device Controller Shell ++Using host libthread_db library "/usr/lib/libthread_db.so.1". ++Python 3.11.9 (main, Apr 29 2024, 11:59:58) [GCC 13.2.1 20240417] ++Type 'copyright', 'credits' or 'license' for more information ++IPython 8.24.0 -- An enhanced Interactive Python. Type '?' for help. ++[1716395111.775747][364405:364405] CHIP:CTL: Setting attestation nonce to random value ++[1716395111.776196][364405:364405] CHIP:CTL: Setting CSR nonce to random value ++InitBLE 0[1716395111.776809][364405:364405] CHIP:DL: writing settings to file (/tmp/chip_counters.ini-T7hX27) ++[1716395111.776854][364405:364405] CHIP:DL: renamed tmp file to file (/tmp/chip_counters.ini) ++[1716395111.776860][364405:364405] CHIP:DL: NVS set: chip-counters/reboot-count = 9 (0x9) ++[1716395111.777261][364405:364405] CHIP:DL: Got Ethernet interface: eno2 ++[1716395111.777555][364405:364405] CHIP:DL: Found the primary Ethernet interface:eno2 ++[1716395111.777868][364405:364405] CHIP:DL: Got WiFi interface: wlp7s0 ++[1716395111.777877][364405:364405] CHIP:DL: Failed to reset WiFi statistic counts ++────────────────────────────────────────────────────────────────────────────────────────────────────────── Matter REPL ────────────────────────────────────────────────────────────────────────────────────────────────────────── ++ ++ ++ ++ Welcome to the Matter Python REPL! ++ ++ For help, please type matterhelp() ++ ++ To get more information on a particular object/class, you can pass ++ that into matterhelp() as well. ++ ++ ++───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ++2024-05-22 18:25:11 allenwind PersistentStorage[364405] WARNING Initializing persistent storage from file: /tmp/repl-storage.json ++2024-05-22 18:25:11 allenwind PersistentStorage[364405] WARNING Loading configuration from /tmp/repl-storage.json... ++2024-05-22 18:25:11 allenwind CertificateAuthorityManager[364405] WARNING Loading certificate authorities from storage... ++2024-05-22 18:25:11 allenwind CertificateAuthority[364405] WARNING New CertificateAuthority at index 1 ++2024-05-22 18:25:11 allenwind CertificateAuthority[364405] WARNING Loading fabric admins from storage... ++2024-05-22 18:25:11 allenwind FabricAdmin[364405] WARNING New FabricAdmin: FabricId: 0x0000000000000001, VendorId = 0xFFF1 ++2024-05-22 18:25:11 allenwind FabricAdmin[364405] WARNING Allocating new controller with CaIndex: 1, FabricId: 0x0000000000000001, NodeId: 0x000000000001B669, CatTags: [] ++ ++ ++The following objects have been created: ++ certificateAuthorityManager: Manages a list of CertificateAuthority instances. ++ caList: The list of CertificateAuthority instances. ++ caList: A specific FabricAdmin object at index m for the nth CertificateAuthority instance. + +-chip-device-ctrl > ++ ++Default CHIP Device Controller (NodeId: 112233): has been initialized to manage caList[0].adminList[0] (FabricId = 1), and is available as devCtrl ++ ++In [1]: + ``` + +-The prompt `chip-device-ctrl >` indicates that the Matter core library is loaded +-by Python, you can browse the symbols in the Matter core library, setting +-breakpoints on functions and many other functions provided by GDB. ++The prompt `In [1]:` indicates that the Matter SDK library has been loaded and ++initialized by the Python Controller REPL, you can browse the symbols in the ++Matter core library, setting breakpoints on functions and many other functions ++provided by GDB. + +-You can use `Ctrl-C` to send SIGINT to the controller anytime you want so you +-can set breakpoints. ++You can use `Ctrl-Z` to send `SIGTSTP` to the Python 3 REPL process anytime you ++want so you can set breakpoints (unfortunately Ctrl+C seems to be captured by ++the REPL). + +-> (`Ctrl-C` pressed here.) ++In [1]: (`Ctrl-Z` pressed here.) + + ``` +-Thread 1 "python3" received signal SIGINT, Interrupt. +-0x0000fffff7db79ec in __GI___select (nfds=, readfds=0xffffffffe760, writefds=0x0, exceptfds=0x0, timeout=) at ../sysdeps/unix/sysv/linux/select.c:49 +-49 ../sysdeps/unix/sysv/linux/select.c: No such file or directory. ++Thread 1 "python3" received signal SIGTSTP, Stopped (user). ++0x00007ffff7650ceb in kill () from /usr/lib/libc.so.6 + (gdb) + ``` + +@@ -159,40 +196,27 @@ command in GDB (`b` for short) + + ``` + (gdb) b DeviceCommissioner::PairDevice +-Breakpoint 1 at 0xfffff5b0f6b4 (2 locations) ++Breakpoint 1 at 0x7fffed453943: DeviceCommissioner::PairDevice. (4 locations) + (gdb) + ``` + +-Type `continue` (`c` for short) to continue the device controller, you may need +-another hit of `Enter` to see the prompt. ++Type `signal SIGCONT` to continue the device controller after stopping it with ++signal stop, you may need another hit of `Enter` to see the prompt. + + ``` +-(gdb) c +-Continuing. +- +-chip-device-ctrl > ++(gdb) signal SIGCONT ++Continuing with signal SIGCONT. ++In [1]: + ``` + + Let do pairing over IP to see the effect of the breakpoint we just set. + + ``` +-chip-device-ctrl > connect -ip 192.168.50.5 20202021 1 +-Device is assigned with nodeid = 1 +- +-Thread 1 "python3" hit Breakpoint 1, 0x0000fffff5b0f6b4 in chip::Controller::DeviceCommissioner::PairDevice(unsigned long, chip::RendezvousParameters&)@plt () +- from /home/ubuntu/.local/lib/python3.9/site-packages/chip/_ChipDeviceCtrl.so +-(gdb) +-``` +- +-The `@plt` symbol means it is a symbol used by dynamic library loader, type `c` +-(for `continue`) and it will break on the real function. ++In [1]: devCtrl.CommissionWithCode("MT:-24J0AFN00KA0648G00", 1234) + +-``` +-(gdb) c +-Continuing. +- +-Thread 1 "python3" hit Breakpoint 1, chip::Controller::DeviceCommissioner::PairDevice (this=0xd28540, remoteDeviceId=1, params=...) at ../../src/controller/CHIPDeviceController.cpp:827 +-827 { ++Thread 5 "python3" hit Breakpoint 1.1, chip::Controller::DeviceCommissioner::PairDevice (this=0x7fffd8003a90, remoteDeviceId=1234, setUpCode=0x7ffff453d490 "MT:-24J0AFN00KA0648G00", params=..., ++ discoveryType=chip::Controller::DiscoveryType::kAll, resolutionData=...) at ../../src/controller/CHIPDeviceController.cpp:646 ++646 { + (gdb) + ``` + +@@ -201,46 +225,44 @@ then you can use `bt` (for `backtrace`) to see the backtrace of the call stack. + + ``` + (gdb) bt +-#0 chip::Controller::DeviceCommissioner::PairDevice(unsigned long, chip::RendezvousParameters&) (this=0xd28540, remoteDeviceId=1, params=...) +- at ../../src/controller/CHIPDeviceController.cpp:827 +-#1 0x0000fffff5b3095c in pychip_DeviceController_ConnectIP(chip::Controller::DeviceCommissioner*, char const*, uint32_t, chip::NodeId) +- (devCtrl=0xd28540, peerAddrStr=0xfffff467ace0 "192.168.50.5", setupPINCode=20202021, nodeid=1) at ../../src/controller/python/ChipDeviceController-ScriptBinding.cpp:234 +-#2 0x0000fffff7639148 in () at /lib/aarch64-linux-gnu/libffi.so.8 +-#3 0x0000fffff7638750 in () at /lib/aarch64-linux-gnu/libffi.so.8 +-#4 0x0000fffff7665a44 in () at /usr/lib/python3.9/lib-dynload/_ctypes.cpython-39-aarch64-linux-gnu.so +-#5 0x0000fffff7664c7c in () at /usr/lib/python3.9/lib-dynload/_ctypes.cpython-39-aarch64-linux-gnu.so +-#6 0x00000000004a54f0 in _PyObject_MakeTpCall () +-#7 0x000000000049cb10 in _PyEval_EvalFrameDefault () +-#8 0x0000000000496d1c in () +-#9 0x00000000004b1eb0 in _PyFunction_Vectorcall () +-#10 0x0000000000498264 in _PyEval_EvalFrameDefault () +-#11 0x00000000004b1cb8 in _PyFunction_Vectorcall () +-#12 0x0000000000498418 in _PyEval_EvalFrameDefault () +-#13 0x0000000000496d1c in () +-#14 0x00000000004b1eb0 in _PyFunction_Vectorcall () +-#15 0x0000000000498418 in _PyEval_EvalFrameDefault () +-#16 0x00000000004b1cb8 in _PyFunction_Vectorcall () +-#17 0x00000000004c6bc8 in () +-#18 0x0000000000498264 in _PyEval_EvalFrameDefault () +-#19 0x00000000004b1cb8 in _PyFunction_Vectorcall () +-#20 0x0000000000498418 in _PyEval_EvalFrameDefault () +-#21 0x00000000004966f8 in () +-#22 0x00000000004b1f18 in _PyFunction_Vectorcall () +-#23 0x0000000000498418 in _PyEval_EvalFrameDefault () +-#24 0x00000000004b1cb8 in _PyFunction_Vectorcall () +-#25 0x0000000000498264 in _PyEval_EvalFrameDefault () +-#26 0x00000000004966f8 in () +-#27 0x0000000000496490 in _PyEval_EvalCodeWithName () +-#28 0x0000000000595b7c in PyEval_EvalCode () +-#29 0x00000000005c6a5c in () +-#30 0x00000000005c0a70 in () +-#31 0x00000000005c69a8 in () +-#32 0x00000000005c6148 in PyRun_SimpleFileExFlags () +-#33 0x00000000005b60bc in Py_RunMain () +-#34 0x0000000000585a08 in Py_BytesMain () +-#35 0x0000fffff7d0c9d4 in __libc_start_main (main= +- 0x5858fc <_start+60>, argc=2, argv=0xfffffffff498, init=, fini=, rtld_fini=, stack_end=) at ../csu/libc-start.c:332 +-#36 0x00000000005858f8 in _start () ++(gdb) bt ++#0 chip::Controller::DeviceCommissioner::PairDevice ++ (this=0x7fffd8003a90, remoteDeviceId=1234, setUpCode=0x7fffef2555d0 "MT:-24J0AFN00KA0648G00", params=..., discoveryType=chip::Controller::DiscoveryType::kAll, resolutionData=...) ++ at ../../src/controller/CHIPDeviceController.cpp:646 ++#1 0x00007fffed040825 in pychip_DeviceController_ConnectWithCode (devCtrl=0x7fffd8003a90, onboardingPayload=0x7fffef2555d0 "MT:-24J0AFN00KA0648G00", nodeid=1234, discoveryType=2 '\002') ++ at ../../src/controller/python/ChipDeviceController-ScriptBinding.cpp:395 ++#2 0x00007ffff6ad5596 in ??? () at /usr/lib/libffi.so.8 ++#3 0x00007ffff6ad200e in ??? () at /usr/lib/libffi.so.8 ++#4 0x00007ffff6ad4bd3 in ffi_call () at /usr/lib/libffi.so.8 ++#5 0x00007ffff6aeaffc in ??? () at /usr/lib/python3.11/lib-dynload/_ctypes.cpython-311-x86_64-linux-gnu.so ++#6 0x00007ffff6aeb4b4 in ??? () at /usr/lib/python3.11/lib-dynload/_ctypes.cpython-311-x86_64-linux-gnu.so ++#7 0x00007ffff794a618 in _PyObject_MakeTpCall () at /usr/lib/libpython3.11.so.1.0 ++#8 0x00007ffff78f3d03 in _PyEval_EvalFrameDefault () at /usr/lib/libpython3.11.so.1.0 ++#9 0x00007ffff7adef90 in ??? () at /usr/lib/libpython3.11.so.1.0 ++#10 0x00007ffff79ebc0b in _PyObject_FastCallDictTstate () at /usr/lib/libpython3.11.so.1.0 ++#11 0x00007ffff79ebe02 in _PyObject_Call_Prepend () at /usr/lib/libpython3.11.so.1.0 ++#12 0x00007ffff79ec114 in ??? () at /usr/lib/libpython3.11.so.1.0 ++#13 0x00007ffff794a618 in _PyObject_MakeTpCall () at /usr/lib/libpython3.11.so.1.0 ++#14 0x00007ffff78f3d03 in _PyEval_EvalFrameDefault () at /usr/lib/libpython3.11.so.1.0 ++#15 0x00007ffff7adef90 in ??? () at /usr/lib/libpython3.11.so.1.0 ++#16 0x00007ffff7955b97 in PyObject_Vectorcall () at /usr/lib/libpython3.11.so.1.0 ++#17 0x00007ffff6aea174 in ??? () at /usr/lib/python3.11/lib-dynload/_ctypes.cpython-311-x86_64-linux-gnu.so ++#18 0x00007ffff6aea28c in ??? () at /usr/lib/python3.11/lib-dynload/_ctypes.cpython-311-x86_64-linux-gnu.so ++#19 0x00007ffff6ad5152 in ??? () at /usr/lib/libffi.so.8 ++#20 0x00007ffff6ad57b8 in ??? () at /usr/lib/libffi.so.8 ++#21 0x00007fffed5de848 in chip::DeviceLayer::Internal::GenericPlatformManagerImpl::_DispatchEvent ++ (this=0x7fffed88dc90 , event=0x7fffe6fffe30) at ../../src/include/platform/internal/GenericPlatformManagerImpl.ipp:304 ++#22 0x00007fffed5dd90d in chip::DeviceLayer::PlatformManager::DispatchEvent (this=0x7fffed88dc80 , event=0x7fffe6fffe30) at ../../src/include/platform/PlatformManager.h:503 ++#23 0x00007fffed5df45b in chip::DeviceLayer::Internal::GenericPlatformManagerImpl_POSIX::ProcessDeviceEvents ++ (this=0x7fffed88dc90 ) at ../../src/include/platform/internal/GenericPlatformManagerImpl_POSIX.ipp:185 ++#24 0x00007fffed5dee64 in chip::DeviceLayer::Internal::GenericPlatformManagerImpl_POSIX::_RunEventLoop (this=0x7fffed88dc90 ) ++--Type for more, q to quit, c to continue without paging-- ++ at ../../src/include/platform/internal/GenericPlatformManagerImpl_POSIX.ipp:227 ++#25 0x00007fffed5dd888 in chip::DeviceLayer::PlatformManager::RunEventLoop (this=0x7fffed88dc80 ) at ../../src/include/platform/PlatformManager.h:403 ++#26 0x00007fffed5df3fe in chip::DeviceLayer::Internal::GenericPlatformManagerImpl_POSIX::EventLoopTaskMain (arg=0x7fffed88dc90 ) ++ at ../../src/include/platform/internal/GenericPlatformManagerImpl_POSIX.ipp:256 ++#27 0x00007ffff76a6ded in ??? () at /usr/lib/libc.so.6 ++#28 0x00007ffff772a0dc in ??? () at /usr/lib/libc.so.6 + (gdb) + ``` + +diff --git a/docs/guides/python_chip_controller_building.md b/docs/guides/python_chip_controller_building.md +index c940f2c925..8a7acc884a 100644 +--- a/docs/guides/python_chip_controller_building.md ++++ b/docs/guides/python_chip_controller_building.md +@@ -1,25 +1,20 @@ +-# Deprecation notice +- +-chip-device-ctrl is no longer maintained and should not be used. +- +-Matter-repl is the current python controller implementation. +- + # Working with Python CHIP Controller + +-The Python CHIP Controller is a tool that allows to commission a Matter device +-into the network and to communicate with it using the Zigbee Cluster Library +-(ZCL) messages. ++The Python CHIP controller is a library that allows to create a Matter fabric ++and commission Matter devices with it. + +-> The chip-device-ctrl tool will be deprecated, and will be replaced by +-> chip-repl. Continue reading to see how to do the same thing with chip-repl. ++The `chip-repl` is a REPl which sets up a Python CHIP Controller and allows to ++explore the Python CHIP Controller API and communicate with devices from the ++command line. + +
    + + - [Source files](#source-files) +-- [Building Android CHIPTool](#building-and-installing) +-- [Running the tool](#running-the-tool) +-- [Using Python CHIP Controller for Matter accessory testing](#using-python-chip-controller-for-matter-accessory-testing) +-- [List of commands](#list-of-commands) ++- [Building Python CHIP Controller](#building-and-installing) ++- [Running the CHIP REPL](#running-the-chip-repl) ++- [Using Python CHIP Controller REPL for Matter accessory testing](#using-python-chip-controller-repl-for-matter-accessory-testing) ++- [Example usage of the Python CHIP Controller REPL](#example-usage-of-the-python-chip-controller-repl) ++- [Explore Clusters, Attributes and Commands](#explore-clusters-attributes-and-commands) + +
    + +@@ -85,35 +80,31 @@ To build and run the Python CHIP controller: + scripts/build_python.sh -m platform -i separate + ``` + +- > Note: To get more details about available build configurations, run the ++ > Note: This builds the Python CHIP Controller along with the CHIP REPL as ++ > Python wheels and installs it into a separate Python virtual environment. ++ > To get more details about available build configurations, run the + > following command: `scripts/build_python.sh --help` + +
    + +-## Running the tool ++## Running the CHIP REPL + +-1. Activate the Python virtual environment: ++1. Activate the Python virtual environment with the Python CHIP Controller ++ installed: + + ``` + source out/python_env/bin/activate + ``` + +-2. Run the Python CHIP controller with root privileges, which is required to +- obtain access to the Bluetooth interface: +- +- ``` +- sudo out/python_env/bin/chip-device-ctrl +- ``` +- +- You can also select the Bluetooth LE interface using command line argument: ++2. Run the CHIP REPL to explore the API of the Python CHIP controller: + + ``` +- sudo out/python_env/bin/chip-device-ctrl --bluetooth-adapter=hci2 ++ chip-repl + ``` + +
    + +-## Using Python CHIP Controller for Matter accessory testing ++## Using Python CHIP Controller REPL for Matter accessory testing + + This section describes how to use Python CHIP controller to test the Matter + accessory. Below steps depend on the application clusters that you implemented +@@ -135,13 +126,14 @@ require physical trigger, for example pushing a button. Follow the documentation + of the Matter accessory example to learn how Bluetooth LE advertising is enabled + for the given example. + +-### Step 3: Discover Matter accessory device over Bluetooth LE ++### Step 3: Discover commissionable Matter accessory device + +-An uncommissioned accessory device advertises over Bluetooth LE. Run the +-following command to scan all advertised Matter devices: ++An uncommissioned accessory device advertises over Bluetooth LE or via mDNS if ++already on the network. Run the following command to scan all advertised Matter ++devices: + + ``` +-chip-device-ctrl > ble-scan ++devCtrl.DiscoverCommissionableNodes() + ``` + + ### Step 4: Set network pairing credentials +@@ -177,11 +169,12 @@ network interface, such as Thread or Wi-Fi. + datasets directly from the Thread Border Router, you might also use a + different out-of-band method. + +-2. Set the previously obtained Active Operational Dataset as a hex-encoded value +- using the following command: ++2. Set the previously obtained Active Operational Dataset as a byte array using ++ the following command: + + ``` +- chip-device-ctrl > set-pairing-thread-credential 0e080000000000010000000300001335060004001fffe002084fe76e9a8b5edaf50708fde46f999f0698e20510d47f5027a414ffeebaefa92285cc84fa030f4f70656e5468726561642d653439630102e49c0410b92f8c7fbb4f9f3e08492ee3915fbd2f0c0402a0fff8 ++ thread_dataset = bytes.fromhex("0e080000000000010000000300001335060004001fffe002084fe76e9a8b5edaf50708fde46f999f0698e20510d47f5027a414ffeebaefa92285cc84fa030f4f70656e5468726561642d653439630102e49c0410b92f8c7fbb4f9f3e08492ee3915fbd2f0c0402a0fff8") ++ devCtrl.SetThreadOperationalDataset(thread_dataset) + ``` + + #### Setting Wi-Fi network credentials +@@ -190,11 +183,9 @@ Assuming your Wi-Fi SSID is _TESTSSID_, and your Wi-Fi password is _P455W4RD_, + set the credentials to the controller by executing the following command: + + ``` +-chip-device-ctrl > set-pairing-wifi-credential TESTSSID P455W4RD ++devCtrl.SetWiFiCredentials(, ) + ``` + +-**REPL Command**: `devCtrl.SetWiFiCredentials(, )` +- + ### Step 5: Commission the Matter accessory device over Bluetooth LE + + The controller uses a 12-bit value called **discriminator** to discern between +@@ -222,16 +213,26 @@ with the following assumptions for the Matter accessory device: + - The temporary Node ID is _1234_ + + ``` +-chip-device-ctrl > connect -ble 3840 20202021 1234 ++devCtrl.ConnectBLE(3840, 20202021, 1234) + ``` + +-**REPL Command:** +-`devCtrl.ConnectBLE(, , )` +- + You can skip the last parameter, the Node ID, in the command. If you skip it, + the controller will assign it randomly. In that case, note down the Node ID, + because it is required later in the configuration process. + ++It is also possible to use the QR setup code instead. It typically is shown on ++the terminal of the device as well. For example: ++ ++``` ++CHIP:SVR: SetupQRCode: [MT:-24J0AFN00KA0648G00] ++``` ++ ++Use the following command to commission the device with the QR code: ++ ++``` ++devCtrl.CommissionWithCode("MT:-24J0AFN00KA0648G00", 1234) ++``` ++ + After connecting the device over Bluetooth LE, the controller will go through + the following stages: + +@@ -255,429 +256,155 @@ the following stages: + finished and the Python CHIP controller is now using only the IPv6 traffic + to reach the device. + +-### Step 6: Control application ZCL clusters. ++### Step 6: Control application clusters. + + For the light bulb example, execute the following command to toggle the LED + state: + + ``` +-chip-device-ctrl > zcl OnOff Toggle 1234 1 0 ++await devCtrl.SendCommand(1234, 1, Clusters.OnOff.Commands.Toggle()) + ``` + +-**REPL Command:** +-`await devCtrl.SendCommand(1234, 1, Clusters.OnOff.Commands.Toggle())` +- + To change the brightness of the LED, use the following command, with the level + value somewhere between 0 and 255. + + ``` +-chip-device-ctrl > zcl LevelControl MoveToLevel 1234 1 0 level=50 ++commandToSend = LevelControl.Commands.MoveToLevel(level=50, transitionTime=Null, optionsMask=0, optionsOverride=0) ++await devCtrl.SendCommand(1234, 1, commandToSend) + ``` + +-**REPL Command:** +-`await devCtrl.SendCommand(1234, 1, LevelControl.Commands.MoveToLevel(level=50, transitionTime=Null, optionsMask=0, optionsOverride=0))` +- + ### Step 7: Read basic information out of the accessory. + + Every Matter accessory device supports a Basic Information Cluster, which + maintains collection of attributes that a controller can obtain from a device, +-such as the vendor name, the product name, or software version. Use `zclread` +-command to read those values from the device: ++such as the vendor name, the product name, or software version. Use ++`ReadAttribute()` command to read those values from the device: + + ``` +-chip-device-ctrl > zclread BasicInformation VendorName 1234 1 0 +-chip-device-ctrl > zclread BasicInformation ProductName 1234 1 0 +-chip-device-ctrl > zclread BasicInformation SoftwareVersion 1234 1 0 ++attributes = [ ++ (0, Clusters.BasicInformation.Attributes.VendorName), ++ (0, Clusters.BasicInformation.Attributes.ProductName), ++ (0, Clusters.BasicInformation.Attributes.SoftwareVersion), ++] ++await devCtrl.ReadAttribute(1234, attributes) + ``` + +-**REPL Command:** +-`await devCtrl.ReadAttribute(1234, [(1, Clusters.BasicInformation.Attributes.VendorName)])` +- +-> Use the `zcl ? BasicInformation` command to list all available commands for +-> Basic Information Cluster. +-> + > In REPL, you can type `Clusters.BasicInformation.Attributes.` and then use the + > TAB key. + +
    + +-## List of commands +- +-### `ble-adapter-print` ++## Example usage of the Python CHIP Controller REPL + +-> BLE adapter operations is not yet supported in REPL ++These section covers a few useful commands of the Python CHIP Controller along ++with examples demonstrating how they can be called from the REPL. + +-Print the available Bluetooth adapters on device. Takes no arguments: +- +-``` +-chip-device-ctrl > ble-adapter-print +-2021-03-04 16:09:40,930 ChipBLEMgr INFO AdapterName: hci0 AdapterAddress: 00:AA:01:00:00:23 +-``` ++The ++[CHIP Device Controller API documentation offer](https://project-chip.github.io/connectedhomeip-doc/testing/ChipDeviceCtrlAPI.html#chip-chipdevicectrl) ++the full list of available commands. + +-### `ble-debug-log` +- +-> BLE adapter operations is not yet supported in REPL +- +-Enable the Bluetooth LE debug logs. +- +-``` +-chip-device-ctrl > ble-debug-log 1 +-``` +- +-### `ble-scan [-t ] [identifier]` +- +-> BLE adapter operations is not yet supported in REPL +- +-Start a scan action to search for valid CHIP devices over Bluetooth LE (for at +-most _timeout_ seconds). Stop when the device is matching the identifier or the +-counter times out. +- +-``` +-chip-device-ctrl > ble-scan +-2021-05-29 22:28:05,461 ChipBLEMgr INFO scanning started +-2021-05-29 22:28:07,206 ChipBLEMgr INFO Name = ChipLight +-2021-05-29 22:28:07,206 ChipBLEMgr INFO ID = f016e23d-0d00-35d5-93e7-588acdbc7e54 +-2021-05-29 22:28:07,207 ChipBLEMgr INFO RSSI = -79 +-2021-05-29 22:28:07,207 ChipBLEMgr INFO Address = E0:4D:84:3C:BB:C3 +-2021-05-29 22:28:07,209 ChipBLEMgr INFO Pairing State = 0 +-2021-05-29 22:28:07,209 ChipBLEMgr INFO Discriminator = 3840 +-2021-05-29 22:28:07,209 ChipBLEMgr INFO Vendor Id = 9050 +-2021-05-29 22:28:07,209 ChipBLEMgr INFO Product Id = 20044 +-2021-05-29 22:28:07,210 ChipBLEMgr INFO Adv UUID = 0000fff6-0000-1000-8000-00805f9b34fb +-2021-05-29 22:28:07,210 ChipBLEMgr INFO Adv Data = 00000f5a234c4e +-2021-05-29 22:28:07,210 ChipBLEMgr INFO +-2021-05-29 22:28:16,246 ChipBLEMgr INFO scanning stopped +-``` +- +-### `set-pairing-thread-credential ` ++### `SetThreadOperationalDataset()` + + Provides the controller with Thread network credentials that will be used in the + device commissioning procedure to configure the device with a Thread interface. + + ``` +-chip-device-ctrl > set-pairing-thread-credential 0e080000000000010000000300001335060004001fffe002084fe76e9a8b5edaf50708fde46f999f0698e20510d47f5027a414ffeebaefa92285cc84fa030f4f70656e5468726561642d653439630102e49c0410b92f8c7fbb4f9f3e08492ee3915fbd2f0c0402a0fff8 ++thread_dataset = bytes.fromhex("0e080000000000010000000300001335060004001fffe002084fe76e9a8b5edaf50708fde46f999f0698e20510d47f5027a414ffeebaefa92285cc84fa030f4f70656e5468726561642d653439630102e49c0410b92f8c7fbb4f9f3e08492ee3915fbd2f0c0402a0fff8") ++devCtrl.SetThreadOperationalDataset(thread_dataset) + ``` + +-**REPL Commands:** +-`devCtrl.SetThreadOperationalDataset(bytes.FromHex("0e080000000000010000000300001335060004001fffe002084fe76e9a8b5edaf50708fde46f999f0698e20510d47f5027a414ffeebaefa92285cc84fa030f4f70656e5468726561642d653439630102e49c0410b92f8c7fbb4f9f3e08492ee3915fbd2f0c0402a0fff8"))` +- +-### `set-pairing-wifi-credential ` ++### `SetWiFiCredentials(: str, : str)` + + Provides the controller with Wi-Fi network credentials that will be used in the + device commissioning procedure to configure the device with a Wi-Fi interface. + + ``` +-chip-device-ctrl > set-pairing-wifi-credential TESTSSID P455W4RD ++devCtrl.SetWiFiCredentials('TESTSSID', 'P455W4RD') + ``` + +-**REPL Commands:** `devCtrl.SetWiFiCredentials('TESTSSID', 'P455W4RD')` +- +-### `connect -ip
    []` +- +-Do key exchange and establish a secure session between controller and device +-using IP transport. +- +-The Node ID will be used by controller to distinguish multiple devices. This +-does not match the spec and will be removed later. The nodeid will not be +-persisted by controller / device. +- +-If no nodeid given, a random Node ID will be used. +- +-**REPL Commands:** +-`devCtrl.CommissionIP(b'', , )` +- +-### `connect -ble []` +- +-Do key exchange and establish a secure session between controller and device +-using Bluetooth LE transport. +- +-The Node ID will be used by controller to distinguish multiple devices. This +-does not match the spec and will be removed later. The nodeid will not be +-persisted by controller / device. +- +-If no nodeid given, a random Node ID will be used. +- +-**REPL Commands:** +-`devCtrl.ConnectBLE(, , )` ++### `CommissionWithCode(: str, : int, : DiscoveryType)` + +-### `close-session ` ++Commission with the given nodeid from the setupPayload. setupPayload may be a QR ++or the manual setup code. + +-If case there exists an open session (PASE or CASE) to the device with a given +-Node ID, mark it as expired. +- +-**REPL Commands:** `devCtrl.CloseSession()` +- +-### `discover` +- +-> To be implemented in REPL +- +-Discover available Matter accessory devices: +- +-``` +-chip-device-ctrl > discover -all + ``` +- +-### `resolve ` +- +-> To be implemented in REPL +- +-Resolve DNS-SD name corresponding with the given Node ID and update address of +-the node in the device controller: +- +-``` +-chip-device-ctrl > resolve 1234 ++devCtrl.CommissionWithCode("MT:-24J0AFN00KA0648G00", 1234) + ``` + +-### `setup-payload generate [-v ] [-p ] [-cf ] [-dc ] [-dv ] [-ps ]` +- +-> To be implemented in REPL ++### `SendCommand(: int, : int, Clusters..Commands.())` + +-Print the generated Onboarding Payload Contents in human-readable (Manual +-Pairing Code) and machine-readable (QR Code) format: ++Send a Matter command to the device. For example: + ++```python ++commandToSend = Clusters.LevelControl.Commands.MoveWithOnOff(moveMode=1, rate=2, optionsMask=0, optionsOverride=0) ++await devCtrl.SendCommand(1234, 1, commandToSend) + ``` +-chip-device-ctrl > setup-payload generate -v 9050 -p 65279 -cf 0 -dc 2 -dv 2976 -ps 34567890 +-Manual pairing code: [26318621095] +-SetupQRCode: [MT:YNJV7VSC00CMVH7SR00] +-``` +- +-### `setup-payload parse-manual ` +- +-> To be implemented in REPL +- +-Print the commissioning information encoded in the Manual Pairing Code: +- +-``` +-chip-device-ctrl > setup-payload parse-manual 34970112332 +-Version: 0 +-VendorID: 0 +-ProductID: 0 +-CommissioningFlow: 0 +-RendezvousInformation: 0 +-Discriminator: 3840 +-SetUpPINCode: 20202021 +-``` +- +-### `setup-payload parse-qr ` + +-> To be implemented in REPL +- +-Print the commissioning information encoded in the QR Code payload: ++To see available arguments just create a command object without argument: + + ``` +-chip-device-ctrl > setup-payload parse-qr "VP:vendorpayload%MT:W0GU2OTB00KA0648G00" +-Version: 0 +-VendorID: 9050 +-ProductID: 20043 +-CommissioningFlow: 0 +-RendezvousInformation: 2 [BLE] +-Discriminator: 3840 +-SetUpPINCode: 20202021 ++Clusters.LevelControl.Commands.MoveWithOnOff() + ``` + +-### `zcl [arguments]` +- +-Send a ZCL command to the device. For example: ++Shows which arguments are available: + + ``` +-chip-device-ctrl > zcl LevelControl MoveWithOnOff 12344321 1 0 moveMode=1 rate=2 ++MoveWithOnOff( ++│ moveMode=0, ++│ rate=Null, ++│ optionsMask=0, ++│ optionsOverride=0 ++) + ``` + +-**Format of arguments** ++### `ReadAttribute(: int, [(: int, Clusters..Attributes.)])` + +-For any integer and char string (null terminated) types, just use `key=value`, +-for example: `rate=2`, `string=123`, `string_2="123 456"` +- +-For byte string type, use `key=encoding:value`, currently, we support `str` and +-`hex` encoding, the `str` encoding will encode a NULL terminated string. For +-example, `networkId=hex:0123456789abcdef` (for +-`[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]`), `ssid=str:Test` (for +-`['T', 'e', 's', 't', 0x00]`). +- +-For boolean type, use `key=True` or `key=False`. +- +-**REPL Commands:** ++Read the value of an attribute. For example: + + ```python +-# await devCtrl.SendCommand(, , Clusters..Commands.()) +-# e.g. +-await devCtrl.SendCommand(12344321, 1, Clusters.LevelControl.Commands.MoveWithOnOff(moveMode=1, rate=2, optionsMask=0, optionsOverride=0)) +-``` +- +-### `zcl ?` +- +-List available clusters: +- +-``` +-chip-device-ctrl > zcl ? +-AccountLogin +-ApplicationBasic +-ApplicationLauncher +-AudioOutput +-BarrierControl +-BasicInformation +-Binding +-BridgedDeviceBasicInformation +-ColorControl +-ContentLaunch +-Descriptor +-DoorLock +-EthernetNetworkDiagnostics +-FixedLabel +-GeneralCommissioning +-GeneralDiagnostics +-GroupKeyManagement +-Groups +-Identify +-KeypadInput +-LevelControl +-LowPower +-MediaInput +-MediaPlayback +-NetworkCommissioning +-OnOff +-OperationalCredentials +-PumpConfigurationAndControl +-RelativeHumidityMeasurement +-ScenesManagement +-SoftwareDiagnostics +-Switch +-Channel +-TargetNavigator +-TemperatureMeasurement +-TestCluster +-Thermostat +-TrustedRootCertificates +-WakeOnLan +-WindowCovering +-``` +- +-**REPL Commands** +- +-Type `Clusters.` and hit TAB +- +-### `zcl ? ` +- +-List available commands in cluster. For example, for _Basic Information_ +-cluster: +- +-``` +-chip-device-ctrl > zcl ? BasicInformation +-DataModelRevision +-VendorName +-VendorID +-ProductName +-ProductID +-UserLabel +-Location +-HardwareVersion +-HardwareVersionString +-SoftwareVersion +-SoftwareVersionString +-ManufacturingDate +-PartNumber +-ProductURL +-ProductLabel +-SerialNumber +-LocalConfigDisabled +-ClusterRevision +-``` +- +-**REPL Commands** +- +-Type `Clusters.(cluster name).Commands.` and hit TAB +- +-### `zclread [arguments]` +- +-Read the value of ZCL attribute. For example: +- +-``` +-chip-device-ctrl > zclread BasicInformation VendorName 1234 1 0 +-``` +- +-**REPL Commands** +- +-```python +-# devCtrl.ReadAttribute(, [(, Clusters..Attributes.)]) +-# e.g. +-await devCtrl.ReadAttribute(1234, [(1, Clusters.BasicInformation.Attributes.VendorName)]) +-``` +- +-### `zclwrite ` +- +-Write the value to a ZCL attribute. For example: +- +-``` +-chip-device-ctrl > zclwrite TestCluster Int8u 1 1 0 1 +-chip-device-ctrl > zclwrite TestCluster Boolean 1 1 0 True +-chip-device-ctrl > zclwrite TestCluster OctetString 1 1 0 str:123123 +-chip-device-ctrl > zclwrite TestCluster CharString 1 1 0 233233 ++await devCtrl.ReadAttribute(1234, [(0, Clusters.BasicInformation.Attributes.VendorName)]) + ``` + +-Note: The format of the value is the same as the format of argument values for +-ZCL cluster commands. ++### `WriteAttribute(: int, [(: int, Clusters..Attributes.(value=))])` + +-**REPL Commands** ++Write a value to an attribute. For example: + + ```python +-# devCtrl.WriteAttribute(, [(, Clusters..Attributes.(value=))]) +-# e.g. +-await devCtrl.WriteAttribute(1, [(1, Clusters.UnitTesting.Attributes.Int8u(value=1))]) +-await devCtrl.WriteAttribute(1, [(1, Clusters.UnitTesting.Attributes.Boolean(value=True))]) +-await devCtrl.WriteAttribute(1, [(1, Clusters.UnitTesting.Attributes.OctetString(value=b'123123\x00'))]) +-await devCtrl.WriteAttribute(1, [(1, Clusters.UnitTesting.Attributes.CharString(value='233233'))]) ++await devCtrl.WriteAttribute(1234, [(1, Clusters.UnitTesting.Attributes.Int8u(value=1))]) ++await devCtrl.WriteAttribute(1234, [(1, Clusters.UnitTesting.Attributes.Boolean(value=True))]) ++await devCtrl.WriteAttribute(1234, [(1, Clusters.UnitTesting.Attributes.OctetString(value=b'123123\x00'))]) ++await devCtrl.WriteAttribute(1234, [(1, Clusters.UnitTesting.Attributes.CharString(value='233233'))]) + ``` + +-### `zclsubscribe ` ++### `ReadAttribute(: int, [(: int, Clusters..Attributes.)], reportInterval=(: int, : int))` + +-Configure ZCL attribute reporting settings. For example: +- +-``` +-chip-device-ctrl > zclsubscribe OccupancySensing Occupancy 1234 1 10 20 +-``` +- +-**REPL Commands** ++Configure Matter attribute reporting settings. For example: + + ```python +-# devCtrl.ReadAttribute(, [(, Clusters..Attributes.)], reportInterval=(, )) +-# e.g. +-await devCtrl.ReadAttribute(1, [(1, Clusters.OccupancySensing.Attributes.Occupancy)], reportInterval=(10, 20)) ++await devCtrl.ReadAttribute(1234, [(1, Clusters.OccupancySensing.Attributes.Occupancy)], reportInterval=(10, 20)) + ``` + +-### `zclsubscribe -shutdown ` +- +-Shutdown an existing attribute subscription. ++To shutdown an existing attribute subscription use the `Shutdown()` function on ++the returned subscription object: + +-``` +-chip-device-ctrl > zclsubscribe -shutdown 0xdeadbeefcafe ++```python ++sub = await devCtrl.ReadAttribute(1234, [(1, Clusters.OccupancySensing.Attributes.Occupancy)], reportInterval=(10, 20)) ++sub.Shutdown() + ``` + +-The subscription id can be obtained from previous subscription messages: ++## Explore Clusters, Attributes and Commands + +-``` +-chip-device-ctrl > zclsubscribe OnOff OnOff 1 1 10 20 +-(omitted messages) +-[1633922898.965587][1117858:1117866] CHIP:DMG: SubscribeResponse = +-[1633922898.965599][1117858:1117866] CHIP:DMG: { +-[1633922898.965610][1117858:1117866] CHIP:DMG: SubscriptionId = 0xdeadbeefcafe, +-[1633922898.965622][1117858:1117866] CHIP:DMG: MinIntervalFloorSeconds = 0xa, +-[1633922898.965633][1117858:1117866] CHIP:DMG: MaxIntervalCeilingSeconds = 0x14, +-[1633922898.965644][1117858:1117866] CHIP:DMG: } +-[1633922898.965662][1117858:1117866] CHIP:ZCL: SubscribeResponse: +-[1633922898.965673][1117858:1117866] CHIP:ZCL: SubscriptionId: 0xdeadbeefcafe +-[1633922898.965683][1117858:1117866] CHIP:ZCL: ApplicationIdentifier: 0 +-[1633922898.965694][1117858:1117866] CHIP:ZCL: status: EMBER_ZCL_STATUS_SUCCESS (0x00) +-[1633922898.965709][1117858:1117866] CHIP:ZCL: attributeValue: false +-(omitted messages) +-``` ++In the Python REPL the Clusters and Attributes are classes. The `Clusters` ++module contains all clusters. Tab completion can be used to explore available ++clusters, attributes and commands. + +-The subscription id is `0xdeadbeefcafe` in this case ++For example, to get a list of Clusters, type `Clusters.` and hit tab. Continue ++to hit tab to cycle through the available Clusters. Pressing return will select ++the Cluster. + +-**REPL Commands** ++To explore Attributes, use the same technique but with the Attributes sub-class ++of the Clusters class, for example, type `Clusters.(cluster name).Attributes.` ++and hit tab. + +-```python +-# SubscriptionTransaction.Shutdown() +-# e.g. +-sub = await devCtrl.ReadAttribute(1, [(1, Clusters.OccupancySensing.Attributes.Occupancy)], reportInterval=(10, 20)) +-sub.Shutdown() +-``` ++The same is true for Commands, use the Commands sub-class. type ++`Clusters.(cluster name).Commands.` and hit tab. +diff --git a/examples/lighting-app/infineon/cyw30739/README.md b/examples/lighting-app/infineon/cyw30739/README.md +index f085272b1c..ac11bb3033 100644 +--- a/examples/lighting-app/infineon/cyw30739/README.md ++++ b/examples/lighting-app/infineon/cyw30739/README.md +@@ -104,19 +104,7 @@ Put the CYW30739 in to the recovery mode before running the flash script. + [Openthread_border_router](https://github.com/project-chip/connectedhomeip/blob/master/docs/guides/openthread_border_router_pi.md) + for more information on how to setup a border router on a raspberryPi. + +-- You can provision and control the Chip device using the python controller, +- Chip tool standalone, Android or iOS app ++- You can provision and control the device using the Python controller REPL, ++ chip-tool standalone, Android or iOS app + + [Python Controller](https://github.com/project-chip/connectedhomeip/blob/master/src/controller/python/README.md) +- +- Here is an example with the Python controller: +- +- ```bash +- $ chip-device-ctrl +- chip-device-ctrl > connect -ble 3840 20202021 1234 +- chip-device-ctrl > zcl NetworkCommissioning AddThreadNetwork 1234 0 0 operationalDataset=hex:0e080000000000000000000300000b35060004001fffe00208dead00beef00cafe0708fddead00beef000005108e11d8ea8ffaa875713699f59e8807e0030a4f70656e5468726561640102c2980410edc641eb63b100b87e90a9980959befc0c0402a0fff8 breadcrumb=0 timeoutMs=1000 +- chip-device-ctrl > zcl NetworkCommissioning EnableNetwork 1234 0 0 networkID=hex:dead00beef00cafe breadcrumb=0 timeoutMs=1000 +- chip-device-ctrl > close-ble +- chip-device-ctrl > resolve 1234 +- chip-device-ctrl > zcl OnOff Toggle 1234 1 0 +- ``` +diff --git a/examples/lighting-app/python/README.md b/examples/lighting-app/python/README.md +index 8e34e59dea..a935a165c9 100644 +--- a/examples/lighting-app/python/README.md ++++ b/examples/lighting-app/python/README.md +@@ -32,17 +32,6 @@ cd examples/lighting-app/python + python lighting.py + ``` + +-Control the Python lighting matter device: ++Control the Python lighting matter device using the Python controller REPL: + +-```shell +-source ./out/python_env/bin/activate +- +-chip-device-ctrl +- +-chip-device-ctrl > connect -ble 3840 20202021 12344321 +-chip-device-ctrl > zcl NetworkCommissioning AddOrUpdateWiFiNetwork 12344321 0 0 ssid=str:YOUR_SSID credentials=str:YOUR_PASSWORD breadcrumb=0 +-chip-device-ctrl > zcl NetworkCommissioning ConnectNetwork 12344321 0 0 networkID=str:YOUR_SSID breadcrumb=0 +-chip-device-ctrl > close-ble +-chip-device-ctrl > resolve 5544332211 1 (pass appropriate fabric ID and node ID, you can get this from get-fabricid) +-chip-device-ctrl > zcl OnOff Toggle 12344321 1 0 +-``` ++[Python Controller](https://github.com/project-chip/connectedhomeip/blob/master/src/controller/python/README.md) +diff --git a/examples/lock-app/infineon/cyw30739/README.md b/examples/lock-app/infineon/cyw30739/README.md +index b91f4b9157..86c9cf0490 100644 +--- a/examples/lock-app/infineon/cyw30739/README.md ++++ b/examples/lock-app/infineon/cyw30739/README.md +@@ -104,19 +104,7 @@ Put the CYW30739 in to the recovery mode before running the flash script. + [Openthread_border_router](https://github.com/project-chip/connectedhomeip/blob/master/docs/guides/openthread_border_router_pi.md) + for more information on how to setup a border router on a raspberryPi. + +-- You can provision and control the Chip device using the python controller, +- Chip tool standalone, Android or iOS app ++- You can provision and control the device using the Python controller REPL, ++ chip-tool standalone, Android or iOS app + + [Python Controller](https://github.com/project-chip/connectedhomeip/blob/master/src/controller/python/README.md) +- +- Here is an example with the Python controller: +- +- ```bash +- $ chip-device-ctrl +- chip-device-ctrl > connect -ble 3840 20202021 1234 +- chip-device-ctrl > zcl NetworkCommissioning AddThreadNetwork 1234 0 0 operationalDataset=hex:0e080000000000000000000300000b35060004001fffe00208dead00beef00cafe0708fddead00beef000005108e11d8ea8ffaa875713699f59e8807e0030a4f70656e5468726561640102c2980410edc641eb63b100b87e90a9980959befc0c0402a0fff8 breadcrumb=0 timeoutMs=1000 +- chip-device-ctrl > zcl NetworkCommissioning EnableNetwork 1234 0 0 networkID=hex:dead00beef00cafe breadcrumb=0 timeoutMs=1000 +- chip-device-ctrl > close-ble +- chip-device-ctrl > resolve 1234 +- chip-device-ctrl > zcl OnOff Toggle 1234 1 0 +- ``` +diff --git a/scripts/tools/linux_ip_namespace_setup.sh b/scripts/tools/linux_ip_namespace_setup.sh +index 5d467b4b48..f761eea384 100755 +--- a/scripts/tools/linux_ip_namespace_setup.sh ++++ b/scripts/tools/linux_ip_namespace_setup.sh +@@ -124,7 +124,7 @@ function help() { + echo "sudo /$file_name -r /" + echo "" + echo "Terminal 2:" +- echo "/chip-device-ctrl" ++ echo "/chip-repl" + echo "" + echo "This script requires sudo for setup and requires access to ebtables-legacy" + echo "to set up dual ipv4/ipv6 namespaces. Defaults to ipv6 only." +diff --git a/src/controller/README.md b/src/controller/README.md +index e46c191f55..70a3fa5e8f 100644 +--- a/src/controller/README.md ++++ b/src/controller/README.md +@@ -26,7 +26,7 @@ The POSIX CLI chip-tool is located in + + ### Python + +-The Python chip-device-ctrl is located in ++The Python CHIP Controller library is located in + [../controller/python/](../controller/python). + + ## Feature Overview +diff --git a/src/controller/python/BUILD.gn b/src/controller/python/BUILD.gn +index caa33c3a40..e7b06d808e 100644 +--- a/src/controller/python/BUILD.gn ++++ b/src/controller/python/BUILD.gn +@@ -414,10 +414,7 @@ chip_python_wheel_action("chip-clusters") { + } + + chip_python_wheel_action("chip-repl") { +- py_scripts = [ +- "chip-device-ctrl.py", +- "chip-repl.py", +- ] ++ py_scripts = [ "chip-repl.py" ] + + py_manifest_files = [ + { +diff --git a/src/controller/python/README.md b/src/controller/python/README.md +index 34edea4446..e94a95e158 100644 +--- a/src/controller/python/README.md ++++ b/src/controller/python/README.md +@@ -1,10 +1,14 @@ +-# Python CHIP Device Controller ++# Python CHIP Controller + +-The Python CHIP controller is a tool that allows to commission a Matter device +-into the network and to communicate with it using the Zigbee Cluster Library +-(ZCL) messages. The tool uses the generic [Chip Device Controller](../) library. ++The Python CHIP controller is a library that allows to create a Matter fabric ++and commission Matter devices with it, as well as communicate with commissioned ++devices by reading/subscribing and writing Attributes and sending Commands. The ++Python CHIP controller is based on the native [Chip Device Controller](../) ++library. + +-To learn more about the tool, how to build it and use its commands and advanced ++The Python CHIP Controller comes with a REPL which allows to explore and use the ++Python CHIP controller library from a shell. To learn more about the Python CHIP ++Controller and the REPL, how to build it and use its commands and advanced + features, read the following guides: + + - [Working with Python CHIP Controller](../../../docs/guides/python_chip_controller_building.md) +diff --git a/src/controller/python/chip-device-ctrl.py b/src/controller/python/chip-device-ctrl.py +deleted file mode 100755 +index 469fb2c49f..0000000000 +--- a/src/controller/python/chip-device-ctrl.py ++++ /dev/null +@@ -1,1202 +0,0 @@ +-#!/usr/bin/env python +- +-# +-# Copyright (c) 2020-2021 Project CHIP Authors +-# Copyright (c) 2013-2018 Nest Labs, Inc. +-# All rights reserved. +-# +-# Licensed under the Apache License, Version 2.0 (the "License"); +-# you may not use this file except in compliance with the License. +-# You may obtain a copy of the License at +-# +-# http://www.apache.org/licenses/LICENSE-2.0 +-# +-# Unless required by applicable law or agreed to in writing, software +-# distributed under the License is distributed on an "AS IS" BASIS, +-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-# See the License for the specific language governing permissions and +-# limitations under the License. +-# +- +-# +-# @file +-# This file implements the Python-based Chip Device Controller Shell. +-# +- +-from __future__ import absolute_import, print_function +- +-import argparse +-import base64 +-import ctypes +-import logging +-import os +-import platform +-import random +-import shlex +-import string +-import sys +-import textwrap +-import time +-import traceback +-import warnings +-from cmd import Cmd +-from optparse import OptionParser, OptionValueError +- +-import chip.logging +-import coloredlogs +-from chip import ChipCommissionableNodeCtrl, ChipStack, exceptions, native +-from chip.setup_payload import SetupPayload +-from rich import pretty, print +- +-# Extend sys.path with one or more directories, relative to the location of the +-# running script, in which the chip package might be found . This makes it +-# possible to run the device manager shell from a non-standard install location, +-# as well as directly from its location the CHIP source tree. +-# +-# Note that relative package locations are prepended to sys.path so as to give +-# the local version of the package higher priority over any version installed in +-# a standard location. +-# +-scriptDir = os.path.dirname(os.path.abspath(__file__)) +-relChipPackageInstallDirs = [ +- ".", +- "../lib/python", +- "../lib/python%s.%s" % (sys.version_info.major, sys.version_info.minor), +- "../lib/Python%s%s" % (sys.version_info.major, sys.version_info.minor), +-] +-for relInstallDir in relChipPackageInstallDirs: +- absInstallDir = os.path.realpath(os.path.join(scriptDir, relInstallDir)) +- if os.path.isdir(os.path.join(absInstallDir, "chip")): +- sys.path.insert(0, absInstallDir) +- +- +-if platform.system() == 'Darwin': +- from chip.ChipCoreBluetoothMgr import CoreBluetoothManager as BleManager +-elif sys.platform.startswith('linux'): +- from chip.ChipBluezMgr import BluezManager as BleManager +- +-# The exceptions for CHIP Device Controller CLI +- +- +-class ChipDevCtrlException(exceptions.ChipStackException): +- pass +- +- +-class ParsingError(ChipDevCtrlException): +- def __init__(self, msg=None): +- self.msg = "Parsing Error: " + msg +- +- def __str__(self): +- return self.msg +- +- +-def DecodeBase64Option(option, opt, value): +- try: +- return base64.standard_b64decode(value) +- except TypeError: +- raise OptionValueError( +- "option %s: invalid base64 value: %r" % (opt, value)) +- +- +-def DecodeHexIntOption(option, opt, value): +- try: +- return int(value, 16) +- except ValueError: +- raise OptionValueError("option %s: invalid value: %r" % (opt, value)) +- +- +-def ParseEncodedString(value): +- if value.find(":") < 0: +- raise ParsingError( +- "value should be encoded in encoding:encodedvalue format") +- enc, encValue = value.split(":", 1) +- if enc == "str": +- return encValue.encode("utf-8") + b'\x00' +- elif enc == "hex": +- return bytes.fromhex(encValue) +- raise ParsingError("only str and hex encoding is supported") +- +- +-def ParseValueWithType(value, type): +- if type == 'int': +- return int(value) +- elif type == 'str': +- return value +- elif type == 'bytes': +- return ParseEncodedString(value) +- elif type == 'bool': +- return (value.upper() not in ['F', 'FALSE', '0']) +- else: +- raise ParsingError('cannot recognize type: {}'.format(type)) +- +- +-def FormatZCLArguments(args, command): +- commandArgs = {} +- for kvPair in args: +- if kvPair.find("=") < 0: +- raise ParsingError("Argument should in key=value format") +- key, value = kvPair.split("=", 1) +- valueType = command.get(key, None) +- commandArgs[key] = ParseValueWithType(value, valueType) +- return commandArgs +- +- +-def ShowColoredWarnings(message, category, filename, lineno, file=None, line=None): +- logging.warning(' %s:%s: %s:%s' % +- (filename, lineno, category.__name__, message)) +- return +- +- +-class DeviceMgrCmd(Cmd): +- def __init__(self, rendezvousAddr=None, controllerNodeId=1, bluetoothAdapter=None): +- self.lastNetworkId = None +- self.replHint = None +- +- pretty.install(indent_guides=True, expand_all=True) +- +- coloredlogs.install(level='DEBUG') +- chip.logging.RedirectToPythonLogging() +- +- logging.getLogger().setLevel(logging.DEBUG) +- warnings.showwarning = ShowColoredWarnings +- +- Cmd.__init__(self) +- +- Cmd.identchars = string.ascii_letters + string.digits + "-" +- +- if sys.stdin.isatty(): +- self.prompt = "chip-device-ctrl > " +- else: +- self.use_rawinput = 0 +- self.prompt = "" +- +- DeviceMgrCmd.command_names.sort() +- +- self.bleMgr = None +- +- self.chipStack = ChipStack.ChipStack( +- bluetoothAdapter=bluetoothAdapter, persistentStoragePath='/tmp/chip-device-ctrl-storage.json') +- self.certificateAuthorityManager = chip.CertificateAuthority.CertificateAuthorityManager(chipStack=self.chipStack) +- self.certificateAuthority = self.certificateAuthorityManager.NewCertificateAuthority() +- self.fabricAdmin = self.certificateAuthority.NewFabricAdmin(vendorId=0xFFF1, fabricId=1) +- self.devCtrl = self.fabricAdmin.NewController( +- nodeId=controllerNodeId, useTestCommissioner=True) +- +- self.commissionableNodeCtrl = ChipCommissionableNodeCtrl.ChipCommissionableNodeController( +- self.chipStack) +- +- # If we are on Linux and user selects non-default bluetooth adapter. +- if sys.platform.startswith("linux") and (bluetoothAdapter is not None): +- try: +- self.bleMgr = BleManager(self.devCtrl) +- self.bleMgr.ble_adapter_select( +- "hci{}".format(bluetoothAdapter)) +- except Exception as ex: +- traceback.print_exc() +- print( +- "Failed to initialize BLE, if you don't have BLE, run chip-device-ctrl with --no-ble") +- raise ex +- +- self.historyFileName = os.path.expanduser( +- "~/.chip-device-ctrl-history") +- +- try: +- import readline +- +- if "libedit" in readline.__doc__: +- readline.parse_and_bind("bind ^I rl_complete") +- readline.set_completer_delims(" ") +- try: +- readline.read_history_file(self.historyFileName) +- except IOError: +- pass +- except ImportError: +- pass +- +- command_names = [ +- "setup-payload", +- +- "ble-scan", +- "ble-adapter-select", +- "ble-adapter-print", +- "ble-debug-log", +- +- "connect", +- "close-ble", +- "close-session", +- "resolve", +- "paseonly", +- "commission", +- "zcl", +- "zclread", +- "zclsubscribe", +- +- "discover", +- +- "set-pairing-wifi-credential", +- "set-pairing-thread-credential", +- +- "open-commissioning-window", +- +- "get-fabricid", +- ] +- +- def parseline(self, line): +- cmd, arg, line = Cmd.parseline(self, line) +- if cmd: +- cmd = self.shortCommandName(cmd) +- line = cmd + " " + arg +- return cmd, arg, line +- +- def completenames(self, text, *ignored): +- return [ +- name + " " +- for name in DeviceMgrCmd.command_names +- if name.startswith(text) or self.shortCommandName(name).startswith(text) +- ] +- +- def shortCommandName(self, cmd): +- return cmd.replace("-", "") +- +- def precmd(self, line): +- if not self.use_rawinput and line != "EOF" and line != "": +- print(">>> " + line) +- return line +- +- def postcmd(self, stop, line): +- if self.replHint is not None: +- print("Try the following command in repl: ") +- print(self.replHint) +- print("") +- self.replHint = None +- if not stop and self.use_rawinput: +- self.prompt = "chip-device-ctrl > " +- return stop +- +- def postloop(self): +- try: +- import readline +- +- try: +- readline.write_history_file(self.historyFileName) +- except IOError: +- pass +- except ImportError: +- pass +- +- def do_help(self, line): +- if line: +- cmd, arg, unused = self.parseline(line) +- try: +- doc = getattr(self, "do_" + cmd).__doc__ +- except AttributeError: +- doc = None +- if doc: +- self.stdout.write("%s\n" % textwrap.dedent(doc)) +- else: +- self.stdout.write("No help on %s\n" % (line)) +- else: +- self.print_topics( +- "\nAvailable commands (type help for more information):", +- DeviceMgrCmd.command_names, +- 15, +- 80, +- ) +- +- def do_closeble(self, line): +- """ +- close-ble +- +- Close the ble connection to the device. +- """ +- +- warnings.warn( +- "This method is being deprecated. " +- "Please use the DeviceController.CloseBLEConnection method directly in the REPL", DeprecationWarning) +- +- args = shlex.split(line) +- +- if len(args) != 0: +- print("Usage:") +- self.do_help("close") +- return +- +- try: +- self.devCtrl.CloseBLEConnection() +- except exceptions.ChipStackException as ex: +- print(str(ex)) +- +- def do_setlogoutput(self, line): +- """ +- set-log-output [ none | error | progress | detail ] +- +- Set the level of Chip logging output. +- """ +- +- warnings.warn( +- "This method is being deprecated. " +- "Please use the DeviceController.SetLogFilter method directly in the REPL", DeprecationWarning) +- +- args = shlex.split(line) +- +- if len(args) == 0: +- print("Usage:") +- self.do_help("set-log-output") +- return +- if len(args) > 1: +- print("Unexpected argument: " + args[1]) +- return +- +- category = args[0].lower() +- if category == "none": +- category = 0 +- elif category == "error": +- category = 1 +- elif category == "progress": +- category = 2 +- elif category == "detail": +- category = 3 +- else: +- print("Invalid argument: " + args[0]) +- return +- +- try: +- self.devCtrl.SetLogFilter(category) +- except exceptions.ChipStackException as ex: +- print(str(ex)) +- return +- +- def do_setuppayload(self, line): +- """ +- setup-payload generate [options] +- +- Options: +- -vr Version +- -vi Vendor ID +- -pi Product ID +- -cf Custom Flow [Standard = 0, UserActionRequired = 1, Custom = 2] +- -dc Discovery Capabilities [SoftAP = 1 | BLE = 2 | OnNetwork = 4] +- -dv Discriminator Value +- -ps Passcode +- +- setup-payload parse-manual +- setup-payload parse-qr +- """ +- +- warnings.warn( +- "This method is being deprecated. " +- "Please use the SetupPayload function in the chip.setup_payload package directly", DeprecationWarning) +- +- try: +- arglist = shlex.split(line) +- if arglist[0] not in ("generate", "parse-manual", "parse-qr"): +- self.do_help("setup-payload") +- return +- +- if arglist[0] == "generate": +- parser = argparse.ArgumentParser() +- parser.add_argument("-vr", type=int, default=0, dest='version') +- parser.add_argument( +- "-pi", type=int, default=0, dest='productId') +- parser.add_argument( +- "-vi", type=int, default=0, dest='vendorId') +- parser.add_argument( +- '-cf', type=int, default=0, dest='customFlow') +- parser.add_argument( +- "-dc", type=int, default=0, dest='capabilities') +- parser.add_argument( +- "-dv", type=int, default=0, dest='discriminator') +- parser.add_argument("-ps", type=int, dest='passcode') +- args = parser.parse_args(arglist[1:]) +- +- SetupPayload().PrintOnboardingCodes(args.passcode, args.vendorId, args.productId, +- args.discriminator, args.customFlow, args.capabilities, args.version) +- +- if arglist[0] == "parse-manual": +- SetupPayload().ParseManualPairingCode(arglist[1]).Print() +- +- if arglist[0] == "parse-qr": +- SetupPayload().ParseQrCode(arglist[1]).Print() +- +- except exceptions.ChipStackException as ex: +- print(str(ex)) +- return +- +- def do_bleadapterselect(self, line): +- """ +- ble-adapter-select +- +- Start BLE adapter select, deprecated, you can select adapter by command line arguments. +- """ +- if sys.platform.startswith("linux"): +- if not self.bleMgr: +- self.bleMgr = BleManager(self.devCtrl) +- +- self.bleMgr.ble_adapter_select(line) +- print( +- "This change only applies to ble-scan\n" +- "Please run device controller with --bluetooth-adapter= to select adapter\n" + +- "e.g. chip-device-ctrl --bluetooth-adapter hci0" +- ) +- else: +- print( +- "ble-adapter-select only works in Linux, ble-adapter-select mac_address" +- ) +- +- return +- +- def do_bleadapterprint(self, line): +- """ +- ble-adapter-print +- +- Print attached BLE adapter. +- """ +- if sys.platform.startswith("linux"): +- if not self.bleMgr: +- self.bleMgr = BleManager(self.devCtrl) +- +- self.bleMgr.ble_adapter_print() +- else: +- print("ble-adapter-print only works in Linux") +- +- return +- +- def do_bledebuglog(self, line): +- """ +- ble-debug-log 0:1 +- 0: disable BLE debug log +- 1: enable BLE debug log +- """ +- if not self.bleMgr: +- self.bleMgr = BleManager(self.devCtrl) +- +- self.bleMgr.ble_debug_log(line) +- +- return +- +- def do_blescan(self, line): +- """ +- ble-scan +- +- Start BLE scanning operations. +- """ +- +- if not self.bleMgr: +- self.bleMgr = BleManager(self.devCtrl) +- +- self.bleMgr.scan(line) +- +- return +- +- def ConnectFromSetupPayload(self, setupPayload, nodeid): +- # TODO(cecille): Get this from the C++ code? +- ble = 1 << 1 +- # Devices may be uncommissioned, or may already be on the network. Need to check both ways. +- # TODO(cecille): implement soft-ap connection. +- +- # Any device that is already commissioned into a fabric needs to use on-network +- # pairing, so look first on the network regardless of the QR code contents. +- print("Attempting to find device on Network") +- longDiscriminator = ctypes.c_uint16( +- int(setupPayload.attributes['Discriminator'])) +- self.devCtrl.DiscoverCommissionableNodesLongDiscriminator( +- longDiscriminator) +- print("Waiting for device responses...") +- strlen = 100 +- addrStrStorage = ctypes.create_string_buffer(strlen) +- # If this device is on the network and we're looking specifically for 1 device, +- # expect a quick response. +- if self.wait_for_one_discovered_device(): +- self.devCtrl.GetIPForDiscoveredDevice( +- 0, addrStrStorage, strlen) +- addrStr = addrStrStorage.value.decode('utf-8') +- print("Connecting to device at " + addrStr) +- pincode = ctypes.c_uint32( +- int(setupPayload.attributes['SetUpPINCode'])) +- try: +- self.devCtrl.CommissionIP(addrStrStorage, pincode, nodeid) +- print("Connected") +- return 0 +- except Exception as ex: +- print(f"Unable to connect on network: {ex}") +- else: +- print("Unable to locate device on network") +- +- if int(setupPayload.attributes["RendezvousInformation"]) & ble: +- print("Attempting to connect via BLE") +- longDiscriminator = ctypes.c_uint16( +- int(setupPayload.attributes['Discriminator'])) +- pincode = ctypes.c_uint32( +- int(setupPayload.attributes['SetUpPINCode'])) +- try: +- self.devCtrl.ConnectBLE(longDiscriminator, pincode, nodeid) +- print("Connected") +- return 0 +- except Exception as ex: +- print(f"Unable to connect: {ex}") +- return -1 +- +- def do_paseonly(self, line): +- """ +- paseonly -ip [] +- +- TODO: Add more methods to connect to device (like cert for auth, and IP +- for connection) +- """ +- +- try: +- args = shlex.split(line) +- if len(args) <= 1: +- print("Usage:") +- self.do_help("paseonly") +- return +- nodeid = random.randint(1, 1000000) # Just a random number +- if len(args) == 4: +- nodeid = int(args[3]) +- print("Device is assigned with nodeid = {}".format(nodeid)) +- self.replHint = f"devCtrl.EstablishPASESessionIP({repr(args[1])}, {int(args[2])}, {nodeid})" +- if args[0] == "-ip" and len(args) >= 3: +- self.devCtrl.EstablishPASESessionIP(args[1], int(args[2]), nodeid) +- else: +- print("Usage:") +- self.do_help("paseonly") +- return +- print( +- "Device temporary node id (**this does not match spec**): {}".format(nodeid)) +- except Exception as ex: +- print(str(ex)) +- return +- +- def do_commission(self, line): +- """ +- commission nodeid +- +- Runs commissioning on a device that has been connected with paseonly +- """ +- try: +- args = shlex.split(line) +- if len(args) != 1: +- print("Usage:") +- self.do_help("commission") +- return +- nodeid = int(args[0]) +- self.replHint = f"devCtrl.Commission({nodeid})" +- self.devCtrl.Commission(nodeid) +- except Exception as ex: +- print(str(ex)) +- return +- +- def do_connect(self, line): +- """ +- connect -ip [] +- connect -ble [] +- connect -qr [] +- connect -code [] +- +- connect command is used for establishing a rendezvous session to the device. +- currently, only connect using setupPinCode is supported. +- -qr option will connect to the first device with a matching long discriminator. +- +- TODO: Add more methods to connect to device (like cert for auth, and IP +- for connection) +- """ +- +- warnings.warn( +- "This method is being deprecated. " +- "Please use the DeviceController.[ConnectBLE|CommissionIP] methods directly in the REPL", DeprecationWarning) +- +- try: +- args = shlex.split(line) +- if len(args) <= 1: +- print("Usage:") +- self.do_help("connect SetupPinCode") +- return +- +- nodeid = random.randint(1, 1000000) # Just a random number +- if len(args) == 4: +- nodeid = int(args[3]) +- print("Device is assigned with nodeid = {}".format(nodeid)) +- +- if args[0] == "-ip" and len(args) >= 3: +- self.replHint = f"devCtrl.CommissionIP({repr(args[1])}, {int(args[2])}, {nodeid})" +- self.devCtrl.CommissionIP(args[1], int(args[2]), nodeid) +- elif args[0] == "-ble" and len(args) >= 3: +- self.replHint = f"devCtrl.ConnectBLE({int(args[1])}, {int(args[2])}, {nodeid})" +- self.devCtrl.ConnectBLE(int(args[1]), int(args[2]), nodeid) +- elif args[0] in ['-qr', '-code'] and len(args) >= 2: +- if len(args) == 3: +- nodeid = int(args[2]) +- print("Parsing QR code {}".format(args[1])) +- +- setupPayload = None +- if args[0] == '-qr': +- setupPayload = SetupPayload().ParseQrCode(args[1]) +- elif args[0] == '-code': +- setupPayload = SetupPayload( +- ).ParseManualPairingCode(args[1]) +- +- if not int(setupPayload.attributes.get("RendezvousInformation", 0)): +- print("No rendezvous information provided, default to all.") +- setupPayload.attributes["RendezvousInformation"] = 0b111 +- setupPayload.Print() +- self.replHint = f"devCtrl.CommissionWithCode(setupPayload={repr(setupPayload)}, nodeid={nodeid})" +- self.ConnectFromSetupPayload(setupPayload, nodeid) +- else: +- print("Usage:") +- self.do_help("connect SetupPinCode") +- return +- print( +- "Device temporary node id (**this does not match spec**): {}".format(nodeid)) +- except exceptions.ChipStackException as ex: +- print(str(ex)) +- return +- +- def do_closesession(self, line): +- """ +- close-session +- +- Close any session associated with a given node ID. +- """ +- try: +- parser = argparse.ArgumentParser() +- parser.add_argument('nodeid', type=int, help='Peer node ID') +- args = parser.parse_args(shlex.split(line)) +- self.replHint = f"devCtrl.CloseSession({args.nodeid})" +- self.devCtrl.CloseSession(args.nodeid) +- except exceptions.ChipStackException as ex: +- print(str(ex)) +- except Exception: +- self.do_help("close-session") +- +- def do_resolve(self, line): +- """ +- resolve +- +- Resolve DNS-SD name corresponding with the given node ID and +- update address of the node in the device controller. +- """ +- try: +- args = shlex.split(line) +- if len(args) == 1: +- try: +- self.replHint = f"devCtrl.ResolveNode({int(args[0])});devCtrl.GetAddressAndPort({int(args[0])})" +- self.devCtrl.ResolveNode(int(args[0])) +- address = self.devCtrl.GetAddressAndPort(int(args[0])) +- address = "{}:{}".format( +- *address) if address else "unknown" +- print("Current address: " + address) +- except exceptions.ChipStackException as ex: +- print(str(ex)) +- else: +- self.do_help("resolve") +- except exceptions.ChipStackException as ex: +- print(str(ex)) +- return +- +- def wait_for_one_discovered_device(self): +- print("Waiting for device responses...") +- strlen = 100 +- addrStrStorage = ctypes.create_string_buffer(strlen) +- count = 0 +- maxWaitTime = 2 +- while (not self.devCtrl.GetIPForDiscoveredDevice(0, addrStrStorage, strlen) and count < maxWaitTime): +- time.sleep(0.2) +- count = count + 0.2 +- return count < maxWaitTime +- +- def wait_for_many_discovered_devices(self): +- # Discovery happens through mdns, which means we need to wait for responses to come back. +- # TODO(cecille): I suppose we could make this a command line arg. Or Add a callback when +- # x number of responses are received. For now, just 2 seconds. We can all wait that long. +- print("Waiting for device responses...") +- time.sleep(2) +- +- def do_discover(self, line): +- """ +- discover -qr qrcode +- discover -all +- discover -l long_discriminator +- discover -s short_discriminator +- discover -v vendor_id +- discover -t device_type +- discover -c +- +- discover command is used to discover available devices. +- """ +- try: +- arglist = shlex.split(line) +- if len(arglist) < 1: +- print("Usage:") +- self.do_help("discover") +- return +- parser = argparse.ArgumentParser() +- group = parser.add_mutually_exclusive_group() +- group.add_argument( +- '-all', help='discover all commissionable nodes and commissioners', action='store_true') +- group.add_argument( +- '-qr', help='discover commissionable nodes matching provided QR code', type=str) +- group.add_argument( +- '-l', help='discover commissionable nodes with given long discriminator', type=int) +- group.add_argument( +- '-s', help='discover commissionable nodes with given short discriminator', type=int) +- group.add_argument( +- '-v', help='discover commissionable nodes with given vendor ID', type=int) +- group.add_argument( +- '-t', help='discover commissionable nodes with given device type', type=int) +- group.add_argument( +- '-c', help='discover commissionable nodes in commissioning mode', action='store_true') +- args = parser.parse_args(arglist) +- if args.all: +- self.commissionableNodeCtrl.DiscoverCommissioners() +- self.wait_for_many_discovered_devices() +- self.commissionableNodeCtrl.PrintDiscoveredCommissioners() +- self.devCtrl.DiscoverAllCommissioning() +- self.wait_for_many_discovered_devices() +- elif args.qr is not None: +- setupPayload = SetupPayload().ParseQrCode(args.qr) +- longDiscriminator = ctypes.c_uint16( +- int(setupPayload.attributes['Discriminator'])) +- self.devCtrl.DiscoverCommissionableNodesLongDiscriminator( +- longDiscriminator) +- self.wait_for_one_discovered_device() +- elif args.l is not None: +- self.devCtrl.DiscoverCommissionableNodesLongDiscriminator( +- ctypes.c_uint16(args.l)) +- self.wait_for_one_discovered_device() +- elif args.s is not None: +- self.devCtrl.DiscoverCommissionableNodesShortDiscriminator( +- ctypes.c_uint16(args.s)) +- self.wait_for_one_discovered_device() +- elif args.v is not None: +- self.devCtrl.DiscoverCommissionableNodesVendor( +- ctypes.c_uint16(args.v)) +- self.wait_for_many_discovered_devices() +- elif args.t is not None: +- self.devCtrl.DiscoverCommissionableNodesDeviceType( +- ctypes.c_uint16(args.t)) +- self.wait_for_many_discovered_devices() +- elif args.c is not None: +- self.devCtrl.DiscoverCommissionableNodesCommissioningEnabled() +- self.wait_for_many_discovered_devices() +- else: +- self.do_help("discover") +- return +- self.devCtrl.PrintDiscoveredDevices() +- except exceptions.ChipStackException as ex: +- print('exception') +- print(str(ex)) +- return +- except Exception: +- self.do_help("discover") +- return +- +- def do_zcl(self, line): +- """ +- To send ZCL message to device: +- zcl [key=value]... +- To get a list of clusters: +- zcl ? +- To get a list of commands in cluster: +- zcl ? +- +- Send ZCL command to device nodeid +- """ +- try: +- args = shlex.split(line) +- all_commands = self.devCtrl.ZCLCommandList() +- if len(args) == 1 and args[0] == '?': +- print('\n'.join(all_commands.keys())) +- elif len(args) == 2 and args[0] == '?': +- if args[1] not in all_commands: +- raise exceptions.UnknownCluster(args[1]) +- for commands in all_commands.get(args[1]).items(): +- args = ", ".join(["{}: {}".format(argName, argType) +- for argName, argType in commands[1].items()]) +- print(commands[0]) +- if commands[1]: +- print(" ", args) +- else: +- print(" ") +- elif len(args) > 4: +- if args[0] not in all_commands: +- raise exceptions.UnknownCluster(args[0]) +- command = all_commands.get(args[0]).get(args[1], None) +- # When command takes no arguments, (not command) is True +- if command is None: +- raise exceptions.UnknownCommand(args[0], args[1]) +- req = eval(f"Clusters.{args[0]}.Commands.{args[1]}")(**FormatZCLArguments(args[5:], command)) +- self.replHint = f"await devCtrl.SendCommand({int(args[2])}, {int(args[3])}, Clusters.{repr(req)})" +- err, res = self.devCtrl.ZCLSend(args[0], args[1], int( +- args[2]), int(args[3]), int(args[4]), FormatZCLArguments(args[5:], command), blocking=True) +- if err != 0: +- print("Failed to receive command response: {}".format(res)) +- elif res is not None: +- print("Received command status response:") +- print(res) +- else: +- print("Success, no status code is attached with response.") +- else: +- self.do_help("zcl") +- except exceptions.ChipStackException as ex: +- print("An exception occurred during process ZCL command:") +- print(str(ex)) +- except Exception as ex: +- print("An exception occurred during processing input:") +- traceback.print_exc() +- print(str(ex)) +- +- def do_zclread(self, line): +- """ +- To read ZCL attribute: +- zclread +- """ +- try: +- args = shlex.split(line) +- all_attrs = self.devCtrl.ZCLAttributeList() +- if len(args) == 1 and args[0] == '?': +- print('\n'.join(all_attrs.keys())) +- elif len(args) == 2 and args[0] == '?': +- if args[1] not in all_attrs: +- raise exceptions.UnknownCluster(args[1]) +- print('\n'.join(all_attrs.get(args[1]).keys())) +- elif len(args) == 5: +- if args[0] not in all_attrs: +- raise exceptions.UnknownCluster(args[0]) +- self.replHint = (f"await devCtrl.ReadAttribute({int(args[2])}, [({int(args[3])}, " +- f"Clusters.{args[0]}.Attributes.{args[1]})])") +- res = self.devCtrl.ZCLReadAttribute(args[0], args[1], int( +- args[2]), int(args[3]), int(args[4])) +- if res is not None: +- print(repr(res)) +- else: +- self.do_help("zclread") +- except exceptions.ChipStackException as ex: +- print("An exception occurred during reading ZCL attribute:") +- print(str(ex)) +- except Exception as ex: +- print("An exception occurred during processing input:") +- print(str(ex)) +- +- def do_zclwrite(self, line): +- """ +- To write ZCL attribute: +- zclwrite +- """ +- try: +- args = shlex.split(line) +- all_attrs = self.devCtrl.ZCLAttributeList() +- if len(args) == 1 and args[0] == '?': +- print('\n'.join(all_attrs.keys())) +- elif len(args) == 2 and args[0] == '?': +- if args[1] not in all_attrs: +- raise exceptions.UnknownCluster(args[1]) +- cluster_attrs = all_attrs.get(args[1], {}) +- print('\n'.join(["{}: {}".format(key, cluster_attrs[key]["type"]) +- for key in cluster_attrs.keys() if cluster_attrs[key].get("writable", False)])) +- elif len(args) == 6: +- if args[0] not in all_attrs: +- raise exceptions.UnknownCluster(args[0]) +- attribute_type = all_attrs.get(args[0], {}).get( +- args[1], {}).get("type", None) +- self.replHint = ( +- f"await devCtrl.WriteAttribute({int(args[2])}, [({int(args[3])}, " +- f"Clusters.{args[0]}.Attributes.{args[1]}(value={repr(ParseValueWithType(args[5], attribute_type))}))])") +- res = self.devCtrl.ZCLWriteAttribute(args[0], args[1], int( +- args[2]), int(args[3]), int(args[4]), ParseValueWithType(args[5], attribute_type)) +- print(repr(res)) +- else: +- self.do_help("zclwrite") +- except exceptions.ChipStackException as ex: +- print("An exception occurred during writing ZCL attribute:") +- print(str(ex)) +- except Exception as ex: +- print("An exception occurred during processing input:") +- print(str(ex)) +- +- def do_zclsubscribe(self, line): +- """ +- To subscribe ZCL attribute reporting: +- zclsubscribe +- +- To shut down a subscription: +- zclsubscribe -shutdown +- """ +- try: +- args = shlex.split(line) +- all_attrs = self.devCtrl.ZCLAttributeList() +- if len(args) == 1 and args[0] == '?': +- print('\n'.join(all_attrs.keys())) +- elif len(args) == 2 and args[0] == '?': +- if args[1] not in all_attrs: +- raise exceptions.UnknownCluster(args[1]) +- cluster_attrs = all_attrs.get(args[1], {}) +- print('\n'.join([key for key in cluster_attrs.keys( +- ) if cluster_attrs[key].get("reportable", False)])) +- elif len(args) == 6: +- if args[0] not in all_attrs: +- raise exceptions.UnknownCluster(args[0]) +- res = self.devCtrl.ZCLSubscribeAttribute(args[0], args[1], int( +- args[2]), int(args[3]), int(args[4]), int(args[5])) +- self.replHint = (f"sub = await devCtrl.ReadAttribute({int(args[2])}, [({int(args[3])}, " +- f"Clusters.{args[0]}.Attributes.{args[1]})], reportInterval=({int(args[4])}, {int(args[5])}))") +- print(res.GetAllValues()) +- print(f"Subscription Established: {res}") +- elif len(args) == 2 and args[0] == '-shutdown': +- subscriptionId = int(args[1], base=0) +- self.replHint = "You can call sub.Shutdown() (sub is the return value of ReadAttribute() called before)" +- self.devCtrl.ZCLShutdownSubscription(subscriptionId) +- else: +- self.do_help("zclsubscribe") +- except exceptions.ChipStackException as ex: +- print("An exception occurred during configuring reporting of ZCL attribute:") +- print(str(ex)) +- except Exception as ex: +- print("An exception occurred during processing input:") +- print(str(ex)) +- +- def do_setpairingwificredential(self, line): +- """ +- set-pairing-wifi-credential ssid credentials +- """ +- try: +- args = shlex.split(line) +- if len(args) < 2: +- print("Usage:") +- self.do_help("set-pairing-wifi-credential") +- return +- self.devCtrl.SetWiFiCredentials( +- args[0], args[1]) +- self.replHint = f"devCtrl.SetWiFiCredentials({repr(args[0])}, {repr(args[1])})" +- except Exception as ex: +- print(str(ex)) +- return +- +- def do_setpairingthreadcredential(self, line): +- """ +- set-pairing-thread-credential threadOperationalDataset +- """ +- try: +- args = shlex.split(line) +- if len(args) < 1: +- print("Usage:") +- self.do_help("set-pairing-thread-credential") +- return +- self.replHint = f"devCtrl.SetThreadOperationalDataset(bytes.fromhex({repr(args[0])}))" +- self.devCtrl.SetThreadOperationalDataset(bytes.fromhex(args[0])) +- except Exception as ex: +- print(str(ex)) +- return +- +- def do_opencommissioningwindow(self, line): +- """ +- open-commissioning-window [options] +- +- Options: +- -t Timeout (in seconds) +- -o Option [TokenWithRandomPIN = 1, TokenWithProvidedPIN = 2] +- -d Discriminator Value +- -i Iteration +- +- This command is used by a current Administrator to instruct a Node to go into commissioning mode +- """ +- try: +- arglist = shlex.split(line) +- +- if len(arglist) <= 1: +- print("Usage:") +- self.do_help("open-commissioning-window") +- return +- parser = argparse.ArgumentParser() +- parser.add_argument( +- "-t", type=int, default=0, dest='timeout') +- parser.add_argument( +- "-o", type=int, default=1, dest='option') +- parser.add_argument( +- "-i", type=int, default=0, dest='iteration') +- parser.add_argument( +- "-d", type=int, default=0, dest='discriminator') +- args = parser.parse_args(arglist[1:]) +- +- if args.option < 1 or args.option > 2: +- print("Invalid option specified!") +- raise ValueError("Invalid option specified") +- +- self.replHint = (f"devCtrl.OpenCommissioningWindow(nodeid={int(arglist[0])}, timeout={args.timeout}, " +- f"iteration={args.iteration}, discriminator={args.discriminator}, option={args.option})") +- +- self.devCtrl.OpenCommissioningWindow( +- int(arglist[0]), args.timeout, args.iteration, args.discriminator, args.option) +- +- except exceptions.ChipStackException as ex: +- print(str(ex)) +- return +- except Exception: +- self.do_help("open-commissioning-window") +- return +- +- def do_getfabricid(self, line): +- """ +- get-fabricid +- +- Read the current Compressed Fabric Id of the controller device, return 0 if not available. +- """ +- try: +- args = shlex.split(line) +- +- if (len(args) > 0): +- print("Unexpected argument: " + args[1]) +- return +- +- compressed_fabricid = self.devCtrl.GetCompressedFabricId() +- raw_fabricid = self.devCtrl.fabricId +- +- self.replHint = "devCtrl.GetCompressedFabricId(), devCtrl.fabricId" +- except exceptions.ChipStackException as ex: +- print("An exception occurred during reading FabricID:") +- print(str(ex)) +- return +- +- print("Get fabric ID complete") +- +- print("Raw Fabric ID: 0x{:016x}".format(raw_fabricid) +- + " (" + str(raw_fabricid) + ")") +- +- print("Compressed Fabric ID: 0x{:016x}".format(compressed_fabricid) +- + " (" + str(compressed_fabricid) + ")") +- +- def do_history(self, line): +- """ +- history +- +- Show previously executed commands. +- """ +- +- try: +- import readline +- +- h = readline.get_current_history_length() +- for n in range(1, h + 1): +- print(readline.get_history_item(n)) +- except ImportError: +- pass +- +- def do_h(self, line): +- self.do_history(line) +- +- def do_exit(self, line): +- return True +- +- def do_quit(self, line): +- return True +- +- def do_q(self, line): +- return True +- +- def do_EOF(self, line): +- print() +- return True +- +- def emptyline(self): +- pass +- +- +-def main(): +- optParser = OptionParser() +- optParser.add_option( +- "-r", +- "--rendezvous-addr", +- action="store", +- dest="rendezvousAddr", +- help="Device rendezvous address", +- metavar="", +- ) +- optParser.add_option( +- "-n", +- "--controller-nodeid", +- action="store", +- dest="controllerNodeId", +- default=1, +- type='int', +- help="Controller node ID", +- metavar="", +- ) +- +- if sys.platform.startswith("linux"): +- optParser.add_option( +- "-b", +- "--bluetooth-adapter", +- action="store", +- dest="bluetoothAdapter", +- default="hci0", +- type="str", +- help="Controller bluetooth adapter ID, use --no-ble to disable bluetooth functions.", +- metavar="", +- ) +- optParser.add_option( +- "--no-ble", +- action="store_true", +- dest="disableBluetooth", +- help="Disable bluetooth, calling BLE related feature with this flag results in undefined behavior.", +- ) +- (options, remainingArgs) = optParser.parse_args(sys.argv[1:]) +- +- if len(remainingArgs) != 0: +- print("Unexpected argument: %s" % remainingArgs[0]) +- sys.exit(-1) +- +- adapterId = None +- if sys.platform.startswith("linux"): +- if options.disableBluetooth: +- adapterId = None +- elif not options.bluetoothAdapter.startswith("hci"): +- print( +- "Invalid bluetooth adapter: {}, adapter name looks like hci0, hci1 etc.") +- sys.exit(-1) +- else: +- try: +- adapterId = int(options.bluetoothAdapter[3:]) +- except ValueError: +- print( +- "Invalid bluetooth adapter: {}, adapter name looks like hci0, hci1 etc.") +- sys.exit(-1) +- native.Init(bluetoothAdapter=adapterId) +- try: +- devMgrCmd = DeviceMgrCmd(rendezvousAddr=options.rendezvousAddr, +- controllerNodeId=options.controllerNodeId, bluetoothAdapter=adapterId) +- except Exception as ex: +- print(ex) +- print("Failed to bringup CHIPDeviceController CLI") +- sys.exit(1) +- +- print("Chip Device Controller Shell") +- if options.rendezvousAddr: +- print("Rendezvous address set to %s" % options.rendezvousAddr) +- +- # Adapter ID will always be 0 +- if adapterId != 0: +- print("Bluetooth adapter set to hci{}".format(adapterId)) +- print() +- +- try: +- devMgrCmd.cmdloop() +- except KeyboardInterrupt: +- print("\nQuitting") +- +- sys.exit(0) +- +- +-if __name__ == "__main__": +- print(""" +- chip-device-ctrl will be deprecated and will be removed in the future. Please try chip-repl, which provides a lot of features. +- +- - Multi-fabric support, +- - Better complex type support for sending commands, +- - Native command highlight, +- - Parallel commands with asyncio, +- - Writing complex logic inline. +- +- You can still use chip-device-ctrl as usual for now, and you will learn how to do the same thing in chip-repl. +- +- Feel free to file an issue if some features are not supported by chip-repl yet. +- """) +- main() +diff --git a/src/controller/python/chip/ChipDeviceCtrl.py b/src/controller/python/chip/ChipDeviceCtrl.py +index 4d14a42f18..1d1627c46d 100644 +--- a/src/controller/python/chip/ChipDeviceCtrl.py ++++ b/src/controller/python/chip/ChipDeviceCtrl.py +@@ -50,12 +50,9 @@ from . import discovery + from .clusters import Attribute as ClusterAttribute + from .clusters import ClusterObjects as ClusterObjects + from .clusters import Command as ClusterCommand +-from .clusters import Objects as GeneratedObjects + from .clusters.CHIPClusters import ChipClusters + from .crypto import p256keypair +-from .exceptions import UnknownAttribute, UnknownCommand +-from .interaction_model import InteractionModelError, SessionParameters, SessionParametersStruct +-from .interaction_model import delegate as im ++from .interaction_model import SessionParameters, SessionParametersStruct + from .native import PyChipError + + __all__ = ["ChipDeviceController", "CommissioningParameters"] +@@ -1464,83 +1461,6 @@ class ChipDeviceControllerBase(): + else: + return res.events + +- def ZCLSend(self, cluster, command, nodeid, endpoint, groupid, args, blocking=False): +- ''' Wrapper over SendCommand that catches the exceptions +- Returns a tuple of (errorCode, CommandResponse) +- ''' +- self.CheckIsActive() +- +- req = None +- try: +- req = eval( +- f"GeneratedObjects.{cluster}.Commands.{command}")(**args) +- except BaseException: +- raise UnknownCommand(cluster, command) +- try: +- res = asyncio.run(self.SendCommand(nodeid, endpoint, req)) +- logging.debug(f"CommandResponse {res}") +- return (0, res) +- except InteractionModelError as ex: +- return (int(ex.status), None) +- +- def ZCLReadAttribute(self, cluster, attribute, nodeid, endpoint, groupid, blocking=True): +- ''' Wrapper over ReadAttribute for a single attribute +- Returns an AttributeReadResult +- ''' +- self.CheckIsActive() +- +- clusterType = getattr(GeneratedObjects, cluster) +- +- try: +- attributeType = eval( +- f"GeneratedObjects.{cluster}.Attributes.{attribute}") +- except BaseException: +- raise UnknownAttribute(cluster, attribute) +- +- result = asyncio.run(self.ReadAttribute( +- nodeid, [(endpoint, attributeType)])) +- path = ClusterAttribute.AttributePath.from_attribute( +- EndpointId=endpoint, Attribute=attributeType) +- return im.AttributeReadResult(path=im.AttributePath(nodeId=nodeid, endpointId=path.EndpointId, clusterId=path.ClusterId, attributeId=path.AttributeId), +- status=0, value=result[endpoint][clusterType][attributeType], dataVersion=result[endpoint][clusterType][ClusterAttribute.DataVersion]) +- +- def ZCLWriteAttribute(self, cluster: str, attribute: str, nodeid, endpoint, groupid, value, dataVersion=0, blocking=True): +- ''' Wrapper over WriteAttribute for a single attribute +- return PyChipError +- ''' +- req = None +- try: +- req = eval( +- f"GeneratedObjects.{cluster}.Attributes.{attribute}")(value) +- except BaseException: +- raise UnknownAttribute(cluster, attribute) +- +- return asyncio.run(self.WriteAttribute(nodeid, [(endpoint, req, dataVersion)])) +- +- def ZCLSubscribeAttribute(self, cluster, attribute, nodeid, endpoint, minInterval, maxInterval, blocking=True, +- keepSubscriptions=False, autoResubscribe=True): +- ''' Wrapper over ReadAttribute for a single attribute +- Returns a SubscriptionTransaction. See ReadAttribute for more information. +- ''' +- self.CheckIsActive() +- +- req = None +- try: +- req = eval(f"GeneratedObjects.{cluster}.Attributes.{attribute}") +- except BaseException: +- raise UnknownAttribute(cluster, attribute) +- return asyncio.run(self.ReadAttribute(nodeid, [(endpoint, req)], None, False, reportInterval=(minInterval, maxInterval), +- keepSubscriptions=keepSubscriptions, autoResubscribe=autoResubscribe)) +- +- def ZCLCommandList(self): +- self.CheckIsActive() +- return self._Cluster.ListClusterCommands() +- +- def ZCLAttributeList(self): +- self.CheckIsActive() +- +- return self._Cluster.ListClusterAttributes() +- + def SetBlockingCB(self, blockingCB): + self.CheckIsActive() + +diff --git a/src/controller/python/chip/ChipReplStartup.py b/src/controller/python/chip/ChipReplStartup.py +index a49638cb99..b75c77efcd 100644 +--- a/src/controller/python/chip/ChipReplStartup.py ++++ b/src/controller/python/chip/ChipReplStartup.py +@@ -94,6 +94,8 @@ def main(): + "-d", "--debug", help="Set default logging level to debug.", action="store_true") + parser.add_argument( + "-t", "--trust-store", help="Path to the PAA trust store.", action="store", default="./credentials/development/paa-root-certs") ++ parser.add_argument( ++ "-b", "--ble-adapter", help="Set the Bluetooth adapter index.", type=int, default=None) + args = parser.parse_args() + + if not os.path.exists(args.trust_store): +@@ -128,7 +130,7 @@ or run `os.chdir` to the root of your CHIP repository checkout. + # nothing we can do ... things will NOT work + return + +- chip.native.Init() ++ chip.native.Init(bluetoothAdapter=args.ble_adapter) + + global certificateAuthorityManager + global chipStack +diff --git a/src/pybindings/pycontroller/build-chip-wheel.py b/src/pybindings/pycontroller/build-chip-wheel.py +index a61b591d5c..61bdf373e8 100644 +--- a/src/pybindings/pycontroller/build-chip-wheel.py ++++ b/src/pybindings/pycontroller/build-chip-wheel.py +@@ -60,7 +60,6 @@ packageName = args.package_name + chipPackageVer = args.build_number + + installScripts = [ +- # InstalledScriptInfo('chip-device-ctrl.py'), + # InstalledScriptInfo('chip-repl.py'), + ] + +-- +2.45.2 + diff --git a/0007-Python-Remove-obsolete-callback-handling.patch b/0014-Python-Remove-obsolete-callback-handling-33665.patch similarity index 68% rename from 0007-Python-Remove-obsolete-callback-handling.patch rename to 0014-Python-Remove-obsolete-callback-handling-33665.patch index b8cb9c4..cbbe00a 100644 --- a/0007-Python-Remove-obsolete-callback-handling.patch +++ b/0014-Python-Remove-obsolete-callback-handling-33665.patch @@ -1,8 +1,7 @@ -From 20f1c72293991ad01043660b777e53be0992bae5 Mon Sep 17 00:00:00 2001 -Message-ID: <20f1c72293991ad01043660b777e53be0992bae5.1717003814.git.stefan@agner.ch> +From 3997a66e9e436646a4652448bc72309d8afee2bf Mon Sep 17 00:00:00 2001 From: Stefan Agner -Date: Wed, 29 May 2024 19:05:43 +0200 -Subject: [PATCH] [Python] Remove obsolete callback handling +Date: Thu, 30 May 2024 22:41:35 +0200 +Subject: [PATCH] [Python] Remove obsolete callback handling (#33665) The Call() function currently still has some callback handling code the completeEvent and callbackRes variables. These are only used when @@ -16,15 +15,25 @@ However, when calling the SDK from multiple threads, then another Call() Might accidentally release a call to CallAsyncWithCompleteCallback() early. --- - src/controller/python/chip/ChipStack.py | 19 ------------------- - 1 file changed, 19 deletions(-) + src/controller/python/chip/ChipStack.py | 22 +--------------------- + 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/src/controller/python/chip/ChipStack.py b/src/controller/python/chip/ChipStack.py -index 6df7e41de4..a8f07941a2 100644 +index 35f9e24ef4..3a167bb6bc 100644 --- a/src/controller/python/chip/ChipStack.py +++ b/src/controller/python/chip/ChipStack.py -@@ -164,9 +164,6 @@ class AsyncCallableHandle: - return self._res +@@ -32,8 +32,7 @@ import logging + import os + import sys + import time +-from ctypes import (CFUNCTYPE, POINTER, Structure, c_bool, c_char_p, c_int64, c_uint8, c_uint16, c_uint32, c_ulong, c_void_p, +- py_object, pythonapi) ++from ctypes import CFUNCTYPE, Structure, c_bool, c_char_p, c_int64, c_uint8, c_uint16, c_uint32, c_void_p, py_object, pythonapi + from threading import Condition, Event, Lock + + import chip.native +@@ -194,9 +193,6 @@ class AsyncioCallableHandle: + pythonapi.Py_DecRef(py_object(self)) -_CompleteFunct = CFUNCTYPE(None, c_void_p, c_void_p) @@ -33,7 +42,7 @@ index 6df7e41de4..a8f07941a2 100644 _LogMessageFunct = CFUNCTYPE( None, c_int64, c_int64, c_char_p, c_uint8, c_char_p) _ChipThreadTaskRunnerFunct = CFUNCTYPE(None, py_object) -@@ -241,21 +238,11 @@ class ChipStack(object): +@@ -272,21 +268,11 @@ class ChipStack(object): self.logger.addHandler(logHandler) self.logger.setLevel(logging.DEBUG) @@ -55,13 +64,14 @@ index 6df7e41de4..a8f07941a2 100644 # set by other modules(BLE) that require service by thread while thread blocks. self.blockingCB = None -@@ -357,14 +344,8 @@ class ChipStack(object): +@@ -389,15 +375,9 @@ class ChipStack(object): This function is a wrapper of PostTaskOnChipThread, which includes some handling of application specific logics. Calling this function on CHIP on CHIP mainloop thread will cause deadlock. ''' - # throw error if op in progress - self.callbackRes = None - self.completeEvent.clear() + # TODO: Lock probably no longer necessary, see https://github.com/project-chip/connectedhomeip/issues/33321. with self.networkLock: res = self.PostTaskOnChipThread(callFunct).Wait(timeoutMs) - self.completeEvent.set() @@ -69,7 +79,7 @@ index 6df7e41de4..a8f07941a2 100644 - return self.callbackRes return res - def CallAsync(self, callFunct): + async def CallAsync(self, callFunct, timeoutMs: int = None): -- -2.45.1 +2.45.2 diff --git a/0015-Python-Add-automation-level-to-log-defines-33670.patch b/0015-Python-Add-automation-level-to-log-defines-33670.patch new file mode 100644 index 0000000..9b1cb60 --- /dev/null +++ b/0015-Python-Add-automation-level-to-log-defines-33670.patch @@ -0,0 +1,94 @@ +From 0fe7d8b3d9c920ce7dc970097962dca14f9d8f03 Mon Sep 17 00:00:00 2001 +From: Stefan Agner +Date: Fri, 31 May 2024 15:24:42 +0200 +Subject: [PATCH] [Python] Add "automation" level to log defines (#33670) + +So far the automation log level was missing. Add it to the log level +defines in the logging module. + +While at it, also rename to LOG_CATEGORY (instead of ERROR_CATEGORY) +and remove duplicated log level definitions in ChipStack. +--- + src/controller/python/chip/ChipStack.py | 16 ++++------------ + src/controller/python/chip/logging/__init__.py | 17 +++++++++-------- + 2 files changed, 13 insertions(+), 20 deletions(-) + +diff --git a/src/controller/python/chip/ChipStack.py b/src/controller/python/chip/ChipStack.py +index 3a167bb6bc..beeaedd6ae 100644 +--- a/src/controller/python/chip/ChipStack.py ++++ b/src/controller/python/chip/ChipStack.py +@@ -36,6 +36,7 @@ from ctypes import CFUNCTYPE, Structure, c_bool, c_char_p, c_int64, c_uint8, c_u + from threading import Condition, Event, Lock + + import chip.native ++from chip.logging import LOG_CATEGORY_AUTOMATION, LOG_CATEGORY_DETAIL, LOG_CATEGORY_ERROR, LOG_CATEGORY_PROGRESS + from chip.native import PyChipError + + from .ChipUtility import ChipUtility +@@ -78,23 +79,14 @@ class DeviceStatusStruct(Structure): + class LogCategory(object): + """Debug logging categories used by chip.""" + +- # NOTE: These values must correspond to those used in the chip C++ code. +- Disabled = 0 +- Error = 1 +- Progress = 2 +- Detail = 3 +- Retain = 4 +- + @staticmethod + def categoryToLogLevel(cat): +- if cat == LogCategory.Error: ++ if cat == LOG_CATEGORY_ERROR: + return logging.ERROR +- elif cat == LogCategory.Progress: ++ elif cat == LOG_CATEGORY_PROGRESS: + return logging.INFO +- elif cat == LogCategory.Detail: ++ elif cat in (LOG_CATEGORY_DETAIL, LOG_CATEGORY_AUTOMATION): + return logging.DEBUG +- elif cat == LogCategory.Retain: +- return logging.CRITICAL + else: + return logging.NOTSET + +diff --git a/src/controller/python/chip/logging/__init__.py b/src/controller/python/chip/logging/__init__.py +index 047d3f4f8e..aca671997d 100644 +--- a/src/controller/python/chip/logging/__init__.py ++++ b/src/controller/python/chip/logging/__init__.py +@@ -19,11 +19,12 @@ import logging + from chip.logging.library_handle import _GetLoggingLibraryHandle + from chip.logging.types import LogRedirectCallback_t + +-# Defines match support/logging/Constants.h (LogCategory enum) +-ERROR_CATEGORY_NONE = 0 +-ERROR_CATEGORY_ERROR = 1 +-ERROR_CATEGORY_PROGRESS = 2 +-ERROR_CATEGORY_DETAIL = 3 ++# Defines match src/lib/support/logging/Constants.h (LogCategory enum) ++LOG_CATEGORY_NONE = 0 ++LOG_CATEGORY_ERROR = 1 ++LOG_CATEGORY_PROGRESS = 2 ++LOG_CATEGORY_DETAIL = 3 ++LOG_CATEGORY_AUTOMATION = 4 + + + @LogRedirectCallback_t +@@ -34,11 +35,11 @@ def _RedirectToPythonLogging(category, module, message): + + logger = logging.getLogger('chip.native.%s' % module) + +- if category == ERROR_CATEGORY_ERROR: ++ if category == LOG_CATEGORY_ERROR: + logger.error("%s", message) +- elif category == ERROR_CATEGORY_PROGRESS: ++ elif category == LOG_CATEGORY_PROGRESS: + logger.info("%s", message) +- elif category == ERROR_CATEGORY_DETAIL: ++ elif category in (LOG_CATEGORY_DETAIL, LOG_CATEGORY_AUTOMATION): + logger.debug("%s", message) + else: + # All logs are expected to have some reasonable category. This treats +-- +2.45.2 + diff --git a/0016-Python-Remove-obsolete-logging-callbacks-33718.patch b/0016-Python-Remove-obsolete-logging-callbacks-33718.patch new file mode 100644 index 0000000..4d75379 --- /dev/null +++ b/0016-Python-Remove-obsolete-logging-callbacks-33718.patch @@ -0,0 +1,256 @@ +From 92dc4417ed88d016d0b21e377d1c04a97e55f34e Mon Sep 17 00:00:00 2001 +From: Stefan Agner +Date: Tue, 4 Jun 2024 07:14:58 +0200 +Subject: [PATCH] [Python] Remove obsolete logging callbacks (#33718) + +Since #5024 there is a new logging callback working. The old code has +partially been removed in #4690, but never completely. Drop the old +logging code for good. +--- + .../ChipDeviceController-ScriptBinding.cpp | 12 -- + src/controller/python/chip/ChipStack.py | 148 +----------------- + 2 files changed, 2 insertions(+), 158 deletions(-) + +diff --git a/src/controller/python/ChipDeviceController-ScriptBinding.cpp b/src/controller/python/ChipDeviceController-ScriptBinding.cpp +index a55d3865bd..728fd5801f 100644 +--- a/src/controller/python/ChipDeviceController-ScriptBinding.cpp ++++ b/src/controller/python/ChipDeviceController-ScriptBinding.cpp +@@ -212,7 +212,6 @@ PyChipError pychip_DeviceCommissioner_CloseBleConnection(chip::Controller::Devic + + const char * pychip_Stack_ErrorToString(ChipError::StorageType err); + const char * pychip_Stack_StatusReportToString(uint32_t profileId, uint16_t statusCode); +-void pychip_Stack_SetLogFunct(LogMessageFunct logFunct); + + PyChipError pychip_GetConnectedDeviceByNodeId(chip::Controller::DeviceCommissioner * devCtrl, chip::NodeId nodeId, + chip::Controller::Python::PyObject * context, DeviceAvailableFunc callback); +@@ -863,17 +862,6 @@ uint64_t pychip_GetCommandSenderHandle(chip::DeviceProxy * device) + return 0; + } + +-void pychip_Stack_SetLogFunct(LogMessageFunct logFunct) +-{ +- // TODO: determine if log redirection is supposed to be functioning in CHIP +- // +- // Background: original log baseline supported 'redirect logs to this +- // function' however CHIP does not currently provide this. +- // +- // Ideally log redirection should work so that python code can do things +- // like using the log module. +-} +- + PyChipError pychip_DeviceController_PostTaskOnChipThread(ChipThreadTaskRunnerFunct callback, void * pythonContext) + { + if (callback == nullptr || pythonContext == nullptr) +diff --git a/src/controller/python/chip/ChipStack.py b/src/controller/python/chip/ChipStack.py +index beeaedd6ae..06afff3ef3 100644 +--- a/src/controller/python/chip/ChipStack.py ++++ b/src/controller/python/chip/ChipStack.py +@@ -28,15 +28,11 @@ from __future__ import absolute_import, print_function + + import asyncio + import builtins +-import logging + import os +-import sys +-import time +-from ctypes import CFUNCTYPE, Structure, c_bool, c_char_p, c_int64, c_uint8, c_uint16, c_uint32, c_void_p, py_object, pythonapi ++from ctypes import CFUNCTYPE, Structure, c_bool, c_char_p, c_uint16, c_uint32, c_void_p, py_object, pythonapi + from threading import Condition, Event, Lock + + import chip.native +-from chip.logging import LOG_CATEGORY_AUTOMATION, LOG_CATEGORY_DETAIL, LOG_CATEGORY_ERROR, LOG_CATEGORY_PROGRESS + from chip.native import PyChipError + + from .ChipUtility import ChipUtility +@@ -76,51 +72,6 @@ class DeviceStatusStruct(Structure): + ] + + +-class LogCategory(object): +- """Debug logging categories used by chip.""" +- +- @staticmethod +- def categoryToLogLevel(cat): +- if cat == LOG_CATEGORY_ERROR: +- return logging.ERROR +- elif cat == LOG_CATEGORY_PROGRESS: +- return logging.INFO +- elif cat in (LOG_CATEGORY_DETAIL, LOG_CATEGORY_AUTOMATION): +- return logging.DEBUG +- else: +- return logging.NOTSET +- +- +-class ChipLogFormatter(logging.Formatter): +- """A custom logging.Formatter for logging chip library messages.""" +- +- def __init__( +- self, +- datefmt=None, +- logModulePrefix=False, +- logLevel=False, +- logTimestamp=False, +- logMSecs=True, +- ): +- fmt = "%(message)s" +- if logModulePrefix: +- fmt = "CHIP:%(chip-module)s: " + fmt +- if logLevel: +- fmt = "%(levelname)s:" + fmt +- if datefmt is not None or logTimestamp: +- fmt = "%(asctime)s " + fmt +- super(ChipLogFormatter, self).__init__(fmt=fmt, datefmt=datefmt) +- self.logMSecs = logMSecs +- +- def formatTime(self, record, datefmt=None): +- if datefmt is None: +- timestampStr = time.strftime("%Y-%m-%d %H:%M:%S%z") +- if self.logMSecs: +- timestampUS = record.__dict__.get("timestamp-usec", 0) +- timestampStr = "%s.%03ld" % (timestampStr, timestampUS / 1000) +- return timestampStr +- +- + class AsyncCallableHandle: + def __init__(self, callback): + self._callback = callback +@@ -185,15 +136,12 @@ class AsyncioCallableHandle: + pythonapi.Py_DecRef(py_object(self)) + + +-_LogMessageFunct = CFUNCTYPE( +- None, c_int64, c_int64, c_char_p, c_uint8, c_char_p) + _ChipThreadTaskRunnerFunct = CFUNCTYPE(None, py_object) + + + @_singleton + class ChipStack(object): +- def __init__(self, persistentStoragePath: str, installDefaultLogHandler=True, +- bluetoothAdapter=None, enableServerInteractions=True): ++ def __init__(self, persistentStoragePath: str, enableServerInteractions=True): + builtins.enableDebugMode = False + + # TODO: Probably no longer necessary, see https://github.com/project-chip/connectedhomeip/issues/33321. +@@ -206,8 +154,6 @@ class ChipStack(object): + self.callbackRes = None + self.commissioningEventRes = None + self.openCommissioningWindowPincode = {} +- self._activeLogFunct = None +- self.addModulePrefixToLogMessage = True + self._enableServerInteractions = enableServerInteractions + + # +@@ -216,50 +162,6 @@ class ChipStack(object): + # + self._loadLib() + +- # Arrange to log output from the chip library to a python logger object with the +- # name 'chip.ChipStack'. If desired, applications can override this behavior by +- # setting self.logger to a different python logger object, or by calling setLogFunct() +- # with their own logging function. +- self.logger = logging.getLogger(__name__) +- self.setLogFunct(self.defaultLogFunct) +- +- # Determine if there are already handlers installed for the logger. Python 3.5+ +- # has a method for this; on older versions the check has to be done manually. +- if hasattr(self.logger, "hasHandlers"): +- hasHandlers = self.logger.hasHandlers() +- else: +- hasHandlers = False +- logger = self.logger +- while logger is not None: +- if len(logger.handlers) > 0: +- hasHandlers = True +- break +- if not logger.propagate: +- break +- logger = logger.parent +- +- # If a logging handler has not already been initialized for 'chip.ChipStack', +- # or any one of its parent loggers, automatically configure a handler to log to +- # stdout. This maintains compatibility with a number of applications which expect +- # chip log output to go to stdout by default. +- # +- # This behavior can be overridden in a variety of ways: +- # - Initialize a different log handler before ChipStack is initialized. +- # - Pass installDefaultLogHandler=False when initializing ChipStack. +- # - Replace the StreamHandler on self.logger with a different handler object. +- # - Set a different Formatter object on the existing StreamHandler object. +- # - Reconfigure the existing ChipLogFormatter object. +- # - Configure chip to call an application-specific logging function by +- # calling self.setLogFunct(). +- # - Call self.setLogFunct(None), which will configure the chip library +- # to log directly to stdout, bypassing python altogether. +- # +- if installDefaultLogHandler and not hasHandlers: +- logHandler = logging.StreamHandler(stream=sys.stdout) +- logHandler.setFormatter(ChipLogFormatter()) +- self.logger.addHandler(logHandler) +- self.logger.setLevel(logging.DEBUG) +- + @_ChipThreadTaskRunnerFunct + def HandleChipThreadRun(callback): + callback() +@@ -292,49 +194,6 @@ class ChipStack(object): + def enableServerInteractions(self): + return self._enableServerInteractions + +- @property +- def defaultLogFunct(self): +- """Returns a python callable which, when called, logs a message to the python logger object +- currently associated with the ChipStack object. +- The returned function is suitable for passing to the setLogFunct() method.""" +- +- def logFunct(timestamp, timestampUSec, moduleName, logCat, message): +- moduleName = ChipUtility.CStringToString(moduleName) +- message = ChipUtility.CStringToString(message) +- if self.addModulePrefixToLogMessage: +- message = "CHIP:%s: %s" % (moduleName, message) +- logLevel = LogCategory.categoryToLogLevel(logCat) +- msgAttrs = { +- "chip-module": moduleName, +- "timestamp": timestamp, +- "timestamp-usec": timestampUSec, +- } +- self.logger.log(logLevel, message, extra=msgAttrs) +- +- return logFunct +- +- def setLogFunct(self, logFunct): +- """Set the function used by the chip library to log messages. +- The supplied object must be a python callable that accepts the following +- arguments: +- timestamp (integer) +- timestampUS (integer) +- module name (encoded UTF-8 string) +- log category (integer) +- message (encoded UTF-8 string) +- Specifying None configures the chip library to log directly to stdout.""" +- if logFunct is None: +- logFunct = 0 +- if not isinstance(logFunct, _LogMessageFunct): +- logFunct = _LogMessageFunct(logFunct) +- # TODO: Lock probably no longer necessary, see https://github.com/project-chip/connectedhomeip/issues/33321. +- with self.networkLock: +- # NOTE: ChipStack must hold a reference to the CFUNCTYPE object while it is +- # set. Otherwise it may get garbage collected, and logging calls from the +- # chip library will fail. +- self._activeLogFunct = logFunct +- self._ChipStackLib.pychip_Stack_SetLogFunct(logFunct) +- + def Shutdown(self): + # + # Terminate Matter thread and shutdown the stack. +@@ -484,9 +343,6 @@ class ChipStack(object): + self._ChipStackLib.pychip_Stack_StatusReportToString.restype = c_char_p + self._ChipStackLib.pychip_Stack_ErrorToString.argtypes = [c_uint32] + self._ChipStackLib.pychip_Stack_ErrorToString.restype = c_char_p +- self._ChipStackLib.pychip_Stack_SetLogFunct.argtypes = [ +- _LogMessageFunct] +- self._ChipStackLib.pychip_Stack_SetLogFunct.restype = PyChipError + + self._ChipStackLib.pychip_DeviceController_PostTaskOnChipThread.argtypes = [ + _ChipThreadTaskRunnerFunct, py_object] +-- +2.45.2 + diff --git a/0017-Python-Drop-network-lock-33720.patch b/0017-Python-Drop-network-lock-33720.patch new file mode 100644 index 0000000..c718aae --- /dev/null +++ b/0017-Python-Drop-network-lock-33720.patch @@ -0,0 +1,63 @@ +From 025bdb7a1e9b9669ab091b96f9ac0adefe3a53dd Mon Sep 17 00:00:00 2001 +From: Stefan Agner +Date: Wed, 5 Jun 2024 16:06:15 +0200 +Subject: [PATCH] [Python] Drop network lock (#33720) + +The network lock is not needed in the Python controller, as all calls +to the SDK are made by posting to the Matter SDK event loop through +ScheduleWork(), hence are guaranteed to be serialized. + +From how I understand ScheduleWork() works, it pushes the work to the +event loop through PostEvent() which at least on POSIX is using the +thread safe device queue (see GenericPlatformManagerImpl_POSIX.cpp). +--- + src/controller/python/chip/ChipStack.py | 12 ++---------- + 1 file changed, 2 insertions(+), 10 deletions(-) + +diff --git a/src/controller/python/chip/ChipStack.py b/src/controller/python/chip/ChipStack.py +index 06afff3ef3..b47c463982 100644 +--- a/src/controller/python/chip/ChipStack.py ++++ b/src/controller/python/chip/ChipStack.py +@@ -144,8 +144,6 @@ class ChipStack(object): + def __init__(self, persistentStoragePath: str, enableServerInteractions=True): + builtins.enableDebugMode = False + +- # TODO: Probably no longer necessary, see https://github.com/project-chip/connectedhomeip/issues/33321. +- self.networkLock = Lock() + self.completeEvent = Event() + self.commissioningCompleteEvent = Event() + self._ChipStackLib = None +@@ -212,7 +210,6 @@ class ChipStack(object): + # #20437 tracks consolidating these. + # + self._ChipStackLib.pychip_CommonStackShutdown() +- self.networkLock = None + self.completeEvent = None + self._ChipStackLib = None + self._chipDLLPath = None +@@ -226,10 +223,7 @@ class ChipStack(object): + This function is a wrapper of PostTaskOnChipThread, which includes some handling of application specific logics. + Calling this function on CHIP on CHIP mainloop thread will cause deadlock. + ''' +- # TODO: Lock probably no longer necessary, see https://github.com/project-chip/connectedhomeip/issues/33321. +- with self.networkLock: +- res = self.PostTaskOnChipThread(callFunct).Wait(timeoutMs) +- return res ++ return self.PostTaskOnChipThread(callFunct).Wait(timeoutMs) + + async def CallAsync(self, callFunct, timeoutMs: int = None): + '''Run a Python function on CHIP stack, and wait for the response. +@@ -256,9 +250,7 @@ class ChipStack(object): + # throw error if op in progress + self.callbackRes = None + self.completeEvent.clear() +- # TODO: Lock probably no longer necessary, see https://github.com/project-chip/connectedhomeip/issues/33321. +- with self.networkLock: +- res = self.PostTaskOnChipThread(callFunct).Wait() ++ res = self.PostTaskOnChipThread(callFunct).Wait() + + if not res.is_success: + self.completeEvent.set() +-- +2.45.2 + From 94d1ad857af22616ec9d2a08678b4c58c827446d Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Fri, 7 Jun 2024 17:40:00 +0200 Subject: [PATCH 2/2] Add two more patches - [Python] Remove Python Bluetooth and ChipStack event loop integration - [Python] Add TriggerResubscribeIfScheduled to SubscriptionTransaction --- ...thon-Bluetooth-and-ChipStack-event-l.patch | 95 +++++++++++++++++++ ...erResubscribeIfScheduled-to-Subscrip.patch | 54 +++++++++++ 2 files changed, 149 insertions(+) create mode 100644 0018-Python-Remove-Python-Bluetooth-and-ChipStack-event-l.patch create mode 100644 0019-Python-Add-TriggerResubscribeIfScheduled-to-Subscrip.patch diff --git a/0018-Python-Remove-Python-Bluetooth-and-ChipStack-event-l.patch b/0018-Python-Remove-Python-Bluetooth-and-ChipStack-event-l.patch new file mode 100644 index 0000000..649bb66 --- /dev/null +++ b/0018-Python-Remove-Python-Bluetooth-and-ChipStack-event-l.patch @@ -0,0 +1,95 @@ +From a4fb58131d9023ad3a3ec01d44c5ae87946d6b59 Mon Sep 17 00:00:00 2001 +From: Stefan Agner +Date: Thu, 6 Jun 2024 17:11:34 +0200 +Subject: [PATCH] [Python] Remove Python Bluetooth and ChipStack event loop + integration (#33775) + +The Python Bluetooth implementation for Linux (`BluezManager` in +ChipBluezMgr.py) and macOS (`CoreBluetoothManager` in +ChipCoreBluetoothMgr.py) integrate with ChipStack to pump their event +loops on long running operations such as commissioning (through +`CallAsyncWithCompleteCallback()`). From what I can tell, the Python +Bluetooth stack is only used for some mbed integration tests. +Specifically through `scan_chip_ble_devices()` +in src/test_driver/mbed/integration_tests/common/utils.py. This +operation doesn't need the event loop integration. + +So as a first step, this PR simply breaks this tie and removes the +event loop integration with the Device ChipStack/ChipDeviceController. +--- + src/controller/python/chip/ChipBluezMgr.py | 1 - + src/controller/python/chip/ChipCoreBluetoothMgr.py | 2 -- + src/controller/python/chip/ChipDeviceCtrl.py | 5 ----- + src/controller/python/chip/ChipStack.py | 8 +------- + 4 files changed, 1 insertion(+), 15 deletions(-) + +diff --git a/src/controller/python/chip/ChipBluezMgr.py b/src/controller/python/chip/ChipBluezMgr.py +index e480750b60..bacf383710 100644 +--- a/src/controller/python/chip/ChipBluezMgr.py ++++ b/src/controller/python/chip/ChipBluezMgr.py +@@ -807,7 +807,6 @@ class BluezManager(ChipBleBase): + self.rx = None + self.setInputHook(self.readlineCB) + self.devMgr = devMgr +- self.devMgr.SetBlockingCB(self.devMgrCB) + + def __del__(self): + self.disconnect() +diff --git a/src/controller/python/chip/ChipCoreBluetoothMgr.py b/src/controller/python/chip/ChipCoreBluetoothMgr.py +index 3f792a5a4d..4a65f1e237 100644 +--- a/src/controller/python/chip/ChipCoreBluetoothMgr.py ++++ b/src/controller/python/chip/ChipCoreBluetoothMgr.py +@@ -184,8 +184,6 @@ class CoreBluetoothManager(ChipBleBase): + def __del__(self): + self.disconnect() + self.setInputHook(self.orig_input_hook) +- self.devCtrl.SetBlockingCB(None) +- self.devCtrl.SetBleEventCB(None) + + def devMgrCB(self): + """A callback used by ChipDeviceCtrl.py to drive the OSX runloop while the +diff --git a/src/controller/python/chip/ChipDeviceCtrl.py b/src/controller/python/chip/ChipDeviceCtrl.py +index 1d1627c46d..acbbc88b3e 100644 +--- a/src/controller/python/chip/ChipDeviceCtrl.py ++++ b/src/controller/python/chip/ChipDeviceCtrl.py +@@ -1461,11 +1461,6 @@ class ChipDeviceControllerBase(): + else: + return res.events + +- def SetBlockingCB(self, blockingCB): +- self.CheckIsActive() +- +- self._ChipStack.blockingCB = blockingCB +- + def SetIpk(self, ipk: bytes): + self._ChipStack.Call( + lambda: self._dmLib.pychip_DeviceController_SetIpk(self.devCtrl, ipk, len(ipk)) +diff --git a/src/controller/python/chip/ChipStack.py b/src/controller/python/chip/ChipStack.py +index b47c463982..5fd0601ba2 100644 +--- a/src/controller/python/chip/ChipStack.py ++++ b/src/controller/python/chip/ChipStack.py +@@ -165,8 +165,6 @@ class ChipStack(object): + callback() + + self.cbHandleChipThreadRun = HandleChipThreadRun +- # set by other modules(BLE) that require service by thread while thread blocks. +- self.blockingCB = None + + # + # Storage has to be initialized BEFORE initializing the stack, since the latter +@@ -255,11 +253,7 @@ class ChipStack(object): + if not res.is_success: + self.completeEvent.set() + raise res.to_exception() +- while not self.completeEvent.isSet(): +- if self.blockingCB: +- self.blockingCB() +- +- self.completeEvent.wait(0.05) ++ self.completeEvent.wait() + if isinstance(self.callbackRes, ChipStackException): + raise self.callbackRes + return self.callbackRes +-- +2.45.2 + diff --git a/0019-Python-Add-TriggerResubscribeIfScheduled-to-Subscrip.patch b/0019-Python-Add-TriggerResubscribeIfScheduled-to-Subscrip.patch new file mode 100644 index 0000000..a32b77e --- /dev/null +++ b/0019-Python-Add-TriggerResubscribeIfScheduled-to-Subscrip.patch @@ -0,0 +1,54 @@ +From 9e3eeeaf21a3a258ea6d068f8ade8c321b7b5563 Mon Sep 17 00:00:00 2001 +From: Stefan Agner +Date: Fri, 7 Jun 2024 15:50:34 +0200 +Subject: [PATCH] [Python] Add TriggerResubscribeIfScheduled to + SubscriptionTransaction (#33774) + +Add TriggerResubscribeIfScheduled to SubscriptionTransaction. If the +ReadClient currently has a resubscription attempt scheduled, This +function allows to trigger that attempt immediately. This is useful +when the server side is up and communicating, and it's a good time to +try to resubscribe. +--- + src/controller/python/chip/clusters/Attribute.py | 7 +++++++ + src/controller/python/chip/clusters/attribute.cpp | 6 ++++++ + 2 files changed, 13 insertions(+) + +diff --git a/src/controller/python/chip/clusters/Attribute.py b/src/controller/python/chip/clusters/Attribute.py +index 51389e19a1..838936e83b 100644 +--- a/src/controller/python/chip/clusters/Attribute.py ++++ b/src/controller/python/chip/clusters/Attribute.py +@@ -478,6 +478,13 @@ class SubscriptionTransaction: + lambda: handle.pychip_ReadClient_OverrideLivenessTimeout(self._readTransaction._pReadClient, timeoutMs) + ) + ++ async def TriggerResubscribeIfScheduled(self, reason: str): ++ handle = chip.native.GetLibraryHandle() ++ await builtins.chipStack.CallAsync( ++ lambda: handle.pychip_ReadClient_TriggerResubscribeIfScheduled( ++ self._readTransaction._pReadClient, reason.encode("utf-8")) ++ ) ++ + def GetReportingIntervalsSeconds(self) -> Tuple[int, int]: + ''' + Retrieve the reporting intervals associated with an active subscription. +diff --git a/src/controller/python/chip/clusters/attribute.cpp b/src/controller/python/chip/clusters/attribute.cpp +index b73b4a49b4..7c5b2c906a 100644 +--- a/src/controller/python/chip/clusters/attribute.cpp ++++ b/src/controller/python/chip/clusters/attribute.cpp +@@ -464,6 +464,12 @@ void pychip_ReadClient_OverrideLivenessTimeout(ReadClient * pReadClient, uint32_ + pReadClient->OverrideLivenessTimeout(System::Clock::Milliseconds32(livenessTimeoutMs)); + } + ++void pychip_ReadClient_TriggerResubscribeIfScheduled(ReadClient * pReadClient, const char * reason) ++{ ++ VerifyOrDie(pReadClient != nullptr); ++ pReadClient->TriggerResubscribeIfScheduled(reason); ++} ++ + PyChipError pychip_ReadClient_GetReportingIntervals(ReadClient * pReadClient, uint16_t * minIntervalSec, uint16_t * maxIntervalSec) + { + VerifyOrDie(pReadClient != nullptr); +-- +2.45.2 +