[Python] Store original PyChipError in ChipStackException (#33954)

* [Python] Drop unused ErrorToException function

Also remove the now unused pychip_Stack_ErrorToString function. This
is handled in pychip_FormatError today.

* [Python] Cleanup PyChipError return values

Use PyChipError return value where PyChipErrors are actually returned.
This then also allows us to use the typical .raise_on_error() pattern.

* [Python] Store original PyChipError in ChipStackException

This change stores the original PyChipError in ChipStackException so
that details of the original error code can still be retrieved. This
is interesting to use the properties returning processed information
about the original error code. It also preserves the line and code
file which can be helpful.

* [Python] Fix Command API argument type errors

NativeLibraryHandleMethodArguments correctly setting the arguments
uncovered some incorrectly set arguments.

* [Python] Use to_exception() to convert PyChipError to ChipStackError

* [Python] Fix Cert API argument type errors

NativeLibraryHandleMethodArguments correctly setting the argument
types causes argument type errors:
ctypes.ArgumentError: argument 1: TypeError: expected LP_c_ubyte instance instead of bytes

We can safely cast bytes as the native side marks it const.
diff --git a/src/controller/python/ChipDeviceController-ScriptBinding.cpp b/src/controller/python/ChipDeviceController-ScriptBinding.cpp
index 323c3ad..a98c808 100644
--- a/src/controller/python/ChipDeviceController-ScriptBinding.cpp
+++ b/src/controller/python/ChipDeviceController-ScriptBinding.cpp
@@ -210,7 +210,6 @@
 // BLE
 PyChipError pychip_DeviceCommissioner_CloseBleConnection(chip::Controller::DeviceCommissioner * devCtrl);
 
-const char * pychip_Stack_ErrorToString(ChipError::StorageType err);
 const char * pychip_Stack_StatusReportToString(uint32_t profileId, uint16_t statusCode);
 
 PyChipError pychip_GetConnectedDeviceByNodeId(chip::Controller::DeviceCommissioner * devCtrl, chip::NodeId nodeId,
@@ -366,11 +365,6 @@
     return ToPyChipError(CHIP_NO_ERROR);
 }
 
-const char * pychip_DeviceController_ErrorToString(ChipError::StorageType err)
-{
-    return chip::ErrorStr(CHIP_ERROR(err));
-}
-
 const char * pychip_DeviceController_StatusReportToString(uint32_t profileId, uint16_t statusCode)
 {
     // return chip::StatusReportStr(profileId, statusCode);
@@ -769,11 +763,6 @@
     return ToPyChipError(CHIP_NO_ERROR);
 }
 
-const char * pychip_Stack_ErrorToString(ChipError::StorageType err)
-{
-    return chip::ErrorStr(CHIP_ERROR(err));
-}
-
 const char * pychip_Stack_StatusReportToString(uint32_t profileId, uint16_t statusCode)
 {
     // return chip::StatusReportStr(profileId, statusCode);
diff --git a/src/controller/python/chip/ChipDeviceCtrl.py b/src/controller/python/chip/ChipDeviceCtrl.py
index a3a9b30..6e2e133 100644
--- a/src/controller/python/chip/ChipDeviceCtrl.py
+++ b/src/controller/python/chip/ChipDeviceCtrl.py
@@ -1013,12 +1013,8 @@
         sessionParametersStruct = SessionParametersStruct.parse(b'\x00' * SessionParametersStruct.sizeof())
         sessionParametersByteArray = SessionParametersStruct.build(sessionParametersStruct)
         device = self.GetConnectedDeviceSync(nodeid)
-        res = self._ChipStack.Call(lambda: self._dmLib.pychip_DeviceProxy_GetRemoteSessionParameters(
-            device.deviceProxy, ctypes.c_char_p(sessionParametersByteArray)))
-
-        # 0 is CHIP_NO_ERROR
-        if res != 0:
-            return None
+        self._ChipStack.Call(lambda: self._dmLib.pychip_DeviceProxy_GetRemoteSessionParameters(
+            device.deviceProxy, ctypes.c_char_p(sessionParametersByteArray))).raise_on_error()
 
         sessionParametersStruct = SessionParametersStruct.parse(sessionParametersByteArray)
         return SessionParameters(
@@ -1030,8 +1026,6 @@
             specficiationVersion=sessionParametersStruct.SpecificationVersion if sessionParametersStruct.SpecificationVersion != 0 else None,
             maxPathsPerInvoke=sessionParametersStruct.MaxPathsPerInvoke)
 
-        return res
-
     async def TestOnlySendBatchCommands(self, nodeid: int, commands: typing.List[ClusterCommand.InvokeRequestInfo],
                                         timedRequestTimeoutMs: typing.Optional[int] = None,
                                         interactionTimeoutMs: typing.Optional[int] = None, busyWaitMs: typing.Optional[int] = None,
@@ -1804,6 +1798,9 @@
 
             self._dmLib.pychip_CheckInDelegate_SetOnCheckInCompleteCallback(_OnCheckInComplete)
 
+            self._dmLib.pychip_DeviceProxy_GetRemoteSessionParameters.restype = PyChipError
+            self._dmLib.pychip_DeviceProxy_GetRemoteSessionParameters.argtypes = [c_void_p, c_char_p]
+
 
 class ChipDeviceController(ChipDeviceControllerBase):
     ''' The ChipDeviceCommissioner binding, named as ChipDeviceController
diff --git a/src/controller/python/chip/ChipStack.py b/src/controller/python/chip/ChipStack.py
index 4f19776..dc4efc2 100644
--- a/src/controller/python/chip/ChipStack.py
+++ b/src/controller/python/chip/ChipStack.py
@@ -35,7 +35,6 @@
 import chip.native
 from chip.native import PyChipError
 
-from .ChipUtility import ChipUtility
 from .clusters import Attribute as ClusterAttribute
 from .clusters import Command as ClusterCommand
 from .exceptions import ChipStackError, ChipStackException, DeviceError
@@ -247,33 +246,6 @@
             raise res.to_exception()
         return callObj
 
-    def ErrorToException(self, err, devStatusPtr=None):
-        if err == 0x2C and devStatusPtr:
-            devStatus = devStatusPtr.contents
-            msg = ChipUtility.CStringToString(
-                (
-                    self._ChipStackLib.pychip_Stack_StatusReportToString(
-                        devStatus.ProfileId, devStatus.StatusCode
-                    )
-                )
-            )
-            sysErrorCode = (
-                devStatus.SysErrorCode if (
-                    devStatus.SysErrorCode != 0) else None
-            )
-            if sysErrorCode is not None:
-                msg = msg + " (system err %d)" % (sysErrorCode)
-            return DeviceError(
-                devStatus.ProfileId, devStatus.StatusCode, sysErrorCode, msg
-            )
-        else:
-            return ChipStackError(
-                err,
-                ChipUtility.CStringToString(
-                    (self._ChipStackLib.pychip_Stack_ErrorToString(err))
-                ),
-            )
-
     def LocateChipDLL(self):
         self._loadLib()
         return self._chipDLLPath
@@ -302,8 +274,6 @@
                 c_uint16,
             ]
             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_DeviceController_PostTaskOnChipThread.argtypes = [
                 _ChipThreadTaskRunnerFunct, py_object]
diff --git a/src/controller/python/chip/clusters/Attribute.py b/src/controller/python/chip/clusters/Attribute.py
index 5bb8e7c..36f5a79 100644
--- a/src/controller/python/chip/clusters/Attribute.py
+++ b/src/controller/python/chip/clusters/Attribute.py
@@ -651,7 +651,7 @@
         self._changedPathSet = set()
         self._pReadClient = None
         self._pReadCallback = None
-        self._resultError = None
+        self._resultError: Optional[PyChipError] = None
 
     def SetClientObjPointers(self, pReadClient, pReadCallback):
         self._pReadClient = pReadClient
@@ -718,7 +718,7 @@
             logging.exception(ex)
 
     def handleError(self, chipError: PyChipError):
-        self._resultError = chipError.code
+        self._resultError = chipError
 
     def _handleSubscriptionEstablished(self, subscriptionId):
         if not self._future.done():
@@ -777,11 +777,11 @@
         # move on, possibly invalidating the provided _event_loop.
         #
         if not self._future.done():
-            if self._resultError:
+            if self._resultError is not None:
                 if self._subscription_handler:
-                    self._subscription_handler.OnErrorCb(self._resultError, self._subscription_handler)
+                    self._subscription_handler.OnErrorCb(self._resultError.code, self._subscription_handler)
                 else:
-                    self._future.set_exception(chip.exceptions.ChipStackError(self._resultError))
+                    self._future.set_exception(self._resultError.to_exception())
             else:
                 self._future.set_result(AsyncReadTransaction.ReadResponse(
                     attributes=self._cache.attributeCache, events=self._events, tlvAttributes=self._cache.attributeTLVCache))
@@ -809,7 +809,7 @@
         self._event_loop = eventLoop
         self._future = future
         self._resultData = []
-        self._resultError = None
+        self._resultError: Optional[PyChipError] = None
 
     def handleResponse(self, path: AttributePath, status: int):
         try:
diff --git a/src/controller/python/chip/clusters/Command.py b/src/controller/python/chip/clusters/Command.py
index 6ef25cb..9395133 100644
--- a/src/controller/python/chip/clusters/Command.py
+++ b/src/controller/python/chip/clusters/Command.py
@@ -467,13 +467,13 @@
         setter = chip.native.NativeLibraryHandleMethodArguments(handle)
 
         setter.Set('pychip_CommandSender_SendCommand',
-                   PyChipError, [py_object, c_void_p, c_uint16, c_uint32, c_uint32, c_char_p, c_size_t, c_uint16, c_bool])
+                   PyChipError, [py_object, c_void_p, c_uint16, c_uint16, c_uint32, c_uint32, c_char_p, c_size_t, c_uint16, c_uint16, c_bool])
         setter.Set('pychip_CommandSender_SendBatchCommands',
                    PyChipError, [py_object, c_void_p, c_uint16, c_uint16, c_uint16, c_bool, POINTER(PyInvokeRequestData), c_size_t])
         setter.Set('pychip_CommandSender_TestOnlySendBatchCommands',
                    PyChipError, [py_object, c_void_p, c_uint16, c_uint16, c_uint16, c_bool, TestOnlyPyBatchCommandsOverrides, POINTER(PyInvokeRequestData), c_size_t])
         setter.Set('pychip_CommandSender_TestOnlySendCommandTimedRequestNoTimedInvoke',
-                   PyChipError, [py_object, c_void_p, c_uint32, c_uint32, c_char_p, c_size_t, c_uint16, c_bool])
+                   PyChipError, [py_object, c_void_p, c_uint16, c_uint32, c_uint32, c_char_p, c_size_t, c_uint16, c_uint16, c_bool])
         setter.Set('pychip_CommandSender_SendGroupCommand',
                    PyChipError, [c_uint16, c_void_p, c_uint32, c_uint32, c_char_p, c_size_t, c_uint16])
         setter.Set('pychip_CommandSender_InitCallbacks', None, [
diff --git a/src/controller/python/chip/credentials/cert.py b/src/controller/python/chip/credentials/cert.py
index 786c1a4..df0b282 100644
--- a/src/controller/python/chip/credentials/cert.py
+++ b/src/controller/python/chip/credentials/cert.py
@@ -35,8 +35,10 @@
     """Converts a x509 certificate to CHIP Certificate."""
     output_buffer = (ctypes.c_uint8 * 1024)()
     output_size = ctypes.c_size_t(1024)
+    ptr_type = ctypes.POINTER(ctypes.c_uint8)
 
-    _handle().pychip_ConvertX509CertToChipCert(x509Cert, len(x509Cert), output_buffer, ctypes.byref(output_size)).raise_on_error()
+    _handle().pychip_ConvertX509CertToChipCert(ctypes.cast(x509Cert, ptr_type), len(x509Cert),
+                                               ctypes.cast(output_buffer, ptr_type), ctypes.byref(output_size)).raise_on_error()
 
     return bytes(output_buffer)[:output_size.value]
 
@@ -45,7 +47,9 @@
     """Converts a x509 certificate to CHIP Certificate."""
     output_buffer = (ctypes.c_byte * 1024)()
     output_size = ctypes.c_size_t(1024)
+    ptr_type = ctypes.POINTER(ctypes.c_uint8)
 
-    _handle().pychip_ConvertChipCertToX509Cert(chipCert, len(chipCert), output_buffer, ctypes.byref(output_size)).raise_on_error()
+    _handle().pychip_ConvertChipCertToX509Cert(ctypes.cast(chipCert, ptr_type), len(chipCert),
+                                               ctypes.cast(output_buffer, ptr_type), ctypes.byref(output_size)).raise_on_error()
 
     return bytes(output_buffer)[:output_size.value]
diff --git a/src/controller/python/chip/discovery/__init__.py b/src/controller/python/chip/discovery/__init__.py
index c400d97..a25fb49 100644
--- a/src/controller/python/chip/discovery/__init__.py
+++ b/src/controller/python/chip/discovery/__init__.py
@@ -236,8 +236,7 @@
     )
 
     res = _GetDiscoveryLibraryHandle().pychip_discovery_resolve(fabricid, nodeid)
-    if res != 0:
-        raise Exception("Failed to start node resolution")
+    res.raise_on_error()
 
 
 class _SyncAddressFinder:
diff --git a/src/controller/python/chip/exceptions/__init__.py b/src/controller/python/chip/exceptions/__init__.py
index 6b1969f..c7f692e 100644
--- a/src/controller/python/chip/exceptions/__init__.py
+++ b/src/controller/python/chip/exceptions/__init__.py
@@ -15,6 +15,8 @@
 #    limitations under the License.
 #
 
+from __future__ import annotations
+
 __all__ = [
     "ChipStackException",
     "ChipStackError",
@@ -26,15 +28,32 @@
     "UnknownCommand",
 ]
 
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from chip.native import PyChipError
+
 
 class ChipStackException(Exception):
     pass
 
 
 class ChipStackError(ChipStackException):
-    def __init__(self, err, msg=None):
-        self.err = err
-        self.msg = msg if msg else "Chip Stack Error %d" % err
+    def __init__(self, chip_error: PyChipError, msg=None):
+        self._chip_error = chip_error
+        self.msg = msg if msg else "Chip Stack Error %d" % chip_error.code
+
+    @classmethod
+    def from_chip_error(cls, chip_error: PyChipError) -> ChipStackError:
+        return cls(chip_error, str(chip_error))
+
+    @property
+    def chip_error(self) -> PyChipError | None:
+        return self._chip_error
+
+    @property
+    def err(self) -> int:
+        return self._chip_error.code
 
     def __str__(self):
         return self.msg
diff --git a/src/controller/python/chip/interaction_model/delegate.py b/src/controller/python/chip/interaction_model/delegate.py
index 1451200..4741e5f 100644
--- a/src/controller/python/chip/interaction_model/delegate.py
+++ b/src/controller/python/chip/interaction_model/delegate.py
@@ -330,7 +330,7 @@
         setter.Set("pychip_InteractionModelDelegate_SetCommandResponseErrorCallback", None, [
                    _OnCommandResponseFunct])
         setter.Set("pychip_InteractionModel_GetCommandSenderHandle",
-                   c_uint32, [ctypes.POINTER(c_uint64)])
+                   chip.native.PyChipError, [ctypes.POINTER(c_uint64)])
         setter.Set("pychip_InteractionModelDelegate_SetOnWriteResponseStatusCallback", None, [
                    _OnWriteResponseStatusFunct])
 
@@ -389,10 +389,8 @@
 def GetCommandSenderHandle() -> int:
     handle = chip.native.GetLibraryHandle()
     resPointer = c_uint64()
-    res = handle.pychip_InteractionModel_GetCommandSenderHandle(
-        ctypes.pointer(resPointer))
-    if res != 0:
-        raise chip.exceptions.ChipStackError(res)
+    handle.pychip_InteractionModel_GetCommandSenderHandle(
+        ctypes.pointer(resPointer)).raise_on_error()
     ClearCommandStatus(resPointer.value)
     return resPointer.value
 
diff --git a/src/controller/python/chip/native/__init__.py b/src/controller/python/chip/native/__init__.py
index ce8b762..9ee94c6 100644
--- a/src/controller/python/chip/native/__init__.py
+++ b/src/controller/python/chip/native/__init__.py
@@ -116,7 +116,7 @@
 
     def to_exception(self) -> typing.Union[None, chip.exceptions.ChipStackError]:
         if not self.is_success:
-            return chip.exceptions.ChipStackError(self.code, str(self))
+            return chip.exceptions.ChipStackError.from_chip_error(self)
 
     def __str__(self):
         buf = ctypes.create_string_buffer(256)
@@ -199,7 +199,7 @@
     def Set(self, methodName: str, resultType, argumentTypes: list):
         method = getattr(self.handle, methodName)
         method.restype = resultType
-        method.argtype = argumentTypes
+        method.argtypes = argumentTypes
 
 
 @dataclass
diff --git a/src/controller/python/chip/setup_payload/setup_payload.py b/src/controller/python/chip/setup_payload/setup_payload.py
index 1f70983..702fb31 100644
--- a/src/controller/python/chip/setup_payload/setup_payload.py
+++ b/src/controller/python/chip/setup_payload/setup_payload.py
@@ -14,11 +14,10 @@
 #    limitations under the License.
 #
 
-from ctypes import CFUNCTYPE, c_char_p, c_int32, c_uint8, c_uint16, c_uint32
+from ctypes import CFUNCTYPE, c_char_p, c_uint8, c_uint16, c_uint32
 from typing import Optional
 
-from chip.exceptions import ChipStackError
-from chip.native import GetLibraryHandle, NativeLibraryHandleMethodArguments
+from chip.native import GetLibraryHandle, NativeLibraryHandleMethodArguments, PyChipError
 
 
 class SetupPayload:
@@ -46,34 +45,25 @@
 
     def ParseQrCode(self, qrCode: str):
         self.Clear()
-        err = self.chipLib.pychip_SetupPayload_ParseQrCode(qrCode.upper().encode(),
-                                                           self.attribute_visitor,
-                                                           self.vendor_attribute_visitor)
-
-        if err != 0:
-            raise ChipStackError(err)
+        self.chipLib.pychip_SetupPayload_ParseQrCode(qrCode.upper().encode(),
+                                                     self.attribute_visitor,
+                                                     self.vendor_attribute_visitor).raise_on_error()
 
         return self
 
     def ParseManualPairingCode(self, manualPairingCode: str):
         self.Clear()
-        err = self.chipLib.pychip_SetupPayload_ParseManualPairingCode(manualPairingCode.encode(),
-                                                                      self.attribute_visitor,
-                                                                      self.vendor_attribute_visitor)
-
-        if err != 0:
-            raise ChipStackError(err)
+        self.chipLib.pychip_SetupPayload_ParseManualPairingCode(manualPairingCode.encode(),
+                                                                self.attribute_visitor,
+                                                                self.vendor_attribute_visitor).raise_on_error()
 
         return self
 
     # DEPRECATED
     def PrintOnboardingCodes(self, passcode, vendorId, productId, discriminator, customFlow, capabilities, version):
         self.Clear()
-        err = self.chipLib.pychip_SetupPayload_PrintOnboardingCodes(
-            passcode, vendorId, productId, discriminator, customFlow, capabilities, version)
-
-        if err != 0:
-            raise ChipStackError(err)
+        self.chipLib.pychip_SetupPayload_PrintOnboardingCodes(
+            passcode, vendorId, productId, discriminator, customFlow, capabilities, version).raise_on_error()
 
     # DEPRECATED
     def Print(self):
@@ -106,17 +96,17 @@
         return None
 
     def __InitNativeFunctions(self, chipLib):
-        if chipLib.pychip_SetupPayload_ParseQrCode is not None:
+        if chipLib.pychip_SetupPayload_ParseQrCode.argtypes is not None:
             return
         setter = NativeLibraryHandleMethodArguments(chipLib)
         setter.Set("pychip_SetupPayload_ParseQrCode",
-                   c_int32,
+                   PyChipError,
                    [c_char_p, SetupPayload.AttributeVisitor, SetupPayload.VendorAttributeVisitor])
         setter.Set("pychip_SetupPayload_ParseManualPairingCode",
-                   c_int32,
+                   PyChipError,
                    [c_char_p, SetupPayload.AttributeVisitor, SetupPayload.VendorAttributeVisitor])
         setter.Set("pychip_SetupPayload_PrintOnboardingCodes",
-                   c_int32,
+                   PyChipError,
                    [c_uint32, c_uint16, c_uint16, c_uint16, c_uint8, c_uint8, c_uint8])
 
     # Getters from parsed contents.