[python] Add ICD support to Python binding (#33533)

* [icd] Add ICD support to python

* [python] Add ICD support to Python binding

* Update

* update

* update

* Update

* Use ScopedNodeId in ICD commissioning callbacks

* update

* fix
diff --git a/examples/chip-tool/commands/pairing/PairingCommand.cpp b/examples/chip-tool/commands/pairing/PairingCommand.cpp
index 5f6c179..76c8ac8 100644
--- a/examples/chip-tool/commands/pairing/PairingCommand.cpp
+++ b/examples/chip-tool/commands/pairing/PairingCommand.cpp
@@ -436,7 +436,7 @@
                     info.icd.idleModeDuration, info.icd.activeModeDuration, info.icd.activeModeThreshold);
 }
 
-void PairingCommand::OnICDRegistrationComplete(NodeId nodeId, uint32_t icdCounter)
+void PairingCommand::OnICDRegistrationComplete(ScopedNodeId nodeId, uint32_t icdCounter)
 {
     char icdSymmetricKeyHex[chip::Crypto::kAES_CCM128_Key_Length * 2 + 1];
 
@@ -444,7 +444,7 @@
                                sizeof(icdSymmetricKeyHex), chip::Encoding::HexFlags::kNullTerminate);
 
     app::ICDClientInfo clientInfo;
-    clientInfo.peer_node         = ScopedNodeId(nodeId, CurrentCommissioner().GetFabricIndex());
+    clientInfo.peer_node         = nodeId;
     clientInfo.monitored_subject = mICDMonitoredSubject.Value();
     clientInfo.start_icd_counter = icdCounter;
 
@@ -457,7 +457,7 @@
     if (err != CHIP_NO_ERROR)
     {
         CHIPCommand::sICDClientStorage.RemoveKey(clientInfo);
-        ChipLogError(chipTool, "Failed to persist symmetric key for " ChipLogFormatX64 ": %s", ChipLogValueX64(nodeId),
+        ChipLogError(chipTool, "Failed to persist symmetric key for " ChipLogFormatX64 ": %s", ChipLogValueX64(nodeId.GetNodeId()),
                      err.AsString());
         SetCommandExitStatus(err);
         return;
@@ -465,18 +465,18 @@
 
     mDeviceIsICD = true;
 
-    ChipLogProgress(chipTool, "Saved ICD Symmetric key for " ChipLogFormatX64, ChipLogValueX64(nodeId));
+    ChipLogProgress(chipTool, "Saved ICD Symmetric key for " ChipLogFormatX64, ChipLogValueX64(nodeId.GetNodeId()));
     ChipLogProgress(chipTool,
                     "ICD Registration Complete for device " ChipLogFormatX64 " / Check-In NodeID: " ChipLogFormatX64
                     " / Monitored Subject: " ChipLogFormatX64 " / Symmetric Key: %s / ICDCounter %u",
-                    ChipLogValueX64(nodeId), ChipLogValueX64(mICDCheckInNodeId.Value()),
+                    ChipLogValueX64(nodeId.GetNodeId()), ChipLogValueX64(mICDCheckInNodeId.Value()),
                     ChipLogValueX64(mICDMonitoredSubject.Value()), icdSymmetricKeyHex, icdCounter);
 }
 
-void PairingCommand::OnICDStayActiveComplete(NodeId deviceId, uint32_t promisedActiveDuration)
+void PairingCommand::OnICDStayActiveComplete(ScopedNodeId deviceId, uint32_t promisedActiveDuration)
 {
     ChipLogProgress(chipTool, "ICD Stay Active Complete for device " ChipLogFormatX64 " / promisedActiveDuration: %u",
-                    ChipLogValueX64(deviceId), promisedActiveDuration);
+                    ChipLogValueX64(deviceId.GetNodeId()), promisedActiveDuration);
 }
 
 void PairingCommand::OnDiscoveredDevice(const chip::Dnssd::CommissionNodeData & nodeData)
diff --git a/examples/chip-tool/commands/pairing/PairingCommand.h b/examples/chip-tool/commands/pairing/PairingCommand.h
index 99b0fd0..aaa8dc7 100644
--- a/examples/chip-tool/commands/pairing/PairingCommand.h
+++ b/examples/chip-tool/commands/pairing/PairingCommand.h
@@ -197,8 +197,8 @@
     void OnPairingDeleted(CHIP_ERROR error) override;
     void OnReadCommissioningInfo(const chip::Controller::ReadCommissioningInfo & info) override;
     void OnCommissioningComplete(NodeId deviceId, CHIP_ERROR error) override;
-    void OnICDRegistrationComplete(NodeId deviceId, uint32_t icdCounter) override;
-    void OnICDStayActiveComplete(NodeId deviceId, uint32_t promisedActiveDuration) override;
+    void OnICDRegistrationComplete(chip::ScopedNodeId deviceId, uint32_t icdCounter) override;
+    void OnICDStayActiveComplete(chip::ScopedNodeId deviceId, uint32_t promisedActiveDuration) override;
 
     /////////// DeviceDiscoveryDelegate Interface /////////
     void OnDiscoveredDevice(const chip::Dnssd::CommissionNodeData & nodeData) override;
diff --git a/examples/fabric-admin/commands/pairing/PairingCommand.cpp b/examples/fabric-admin/commands/pairing/PairingCommand.cpp
index 379b564..92754c2 100644
--- a/examples/fabric-admin/commands/pairing/PairingCommand.cpp
+++ b/examples/fabric-admin/commands/pairing/PairingCommand.cpp
@@ -436,7 +436,7 @@
                     info.icd.idleModeDuration, info.icd.activeModeDuration, info.icd.activeModeThreshold);
 }
 
-void PairingCommand::OnICDRegistrationComplete(NodeId nodeId, uint32_t icdCounter)
+void PairingCommand::OnICDRegistrationComplete(ScopedNodeId nodeId, uint32_t icdCounter)
 {
     char icdSymmetricKeyHex[chip::Crypto::kAES_CCM128_Key_Length * 2 + 1];
 
@@ -444,7 +444,7 @@
                                sizeof(icdSymmetricKeyHex), chip::Encoding::HexFlags::kNullTerminate);
 
     app::ICDClientInfo clientInfo;
-    clientInfo.peer_node         = ScopedNodeId(nodeId, CurrentCommissioner().GetFabricIndex());
+    clientInfo.peer_node         = nodeId;
     clientInfo.monitored_subject = mICDMonitoredSubject.Value();
     clientInfo.start_icd_counter = icdCounter;
 
