[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
diff --git a/src/controller/python/test/test_scripts/base.py b/src/controller/python/test/test_scripts/base.py
index 9fc9300..3f9f76d 100644
--- a/src/controller/python/test/test_scripts/base.py
+++ b/src/controller/python/test/test_scripts/base.py
@@ -178,29 +178,6 @@
             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 @@
     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 @@
         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 @@
         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 @@
 
         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
@@ -1095,50 +1074,48 @@
         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:
@@ -1171,29 +1148,27 @@
             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:
@@ -1217,16 +1192,16 @@
         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 eca1706..d680682 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 @@
     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 b6adc0f..4a7f15d 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 @@
         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 4b38384..d1a2034 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 @@
                                      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 33ae713..8f6f534 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 @@
     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 @@
               "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 @@
               "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 @@
               "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 47fedb3..9233d58 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 @@
               "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 036b612..2b1db4d 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 @@
         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))