@@ -457,26 +457,26 @@
     if (err != CHIP_NO_ERROR)
     {
         CHIPCommand::sICDClientStorage.RemoveKey(clientInfo);
-        ChipLogError(NotSpecified, "Failed to persist symmetric key for " ChipLogFormatX64 ": %s", ChipLogValueX64(nodeId),
-                     err.AsString());
+        ChipLogError(NotSpecified, "Failed to persist symmetric key for " ChipLogFormatX64 ": %s",
+                     ChipLogValueX64(nodeId.GetNodeId()), err.AsString());
         SetCommandExitStatus(err);
         return;
     }
 
     mDeviceIsICD = true;
 
-    ChipLogProgress(NotSpecified, "Saved ICD Symmetric key for " ChipLogFormatX64, ChipLogValueX64(nodeId));
+    ChipLogProgress(NotSpecified, "Saved ICD Symmetric key for " ChipLogFormatX64, ChipLogValueX64(nodeId.GetNodeId()));
     ChipLogProgress(NotSpecified,
                     "ICD Registration Complete for device " ChipLogFormatX64 " / Check-In NodeID: " ChipLogFormatX64
                     " / Monitored Subject: " ChipLogFormatX64 " / Symmetric Key: %s / ICDCounter %u",
-                    ChipLogValueX64(nodeId), ChipLogValueX64(mICDCheckInNodeId.Value()),
+                    ChipLogValueX64(nodeId.GetNodeId()), ChipLogValueX64(mICDCheckInNodeId.Value()),
                     ChipLogValueX64(mICDMonitoredSubject.Value()), icdSymmetricKeyHex, icdCounter);
 }
 
-void PairingCommand::OnICDStayActiveComplete(NodeId deviceId, uint32_t promisedActiveDuration)
+void PairingCommand::OnICDStayActiveComplete(ScopedNodeId deviceId, uint32_t promisedActiveDuration)
 {
     ChipLogProgress(NotSpecified, "ICD Stay Active Complete for device " ChipLogFormatX64 " / promisedActiveDuration: %u",
-                    ChipLogValueX64(deviceId), promisedActiveDuration);
+                    ChipLogValueX64(deviceId.GetNodeId()), promisedActiveDuration);
 }
 
 void PairingCommand::OnDiscoveredDevice(const chip::Dnssd::CommissionNodeData & nodeData)
diff --git a/examples/fabric-admin/commands/pairing/PairingCommand.h b/examples/fabric-admin/commands/pairing/PairingCommand.h
index 8de34a0..331d177 100644
--- a/examples/fabric-admin/commands/pairing/PairingCommand.h
+++ b/examples/fabric-admin/commands/pairing/PairingCommand.h
@@ -196,8 +196,8 @@
     void OnPairingDeleted(CHIP_ERROR error) override;
     void OnReadCommissioningInfo(const chip::Controller::ReadCommissioningInfo & info) override;
     void OnCommissioningComplete(NodeId deviceId, CHIP_ERROR error) override;
-    void OnICDRegistrationComplete(NodeId deviceId, uint32_t icdCounter) override;
-    void OnICDStayActiveComplete(NodeId deviceId, uint32_t promisedActiveDuration) override;
+    void OnICDRegistrationComplete(chip::ScopedNodeId deviceId, uint32_t icdCounter) override;
+    void OnICDStayActiveComplete(chip::ScopedNodeId deviceId, uint32_t promisedActiveDuration) override;
 
     /////////// DeviceDiscoveryDelegate Interface /////////
     void OnDiscoveredDevice(const chip::Dnssd::CommissionNodeData & nodeData) override;
diff --git a/src/controller/CHIPDeviceController.cpp b/src/controller/CHIPDeviceController.cpp
index 1381ed3..75c95ee 100644
--- a/src/controller/CHIPDeviceController.cpp
+++ b/src/controller/CHIPDeviceController.cpp
@@ -1326,8 +1326,8 @@
 
     if (commissioner->mPairingDelegate != nullptr)
     {
-        commissioner->mPairingDelegate->OnICDRegistrationComplete(commissioner->mDeviceBeingCommissioned->GetDeviceId(),
-                                                                  data.ICDCounter);
+        commissioner->mPairingDelegate->OnICDRegistrationComplete(
+            ScopedNodeId(commissioner->mDeviceBeingCommissioned->GetDeviceId(), commissioner->GetFabricIndex()), data.ICDCounter);
     }
 
 exit:
@@ -1346,8 +1346,10 @@
 
     if (commissioner->mPairingDelegate != nullptr)
     {
-        commissioner->mPairingDelegate->OnICDStayActiveComplete(commissioner->mDeviceBeingCommissioned->GetDeviceId(),
-                                                                data.promisedActiveDuration);
+        commissioner->mPairingDelegate->OnICDStayActiveComplete(
+
+            ScopedNodeId(commissioner->mDeviceBeingCommissioned->GetDeviceId(), commissioner->GetFabricIndex()),
+            data.promisedActiveDuration);
     }
 
 exit:
diff --git a/src/controller/DevicePairingDelegate.h b/src/controller/DevicePairingDelegate.h
index 558a6c1..849df45 100644
--- a/src/controller/DevicePairingDelegate.h
+++ b/src/controller/DevicePairingDelegate.h
@@ -136,7 +136,7 @@
      * @param[in] icdNodeId    The node id of the ICD.
      * @param[in] icdCounter   The ICD Counter received from the device.
      */
-    virtual void OnICDRegistrationComplete(NodeId icdNodeId, uint32_t icdCounter) {}
+    virtual void OnICDRegistrationComplete(ScopedNodeId icdNodeId, uint32_t icdCounter) {}
 
     /**
      * @brief
@@ -147,7 +147,7 @@
      * @param[in] promisedActiveDurationMsec   The actual duration that the ICD server can stay active
      *            from the time it receives the StayActiveRequest command.
      */
-    virtual void OnICDStayActiveComplete(NodeId icdNodeId, uint32_t promisedActiveDurationMsec) {}
+    virtual void OnICDStayActiveComplete(ScopedNodeId icdNodeId, uint32_t promisedActiveDurationMsec) {}
 };
 
 } // namespace Controller
diff --git a/src/controller/java/AndroidDeviceControllerWrapper.cpp b/src/controller/java/AndroidDeviceControllerWrapper.cpp
index 526f683..87f5373 100644
--- a/src/controller/java/AndroidDeviceControllerWrapper.cpp
+++ b/src/controller/java/AndroidDeviceControllerWrapper.cpp
@@ -994,13 +994,13 @@
     env->CallVoidMethod(mJavaObjectRef.ObjectRef(), onICDRegistrationInfoRequiredMethod);
 }
 
-void AndroidDeviceControllerWrapper::OnICDRegistrationComplete(chip::NodeId icdNodeId, uint32_t icdCounter)
+void AndroidDeviceControllerWrapper::OnICDRegistrationComplete(chip::ScopedNodeId icdNodeId, uint32_t icdCounter)
 {
     chip::DeviceLayer::StackUnlock unlock;
 
     CHIP_ERROR err = CHIP_NO_ERROR;
     chip::app::ICDClientInfo clientInfo;
-    clientInfo.peer_node         = ScopedNodeId(icdNodeId, Controller()->GetFabricIndex());
+    clientInfo.peer_node         = icdNodeId;
     clientInfo.monitored_subject = mAutoCommissioner.GetCommissioningParameters().GetICDMonitoredSubject().Value();
     clientInfo.start_icd_counter = icdCounter;
 
@@ -1014,13 +1014,13 @@
 
     if (err == CHIP_NO_ERROR)
     {
-        ChipLogProgress(Controller, "Saved ICD Symmetric key for " ChipLogFormatX64, ChipLogValueX64(icdNodeId));
+        ChipLogProgress(Controller, "Saved ICD Symmetric key for " ChipLogFormatX64, ChipLogValueX64(icdNodeId.GetNodeId()));
     }
     else
     {
         getICDClientStorage()->RemoveKey(clientInfo);
-        ChipLogError(Controller, "Failed to persist symmetric key for " ChipLogFormatX64 ": %s", ChipLogValueX64(icdNodeId),
-                     err.AsString());
+        ChipLogError(Controller, "Failed to persist symmetric key for " ChipLogFormatX64 ": %s",
+                     ChipLogValueX64(icdNodeId.GetNodeId()), err.AsString());
     }
 
     mDeviceIsICD = true;
@@ -1055,7 +1055,7 @@
     icdDeviceInfoObj = env->NewObject(
         icdDeviceInfoClass, icdDeviceInfoStructCtor, jSymmetricKey, static_cast<jint>(mUserActiveModeTriggerHint.Raw()),
         jUserActiveModeTriggerInstruction, static_cast<jlong>(mIdleModeDuration), static_cast<jlong>(mActiveModeDuration),
-        static_cast<jint>(mActiveModeThreshold), static_cast<jlong>(icdNodeId), static_cast<jlong>(icdCounter),
+        static_cast<jint>(mActiveModeThreshold), static_cast<jlong>(icdNodeId.GetNodeId()), static_cast<jlong>(icdCounter),
         static_cast<jlong>(mAutoCommissioner.GetCommissioningParameters().GetICDMonitoredSubject().Value()),
         static_cast<jlong>(Controller()->GetFabricId()), static_cast<jint>(Controller()->GetFabricIndex()));
 
diff --git a/src/controller/java/AndroidDeviceControllerWrapper.h b/src/controller/java/AndroidDeviceControllerWrapper.h
index 1d26d31..02d5049 100644
--- a/src/controller/java/AndroidDeviceControllerWrapper.h
+++ b/src/controller/java/AndroidDeviceControllerWrapper.h
@@ -118,7 +118,7 @@
         const chip::app::Clusters::NetworkCommissioning::Commands::ScanNetworksResponse::DecodableType & dataResponse) override;
     void OnScanNetworksFailure(CHIP_ERROR error) override;
     void OnICDRegistrationInfoRequired() override;
-    void OnICDRegistrationComplete(chip::NodeId icdNodeId, uint32_t icdCounter) override;
+    void OnICDRegistrationComplete(chip::ScopedNodeId icdNodeId, uint32_t icdCounter) override;
 
     // PersistentStorageDelegate implementation
     CHIP_ERROR SyncSetKeyValue(const char * key, const void * value, uint16_t size) override;
diff --git a/src/controller/python/BUILD.gn b/src/controller/python/BUILD.gn
index 4676f89..9adda77 100644
--- a/src/controller/python/BUILD.gn
+++ b/src/controller/python/BUILD.gn
@@ -76,6 +76,8 @@
       "chip/crypto/p256keypair.cpp",
       "chip/crypto/p256keypair.h",
       "chip/discovery/NodeResolution.cpp",
+      "chip/icd/PyChipCheckInDelegate.cpp",
+      "chip/icd/PyChipCheckInDelegate.h",
       "chip/interaction_model/Delegate.cpp",
       "chip/interaction_model/Delegate.h",
       "chip/internal/ChipThreadWork.cpp",
@@ -121,6 +123,7 @@
 
   public_deps = [
     "${chip_root}/src/app",
+    "${chip_root}/src/app/icd/client:handler",
     "${chip_root}/src/app/server",
     "${chip_root}/src/credentials:default_attestation_verifier",
     "${chip_root}/src/lib",
diff --git a/src/controller/python/ChipDeviceController-ScriptBinding.cpp b/src/controller/python/ChipDeviceController-ScriptBinding.cpp
index 728fd58..821b2a3 100644
--- a/src/controller/python/ChipDeviceController-ScriptBinding.cpp
+++ b/src/controller/python/ChipDeviceController-ScriptBinding.cpp
@@ -42,6 +42,9 @@
 
 #include <app/DeviceProxy.h>
 #include <app/InteractionModelEngine.h>
+#include <app/icd/client/CheckInHandler.h>
+#include <app/icd/client/DefaultCheckInDelegate.h>
+#include <app/icd/client/DefaultICDClientStorage.h>
 #include <app/server/Dnssd.h>
 #include <controller/AutoCommissioner.h>
 #include <controller/CHIPDeviceController.h>
@@ -55,7 +58,9 @@
 #include <controller/python/ChipDeviceController-ScriptDevicePairingDelegate.h>
 #include <controller/python/ChipDeviceController-ScriptPairingDeviceDiscoveryDelegate.h>
 #include <controller/python/ChipDeviceController-StorageDelegate.h>
+#include <controller/python/chip/icd/PyChipCheckInDelegate.h>
 #include <controller/python/chip/interaction_model/Delegate.h>
+#include <controller/python/chip/native/ChipMainLoopWork.h>
 #include <controller/python/chip/native/PyChipError.h>
 
 #include <credentials/GroupDataProviderImpl.h>
@@ -101,20 +106,24 @@
 app::Clusters::TimeSynchronization::Structs::DSTOffsetStruct::Type sDSTBuf;
 app::Clusters::TimeSynchronization::Structs::TimeZoneStruct::Type sTimeZoneBuf;
 chip::Platform::ScopedMemoryBuffer<char> sTimeZoneNameBuf;
-chip::Controller::CommissioningParameters sCommissioningParameters;
-
 } // namespace
 
+chip::Controller::CommissioningParameters sCommissioningParameters;
+chip::app::DefaultICDClientStorage sICDClientStorage;
 chip::Controller::ScriptPairingDeviceDiscoveryDelegate sPairingDeviceDiscoveryDelegate;
 chip::Credentials::GroupDataProviderImpl sGroupDataProvider;
 chip::Credentials::PersistentStorageOpCertStore sPersistentStorageOpCertStore;
 chip::Crypto::RawKeySessionKeystore sSessionKeystore;
 
+chip::app::CheckInHandler sCheckInHandler;
+
 // NOTE: Remote device ID is in sync with the echo server device id
 // At some point, we may want to add an option to connect to a device without
 // knowing its id, because the ID can be learned on the first response that is received.
+chip::Controller::PyChipCheckInDelegate sCheckInDelegate;
 chip::NodeId kDefaultLocalDeviceId = chip::kTestControllerNodeId;
 chip::NodeId kRemoteDeviceId       = chip::kTestDeviceNodeId;
+uint8_t sICDSymmetricKey[chip::Crypto::kAES_CCM128_Key_Length];
 
 extern "C" {
 PyChipError pychip_DeviceController_StackInit(Controller::Python::StorageAdapter * storageAdapter, bool enableServerInteractions);
@@ -126,6 +135,7 @@
                                                       char * outAddress, uint64_t maxAddressLen, uint16_t * outPort);
 PyChipError pychip_DeviceController_GetCompressedFabricId(chip::Controller::DeviceCommissioner * devCtrl, uint64_t * outFabricId);
 PyChipError pychip_DeviceController_GetFabricId(chip::Controller::DeviceCommissioner * devCtrl, uint64_t * outFabricId);
+PyChipError pychip_DeviceController_GetFabricIndex(chip::Controller::DeviceCommissioner * devCtrl, uint8_t * outFabricIndex);
 PyChipError pychip_DeviceController_GetNodeId(chip::Controller::DeviceCommissioner * devCtrl, uint64_t * outNodeId);
 
 // Rendezvous
@@ -144,6 +154,8 @@
 PyChipError pychip_DeviceController_SetDefaultNtp(const char * defaultNTP);
 PyChipError pychip_DeviceController_SetTrustedTimeSource(chip::NodeId nodeId, chip::EndpointId endpoint);
 PyChipError pychip_DeviceController_SetCheckMatchingFabric(bool check);
+struct IcdRegistrationParameters;
+PyChipError pychip_DeviceController_SetIcdRegistrationParameters(bool enabled, const IcdRegistrationParameters * params);
 PyChipError pychip_DeviceController_ResetCommissioningParameters();
 PyChipError pychip_DeviceController_CloseSession(chip::Controller::DeviceCommissioner * devCtrl, chip::NodeId nodeid);
 PyChipError pychip_DeviceController_EstablishPASESessionIP(chip::Controller::DeviceCommissioner * devCtrl, const char * peerAddrStr,
@@ -235,6 +247,11 @@
                                                chip::Controller::Python::SetGetKeyValueCb getCb,
                                                chip::Controller::Python::SyncDeleteKeyValueCb deleteCb);
 void pychip_Storage_ShutdownAdapter(chip::Controller::Python::StorageAdapter * storageAdapter);
+
+//
+// ICD
+//
+void pychip_CheckInDelegate_SetOnCheckInCompleteCallback(PyChipCheckInDelegate::OnCheckInCompleteCallback * callback);
 }
 
 void * pychip_Storage_InitializeStorageAdapter(chip::Controller::Python::PyObject * context,
@@ -260,6 +277,8 @@
     factoryParams.fabricIndependentStorage = storageAdapter;
     factoryParams.sessionKeystore          = &sSessionKeystore;
 
+    sICDClientStorage.Init(storageAdapter, &sSessionKeystore);
+
     sGroupDataProvider.SetStorageDelegate(storageAdapter);
     sGroupDataProvider.SetSessionKeystore(factoryParams.sessionKeystore);
     PyReturnErrorOnFailure(ToPyChipError(sGroupDataProvider.Init()));
@@ -290,6 +309,11 @@
     //
     DeviceControllerFactory::GetInstance().RetainSystemState();
 
+    auto engine = chip::app::InteractionModelEngine::GetInstance();
+    PyReturnErrorOnFailure(ToPyChipError(sCheckInDelegate.Init(&sICDClientStorage, engine)));
+    PyReturnErrorOnFailure(ToPyChipError(sCheckInHandler.Init(
+        DeviceControllerFactory::GetInstance().GetSystemState()->ExchangeMgr(), &sICDClientStorage, &sCheckInDelegate, engine)));
+
     //
     // Finally, start up the main Matter thread. Any further interactions with the stack
     // will now need to happen on the Matter thread, OR protected with the stack lock.
@@ -342,6 +366,12 @@
     return ToPyChipError(CHIP_NO_ERROR);
 }
 
+PyChipError pychip_DeviceController_GetFabricIndex(chip::Controller::DeviceCommissioner * devCtrl, uint8_t * outFabricIndex)
+{
+    *outFabricIndex = devCtrl->GetFabricIndex();
+    return ToPyChipError(CHIP_NO_ERROR);
+}
+
 PyChipError pychip_DeviceController_GetNodeId(chip::Controller::DeviceCommissioner * devCtrl, uint64_t * outNodeId)
 {
     *outNodeId = devCtrl->GetNodeId();
@@ -554,6 +584,55 @@
     return ToPyChipError(CHIP_NO_ERROR);
 }
 
+struct IcdRegistrationParameters
+{
+    uint8_t * symmetricKey;
+    size_t symmetricKeyLength;
+    uint64_t checkInNodeId;
+    uint64_t monitoredSubject;
+    uint32_t stayActiveMsec;
+};
+
+PyChipError pychip_DeviceController_SetIcdRegistrationParameters(bool enabled, const IcdRegistrationParameters * params)
+{
+    if (!enabled)
+    {
+        sCommissioningParameters.SetICDRegistrationStrategy(ICDRegistrationStrategy::kIgnore);
+        return ToPyChipError(CHIP_NO_ERROR);
+    }
+
+    if (params == nullptr)
+    {
+        return ToPyChipError(CHIP_ERROR_INVALID_ARGUMENT);
+    }
+
+    if (params->symmetricKey == nullptr || params->symmetricKeyLength != sizeof(sICDSymmetricKey))
+    {
+        return ToPyChipError(CHIP_ERROR_INVALID_ARGUMENT);
+    }
+
+    if (params->checkInNodeId == 0)
+    {
+        return ToPyChipError(CHIP_ERROR_INVALID_ARGUMENT);
+    }
+    if (params->monitoredSubject == 0)
+    {
+        return ToPyChipError(CHIP_ERROR_INVALID_ARGUMENT);
+    }
+
+    memcpy(sICDSymmetricKey, params->symmetricKey, sizeof(sICDSymmetricKey));
+    sCommissioningParameters.SetICDSymmetricKey(ByteSpan(sICDSymmetricKey));
+    if (params->stayActiveMsec != 0)
+    {
+        sCommissioningParameters.SetICDStayActiveDurationMsec(params->stayActiveMsec);
+    }
+    sCommissioningParameters.SetICDCheckInNodeId(params->checkInNodeId);
+    sCommissioningParameters.SetICDMonitoredSubject(params->monitoredSubject);
+    sCommissioningParameters.SetICDRegistrationStrategy(ICDRegistrationStrategy::kBeforeComplete);
+
+    return ToPyChipError(CHIP_NO_ERROR);
+}
+
 PyChipError pychip_DeviceController_ResetCommissioningParameters()
 {
     sCommissioningParameters = CommissioningParameters();
@@ -871,3 +950,8 @@
     PlatformMgr().ScheduleWork(callback, reinterpret_cast<intptr_t>(pythonContext));
     return ToPyChipError(CHIP_NO_ERROR);
 }
+
+void pychip_CheckInDelegate_SetOnCheckInCompleteCallback(PyChipCheckInDelegate::OnCheckInCompleteCallback * callback)
+{
+    chip::MainLoopWork::ExecuteInMainLoop([callback]() { sCheckInDelegate.SetOnCheckInCompleteCallback(callback); });
+}
diff --git a/src/controller/python/ChipDeviceController-ScriptDevicePairingDelegate.cpp b/src/controller/python/ChipDeviceController-ScriptDevicePairingDelegate.cpp
index c1df812..c979e0d 100644
--- a/src/controller/python/ChipDeviceController-ScriptDevicePairingDelegate.cpp
+++ b/src/controller/python/ChipDeviceController-ScriptDevicePairingDelegate.cpp
@@ -19,12 +19,17 @@
 
 #include "ChipDeviceController-ScriptDevicePairingDelegate.h"
 #include "lib/support/TypeTraits.h"
+#include <app/icd/client/DefaultICDClientStorage.h>
 #include <controller/python/chip/native/PyChipError.h>
 #include <setup_payload/ManualSetupPayloadGenerator.h>
 #include <setup_payload/QRCodeSetupPayloadGenerator.h>
 
 #include <string>
 
+extern chip::app::DefaultICDClientStorage sICDClientStorage;
+extern chip::Controller::CommissioningParameters sCommissioningParameters;
+extern uint8_t sICDSymmetricKey[chip::Crypto::kAES_CCM128_Key_Length];
+
 namespace chip {
 namespace Controller {
 
@@ -180,5 +185,40 @@
     return &mOpenWindowCallback;
 }
 
+void ScriptDevicePairingDelegate::OnICDRegistrationComplete(ScopedNodeId nodeId, uint32_t icdCounter)
+{
+    app::ICDClientInfo clientInfo;
+    clientInfo.peer_node         = nodeId;
+    clientInfo.monitored_subject = sCommissioningParameters.GetICDMonitoredSubject().Value();
+    clientInfo.start_icd_counter = icdCounter;
+
+    CHIP_ERROR err = sICDClientStorage.SetKey(clientInfo, ByteSpan(sICDSymmetricKey));
+    if (err == CHIP_NO_ERROR)
+    {
+        err = sICDClientStorage.StoreEntry(clientInfo);
+    }
+
+    if (err != CHIP_NO_ERROR)
+    {
+        sICDClientStorage.RemoveKey(clientInfo);
+        ChipLogError(Controller, "Failed to persist symmetric key for " ChipLogFormatX64 ": %s",
+                     ChipLogValueX64(nodeId.GetNodeId()), err.AsString());
+        return;
+    }
+
+    ChipLogProgress(Controller, "Saved ICD Symmetric key for " ChipLogFormatX64, ChipLogValueX64(nodeId.GetNodeId()));
+    ChipLogProgress(Controller,
+                    "ICD Registration Complete for device " ChipLogFormatX64 " / Check-In NodeID: " ChipLogFormatX64
+                    " / Monitored Subject: " ChipLogFormatX64 " / ICDCounter %u",
+                    ChipLogValueX64(nodeId.GetNodeId()), ChipLogValueX64(sCommissioningParameters.GetICDCheckInNodeId().Value()),
+                    ChipLogValueX64(clientInfo.monitored_subject), icdCounter);
+}
+
+void ScriptDevicePairingDelegate::OnICDStayActiveComplete(ScopedNodeId deviceId, uint32_t promisedActiveDuration)
+{
+    ChipLogProgress(Controller, "ICD Stay Active Complete for device " ChipLogFormatX64 " / promisedActiveDuration: %u",
+                    ChipLogValueX64(deviceId.GetNodeId()), promisedActiveDuration);
+}
+
 } // namespace Controller
 } // namespace chip
diff --git a/src/controller/python/ChipDeviceController-ScriptDevicePairingDelegate.h b/src/controller/python/ChipDeviceController-ScriptDevicePairingDelegate.h
index 2740b6e..d6665f8 100644
--- a/src/controller/python/ChipDeviceController-ScriptDevicePairingDelegate.h
+++ b/src/controller/python/ChipDeviceController-ScriptDevicePairingDelegate.h
@@ -27,6 +27,7 @@
 
 #include <controller/CHIPDeviceController.h>
 #include <controller/CommissioningWindowOpener.h>
+#include <controller/python/chip/icd/PyChipCheckInDelegate.h>
 #include <controller/python/chip/native/PyChipError.h>
 
 namespace chip {
@@ -68,11 +69,14 @@
     void OnCommissioningFailure(PeerId peerId, CHIP_ERROR error, CommissioningStage stageFailed,
                                 Optional<Credentials::AttestationVerificationResult> additionalErrorInfo) override;
     void OnCommissioningStatusUpdate(PeerId peerId, CommissioningStage stageCompleted, CHIP_ERROR error) override;
+    void OnICDRegistrationComplete(ScopedNodeId deviceId, uint32_t icdCounter) override;
+    void OnICDStayActiveComplete(ScopedNodeId deviceId, uint32_t promisedActiveDuration) override;
     void OnFabricCheck(NodeId matchingNodeId) override;
     Callback::Callback<Controller::OnOpenCommissioningWindow> *
     GetOpenWindowCallback(Controller::CommissioningWindowOpener * context);
     void OnOpenCommissioningWindow(NodeId deviceId, CHIP_ERROR status, SetupPayload payload);
     void SetExpectingPairingComplete(bool value) { expectingPairingComplete = value; }
+    void SetFabricIndex(FabricIndex fabricIndex) { mFabricIndex = fabricIndex; }
 
 private:
     DevicePairingDelegate_OnPairingCompleteFunct mOnPairingCompleteCallback                     = nullptr;
@@ -84,7 +88,9 @@
     DevicePairingDelegate_OnFabricCheckFunct mOnFabricCheckCallback                             = nullptr;
     Callback::Callback<Controller::OnOpenCommissioningWindow> mOpenWindowCallback;
     Controller::CommissioningWindowOpener * mWindowOpener = nullptr;
-    bool expectingPairingComplete                         = false;
+
+    bool expectingPairingComplete = false;
+    FabricIndex mFabricIndex      = 0;
 };
 
 } // namespace Controller
diff --git a/src/controller/python/OpCredsBinding.cpp b/src/controller/python/OpCredsBinding.cpp
index 5fd4205..427ee9b 100644
--- a/src/controller/python/OpCredsBinding.cpp
+++ b/src/controller/python/OpCredsBinding.cpp
@@ -26,6 +26,7 @@
 #include "controller/python/chip/crypto/p256keypair.h"
 #include "controller/python/chip/interaction_model/Delegate.h"
 
+#include <app/icd/client/DefaultICDClientStorage.h>
 #include <controller/CHIPDeviceController.h>
 #include <controller/CHIPDeviceControllerFactory.h>
 #include <controller/ExampleOperationalCredentialsIssuer.h>
@@ -104,6 +105,7 @@
 
 extern chip::Credentials::GroupDataProviderImpl sGroupDataProvider;
 extern chip::Controller::ScriptDevicePairingDelegate sPairingDelegate;
+extern chip::app::DefaultICDClientStorage sICDClientStorage;
 
 class TestCommissioner : public chip::Controller::AutoCommissioner
 {
@@ -569,6 +571,9 @@
         chip::Credentials::SetSingleIpkEpochKey(&sGroupDataProvider, devCtrl->GetFabricIndex(), defaultIpk, compressedFabricIdSpan);
     VerifyOrReturnError(err == CHIP_NO_ERROR, ToPyChipError(err));
 
+    sICDClientStorage.UpdateFabricList(devCtrl->GetFabricIndex());
+    pairingDelegate->SetFabricIndex(devCtrl->GetFabricIndex());
+
     *outDevCtrl         = devCtrl.release();
     *outPairingDelegate = pairingDelegate.release();
 
diff --git a/src/controller/python/chip/ChipDeviceCtrl.py b/src/controller/python/chip/ChipDeviceCtrl.py
index 4fb8fb2..2ba7c58 100644
--- a/src/controller/python/chip/ChipDeviceCtrl.py
+++ b/src/controller/python/chip/ChipDeviceCtrl.py
@@ -35,11 +35,12 @@
 import enum
 import json
 import logging
+import secrets
 import threading
 import time
 import typing
-from ctypes import (CDLL, CFUNCTYPE, POINTER, byref, c_bool, c_char, c_char_p, c_int, c_int32, c_size_t, c_uint8, c_uint16,
-                    c_uint32, c_uint64, c_void_p, create_string_buffer, pointer, py_object, resize, string_at)
+from ctypes import (CDLL, CFUNCTYPE, POINTER, Structure, byref, c_bool, c_char, c_char_p, c_int, c_int32, c_size_t, c_uint8,
+                    c_uint16, c_uint32, c_uint64, c_void_p, create_string_buffer, pointer, py_object, resize, string_at)
 from dataclasses import dataclass
 
 import dacite
@@ -98,6 +99,21 @@
     adminSubject: int
 
 
+@dataclass
+class ICDRegistrationParameters:
+    symmetricKey: typing.Optional[bytes]
+    checkInNodeId: typing.Optional[int]
+    monitoredSubject: typing.Optional[int]
+    stayActiveMs: typing.Optional[int]
+
+    class CStruct(Structure):
+        _fields_ = [('symmetricKey', c_char_p), ('symmetricKeyLength', c_size_t), ('checkInNodeId',
+                                                                                   c_uint64), ('monitoredSubject', c_uint64), ('stayActiveMsec', c_uint32)]
+
+    def to_c(self):
+        return ICDRegistrationParameters.CStruct(self.symmetricKey, len(self.symmetricKey), self.checkInNodeId, self.monitoredSubject, self.stayActiveMs)
+
+
 @_DeviceAvailableCallbackFunct
 def _DeviceAvailableCallback(closure, device, err):
     closure.deviceAvailable(device, err)
@@ -123,6 +139,77 @@
         nocChain = NOCChain(nocBytes, icacBytes, rcacBytes, ipkBytes, adminSubject)
     devCtrl.NOCChainCallback(nocChain)
 
+
+# Methods for ICD
+class ScopedNodeId(Structure):
+    _fields_ = [("nodeId", c_uint64), ("fabricIndex", c_uint8)]
+
+    def __hash__(self):
+        return self.nodeId << 8 | self.fabricIndex
+
+    def __str__(self):
+        return f"({self.fabricIndex}:{self.nodeId:16x})"
+
+    def __eq__(self, other):
+        return self.nodeId == other.nodeId and self.fabricIndex == other.fabricIndex
+
+
+_OnCheckInCompleteFunct = CFUNCTYPE(None, ScopedNodeId)
+
+_OnCheckInCompleteWaitListLock = threading.Lock()
+_OnCheckInCompleteWaitList = dict()
+
+
+@_OnCheckInCompleteFunct
+def _OnCheckInComplete(scopedNodeId: ScopedNodeId):
+    callbacks = []
+    with _OnCheckInCompleteWaitListLock:
+        callbacks = list(_OnCheckInCompleteWaitList.get(scopedNodeId, set()))
+
+    for callback in callbacks:
+        callback(scopedNodeId)
+
+
+def RegisterOnActiveCallback(scopedNodeId: ScopedNodeId, callback: typing.Callable[None, [ScopedNodeId]]):
+    ''' Registers a callback when the device with given (fabric index, node id) becomes active.
+
+    Does nothing if the callback is already registered.
+    '''
+    with _OnCheckInCompleteWaitListLock:
+        waitList = _OnCheckInCompleteWaitList.get(scopedNodeId, set())
+        waitList.add(callback)
+        _OnCheckInCompleteWaitList[scopedNodeId] = waitList
+
+
+def UnregisterOnActiveCallback(scopedNodeId: ScopedNodeId, callback: typing.Callable[None, [ScopedNodeId]]):
+    ''' Unregisters a callback when the device with given (fabric index, node id) becomes active.
+
+    Does nothing if the callback has not been registered.
+    '''
+    with _OnCheckInCompleteWaitListLock:
+        _OnCheckInCompleteWaitList.get(scopedNodeId, set()).remove(callback)
+
+
+async def WaitForCheckIn(scopedNodeId: ScopedNodeId, timeoutSeconds: float):
+    ''' Waits for a device becomes active.
+
+    Returns:
+        - A future, completes when the device becomes active.
+    '''
+    eventLoop = asyncio.get_running_loop()
+    future = eventLoop.create_future()
+
+    def OnCheckInCallback(nodeid):
+        eventLoop.call_soon_threadsafe(lambda: future.done() or future.set_result(None))
+
+    RegisterOnActiveCallback(scopedNodeId, OnCheckInCallback)
+
+    try:
+        async with asyncio.timeout(timeoutSeconds):
+            await future
+    finally:
+        UnregisterOnActiveCallback(scopedNodeId, OnCheckInCallback)
+
 # This is a fix for WEAV-429. Jay Logue recommends revisiting this at a later
 # date to allow for truly multiple instances so this is temporary.
 
@@ -269,6 +356,7 @@
             else:
                 logging.warning("Failed to commission: {}".format(err))
 
+            self._dmLib.pychip_DeviceController_SetIcdRegistrationParameters(False, None)
             self.state = DCState.IDLE
             self._ChipStack.callbackRes = err
             self._ChipStack.commissioningEventRes = err
@@ -347,6 +435,7 @@
         self._isActive = True
         # Validate FabricID/NodeID followed from NOC Chain
         self._fabricId = self.GetFabricIdInternal()
+        self._fabricIndex = self.GetFabricIndexInternal()
         self._nodeId = self.GetNodeIdInternal()
 
     def _finish_init(self):
@@ -766,6 +855,19 @@
 
         return fabricid.value
 
+    def GetFabricIndexInternal(self):
+        """Get the fabric index from the object. Only used to validate cached value from property."""
+        self.CheckIsActive()
+
+        fabricindex = c_uint8(0)
+
+        self._ChipStack.Call(
+            lambda: self._dmLib.pychip_DeviceController_GetFabricIndex(
+                self.devCtrl, pointer(fabricindex))
+        ).raise_on_error()
+
+        return fabricindex.value
+
     def GetNodeIdInternal(self) -> int:
         """Get the node ID from the object. Only used to validate cached value from property."""
         self.CheckIsActive()
@@ -841,6 +943,18 @@
 
         return DeviceProxyWrapper(returnDevice, self._dmLib)
 
+    async def WaitForActive(self, nodeid, *, timeoutSeconds=30.0, stayActiveDurationMs=30000):
+        ''' Waits a LIT ICD device to become active. Will send a StayActive command to the device on active to allow human operations.
+
+        nodeId: Node ID of the LID ICD
+        stayActiveDurationMs: The duration in the StayActive command, in milliseconds
+
+        Returns:
+            - StayActiveResponse on success
+        '''
+        await WaitForCheckIn(ScopedNodeId(nodeid, self._fabricIndex), timeoutSeconds=timeoutSeconds)
+        return await self.SendCommand(nodeid, 0, Clusters.IcdManagement.Commands.StayActiveRequest(stayActiveDuration=stayActiveDurationMs))
+
     async def GetConnectedDevice(self, nodeid, allowPASE: bool = True, timeoutMs: int = None):
         ''' Gets an OperationalDeviceProxy or CommissioneeDeviceProxy for the specified Node.
 
@@ -1536,6 +1650,11 @@
             self._dmLib.pychip_DeviceController_SetCheckMatchingFabric.restype = PyChipError
             self._dmLib.pychip_DeviceController_SetCheckMatchingFabric.argtypes = [c_bool]
 
+            self._dmLib.pychip_DeviceController_SetIcdRegistrationParameters.restype = PyChipError
+            self._dmLib.pychip_DeviceController_SetIcdRegistrationParameters.argtypes = [
+                c_bool, c_void_p
+            ]
+
             self._dmLib.pychip_DeviceController_ResetCommissioningParameters.restype = PyChipError
             self._dmLib.pychip_DeviceController_ResetCommissioningParameters.argtypes = []
 
@@ -1722,6 +1841,9 @@
             self._dmLib.pychip_DeviceController_GetFabricId.argtypes = [c_void_p, POINTER(c_uint64)]
             self._dmLib.pychip_DeviceController_GetFabricId.restype = PyChipError
 
+            self._dmLib.pychip_DeviceController_GetFabricIndex.argtypes = [c_void_p, POINTER(c_uint8)]
+            self._dmLib.pychip_DeviceController_GetFabricIndex.restype = PyChipError
+
             self._dmLib.pychip_DeviceController_GetLogFilter = [None]
             self._dmLib.pychip_DeviceController_GetLogFilter = c_uint8
 
@@ -1736,6 +1858,11 @@
             self._dmLib.pychip_DeviceController_SetIpk.argtypes = [c_void_p, POINTER(c_char), c_size_t]
             self._dmLib.pychip_DeviceController_SetIpk.restype = PyChipError
 
+            self._dmLib.pychip_CheckInDelegate_SetOnCheckInCompleteCallback.restype = None
+            self._dmLib.pychip_CheckInDelegate_SetOnCheckInCompleteCallback.argtypes = [_OnCheckInCompleteFunct]
+
+            self._dmLib.pychip_CheckInDelegate_SetOnCheckInCompleteCallback(_OnCheckInComplete)
+
 
 class ChipDeviceController(ChipDeviceControllerBase):
     ''' The ChipDeviceCommissioner binding, named as ChipDeviceController
@@ -1884,6 +2011,38 @@
             lambda: self._dmLib.pychip_DeviceController_SetCheckMatchingFabric(check)
         ).raise_on_error()
 
+    def GenerateICDRegistrationParameters(self):
+        ''' Generates ICD registration parameters for this controller. '''
+        return ICDRegistrationParameters(
+            secrets.token_bytes(16),
+            self._nodeId,
+            self._nodeId,
+            30)
+
+    def EnableICDRegistration(self, parameters: ICDRegistrationParameters):
+        ''' Enables ICD registration for the following commissioning session.
+
+        Args:
+            parameters: A ICDRegistrationParameters for the parameters used for ICD registration, or None for default arguments.
+        '''
+        if parameters is None:
+            raise ValueError("ICD registration parameter required.")
+        if len(parameters.symmetricKey) != 16:
+            raise ValueError("symmetricKey should be 16 bytes")
+
+        self.CheckIsActive()
+        self._ChipStack.Call(
+            lambda: self._dmLib.pychip_DeviceController_SetIcdRegistrationParameters(
+                True, pointer(parameters.to_c()))
+        ).raise_on_error()
+
+    def DisableICDRegistration(self):
+        ''' Disables ICD registration. '''
+        self.CheckIsActive()
+        self._ChipStack.Call(
+            lambda: self._dmLib.pychip_DeviceController_SetIcdRegistrationParameters(False, None)
+        ).raise_on_error()
+
     def GetFabricCheckResult(self) -> int:
         ''' Returns the fabric check result if SetCheckMatchingFabric was used.'''
         return self.fabricCheckNodeId
diff --git a/src/controller/python/chip/ChipReplStartup.py b/src/controller/python/chip/ChipReplStartup.py
index b75c77e..13a93a1 100644
--- a/src/controller/python/chip/ChipReplStartup.py
+++ b/src/controller/python/chip/ChipReplStartup.py
@@ -96,6 +96,8 @@
         "-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)
+    parser.add_argument(
+        "-s", "--server-interactions", help="Enable server interactions.", action="store_true")
     args = parser.parse_args()
 
     if not os.path.exists(args.trust_store):
@@ -139,7 +141,7 @@
 
     ReplInit(args.debug)
 
-    chipStack = ChipStack(persistentStoragePath=args.storagepath, enableServerInteractions=False)
+    chipStack = ChipStack(persistentStoragePath=args.storagepath, enableServerInteractions=args.server_interactions)
     certificateAuthorityManager = chip.CertificateAuthority.CertificateAuthorityManager(chipStack, chipStack.GetStorageManager())
 
     certificateAuthorityManager.LoadAuthoritiesFromStorage()
diff --git a/src/controller/python/chip/icd/PyChipCheckInDelegate.cpp b/src/controller/python/chip/icd/PyChipCheckInDelegate.cpp
new file mode 100644
index 0000000..03f6b7d
--- /dev/null
+++ b/src/controller/python/chip/icd/PyChipCheckInDelegate.cpp
@@ -0,0 +1,33 @@
+/*
+ *
+ *    Copyright (c) 2024 Project CHIP Authors
+ *    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.
+ */
+
+#include "PyChipCheckInDelegate.h"
+
+using namespace ::chip;
+using namespace ::chip::app;
+using namespace ::chip::Controller;
+
+void PyChipCheckInDelegate::OnCheckInComplete(const ICDClientInfo & clientInfo)
+{
+    DefaultCheckInDelegate::OnCheckInComplete(clientInfo);
+
+    if (mCallback != nullptr)
+    {
+        mCallback(clientInfo.peer_node);
+    }
+}
diff --git a/src/controller/python/chip/icd/PyChipCheckInDelegate.h b/src/controller/python/chip/icd/PyChipCheckInDelegate.h
new file mode 100644
index 0000000..3e3a3d9
--- /dev/null
+++ b/src/controller/python/chip/icd/PyChipCheckInDelegate.h
@@ -0,0 +1,42 @@
+/*
+ *
+ *    Copyright (c) 2024 Project CHIP Authors
+ *    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.
+ */
+
+#pragma once
+
+#include <app/icd/client/DefaultCheckInDelegate.h>
+
+namespace chip {
+namespace Controller {
+
+class PyChipCheckInDelegate : public chip::app::DefaultCheckInDelegate
+{
+public:
+    using OnCheckInCompleteCallback = void(chip::ScopedNodeId);
+
+    virtual ~PyChipCheckInDelegate() = default;
+
+    void OnCheckInComplete(const chip::app::ICDClientInfo & clientInfo) override;
+
+    void SetOnCheckInCompleteCallback(OnCheckInCompleteCallback * callback) { mCallback = callback; }
+
+private:
+    OnCheckInCompleteCallback * mCallback;
+};
+
+} // namespace Controller
+} // namespace chip
diff --git a/src/controller/python/chip/internal/types.py b/src/controller/python/chip/internal/types.py
index 1904569..34c1473 100644
--- a/src/controller/python/chip/internal/types.py
+++ b/src/controller/python/chip/internal/types.py
@@ -14,7 +14,7 @@
 #    limitations under the License.
 #
 
-from ctypes import CFUNCTYPE, c_size_t, c_uint32, c_void_p
+from ctypes import CFUNCTYPE, Structure, c_size_t, c_uint32, c_uint64, c_void_p
 
 # General callback of 'network credentials requested. No python-data
 # is available as the underlying callback is used internally
@@ -26,3 +26,7 @@
 
 # Notification that pairing has been coompleted
 PairingComplete = CFUNCTYPE(None, c_uint32)
+
+
+class ScopedNodeId(Structure):
+    _fields_ = [("node_id", c_uint64), ("fabric_index", "c_uint8")]