Address API review issues in MTRDeviceController. (#23512)

This is a re-landing of PR #22596 but with changes made for backwards compat.

* Make MTRBaseDevice creation synchronous.  This requires updates to
  MTRBaseDeviceOverXPC to do the possible async getting of the controller id it
  needs during its async operations, not when getting the device object.
* Deprecate pairDevice methods.
* Rename "commissionDevice" to "commissionNodeWithID".
* Rename "stopDevicePairing" to "cancelCommissioningForNodeID" and document.
* Rename "getDeviceBeingCommissioned" to "getDeviceBeingCommissionedWithNodeID".
* Various documentation improvements.
* Fix signature of computePaseVerifier.

The header changes not accompanied by backwards-compat shims are OK for the
following reasons:

* In MTRDeviceController.h, the change to isRunning is binary and source
  compatible.
* MTRDeviceControllerOverXPC_Internal.h is not public API.
* MTRDeviceController_Internal.h is not public API.
* MTRDeviceOverXPC.h is not public API.
* The changes to MTRSetupPayload.h are backward-compatible.
diff --git a/examples/darwin-framework-tool/commands/clusters/ModelCommandBridge.mm b/examples/darwin-framework-tool/commands/clusters/ModelCommandBridge.mm
index a21e6e2..97b7c09 100644
--- a/examples/darwin-framework-tool/commands/clusters/ModelCommandBridge.mm
+++ b/examples/darwin-framework-tool/commands/clusters/ModelCommandBridge.mm
@@ -25,31 +25,15 @@
 
 CHIP_ERROR ModelCommand::RunCommand()
 {
-    dispatch_queue_t callbackQueue = dispatch_queue_create("com.chip-tool.command", DISPATCH_QUEUE_SERIAL);
-
     MTRDeviceController * commissioner = CurrentCommissioner();
     ChipLogProgress(chipTool, "Sending command to node 0x" ChipLogFormatX64, ChipLogValueX64(mNodeId));
-    [commissioner getBaseDevice:mNodeId
-                          queue:callbackQueue
-                     completion:^(MTRBaseDevice * _Nullable device, NSError * _Nullable error) {
-                         if (error != nil) {
-                             SetCommandExitStatus(error, "Error getting connected device");
-                             return;
-                         }
+    auto * device = [MTRBaseDevice deviceWithNodeID:@(mNodeId) controller:commissioner];
+    CHIP_ERROR err = SendCommand(device, mEndPointId);
 
-                         CHIP_ERROR err;
-                         if (device == nil) {
-                             err = CHIP_ERROR_INTERNAL;
-                         } else {
-                             err = SendCommand(device, mEndPointId);
-                         }
-
-                         if (err != CHIP_NO_ERROR) {
-                             ChipLogError(chipTool, "Error: %s", chip::ErrorStr(err));
-                             SetCommandExitStatus(err);
-                             return;
-                         }
-                     }];
+    if (err != CHIP_NO_ERROR) {
+        ChipLogError(chipTool, "Error: %s", chip::ErrorStr(err));
+        return err;
+    }
     return CHIP_NO_ERROR;
 }
 
diff --git a/examples/darwin-framework-tool/commands/pairing/PairingCommandBridge.mm b/examples/darwin-framework-tool/commands/pairing/PairingCommandBridge.mm
index ae27e63..a05e9ab 100644
--- a/examples/darwin-framework-tool/commands/pairing/PairingCommandBridge.mm
+++ b/examples/darwin-framework-tool/commands/pairing/PairingCommandBridge.mm
@@ -101,47 +101,32 @@
 {
     dispatch_queue_t callbackQueue = dispatch_queue_create("com.chip-tool.command", DISPATCH_QUEUE_SERIAL);
     MTRDeviceController * commissioner = CurrentCommissioner();
-    [commissioner
-        getBaseDevice:mNodeId
-                queue:callbackQueue
-           completion:^(MTRBaseDevice * _Nullable device, NSError * _Nullable error) {
-               CHIP_ERROR err = CHIP_NO_ERROR;
-               if (error) {
-                   err = MTRErrorToCHIPErrorCode(error);
-                   LogNSError("Error: ", error);
-                   SetCommandExitStatus(err);
-               } else if (device == nil) {
-                   ChipLogError(chipTool, "Error: %s", chip::ErrorStr(CHIP_ERROR_INTERNAL));
-                   SetCommandExitStatus(CHIP_ERROR_INTERNAL);
-               } else {
-                   ChipLogProgress(chipTool, "Attempting to unpair device %llu", mNodeId);
-                   MTRBaseClusterOperationalCredentials * opCredsCluster =
-                       [[MTRBaseClusterOperationalCredentials alloc] initWithDevice:device endpointID:@(0) queue:callbackQueue];
-                   [opCredsCluster
-                       readAttributeCurrentFabricIndexWithCompletion:^(NSNumber * _Nullable value, NSError * _Nullable readError) {
-                           if (readError) {
-                               CHIP_ERROR readErr = MTRErrorToCHIPErrorCode(readError);
-                               LogNSError("Failed to get current fabric: ", readError);
-                               SetCommandExitStatus(readErr);
-                               return;
-                           }
-                           MTROperationalCredentialsClusterRemoveFabricParams * params =
-                               [[MTROperationalCredentialsClusterRemoveFabricParams alloc] init];
-                           params.fabricIndex = value;
-                           [opCredsCluster
-                               removeFabricWithParams:params
-                                           completion:^(MTROperationalCredentialsClusterNOCResponseParams * _Nullable data,
-                                               NSError * _Nullable removeError) {
-                                               CHIP_ERROR removeErr = CHIP_NO_ERROR;
-                                               if (removeError) {
-                                                   removeErr = MTRErrorToCHIPErrorCode(removeError);
-                                                   LogNSError("Failed to remove current fabric: ", removeError);
-                                               } else {
-                                                   ChipLogProgress(chipTool, "Successfully unpaired deviceId %llu", mNodeId);
-                                               }
-                                               SetCommandExitStatus(removeErr);
-                                           }];
-                       }];
-               }
-           }];
+    auto * device = [MTRBaseDevice deviceWithNodeID:@(mNodeId) controller:commissioner];
+
+    ChipLogProgress(chipTool, "Attempting to unpair device %llu", mNodeId);
+    MTRBaseClusterOperationalCredentials * opCredsCluster =
+        [[MTRBaseClusterOperationalCredentials alloc] initWithDevice:device endpointID:@(0) queue:callbackQueue];
+    [opCredsCluster readAttributeCurrentFabricIndexWithCompletion:^(NSNumber * _Nullable value, NSError * _Nullable readError) {
+        if (readError) {
+            CHIP_ERROR readErr = MTRErrorToCHIPErrorCode(readError);
+            LogNSError("Failed to get current fabric: ", readError);
+            SetCommandExitStatus(readErr);
+            return;
+        }
+        MTROperationalCredentialsClusterRemoveFabricParams * params =
+            [[MTROperationalCredentialsClusterRemoveFabricParams alloc] init];
+        params.fabricIndex = value;
+        [opCredsCluster removeFabricWithParams:params
+                                    completion:^(MTROperationalCredentialsClusterNOCResponseParams * _Nullable data,
+                                        NSError * _Nullable removeError) {
+                                        CHIP_ERROR removeErr = CHIP_NO_ERROR;
+                                        if (removeError) {
+                                            removeErr = MTRErrorToCHIPErrorCode(removeError);
+                                            LogNSError("Failed to remove current fabric: ", removeError);
+                                        } else {
+                                            ChipLogProgress(chipTool, "Successfully unpaired deviceId %llu", mNodeId);
+                                        }
+                                        SetCommandExitStatus(removeErr);
+                                    }];
+    }];
 }
diff --git a/examples/darwin-framework-tool/commands/pairing/PairingDelegateBridge.mm b/examples/darwin-framework-tool/commands/pairing/PairingDelegateBridge.mm
index 1a58f25..5ea3352 100644
--- a/examples/darwin-framework-tool/commands/pairing/PairingDelegateBridge.mm
+++ b/examples/darwin-framework-tool/commands/pairing/PairingDelegateBridge.mm
@@ -54,7 +54,7 @@
     ChipLogProgress(chipTool, "Pairing Success");
     ChipLogProgress(chipTool, "PASE establishment successful");
     NSError * commissionError;
-    [_commissioner commissionDevice:_deviceID commissioningParams:_params error:&commissionError];
+    [_commissioner commissionNodeWithID:@(_deviceID) commissioningParams:_params error:&commissionError];
     if (commissionError != nil) {
         _commandBridge->SetCommandExitStatus(commissionError);
         return;
diff --git a/examples/darwin-framework-tool/commands/tests/TestCommandBridge.h b/examples/darwin-framework-tool/commands/tests/TestCommandBridge.h
index 6edd466..03f6b70 100644
--- a/examples/darwin-framework-tool/commands/tests/TestCommandBridge.h
+++ b/examples/darwin-framework-tool/commands/tests/TestCommandBridge.h
@@ -156,9 +156,10 @@
 
         SetIdentity(identity);
 
-        // Invalidate our existing CASE session; otherwise getConnectedDevice
-        // will just hand it right back to us without establishing a new CASE
-        // session when a reboot is done on the server.
+        // Invalidate our existing CASE session; otherwise trying to work with
+        // our device will just reuse it without establishing a new CASE
+        // session when a reboot is done on the server, and then our next
+        // interaction will time out.
         if (value.expireExistingSession.ValueOr(true)) {
             if (GetDevice(identity) != nil) {
                 [GetDevice(identity) invalidateCASESession];
@@ -166,17 +167,10 @@
             }
         }
 
-        [controller getBaseDevice:value.nodeId
-                            queue:mCallbackQueue
-                       completion:^(MTRBaseDevice * _Nullable device, NSError * _Nullable error) {
-                           if (error != nil) {
-                               SetCommandExitStatus(error);
-                               return;
-                           }
-
-                           mConnectedDevices[identity] = device;
-                           NextTest();
-                       }];
+        mConnectedDevices[identity] = [MTRBaseDevice deviceWithNodeID:@(value.nodeId) controller:controller];
+        dispatch_async(mCallbackQueue, ^{
+            NextTest();
+        });
         return CHIP_NO_ERROR;
     }
 
@@ -238,7 +232,9 @@
         VerifyOrReturn(commissioner != nil, Exit("No current commissioner"));
 
         NSError * commissionError = nil;
-        [commissioner commissionDevice:nodeId commissioningParams:[[MTRCommissioningParameters alloc] init] error:&commissionError];
+        [commissioner commissionNodeWithID:@(nodeId)
+                       commissioningParams:[[MTRCommissioningParameters alloc] init]
+                                     error:&commissionError];
         CHIP_ERROR err = MTRErrorToCHIPErrorCode(commissionError);
         if (err != CHIP_NO_ERROR) {
             Exit("Failed to kick off commissioning", err);
diff --git a/src/darwin/Framework/CHIP/MTRBaseDevice.h b/src/darwin/Framework/CHIP/MTRBaseDevice.h
index cbd5b15..86da7b8 100644
--- a/src/darwin/Framework/CHIP/MTRBaseDevice.h
+++ b/src/darwin/Framework/CHIP/MTRBaseDevice.h
@@ -20,6 +20,7 @@
 #import <Matter/MTRCluster.h>
 
 @class MTRSetupPayload;
+@class MTRDeviceController;
 
 NS_ASSUME_NONNULL_BEGIN
 
@@ -126,6 +127,14 @@
 + (instancetype)new NS_UNAVAILABLE;
 
 /**
+ * Create a device object with the given node id and controller.  This
+ * will always succeed, even if there is no such node id on the controller's
+ * fabric, but attempts to actually use the MTRBaseDevice will fail
+ * (asynchronously) in that case.
+ */
++ (instancetype)deviceWithNodeID:(NSNumber *)nodeID controller:(MTRDeviceController *)controller MTR_NEWLY_AVAILABLE;
+
+/**
  * Subscribe to receive attribute reports for everything (all endpoints, all
  * clusters, all attributes, all events) on the device.
  *
diff --git a/src/darwin/Framework/CHIP/MTRBaseDevice.mm b/src/darwin/Framework/CHIP/MTRBaseDevice.mm
index e4d56ca..25e8908 100644
--- a/src/darwin/Framework/CHIP/MTRBaseDevice.mm
+++ b/src/darwin/Framework/CHIP/MTRBaseDevice.mm
@@ -231,6 +231,13 @@
     return self;
 }
 
++ (instancetype)deviceWithNodeID:(NSNumber *)nodeID controller:(MTRDeviceController *)controller
+{
+    // Indirect through the controller to give it a chance to create an
+    // MTRBaseDeviceOverXPC instead of just an MTRBaseDevice.
+    return [controller baseDeviceForNodeID:nodeID];
+}
+
 - (void)invalidateCASESession
 {
     if (self.isPASEDevice) {
diff --git a/src/darwin/Framework/CHIP/MTRDeviceController.h b/src/darwin/Framework/CHIP/MTRDeviceController.h
index 76f51a1..3ba788b 100644
--- a/src/darwin/Framework/CHIP/MTRDeviceController.h
+++ b/src/darwin/Framework/CHIP/MTRDeviceController.h
@@ -23,6 +23,7 @@
 
 NS_ASSUME_NONNULL_BEGIN
 
+MTR_NEWLY_DEPRECATED("Please use MTRBaseDevice deviceWithNodeID")
 typedef void (^MTRDeviceConnectionCallback)(MTRBaseDevice * _Nullable device, NSError * _Nullable error);
 
 @class MTRCommissioningParameters;
@@ -31,6 +32,9 @@
 
 @interface MTRDeviceController : NSObject
 
+/**
+ * If true, the controller has not been shut down yet.
+ */
 @property (readonly, nonatomic, getter=isRunning) BOOL running;
 
 /**
@@ -73,70 +77,29 @@
     API_AVAILABLE(ios(16.2), macos(13.1), watchos(9.2), tvos(16.2));
 
 /**
- * Start pairing for a device with the given ID, using the provided setup PIN
- * to establish a PASE connection.
- *
- * The IP and port for the device will be discovered automatically based on the
- * provided discriminator.
- *
- * The pairing process will proceed until a PASE session is established or an
- * error occurs, then notify onPairingComplete on the MTRDevicePairingDelegate
- * for this controller.  That delegate is expected to call commissionDevice
- * after that point if it wants to commission the device.
+ * Commission the node with the given node ID.  The node ID must match the node
+ * ID that was used to set up the commissioning session.
  */
-- (BOOL)pairDevice:(uint64_t)deviceID
-     discriminator:(uint16_t)discriminator
-      setupPINCode:(uint32_t)setupPINCode
-             error:(NSError * __autoreleasing *)error;
-
-/**
- * Start pairing for a device with the given ID, using the provided IP address
- * and port to connect to the device and the provided setup PIN to establish a
- * PASE connection.
- *
- * The pairing process will proceed until a PASE session is established or an
- * error occurs, then notify onPairingComplete on the MTRDevicePairingDelegate
- * for this controller.  That delegate is expected to call commissionDevice
- * after that point if it wants to commission the device.
- */
-- (BOOL)pairDevice:(uint64_t)deviceID
-           address:(NSString *)address
-              port:(uint16_t)port
-      setupPINCode:(uint32_t)setupPINCode
-             error:(NSError * __autoreleasing *)error;
-
-/**
- * Start pairing for a device with the given ID and onboarding payload (QR code
- * or manual setup code).  The payload will be used to discover the device and
- * establish a PASE connection.
- *
- * The pairing process will proceed until a PASE session is established or an
- * error occurs, then notify onPairingComplete on the MTRDevicePairingDelegate
- * for this controller.  That delegate is expected to call commissionDevice
- * after that point if it wants to commission the device.
- */
-- (BOOL)pairDevice:(uint64_t)deviceID onboardingPayload:(NSString *)onboardingPayload error:(NSError * __autoreleasing *)error;
-- (BOOL)commissionDevice:(uint64_t)deviceId
-     commissioningParams:(MTRCommissioningParameters *)commissioningParams
-                   error:(NSError * __autoreleasing *)error;
+- (BOOL)commissionNodeWithID:(NSNumber *)nodeID
+         commissioningParams:(MTRCommissioningParameters *)commissioningParams
+                       error:(NSError * __autoreleasing *)error MTR_NEWLY_AVAILABLE;
 
 - (BOOL)continueCommissioningDevice:(void *)device
            ignoreAttestationFailure:(BOOL)ignoreAttestationFailure
                               error:(NSError * __autoreleasing *)error;
 
-- (BOOL)stopDevicePairing:(uint64_t)deviceID error:(NSError * __autoreleasing *)error;
+/**
+ * Cancel commissioning for the given node id.  This will shut down any existing
+ * commissioning session for that node id.
+ */
+- (BOOL)cancelCommissioningForNodeID:(NSNumber *)nodeID error:(NSError * __autoreleasing *)error MTR_NEWLY_AVAILABLE;
 
-- (nullable MTRBaseDevice *)getDeviceBeingCommissioned:(uint64_t)deviceId error:(NSError * __autoreleasing *)error;
-- (BOOL)getBaseDevice:(uint64_t)deviceID
-                queue:(dispatch_queue_t)queue
-           completion:(MTRDeviceConnectionCallback)completion MTR_NEWLY_AVAILABLE;
-
-- (BOOL)openPairingWindow:(uint64_t)deviceID duration:(NSUInteger)duration error:(NSError * __autoreleasing *)error;
-- (nullable NSString *)openPairingWindowWithPIN:(uint64_t)deviceID
-                                       duration:(NSUInteger)duration
-                                  discriminator:(NSUInteger)discriminator
-                                       setupPIN:(NSUInteger)setupPIN
-                                          error:(NSError * __autoreleasing *)error;
+/**
+ * Get an MTRBaseDevice for a commissioning session that was set up for the
+ * given node ID.  Returns nil if no such commissioning session is available.
+ */
+- (nullable MTRBaseDevice *)deviceBeingCommissionedWithNodeID:(NSNumber *)nodeID
+                                                        error:(NSError * __autoreleasing *)error MTR_NEWLY_AVAILABLE;
 
 /**
  * Controllers are created via the MTRDeviceControllerFactory object.
@@ -176,19 +139,24 @@
 - (NSData * _Nullable)attestationChallengeForDeviceID:(NSNumber *)deviceID MTR_NEWLY_AVAILABLE;
 
 /**
- * Compute a PASE verifier and passcode ID for the desired setup pincode.
+ * Compute a PASE verifier for the desired setup passcode.
  *
- * @param[in] setupPincode    The desired PIN code to use
- * @param[in] iterations      The number of iterations to use when generating the verifier
- * @param[in] salt            The 16-byte salt for verifier computation
+ * @param[in] setupPasscode   The desired passcode to use.
+ * @param[in] iterations      The number of iterations to use when generating the verifier.
+ * @param[in] salt            The 16-byte salt for verifier computation.
  *
  * Returns nil on errors (e.g. salt has the wrong size), otherwise the computed
  * verifier bytes.
  */
-- (nullable NSData *)computePaseVerifier:(uint32_t)setupPincode iterations:(uint32_t)iterations salt:(NSData *)salt;
++ (nullable NSData *)computePASEVerifierForSetupPasscode:(NSNumber *)setupPasscode
+                                              iterations:(NSNumber *)iterations
+                                                    salt:(NSData *)salt
+                                                   error:(NSError * __autoreleasing *)error MTR_NEWLY_AVAILABLE;
 
 /**
- * Shutdown the controller. Calls to shutdown after the first one are NO-OPs.
+ * Shut down the controller. Calls to shutdown after the first one are NO-OPs.
+ * This must be called, either directly or via shutting down the
+ * MTRDeviceControllerFactory, to avoid leaking the controller.
  */
 - (void)shutdown;
 
@@ -206,6 +174,49 @@
     completionHandler:(MTRDeviceConnectionCallback)completionHandler
     MTR_NEWLY_DEPRECATED("Please use [MTRBaseDevice deviceWithNodeID:controller:]");
 
+- (BOOL)pairDevice:(uint64_t)deviceID
+     discriminator:(uint16_t)discriminator
+      setupPINCode:(uint32_t)setupPINCode
+             error:(NSError * __autoreleasing *)error
+    MTR_NEWLY_DEPRECATED("Please use setupCommissioningSessionWithPayload:newNodeID:error:");
+- (BOOL)pairDevice:(uint64_t)deviceID
+           address:(NSString *)address
+              port:(uint16_t)port
+      setupPINCode:(uint32_t)setupPINCode
+             error:(NSError * __autoreleasing *)error
+    MTR_NEWLY_DEPRECATED("Please use setupCommissioningSessionWithPayload:newNodeID:error:");
+- (BOOL)pairDevice:(uint64_t)deviceID
+    onboardingPayload:(NSString *)onboardingPayload
+                error:(NSError * __autoreleasing *)error
+    MTR_NEWLY_DEPRECATED("Please use setupCommissioningSessionWithPayload:newNodeID:error:");
+- (BOOL)commissionDevice:(uint64_t)deviceId
+     commissioningParams:(MTRCommissioningParameters *)commissioningParams
+                   error:(NSError * __autoreleasing *)error
+    MTR_NEWLY_DEPRECATED("Please use commissionNodeWithID:commissioningParams:error:");
+
+- (BOOL)stopDevicePairing:(uint64_t)deviceID
+                    error:(NSError * __autoreleasing *)error MTR_NEWLY_DEPRECATED("Please use cancelCommissioningForNodeID:error:");
+
+- (nullable MTRBaseDevice *)getDeviceBeingCommissioned:(uint64_t)deviceId
+                                                 error:(NSError * __autoreleasing *)error
+    MTR_NEWLY_DEPRECATED("Please use deviceBeingCommissionedWithNodeID:error:");
+
+- (BOOL)openPairingWindow:(uint64_t)deviceID
+                 duration:(NSUInteger)duration
+                    error:(NSError * __autoreleasing *)error
+    MTR_NEWLY_DEPRECATED("Please use MTRDevice or MTRBaseDevice openCommissioningWindowWithSetupPasscode");
+- (nullable NSString *)openPairingWindowWithPIN:(uint64_t)deviceID
+                                       duration:(NSUInteger)duration
+                                  discriminator:(NSUInteger)discriminator
+                                       setupPIN:(NSUInteger)setupPIN
+                                          error:(NSError * __autoreleasing *)error
+    MTR_NEWLY_DEPRECATED("Please use MTRDevice or MTRBaseDevice openCommissioningWindowWithSetupPasscode");
+
+- (nullable NSData *)computePaseVerifier:(uint32_t)setupPincode
+                              iterations:(uint32_t)iterations
+                                    salt:(NSData *)salt
+    MTR_NEWLY_DEPRECATED("Please use computePASEVerifierForSetupPasscode:iterations:salt:error:");
+
 @end
 
 NS_ASSUME_NONNULL_END
diff --git a/src/darwin/Framework/CHIP/MTRDeviceController.mm b/src/darwin/Framework/CHIP/MTRDeviceController.mm
index 07e77b8..08e98db 100644
--- a/src/darwin/Framework/CHIP/MTRDeviceController.mm
+++ b/src/darwin/Framework/CHIP/MTRDeviceController.mm
@@ -74,6 +74,8 @@
 static NSString * const kErrorCSRValidation = @"Extracting public key from CSR failed";
 static NSString * const kErrorGetCommissionee = @"Failure obtaining device being commissioned";
 static NSString * const kErrorGetAttestationChallenge = @"Failure getting attestation challenge";
+static NSString * const kErrorSpake2pVerifierGenerationFailed = @"PASE verifier generation failed";
+static NSString * const kErrorSpake2pVerifierSerializationFailed = @"PASE verifier serialization failed";
 
 @interface MTRDeviceController ()
 
@@ -379,91 +381,22 @@
             pairingCode = [payload manualEntryCode];
         }
         if (pairingCode == nil) {
-            success = ![self checkForError:CHIP_ERROR_INVALID_ARGUMENT logMsg:kErrorSetupCodeGen error:error];
+            success = ![MTRDeviceController checkForError:CHIP_ERROR_INVALID_ARGUMENT logMsg:kErrorSetupCodeGen error:error];
             return;
         }
 
         chip::NodeId nodeId = [newNodeID unsignedLongLongValue];
         _operationalCredentialsDelegate->SetDeviceID(nodeId);
         CHIP_ERROR errorCode = self.cppCommissioner->EstablishPASEConnection(nodeId, [pairingCode UTF8String]);
-        success = ![self checkForError:errorCode logMsg:kErrorPairDevice error:error];
+        success = ![MTRDeviceController checkForError:errorCode logMsg:kErrorPairDevice error:error];
     });
 
     return success;
 }
 
-- (BOOL)pairDevice:(uint64_t)deviceID
-     discriminator:(uint16_t)discriminator
-      setupPINCode:(uint32_t)setupPINCode
-             error:(NSError * __autoreleasing *)error
-{
-    VerifyOrReturnValue([self checkIsRunning:error], NO);
-
-    __block BOOL success = NO;
-    dispatch_sync(_chipWorkQueue, ^{
-        VerifyOrReturn([self checkIsRunning:error]);
-
-        std::string manualPairingCode;
-        chip::SetupPayload payload;
-        payload.discriminator.SetLongValue(discriminator);
-        payload.setUpPINCode = setupPINCode;
-
-        auto errorCode = chip::ManualSetupPayloadGenerator(payload).payloadDecimalStringRepresentation(manualPairingCode);
-        success = ![self checkForError:errorCode logMsg:kErrorSetupCodeGen error:error];
-        VerifyOrReturn(success);
-
-        _operationalCredentialsDelegate->SetDeviceID(deviceID);
-        errorCode = self.cppCommissioner->EstablishPASEConnection(deviceID, manualPairingCode.c_str());
-        success = ![self checkForError:errorCode logMsg:kErrorPairDevice error:error];
-    });
-
-    return success;
-}
-
-- (BOOL)pairDevice:(uint64_t)deviceID
-           address:(NSString *)address
-              port:(uint16_t)port
-      setupPINCode:(uint32_t)setupPINCode
-             error:(NSError * __autoreleasing *)error
-{
-    VerifyOrReturnValue([self checkIsRunning:error], NO);
-
-    __block BOOL success = NO;
-    dispatch_sync(_chipWorkQueue, ^{
-        VerifyOrReturn([self checkIsRunning:error]);
-
-        chip::Inet::IPAddress addr;
-        chip::Inet::IPAddress::FromString([address UTF8String], addr);
-        chip::Transport::PeerAddress peerAddress = chip::Transport::PeerAddress::UDP(addr, port);
-
-        _operationalCredentialsDelegate->SetDeviceID(deviceID);
-
-        auto params = chip::RendezvousParameters().SetSetupPINCode(setupPINCode).SetPeerAddress(peerAddress);
-        auto errorCode = self.cppCommissioner->EstablishPASEConnection(deviceID, params);
-        success = ![self checkForError:errorCode logMsg:kErrorPairDevice error:error];
-    });
-
-    return success;
-}
-
-- (BOOL)pairDevice:(uint64_t)deviceID onboardingPayload:(NSString *)onboardingPayload error:(NSError * __autoreleasing *)error
-{
-    VerifyOrReturnValue([self checkIsRunning:error], NO);
-
-    __block BOOL success = NO;
-    dispatch_sync(_chipWorkQueue, ^{
-        VerifyOrReturn([self checkIsRunning:error]);
-
-        _operationalCredentialsDelegate->SetDeviceID(deviceID);
-        auto errorCode = self.cppCommissioner->EstablishPASEConnection(deviceID, [onboardingPayload UTF8String]);
-        success = ![self checkForError:errorCode logMsg:kErrorPairDevice error:error];
-    });
-    return success;
-}
-
-- (BOOL)commissionDevice:(uint64_t)deviceID
-     commissioningParams:(MTRCommissioningParameters *)commissioningParams
-                   error:(NSError * __autoreleasing *)error
+- (BOOL)commissionNodeWithID:(NSNumber *)nodeID
+         commissioningParams:(MTRCommissioningParameters *)commissioningParams
+                       error:(NSError * __autoreleasing *)error
 {
     VerifyOrReturnValue([self checkIsRunning:error], NO);
 
@@ -508,9 +441,10 @@
             params.SetDeviceAttestationDelegate(_deviceAttestationDelegateBridge);
         }
 
-        _operationalCredentialsDelegate->SetDeviceID(deviceID);
-        auto errorCode = self.cppCommissioner->Commission(deviceID, params);
-        success = ![self checkForError:errorCode logMsg:kErrorPairDevice error:error];
+        chip::NodeId deviceId = [nodeID unsignedLongLongValue];
+        _operationalCredentialsDelegate->SetDeviceID(deviceId);
+        auto errorCode = self.cppCommissioner->Commission(deviceId, params);
+        success = ![MTRDeviceController checkForError:errorCode logMsg:kErrorPairDevice error:error];
     });
     return success;
 }
@@ -532,12 +466,12 @@
         auto deviceProxy = static_cast<chip::DeviceProxy *>(device);
         auto errorCode = self.cppCommissioner->ContinueCommissioningAfterDeviceAttestation(deviceProxy,
             ignoreAttestationFailure ? chip::Credentials::AttestationVerificationResult::kSuccess : lastAttestationResult);
-        success = ![self checkForError:errorCode logMsg:kErrorPairDevice error:error];
+        success = ![MTRDeviceController checkForError:errorCode logMsg:kErrorPairDevice error:error];
     });
     return success;
 }
 
-- (BOOL)stopDevicePairing:(uint64_t)deviceID error:(NSError * __autoreleasing *)error
+- (BOOL)cancelCommissioningForNodeID:(NSNumber *)nodeID error:(NSError * __autoreleasing *)error
 {
     VerifyOrReturnValue([self checkIsRunning:error], NO);
 
@@ -546,14 +480,13 @@
         VerifyOrReturn([self checkIsRunning:error]);
 
         _operationalCredentialsDelegate->ResetDeviceID();
-        auto errorCode = self.cppCommissioner->StopPairing(deviceID);
-        success = ![self checkForError:errorCode logMsg:kErrorStopPairing error:error];
+        auto errorCode = self.cppCommissioner->StopPairing([nodeID unsignedLongLongValue]);
+        success = ![MTRDeviceController checkForError:errorCode logMsg:kErrorStopPairing error:error];
     });
-
     return success;
 }
 
-- (MTRBaseDevice *)getDeviceBeingCommissioned:(uint64_t)deviceId error:(NSError * __autoreleasing *)error
+- (MTRBaseDevice *)deviceBeingCommissionedWithNodeID:(NSNumber *)nodeID error:(NSError * __autoreleasing *)error
 {
     VerifyOrReturnValue([self checkIsRunning:error], nil);
 
@@ -563,8 +496,8 @@
         VerifyOrReturn([self checkIsRunning:error]);
 
         chip::CommissioneeDeviceProxy * deviceProxy;
-        auto errorCode = self->_cppCommissioner->GetDeviceBeingCommissioned(deviceId, &deviceProxy);
-        success = ![self checkForError:errorCode logMsg:kErrorStopPairing error:error];
+        auto errorCode = self->_cppCommissioner->GetDeviceBeingCommissioned(nodeID.unsignedLongLongValue, &deviceProxy);
+        success = ![MTRDeviceController checkForError:errorCode logMsg:kErrorStopPairing error:error];
         device = [[MTRBaseDevice alloc] initWithPASEDevice:deviceProxy controller:self];
     });
     VerifyOrReturnValue(success, nil);
@@ -572,10 +505,9 @@
     return device;
 }
 
-- (BOOL)getBaseDevice:(uint64_t)deviceID queue:(dispatch_queue_t)queue completion:(MTRDeviceConnectionCallback)completion
+- (MTRBaseDevice *)baseDeviceForNodeID:(NSNumber *)nodeID
 {
-    // Will get removed once deviceWithNodeID exists.
-    return [self getBaseDevice:deviceID queue:queue completionHandler:completion];
+    return [[MTRBaseDevice alloc] initWithNodeID:nodeID controller:self];
 }
 
 - (MTRDevice *)deviceForNodeID:(NSNumber *)nodeID
@@ -603,90 +535,6 @@
     os_unfair_lock_unlock(&_deviceMapLock);
 }
 
-- (BOOL)openPairingWindow:(uint64_t)deviceID duration:(NSUInteger)duration error:(NSError * __autoreleasing *)error
-{
-    VerifyOrReturnValue([self checkIsRunning:error], NO);
-
-    if (duration > UINT16_MAX) {
-        MTR_LOG_ERROR("Error: Duration %tu is too large. Max value %d", duration, UINT16_MAX);
-        if (error) {
-            *error = [MTRError errorForCHIPErrorCode:CHIP_ERROR_INVALID_INTEGER_VALUE];
-        }
-        return NO;
-    }
-
-    __block BOOL success = NO;
-    dispatch_sync(_chipWorkQueue, ^{
-        VerifyOrReturn([self checkIsRunning:error]);
-
-        auto errorCode = chip::Controller::AutoCommissioningWindowOpener::OpenBasicCommissioningWindow(
-            self.cppCommissioner, deviceID, chip::System::Clock::Seconds16(static_cast<uint16_t>(duration)));
-        success = ![self checkForError:errorCode logMsg:kErrorOpenPairingWindow error:error];
-    });
-
-    return success;
-}
-
-- (NSString *)openPairingWindowWithPIN:(uint64_t)deviceID
-                              duration:(NSUInteger)duration
-                         discriminator:(NSUInteger)discriminator
-                              setupPIN:(NSUInteger)setupPIN
-                                 error:(NSError * __autoreleasing *)error
-{
-    __block NSString * rv = nil;
-
-    VerifyOrReturnValue([self checkIsRunning:error], rv);
-
-    if (duration > UINT16_MAX) {
-        MTR_LOG_ERROR("Error: Duration %tu is too large. Max value %d", duration, UINT16_MAX);
-        if (error) {
-            *error = [MTRError errorForCHIPErrorCode:CHIP_ERROR_INVALID_INTEGER_VALUE];
-        }
-        return rv;
-    }
-
-    if (discriminator > 0xfff) {
-        MTR_LOG_ERROR("Error: Discriminator %tu is too large. Max value %d", discriminator, 0xfff);
-        if (error) {
-            *error = [MTRError errorForCHIPErrorCode:CHIP_ERROR_INVALID_INTEGER_VALUE];
-        }
-        return rv;
-    }
-
-    if (!chip::CanCastTo<uint32_t>(setupPIN) || !chip::SetupPayload::IsValidSetupPIN(static_cast<uint32_t>(setupPIN))) {
-        MTR_LOG_ERROR("Error: Setup pin %lu is not valid", static_cast<unsigned long>(setupPIN));
-        if (error) {
-            *error = [MTRError errorForCHIPErrorCode:CHIP_ERROR_INVALID_INTEGER_VALUE];
-        }
-        return rv;
-    }
-
-    dispatch_sync(_chipWorkQueue, ^{
-        VerifyOrReturn([self checkIsRunning:error]);
-
-        chip::SetupPayload setupPayload;
-        auto errorCode = chip::Controller::AutoCommissioningWindowOpener::OpenCommissioningWindow(self.cppCommissioner, deviceID,
-            chip::System::Clock::Seconds16(static_cast<uint16_t>(duration)), chip::Crypto::kSpake2p_Min_PBKDF_Iterations,
-            static_cast<uint16_t>(discriminator), chip::MakeOptional(static_cast<uint32_t>(setupPIN)), chip::NullOptional,
-            setupPayload);
-
-        auto success = ![self checkForError:errorCode logMsg:kErrorOpenPairingWindow error:error];
-        VerifyOrReturn(success);
-
-        chip::ManualSetupPayloadGenerator generator(setupPayload);
-        std::string outCode;
-
-        if (generator.payloadDecimalStringRepresentation(outCode) == CHIP_NO_ERROR) {
-            MTR_LOG_ERROR("Setup code is %s", outCode.c_str());
-            rv = [NSString stringWithCString:outCode.c_str() encoding:[NSString defaultCStringEncoding]];
-        } else {
-            MTR_LOG_ERROR("Failed to get decimal setup code");
-        }
-    });
-
-    return rv;
-}
-
 - (void)setPairingDelegate:(id<MTRDevicePairingDelegate>)delegate queue:(dispatch_queue_t)queue
 {
     VerifyOrReturn([self checkIsRunning]);
@@ -714,20 +562,23 @@
     });
 }
 
-- (nullable NSData *)computePaseVerifier:(uint32_t)setupPincode iterations:(uint32_t)iterations salt:(NSData *)salt
++ (nullable NSData *)computePASEVerifierForSetupPasscode:(NSNumber *)setupPasscode
+                                              iterations:(NSNumber *)iterations
+                                                    salt:(NSData *)salt
+                                                   error:(NSError * __autoreleasing *)error
 {
+    // Spake2pVerifier::Generate takes the passcode by non-const reference for some reason.
+    uint32_t unboxedSetupPasscode = [setupPasscode unsignedIntValue];
     chip::Spake2pVerifier verifier;
-    CHIP_ERROR err = verifier.Generate(iterations, AsByteSpan(salt), setupPincode);
-    if (err != CHIP_NO_ERROR) {
-        MTR_LOG_ERROR("computePaseVerifier generation failed: %s", chip::ErrorStr(err));
+    CHIP_ERROR err = verifier.Generate([iterations unsignedIntValue], AsByteSpan(salt), unboxedSetupPasscode);
+    if ([MTRDeviceController checkForError:err logMsg:kErrorSpake2pVerifierGenerationFailed error:error]) {
         return nil;
     }
 
     uint8_t serializedBuffer[chip::Crypto::kSpake2p_VerifierSerialized_Length];
     chip::MutableByteSpan serializedBytes(serializedBuffer);
     err = verifier.Serialize(serializedBytes);
-    if (err != CHIP_NO_ERROR) {
-        MTR_LOG_ERROR("computePaseVerifier serialization failed: %s", chip::ErrorStr(err));
+    if ([MTRDeviceController checkForError:err logMsg:kErrorSpake2pVerifierSerializationFailed error:error]) {
         return nil;
     }
 
@@ -744,14 +595,14 @@
 
         chip::CommissioneeDeviceProxy * deviceProxy;
         auto errorCode = self.cppCommissioner->GetDeviceBeingCommissioned([deviceID unsignedLongLongValue], &deviceProxy);
-        auto success = ![self checkForError:errorCode logMsg:kErrorGetCommissionee error:nil];
+        auto success = ![MTRDeviceController checkForError:errorCode logMsg:kErrorGetCommissionee error:nil];
         VerifyOrReturn(success);
 
         uint8_t challengeBuffer[chip::Crypto::kAES_CCM128_Key_Length];
         chip::ByteSpan challenge(challengeBuffer);
 
         errorCode = deviceProxy->GetAttestationChallenge(challenge);
-        success = ![self checkForError:errorCode logMsg:kErrorGetAttestationChallenge error:nil];
+        success = ![MTRDeviceController checkForError:errorCode logMsg:kErrorGetAttestationChallenge error:nil];
         VerifyOrReturn(success);
 
         attestationChallenge = AsData(challenge);
@@ -792,7 +643,7 @@
     return YES;
 }
 
-- (BOOL)checkForError:(CHIP_ERROR)errorCode logMsg:(NSString *)logMsg error:(NSError * __autoreleasing *)error
++ (BOOL)checkForError:(CHIP_ERROR)errorCode logMsg:(NSString *)logMsg error:(NSError * __autoreleasing *)error
 {
     if (CHIP_NO_ERROR == errorCode) {
         return NO;
@@ -1016,4 +867,179 @@
                         }];
 }
 
+- (BOOL)pairDevice:(uint64_t)deviceID
+     discriminator:(uint16_t)discriminator
+      setupPINCode:(uint32_t)setupPINCode
+             error:(NSError * __autoreleasing *)error
+{
+    VerifyOrReturnValue([self checkIsRunning:error], NO);
+
+    __block BOOL success = NO;
+    dispatch_sync(_chipWorkQueue, ^{
+        VerifyOrReturn([self checkIsRunning:error]);
+
+        std::string manualPairingCode;
+        chip::SetupPayload payload;
+        payload.discriminator.SetLongValue(discriminator);
+        payload.setUpPINCode = setupPINCode;
+
+        auto errorCode = chip::ManualSetupPayloadGenerator(payload).payloadDecimalStringRepresentation(manualPairingCode);
+        success = ![MTRDeviceController checkForError:errorCode logMsg:kErrorSetupCodeGen error:error];
+        VerifyOrReturn(success);
+
+        _operationalCredentialsDelegate->SetDeviceID(deviceID);
+        errorCode = self.cppCommissioner->EstablishPASEConnection(deviceID, manualPairingCode.c_str());
+        success = ![MTRDeviceController checkForError:errorCode logMsg:kErrorPairDevice error:error];
+    });
+
+    return success;
+}
+
+- (BOOL)pairDevice:(uint64_t)deviceID
+           address:(NSString *)address
+              port:(uint16_t)port
+      setupPINCode:(uint32_t)setupPINCode
+             error:(NSError * __autoreleasing *)error
+{
+    VerifyOrReturnValue([self checkIsRunning:error], NO);
+
+    __block BOOL success = NO;
+    dispatch_sync(_chipWorkQueue, ^{
+        VerifyOrReturn([self checkIsRunning:error]);
+
+        chip::Inet::IPAddress addr;
+        chip::Inet::IPAddress::FromString([address UTF8String], addr);
+        chip::Transport::PeerAddress peerAddress = chip::Transport::PeerAddress::UDP(addr, port);
+
+        _operationalCredentialsDelegate->SetDeviceID(deviceID);
+
+        auto params = chip::RendezvousParameters().SetSetupPINCode(setupPINCode).SetPeerAddress(peerAddress);
+        auto errorCode = self.cppCommissioner->EstablishPASEConnection(deviceID, params);
+        success = ![MTRDeviceController checkForError:errorCode logMsg:kErrorPairDevice error:error];
+    });
+
+    return success;
+}
+
+- (BOOL)pairDevice:(uint64_t)deviceID onboardingPayload:(NSString *)onboardingPayload error:(NSError * __autoreleasing *)error
+{
+    VerifyOrReturnValue([self checkIsRunning:error], NO);
+
+    __block BOOL success = NO;
+    dispatch_sync(_chipWorkQueue, ^{
+        VerifyOrReturn([self checkIsRunning:error]);
+
+        _operationalCredentialsDelegate->SetDeviceID(deviceID);
+        auto errorCode = self.cppCommissioner->EstablishPASEConnection(deviceID, [onboardingPayload UTF8String]);
+        success = ![MTRDeviceController checkForError:errorCode logMsg:kErrorPairDevice error:error];
+    });
+    return success;
+}
+
+- (BOOL)commissionDevice:(uint64_t)deviceID
+     commissioningParams:(MTRCommissioningParameters *)commissioningParams
+                   error:(NSError * __autoreleasing *)error
+{
+    return [self commissionNodeWithID:@(deviceID) commissioningParams:commissioningParams error:error];
+}
+
+- (BOOL)stopDevicePairing:(uint64_t)deviceID error:(NSError * __autoreleasing *)error
+{
+    return [self cancelCommissioningForNodeID:@(deviceID) error:error];
+}
+
+- (MTRBaseDevice *)getDeviceBeingCommissioned:(uint64_t)deviceId error:(NSError * __autoreleasing *)error
+{
+    return [self deviceBeingCommissionedWithNodeID:@(deviceId) error:error];
+}
+
+- (BOOL)openPairingWindow:(uint64_t)deviceID duration:(NSUInteger)duration error:(NSError * __autoreleasing *)error
+{
+    VerifyOrReturnValue([self checkIsRunning:error], NO);
+
+    if (duration > UINT16_MAX) {
+        MTR_LOG_ERROR("Error: Duration %tu is too large. Max value %d", duration, UINT16_MAX);
+        if (error) {
+            *error = [MTRError errorForCHIPErrorCode:CHIP_ERROR_INVALID_INTEGER_VALUE];
+        }
+        return NO;
+    }
+
+    __block BOOL success = NO;
+    dispatch_sync(_chipWorkQueue, ^{
+        VerifyOrReturn([self checkIsRunning:error]);
+
+        auto errorCode = chip::Controller::AutoCommissioningWindowOpener::OpenBasicCommissioningWindow(
+            self.cppCommissioner, deviceID, chip::System::Clock::Seconds16(static_cast<uint16_t>(duration)));
+        success = ![MTRDeviceController checkForError:errorCode logMsg:kErrorOpenPairingWindow error:error];
+    });
+
+    return success;
+}
+
+- (NSString *)openPairingWindowWithPIN:(uint64_t)deviceID
+                              duration:(NSUInteger)duration
+                         discriminator:(NSUInteger)discriminator
+                              setupPIN:(NSUInteger)setupPIN
+                                 error:(NSError * __autoreleasing *)error
+{
+    __block NSString * rv = nil;
+
+    VerifyOrReturnValue([self checkIsRunning:error], rv);
+
+    if (duration > UINT16_MAX) {
+        MTR_LOG_ERROR("Error: Duration %tu is too large. Max value %d", duration, UINT16_MAX);
+        if (error) {
+            *error = [MTRError errorForCHIPErrorCode:CHIP_ERROR_INVALID_INTEGER_VALUE];
+        }
+        return rv;
+    }
+
+    if (discriminator > 0xfff) {
+        MTR_LOG_ERROR("Error: Discriminator %tu is too large. Max value %d", discriminator, 0xfff);
+        if (error) {
+            *error = [MTRError errorForCHIPErrorCode:CHIP_ERROR_INVALID_INTEGER_VALUE];
+        }
+        return rv;
+    }
+
+    if (!chip::CanCastTo<uint32_t>(setupPIN) || !chip::SetupPayload::IsValidSetupPIN(static_cast<uint32_t>(setupPIN))) {
+        MTR_LOG_ERROR("Error: Setup pin %lu is not valid", static_cast<unsigned long>(setupPIN));
+        if (error) {
+            *error = [MTRError errorForCHIPErrorCode:CHIP_ERROR_INVALID_INTEGER_VALUE];
+        }
+        return rv;
+    }
+
+    dispatch_sync(_chipWorkQueue, ^{
+        VerifyOrReturn([self checkIsRunning:error]);
+
+        chip::SetupPayload setupPayload;
+        auto errorCode = chip::Controller::AutoCommissioningWindowOpener::OpenCommissioningWindow(self.cppCommissioner, deviceID,
+            chip::System::Clock::Seconds16(static_cast<uint16_t>(duration)), chip::Crypto::kSpake2p_Min_PBKDF_Iterations,
+            static_cast<uint16_t>(discriminator), chip::MakeOptional(static_cast<uint32_t>(setupPIN)), chip::NullOptional,
+            setupPayload);
+
+        auto success = ![MTRDeviceController checkForError:errorCode logMsg:kErrorOpenPairingWindow error:error];
+        VerifyOrReturn(success);
+
+        chip::ManualSetupPayloadGenerator generator(setupPayload);
+        std::string outCode;
+
+        if (generator.payloadDecimalStringRepresentation(outCode) == CHIP_NO_ERROR) {
+            MTR_LOG_ERROR("Setup code is %s", outCode.c_str());
+            rv = [NSString stringWithCString:outCode.c_str() encoding:[NSString defaultCStringEncoding]];
+        } else {
+            MTR_LOG_ERROR("Failed to get decimal setup code");
+        }
+    });
+
+    return rv;
+}
+
+- (nullable NSData *)computePaseVerifier:(uint32_t)setupPincode iterations:(uint32_t)iterations salt:(NSData *)salt
+{
+    return [MTRDeviceController computePASEVerifierForSetupPasscode:@(setupPincode) iterations:@(iterations) salt:salt error:nil];
+}
+
 @end
diff --git a/src/darwin/Framework/CHIP/MTRDeviceControllerOverXPC.m b/src/darwin/Framework/CHIP/MTRDeviceControllerOverXPC.m
index fcca97a..f8f2e18 100644
--- a/src/darwin/Framework/CHIP/MTRDeviceControllerOverXPC.m
+++ b/src/darwin/Framework/CHIP/MTRDeviceControllerOverXPC.m
@@ -63,6 +63,9 @@
              error:(NSError * __autoreleasing *)error
 {
     MTR_LOG_ERROR("MTRDevice doesn't support pairDevice over XPC");
+    if (error != nil) {
+        *error = [NSError errorWithDomain:MTRErrorDomain code:MTRErrorCodeInvalidState userInfo:nil];
+    }
     return NO;
 }
 
@@ -74,12 +77,18 @@
              error:(NSError * __autoreleasing *)error
 {
     MTR_LOG_ERROR("MTRDevice doesn't support pairDevice over XPC");
+    if (error != nil) {
+        *error = [NSError errorWithDomain:MTRErrorDomain code:MTRErrorCodeInvalidState userInfo:nil];
+    }
     return NO;
 }
 
 - (BOOL)pairDevice:(uint64_t)deviceID onboardingPayload:(NSString *)onboardingPayload error:(NSError * __autoreleasing *)error
 {
     MTR_LOG_ERROR("MTRDevice doesn't support pairDevice over XPC");
+    if (error != nil) {
+        *error = [NSError errorWithDomain:MTRErrorDomain code:MTRErrorCodeInvalidState userInfo:nil];
+    }
     return NO;
 }
 
@@ -87,24 +96,57 @@
      commissioningParams:(MTRCommissioningParameters *)commissioningParams
                    error:(NSError * __autoreleasing *)error
 {
-    MTR_LOG_ERROR("MTRDevice doesn't support pairDevice over XPC");
+    MTR_LOG_ERROR("MTRDevice doesn't support commissionDevice over XPC");
+    if (error != nil) {
+        *error = [NSError errorWithDomain:MTRErrorDomain code:MTRErrorCodeInvalidState userInfo:nil];
+    }
     return NO;
 }
 
-- (void)setListenPort:(uint16_t)port
-{
-    MTR_LOG_ERROR("MTRDevice doesn't support setListenPort over XPC");
-}
-
 - (BOOL)stopDevicePairing:(uint64_t)deviceID error:(NSError * __autoreleasing *)error
 {
     MTR_LOG_ERROR("MTRDevice doesn't support stopDevicePairing over XPC");
+    if (error != nil) {
+        *error = [NSError errorWithDomain:MTRErrorDomain code:MTRErrorCodeInvalidState userInfo:nil];
+    }
     return NO;
 }
 
 - (nullable MTRBaseDevice *)getDeviceBeingCommissioned:(uint64_t)deviceID error:(NSError * __autoreleasing *)error
 {
     MTR_LOG_ERROR("MTRDevice doesn't support getDeviceBeingCommissioned over XPC");
+    if (error != nil) {
+        *error = [NSError errorWithDomain:MTRErrorDomain code:MTRErrorCodeInvalidState userInfo:nil];
+    }
+    return nil;
+}
+
+- (BOOL)commissionNodeWithID:(NSNumber *)nodeID
+         commissioningParams:(MTRCommissioningParameters *)commissioningParams
+                       error:(NSError * __autoreleasing *)error;
+{
+    MTR_LOG_ERROR("MTRDeviceController doesn't support commissionNodeWithID over XPC");
+    if (error != nil) {
+        *error = [NSError errorWithDomain:MTRErrorDomain code:MTRErrorCodeInvalidState userInfo:nil];
+    }
+    return NO;
+}
+
+- (BOOL)cancelCommissioningForNodeID:(NSNumber *)nodeID error:(NSError * __autoreleasing *)error
+{
+    MTR_LOG_ERROR("MTRDeviceController doesn't support cancelCommissioningForNodeID over XPC");
+    if (error != nil) {
+        *error = [NSError errorWithDomain:MTRErrorDomain code:MTRErrorCodeInvalidState userInfo:nil];
+    }
+    return NO;
+}
+
+- (nullable MTRBaseDevice *)deviceBeingCommissionedWithNodeID:(NSNumber *)nodeID error:(NSError * __autoreleasing *)error
+{
+    MTR_LOG_ERROR("MTRDeviceController doesn't support deviceBeingCommissionedWithNodeID over XPC");
+    if (error != nil) {
+        *error = [NSError errorWithDomain:MTRErrorDomain code:MTRErrorCodeInvalidState userInfo:nil];
+    }
     return nil;
 }
 
@@ -112,12 +154,30 @@
                 queue:(dispatch_queue_t)queue
     completionHandler:(MTRDeviceConnectionCallback)completionHandler
 {
+    // Consumers expect their getAnyDeviceControllerWithCompletion to be called
+    // under here if we don't have a controller id aleady, so make sure we do
+    // that.
+    [self fetchControllerIdWithQueue:queue
+                          completion:^(id _Nullable controllerID, NSError * _Nullable error) {
+                              if (error != nil) {
+                                  completionHandler(nil, error);
+                                  return;
+                              }
+
+                              __auto_type * device = [self baseDeviceForNodeID:@(deviceID)];
+                              completionHandler(device, nil);
+                          }];
+    return YES;
+}
+
+- (void)fetchControllerIdWithQueue:(dispatch_queue_t)queue completion:(MTRFetchControllerIDCompletion)completion
+{
     dispatch_async(_workQueue, ^{
         dispatch_group_t group = dispatch_group_create();
         if (!self.controllerID) {
             dispatch_group_enter(group);
             [self.xpcConnection getProxyHandleWithCompletion:^(
-                dispatch_queue_t _Nonnull queue, MTRDeviceControllerXPCProxyHandle * _Nullable handle) {
+                dispatch_queue_t _Nonnull proxyQueue, MTRDeviceControllerXPCProxyHandle * _Nullable handle) {
                 if (handle) {
                     [handle.proxy getAnyDeviceControllerWithCompletion:^(id _Nullable controller, NSError * _Nullable error) {
                         if (error) {
@@ -137,16 +197,17 @@
         }
         dispatch_group_notify(group, queue, ^{
             if (self.controllerID) {
-                MTRDeviceOverXPC * device = [[MTRDeviceOverXPC alloc] initWithController:self.controllerID
-                                                                                deviceID:@(deviceID)
-                                                                           xpcConnection:self.xpcConnection];
-                completionHandler(device, nil);
+                completion(self.controllerID, nil);
             } else {
-                completionHandler(nil, [NSError errorWithDomain:MTRErrorDomain code:MTRErrorCodeGeneralError userInfo:nil]);
+                completion(nil, [NSError errorWithDomain:MTRErrorDomain code:MTRErrorCodeGeneralError userInfo:nil]);
             }
         });
     });
-    return YES;
+}
+
+- (MTRBaseDevice *)baseDeviceForNodeID:(NSNumber *)nodeID
+{
+    return [[MTRDeviceOverXPC alloc] initWithControllerOverXPC:self deviceID:nodeID xpcConnection:self.xpcConnection];
 }
 
 - (BOOL)openPairingWindow:(uint64_t)deviceID duration:(NSUInteger)duration error:(NSError * __autoreleasing *)error
diff --git a/src/darwin/Framework/CHIP/MTRDeviceControllerOverXPC_Internal.h b/src/darwin/Framework/CHIP/MTRDeviceControllerOverXPC_Internal.h
index 43d236e..d772dab 100644
--- a/src/darwin/Framework/CHIP/MTRDeviceControllerOverXPC_Internal.h
+++ b/src/darwin/Framework/CHIP/MTRDeviceControllerOverXPC_Internal.h
@@ -20,12 +20,17 @@
 
 NS_ASSUME_NONNULL_BEGIN
 
+typedef void (^MTRFetchControllerIDCompletion)(id _Nullable controllerID, NSError * _Nullable error);
+
 @interface MTRDeviceControllerOverXPC ()
 
 @property (nonatomic, readwrite, strong) id<NSCopying> _Nullable controllerID;
 @property (nonatomic, readonly, strong) dispatch_queue_t workQueue;
 @property (nonatomic, readonly, strong) MTRDeviceControllerXPCConnection * xpcConnection;
 
+// Guarantees that completion is called with either a non-nil controllerID or a
+// non-nil error.
+- (void)fetchControllerIdWithQueue:(dispatch_queue_t)queue completion:(MTRFetchControllerIDCompletion)completion;
 @end
 
 NS_ASSUME_NONNULL_END
diff --git a/src/darwin/Framework/CHIP/MTRDeviceController_Internal.h b/src/darwin/Framework/CHIP/MTRDeviceController_Internal.h
index d787aa0..bed9b70 100644
--- a/src/darwin/Framework/CHIP/MTRDeviceController_Internal.h
+++ b/src/darwin/Framework/CHIP/MTRDeviceController_Internal.h
@@ -154,6 +154,13 @@
 - (void)asyncDispatchToMatterQueue:(void (^)(chip::Controller::DeviceCommissioner *))block
                       errorHandler:(void (^)(NSError *))errorHandler;
 
+/**
+ * Get an MTRBaseDevice for the given node id.  This exists to allow subclasses
+ * of MTRDeviceController (e.g. MTRDeviceControllerOverXPC) to override what
+ * sort of MTRBaseDevice they return.
+ */
+- (MTRBaseDevice *)baseDeviceForNodeID:(NSNumber *)nodeID;
+
 #pragma mark - Device-specific data and SDK access
 // DeviceController will act as a central repository for this opaque dictionary that MTRDevice manages
 - (MTRDevice *)deviceForNodeID:(NSNumber *)nodeID;
diff --git a/src/darwin/Framework/CHIP/MTRDeviceOverXPC.h b/src/darwin/Framework/CHIP/MTRDeviceOverXPC.h
index 2f79150..9718c3e 100644
--- a/src/darwin/Framework/CHIP/MTRDeviceOverXPC.h
+++ b/src/darwin/Framework/CHIP/MTRDeviceOverXPC.h
@@ -19,6 +19,8 @@
 #import "MTRCluster.h" // For MTRSubscriptionEstablishedHandler
 #import "MTRDeviceControllerXPCConnection.h"
 
+@class MTRDeviceControllerOverXPC;
+
 NS_ASSUME_NONNULL_BEGIN
 
 @interface MTRDeviceOverXPC : MTRBaseDevice
@@ -26,9 +28,9 @@
 - (instancetype)init NS_UNAVAILABLE;
 + (instancetype)new NS_UNAVAILABLE;
 
-- (instancetype)initWithController:(id<NSCopying>)controller
-                          deviceID:(NSNumber *)deviceID
-                     xpcConnection:(MTRDeviceControllerXPCConnection *)xpcConnection;
+- (instancetype)initWithControllerOverXPC:(MTRDeviceControllerOverXPC *)controllerOverXPC
+                                 deviceID:(NSNumber *)deviceID
+                            xpcConnection:(MTRDeviceControllerXPCConnection *)xpcConnection;
 
 @end
 
diff --git a/src/darwin/Framework/CHIP/MTRDeviceOverXPC.m b/src/darwin/Framework/CHIP/MTRDeviceOverXPC.m
index 9a3663f..921eef6 100644
--- a/src/darwin/Framework/CHIP/MTRDeviceOverXPC.m
+++ b/src/darwin/Framework/CHIP/MTRDeviceOverXPC.m
@@ -20,6 +20,7 @@
 #import "MTRAttributeCacheContainer+XPC.h"
 #import "MTRCluster.h"
 #import "MTRDeviceController+XPC.h"
+#import "MTRDeviceControllerOverXPC_Internal.h"
 #import "MTRDeviceControllerXPCConnection.h"
 #import "MTRError.h"
 #import "MTRLogging.h"
@@ -28,7 +29,8 @@
 
 @interface MTRDeviceOverXPC ()
 
-@property (nonatomic, strong, readonly) id<NSCopying> controller;
+@property (nonatomic, strong, readonly, nullable) id<NSCopying> controllerID;
+@property (nonatomic, strong, readonly) MTRDeviceControllerOverXPC * controller;
 @property (nonatomic, readonly) NSNumber * nodeID;
 @property (nonatomic, strong, readonly) MTRDeviceControllerXPCConnection * xpcConnection;
 
@@ -36,11 +38,12 @@
 
 @implementation MTRDeviceOverXPC
 
-- (instancetype)initWithController:(id<NSCopying>)controller
-                          deviceID:(NSNumber *)deviceID
-                     xpcConnection:(MTRDeviceControllerXPCConnection *)xpcConnection
+- (instancetype)initWithControllerOverXPC:(MTRDeviceControllerOverXPC *)controllerOverXPC
+                                 deviceID:(NSNumber *)deviceID
+                            xpcConnection:(MTRDeviceControllerXPCConnection *)xpcConnection
 {
-    _controller = controller;
+    _controllerID = controllerOverXPC.controllerID;
+    _controller = controllerOverXPC;
     _nodeID = deviceID;
     _xpcConnection = xpcConnection;
     return self;
@@ -57,13 +60,14 @@
 {
     MTR_LOG_DEBUG("Subscribing all attributes... Note that attributeReportHandler, eventReportHandler, and resubscriptionScheduled "
                   "are not supported.");
-    if (attributeCacheContainer) {
-        [attributeCacheContainer setXPCConnection:_xpcConnection controllerID:self.controller deviceID:self.nodeID];
-    }
-    [_xpcConnection
-        getProxyHandleWithCompletion:^(dispatch_queue_t _Nonnull proxyQueue, MTRDeviceControllerXPCProxyHandle * _Nullable handle) {
+    __auto_type workBlock = ^{
+        if (attributeCacheContainer) {
+            [attributeCacheContainer setXPCConnection:self->_xpcConnection controllerID:self.controllerID deviceID:self.nodeID];
+        }
+        [self->_xpcConnection getProxyHandleWithCompletion:^(
+            dispatch_queue_t _Nonnull proxyQueue, MTRDeviceControllerXPCProxyHandle * _Nullable handle) {
             if (handle) {
-                [handle.proxy subscribeWithController:self.controller
+                [handle.proxy subscribeWithController:self.controllerID
                                                nodeId:self.nodeID.unsignedLongLongValue
                                           minInterval:params.minInterval
                                           maxInterval:params.maxInterval
@@ -87,6 +91,23 @@
                 });
             }
         }];
+    };
+
+    if (self.controllerID != nil) {
+        workBlock();
+    } else {
+        [self.controller fetchControllerIdWithQueue:queue
+                                         completion:^(id _Nullable controllerID, NSError * _Nullable error) {
+                                             if (error != nil) {
+                                                 // We're already running on the right queue.
+                                                 errorHandler(error);
+                                                 return;
+                                             }
+
+                                             self->_controllerID = controllerID;
+                                             workBlock();
+                                         }];
+    }
 }
 
 - (void)readAttributesWithEndpointID:(NSNumber * _Nullable)endpointID
@@ -97,10 +118,11 @@
                           completion:(MTRDeviceResponseHandler)completion
 {
     MTR_LOG_DEBUG("Reading attribute ...");
-    [_xpcConnection
-        getProxyHandleWithCompletion:^(dispatch_queue_t _Nonnull proxyQueue, MTRDeviceControllerXPCProxyHandle * _Nullable handle) {
+    __auto_type workBlock = ^{
+        [self->_xpcConnection getProxyHandleWithCompletion:^(
+            dispatch_queue_t _Nonnull proxyQueue, MTRDeviceControllerXPCProxyHandle * _Nullable handle) {
             if (handle) {
-                [handle.proxy readAttributeWithController:self.controller
+                [handle.proxy readAttributeWithController:self.controllerID
                                                    nodeId:self.nodeID.unsignedLongLongValue
                                                endpointId:endpointID
                                                 clusterId:clusterID
@@ -123,6 +145,23 @@
                 });
             }
         }];
+    };
+
+    if (self.controllerID != nil) {
+        workBlock();
+    } else {
+        [self.controller fetchControllerIdWithQueue:queue
+                                         completion:^(id _Nullable controllerID, NSError * _Nullable error) {
+                                             if (error != nil) {
+                                                 // We're already running on the right queue.
+                                                 completion(nil, error);
+                                                 return;
+                                             }
+
+                                             self->_controllerID = controllerID;
+                                             workBlock();
+                                         }];
+    }
 }
 
 - (void)writeAttributeWithEndpointID:(NSNumber *)endpointID
@@ -134,10 +173,11 @@
                           completion:(MTRDeviceResponseHandler)completion
 {
     MTR_LOG_DEBUG("Writing attribute ...");
-    [_xpcConnection
-        getProxyHandleWithCompletion:^(dispatch_queue_t _Nonnull proxyQueue, MTRDeviceControllerXPCProxyHandle * _Nullable handle) {
+    __auto_type workBlock = ^{
+        [self->_xpcConnection getProxyHandleWithCompletion:^(
+            dispatch_queue_t _Nonnull proxyQueue, MTRDeviceControllerXPCProxyHandle * _Nullable handle) {
             if (handle) {
-                [handle.proxy writeAttributeWithController:self.controller
+                [handle.proxy writeAttributeWithController:self.controllerID
                                                     nodeId:self.nodeID.unsignedLongLongValue
                                                 endpointId:endpointID
                                                  clusterId:clusterID
@@ -161,6 +201,23 @@
                 });
             }
         }];
+    };
+
+    if (self.controllerID != nil) {
+        workBlock();
+    } else {
+        [self.controller fetchControllerIdWithQueue:queue
+                                         completion:^(id _Nullable controllerID, NSError * _Nullable error) {
+                                             if (error != nil) {
+                                                 // We're already running on the right queue.
+                                                 completion(nil, error);
+                                                 return;
+                                             }
+
+                                             self->_controllerID = controllerID;
+                                             workBlock();
+                                         }];
+    }
 }
 
 - (void)invokeCommandWithEndpointID:(NSNumber *)endpointID
@@ -172,10 +229,11 @@
                          completion:(MTRDeviceResponseHandler)completion
 {
     MTR_LOG_DEBUG("Invoking command ...");
-    [_xpcConnection
-        getProxyHandleWithCompletion:^(dispatch_queue_t _Nonnull proxyQueue, MTRDeviceControllerXPCProxyHandle * _Nullable handle) {
+    __auto_type workBlock = ^{
+        [self->_xpcConnection getProxyHandleWithCompletion:^(
+            dispatch_queue_t _Nonnull proxyQueue, MTRDeviceControllerXPCProxyHandle * _Nullable handle) {
             if (handle) {
-                [handle.proxy invokeCommandWithController:self.controller
+                [handle.proxy invokeCommandWithController:self.controllerID
                                                    nodeId:self.nodeID.unsignedLongLongValue
                                                endpointId:endpointID
                                                 clusterId:clusterID
@@ -199,6 +257,23 @@
                 });
             }
         }];
+    };
+
+    if (self.controllerID != nil) {
+        workBlock();
+    } else {
+        [self.controller fetchControllerIdWithQueue:queue
+                                         completion:^(id _Nullable controllerID, NSError * _Nullable error) {
+                                             if (error != nil) {
+                                                 // We're already running on the right queue.
+                                                 completion(nil, error);
+                                                 return;
+                                             }
+
+                                             self->_controllerID = controllerID;
+                                             workBlock();
+                                         }];
+    }
 }
 
 - (void)subscribeToAttributesWithEndpointID:(NSNumber * _Nullable)endpointID
@@ -210,80 +285,120 @@
                     subscriptionEstablished:(void (^_Nullable)(void))subscriptionEstablishedHandler
 {
     MTR_LOG_DEBUG("Subscribing attribute ...");
-    [_xpcConnection getProxyHandleWithCompletion:^(
-        dispatch_queue_t _Nonnull proxyQueue, MTRDeviceControllerXPCProxyHandle * _Nullable handle) {
-        if (handle) {
-            MTR_LOG_DEBUG("Setup report handler");
-            [self.xpcConnection
-                registerReportHandlerWithController:self.controller
-                                             nodeID:self.nodeID
-                                            handler:^(id _Nullable values, NSError * _Nullable error) {
-                                                if (values && ![values isKindOfClass:[NSArray class]]) {
-                                                    MTR_LOG_ERROR("Unsupported report format");
-                                                    return;
-                                                }
-                                                if (!values) {
-                                                    MTR_LOG_DEBUG("Error report received");
-                                                    dispatch_async(queue, ^{
-                                                        reportHandler(values, error);
-                                                    });
-                                                    return;
-                                                }
-                                                __auto_type decodedValues = [MTRDeviceController decodeXPCResponseValues:values];
-                                                NSMutableArray<NSDictionary<NSString *, id> *> * filteredValues =
-                                                    [NSMutableArray arrayWithCapacity:[decodedValues count]];
-                                                for (NSDictionary<NSString *, id> * decodedValue in decodedValues) {
-                                                    MTRAttributePath * attributePath = decodedValue[MTRAttributePathKey];
-                                                    if ((endpointID == nil || [attributePath.endpoint isEqualToNumber:endpointID])
-                                                        && (clusterID == nil || [attributePath.cluster isEqualToNumber:clusterID])
-                                                        && (attributeID == nil ||
-                                                            [attributePath.attribute isEqualToNumber:attributeID])) {
-                                                        [filteredValues addObject:decodedValue];
+    __auto_type workBlock = ^{
+        [self->_xpcConnection getProxyHandleWithCompletion:^(
+            dispatch_queue_t _Nonnull proxyQueue, MTRDeviceControllerXPCProxyHandle * _Nullable handle) {
+            if (handle) {
+                MTR_LOG_DEBUG("Setup report handler");
+                [self.xpcConnection
+                    registerReportHandlerWithController:self.controllerID
+                                                 nodeID:self.nodeID
+                                                handler:^(id _Nullable values, NSError * _Nullable error) {
+                                                    if (values && ![values isKindOfClass:[NSArray class]]) {
+                                                        MTR_LOG_ERROR("Unsupported report format");
+                                                        return;
                                                     }
-                                                }
-                                                if ([filteredValues count] > 0) {
-                                                    MTR_LOG_DEBUG("Report received");
-                                                    dispatch_async(queue, ^{
-                                                        reportHandler(filteredValues, error);
-                                                    });
-                                                }
+                                                    if (!values) {
+                                                        MTR_LOG_DEBUG("Error report received");
+                                                        dispatch_async(queue, ^{
+                                                            reportHandler(values, error);
+                                                        });
+                                                        return;
+                                                    }
+                                                    __auto_type decodedValues =
+                                                        [MTRDeviceController decodeXPCResponseValues:values];
+                                                    NSMutableArray<NSDictionary<NSString *, id> *> * filteredValues =
+                                                        [NSMutableArray arrayWithCapacity:[decodedValues count]];
+                                                    for (NSDictionary<NSString *, id> * decodedValue in decodedValues) {
+                                                        MTRAttributePath * attributePath = decodedValue[MTRAttributePathKey];
+                                                        if ((endpointID == nil ||
+                                                                [attributePath.endpoint isEqualToNumber:endpointID])
+                                                            && (clusterID == nil ||
+                                                                [attributePath.cluster isEqualToNumber:clusterID])
+                                                            && (attributeID == nil ||
+                                                                [attributePath.attribute isEqualToNumber:attributeID])) {
+                                                            [filteredValues addObject:decodedValue];
+                                                        }
+                                                    }
+                                                    if ([filteredValues count] > 0) {
+                                                        MTR_LOG_DEBUG("Report received");
+                                                        dispatch_async(queue, ^{
+                                                            reportHandler(filteredValues, error);
+                                                        });
+                                                    }
+                                                }];
+
+                [handle.proxy subscribeAttributeWithController:self.controllerID
+                                                        nodeId:self.nodeID.unsignedLongLongValue
+                                                    endpointId:endpointID
+                                                     clusterId:clusterID
+                                                   attributeId:attributeID
+                                                   minInterval:params.minInterval
+                                                   maxInterval:params.maxInterval
+                                                        params:[MTRDeviceController encodeXPCSubscribeParams:params]
+                                            establishedHandler:^{
+                                                dispatch_async(queue, ^{
+                                                    MTR_LOG_DEBUG("Subscription established");
+                                                    subscriptionEstablishedHandler();
+                                                    // The following captures the proxy handle in the closure so that the handle
+                                                    // won't be released prior to block call.
+                                                    __auto_type handleRetainer = handle;
+                                                    (void) handleRetainer;
+                                                });
                                             }];
-            [handle.proxy subscribeAttributeWithController:self.controller
-                                                    nodeId:self.nodeID.unsignedLongLongValue
-                                                endpointId:endpointID
-                                                 clusterId:clusterID
-                                               attributeId:attributeID
-                                               minInterval:params.minInterval
-                                               maxInterval:params.maxInterval
-                                                    params:[MTRDeviceController encodeXPCSubscribeParams:params]
-                                        establishedHandler:^{
-                                            dispatch_async(queue, ^{
-                                                MTR_LOG_DEBUG("Subscription established");
-                                                subscriptionEstablishedHandler();
-                                                // The following captures the proxy handle in the closure so that the handle
-                                                // won't be released prior to block call.
-                                                __auto_type handleRetainer = handle;
-                                                (void) handleRetainer;
-                                            });
-                                        }];
-        } else {
-            dispatch_async(queue, ^{
-                MTR_LOG_ERROR("Failed to obtain XPC connection to subscribe to attribute");
-                subscriptionEstablishedHandler();
-                reportHandler(nil, [NSError errorWithDomain:MTRErrorDomain code:MTRErrorCodeGeneralError userInfo:nil]);
-            });
-        }
-    }];
+            } else {
+                dispatch_async(queue, ^{
+                    MTR_LOG_ERROR("Failed to obtain XPC connection to subscribe to attribute");
+                    subscriptionEstablishedHandler();
+                    reportHandler(nil, [NSError errorWithDomain:MTRErrorDomain code:MTRErrorCodeGeneralError userInfo:nil]);
+                });
+            }
+        }];
+    };
+
+    if (self.controllerID != nil) {
+        workBlock();
+    } else {
+        [self.controller fetchControllerIdWithQueue:queue
+                                         completion:^(id _Nullable controllerID, NSError * _Nullable error) {
+                                             if (error != nil) {
+                                                 // We're already running on the right queue.
+                                                 reportHandler(nil, error);
+                                                 return;
+                                             }
+
+                                             self->_controllerID = controllerID;
+                                             workBlock();
+                                         }];
+    }
 }
 
 - (void)deregisterReportHandlersWithQueue:(dispatch_queue_t)queue completion:(void (^)(void))completion
 {
     MTR_LOG_DEBUG("Deregistering report handlers");
-    [_xpcConnection deregisterReportHandlersWithController:self.controller
-                                                    nodeID:self.nodeID
-                                                completion:^{
-                                                    dispatch_async(queue, completion);
-                                                }];
+    __auto_type workBlock = ^{
+        [self->_xpcConnection deregisterReportHandlersWithController:self.controllerID
+                                                              nodeID:self.nodeID
+                                                          completion:^{
+                                                              dispatch_async(queue, completion);
+                                                          }];
+    };
+
+    if (self.controllerID != nil) {
+        workBlock();
+    } else {
+        [self.controller fetchControllerIdWithQueue:queue
+                                         completion:^(id _Nullable controllerID, NSError * _Nullable error) {
+                                             if (error != nil) {
+                                                 // We're already running on the right queue.
+                                                 completion();
+                                                 return;
+                                             }
+
+                                             self->_controllerID = controllerID;
+                                             workBlock();
+                                         }];
+    }
 }
 
 - (void)openCommissioningWindowWithSetupPasscode:(NSNumber *)setupPasscode
diff --git a/src/darwin/Framework/CHIP/MTRSetupPayload.h b/src/darwin/Framework/CHIP/MTRSetupPayload.h
index cbd7b16..3b50b86 100644
--- a/src/darwin/Framework/CHIP/MTRSetupPayload.h
+++ b/src/darwin/Framework/CHIP/MTRSetupPayload.h
@@ -137,6 +137,9 @@
 @property (nonatomic, copy, nullable) NSNumber * rendezvousInformation MTR_NEWLY_DEPRECATED("Please use discoveryCapabilities");
 @property (nonatomic, copy) NSNumber * setUpPINCode MTR_NEWLY_DEPRECATED("Please use setupPasscode");
 
+- (instancetype)init MTR_NEWLY_DEPRECATED("Please use initWithSetupPasscode or setupPayloadWithOnboardingPayload");
++ (instancetype)new MTR_NEWLY_DEPRECATED("Please use initWithSetupPasscode or setupPayloadWithOnboardingPayload");
+
 @end
 
 NS_ASSUME_NONNULL_END
diff --git a/src/darwin/Framework/CHIP/MTRSetupPayload.mm b/src/darwin/Framework/CHIP/MTRSetupPayload.mm
index 054c76c..e3c886a 100644
--- a/src/darwin/Framework/CHIP/MTRSetupPayload.mm
+++ b/src/darwin/Framework/CHIP/MTRSetupPayload.mm
@@ -354,7 +354,7 @@
     payload.commissioningFlow = [MTRSetupPayload unconvertCommissioningFlow:self.commissioningFlow];
     payload.rendezvousInformation = [MTRSetupPayload convertDiscoveryCapabilities:self.discoveryCapabilities];
     payload.discriminator.SetLongValue([self.discriminator unsignedShortValue]);
-    payload.setUpPINCode = [self.setUpPINCode unsignedIntValue];
+    payload.setUpPINCode = [self.setupPasscode unsignedIntValue];
 
     std::string outDecimalString;
     CHIP_ERROR err = chip::QRCodeSetupPayloadGenerator(payload).payloadBase38Representation(outDecimalString);
@@ -432,4 +432,26 @@
     self.setupPasscode = setUpPINCode;
 }
 
+- (instancetype)init
+{
+    if (self = [super init]) {
+        _version = @(0); // Only supported Matter version so far.
+        _vendorID = @(0); // Not available.
+        _productID = @(0); // Not available.
+        _commissioningFlow = MTRCommissioningFlowStandard;
+        _discoveryCapabilities = MTRDiscoveryCapabilitiesUnknown;
+        _hasShortDiscriminator = NO;
+        _discriminator = @(0);
+        _setupPasscode = @(11111111); // Invalid passcode
+        _serialNumber = nil;
+    }
+
+    return self;
+}
+
++ (instancetype)new
+{
+    return [[self alloc] init];
+}
+
 @end
diff --git a/src/darwin/Framework/CHIPTests/MTRControllerTests.m b/src/darwin/Framework/CHIPTests/MTRControllerTests.m
index 7816920..06dfb7a 100644
--- a/src/darwin/Framework/CHIPTests/MTRControllerTests.m
+++ b/src/darwin/Framework/CHIPTests/MTRControllerTests.m
@@ -284,11 +284,6 @@
     [controller shutdown];
 
     XCTAssertFalse([controller isRunning]);
-    XCTAssertFalse([controller getBaseDevice:1234
-                                       queue:dispatch_get_main_queue()
-                                  completion:^(MTRBaseDevice * _Nullable chipDevice, NSError * _Nullable error) {
-                                      XCTAssertEqual(error.code, MTRErrorCodeInvalidState);
-                                  }]);
 
     [factory stopControllerFactory];
     XCTAssertFalse([factory isRunning]);
diff --git a/src/darwin/Framework/CHIPTests/MTRDeviceTests.m b/src/darwin/Framework/CHIPTests/MTRDeviceTests.m
index a01e9b8..a3a55d0 100644
--- a/src/darwin/Framework/CHIPTests/MTRDeviceTests.m
+++ b/src/darwin/Framework/CHIPTests/MTRDeviceTests.m
@@ -39,7 +39,6 @@
 #define MANUAL_INDIVIDUAL_TEST 0
 
 static const uint16_t kPairingTimeoutInSeconds = 10;
-static const uint16_t kCASESetupTimeoutInSeconds = 30;
 static const uint16_t kTimeoutInSeconds = 3;
 static const uint64_t kDeviceId = 0x12344321;
 static NSString * kOnboardingPayload = @"MT:-24J0AFN00KA0648G00";
@@ -61,13 +60,12 @@
     MTRDeviceController * controller = sController;
     XCTAssertNotNil(controller);
 
-    [controller getBaseDevice:kDeviceId
-                        queue:dispatch_get_main_queue()
-                   completion:^(MTRBaseDevice * _Nullable device, NSError * _Nullable error) {
-                       XCTAssertEqual(error.code, 0);
-                       [expectation fulfill];
-                       mConnectedDevice = device;
-                   }];
+    // For now keep the async dispatch, but could we just
+    // synchronously fulfill the expectation here?
+    dispatch_async(dispatch_get_main_queue(), ^{
+        [expectation fulfill];
+        mConnectedDevice = [MTRBaseDevice deviceWithNodeID:@(kDeviceId) controller:controller];
+    });
 }
 
 static MTRBaseDevice * GetConnectedDevice(void)
@@ -101,7 +99,9 @@
     XCTAssertEqual(error.code, 0);
 
     NSError * commissionError = nil;
-    [sController commissionDevice:kDeviceId commissioningParams:[[MTRCommissioningParameters alloc] init] error:&commissionError];
+    [sController commissionNodeWithID:@(kDeviceId)
+                  commissioningParams:[[MTRCommissioningParameters alloc] init]
+                                error:&commissionError];
     XCTAssertNil(commissionError);
 
     // Keep waiting for onCommissioningComplete
@@ -200,16 +200,6 @@
     XCTAssertNil(error);
 
     [self waitForExpectationsWithTimeout:kPairingTimeoutInSeconds handler:nil];
-
-    __block XCTestExpectation * connectionExpectation = [self expectationWithDescription:@"CASE established"];
-    [controller getBaseDevice:kDeviceId
-                        queue:dispatch_get_main_queue()
-                   completion:^(MTRBaseDevice * _Nullable device, NSError * _Nullable error) {
-                       XCTAssertEqual(error.code, 0);
-                       [connectionExpectation fulfill];
-                       connectionExpectation = nil;
-                   }];
-    [self waitForExpectationsWithTimeout:kCASESetupTimeoutInSeconds handler:nil];
 }
 
 - (void)shutdownStack
@@ -1131,17 +1121,7 @@
     MTRDeviceController * controller = sController;
     XCTAssertNotNil(controller);
 
-    __block MTRBaseDevice * device;
-    __block XCTestExpectation * connectionExpectation = [self expectationWithDescription:@"CASE established"];
-    [controller getBaseDevice:kDeviceId
-                        queue:dispatch_get_main_queue()
-                   completion:^(MTRBaseDevice * _Nullable retrievedDevice, NSError * _Nullable error) {
-                       XCTAssertEqual(error.code, 0);
-                       [connectionExpectation fulfill];
-                       connectionExpectation = nil;
-                       device = retrievedDevice;
-                   }];
-    [self waitForExpectationsWithTimeout:kCASESetupTimeoutInSeconds handler:nil];
+    MTRBaseDevice * device = [MTRBaseDevice deviceWithNodeID:@(kDeviceId) controller:controller];
 
     XCTestExpectation * expectation = [self expectationWithDescription:@"ReuseMTRClusterObjectFirstCall"];
 
diff --git a/src/darwin/Framework/CHIPTests/MTRXPCListenerSampleTests.m b/src/darwin/Framework/CHIPTests/MTRXPCListenerSampleTests.m
index df9006b..ad82cd9 100644
--- a/src/darwin/Framework/CHIPTests/MTRXPCListenerSampleTests.m
+++ b/src/darwin/Framework/CHIPTests/MTRXPCListenerSampleTests.m
@@ -41,9 +41,6 @@
 // Singleton controller we use.
 static MTRDeviceController * sController = nil;
 
-// Device we use to send speak CASE to the server.
-static MTRBaseDevice * sDevice = nil;
-
 //
 // Sample XPC Listener implementation that directly communicates with local CHIPDevice instance
 //
@@ -173,25 +170,16 @@
     (void) controller;
     __auto_type sharedController = sController;
     if (sharedController) {
-        [sharedController
-            getBaseDevice:nodeId
-                    queue:dispatch_get_main_queue()
-               completion:^(MTRBaseDevice * _Nullable device, NSError * _Nullable error) {
-                   if (error) {
-                       NSLog(@"Failed to get connected device");
-                       completion(nil, error);
-                   } else {
-                       [device readAttributeWithEndpointId:endpointId
-                                                 clusterId:clusterId
-                                               attributeId:attributeId
-                                                    params:[MTRDeviceController decodeXPCReadParams:params]
-                                               clientQueue:dispatch_get_main_queue()
-                                                completion:^(NSArray<NSDictionary<NSString *, id> *> * _Nullable values,
-                                                    NSError * _Nullable error) {
-                                                    completion([MTRDeviceController encodeXPCResponseValues:values], error);
-                                                }];
-                   }
-               }];
+        __auto_type device = [MTRBaseDevice deviceWithNodeID:@(nodeId) controller:sharedController];
+        [device
+            readAttributesWithEndpointID:endpointId
+                               clusterID:clusterId
+                             attributeID:attributeId
+                                  params:[MTRDeviceController decodeXPCReadParams:params]
+                                   queue:dispatch_get_main_queue()
+                              completion:^(NSArray<NSDictionary<NSString *, id> *> * _Nullable values, NSError * _Nullable error) {
+                                  completion([MTRDeviceController encodeXPCResponseValues:values], error);
+                              }];
     } else {
         NSLog(@"Failed to get shared controller");
         completion(nil, [NSError errorWithDomain:MTRErrorDomain code:MTRErrorCodeGeneralError userInfo:nil]);
@@ -210,27 +198,17 @@
     (void) controller;
     __auto_type sharedController = sController;
     if (sharedController) {
-        [sharedController
-            getBaseDevice:nodeId
-                    queue:dispatch_get_main_queue()
-               completion:^(MTRBaseDevice * _Nullable device, NSError * _Nullable error) {
-                   if (error) {
-                       NSLog(@"Failed to get connected device");
-                       completion(nil, error);
-                   } else {
-
-                       [device writeAttributeWithEndpointId:endpointId
-                                                  clusterId:clusterId
-                                                attributeId:attributeId
-                                                      value:value
-                                          timedWriteTimeout:timeoutMs
-                                                clientQueue:dispatch_get_main_queue()
-                                                 completion:^(NSArray<NSDictionary<NSString *, id> *> * _Nullable values,
-                                                     NSError * _Nullable error) {
-                                                     completion([MTRDeviceController encodeXPCResponseValues:values], error);
-                                                 }];
-                   }
-               }];
+        __auto_type device = [MTRBaseDevice deviceWithNodeID:@(nodeId) controller:sharedController];
+        [device
+            writeAttributeWithEndpointID:endpointId
+                               clusterID:clusterId
+                             attributeID:attributeId
+                                   value:value
+                       timedWriteTimeout:timeoutMs
+                                   queue:dispatch_get_main_queue()
+                              completion:^(NSArray<NSDictionary<NSString *, id> *> * _Nullable values, NSError * _Nullable error) {
+                                  completion([MTRDeviceController encodeXPCResponseValues:values], error);
+                              }];
     } else {
         NSLog(@"Failed to get shared controller");
         completion(nil, [NSError errorWithDomain:MTRErrorDomain code:MTRErrorCodeGeneralError userInfo:nil]);
@@ -249,26 +227,17 @@
     (void) controller;
     __auto_type sharedController = sController;
     if (sharedController) {
-        [sharedController
-            getBaseDevice:nodeId
-                    queue:dispatch_get_main_queue()
-               completion:^(MTRBaseDevice * _Nullable device, NSError * _Nullable error) {
-                   if (error) {
-                       NSLog(@"Failed to get connected device");
-                       completion(nil, error);
-                   } else {
-                       [device invokeCommandWithEndpointId:endpointId
-                                                 clusterId:clusterId
-                                                 commandId:commandId
-                                             commandFields:fields
-                                        timedInvokeTimeout:nil
-                                               clientQueue:dispatch_get_main_queue()
-                                                completion:^(NSArray<NSDictionary<NSString *, id> *> * _Nullable values,
-                                                    NSError * _Nullable error) {
-                                                    completion([MTRDeviceController encodeXPCResponseValues:values], error);
-                                                }];
-                   }
-               }];
+        __auto_type device = [MTRBaseDevice deviceWithNodeID:@(nodeId) controller:sharedController];
+        [device
+            invokeCommandWithEndpointID:endpointId
+                              clusterID:clusterId
+                              commandID:commandId
+                          commandFields:fields
+                     timedInvokeTimeout:nil
+                                  queue:dispatch_get_main_queue()
+                             completion:^(NSArray<NSDictionary<NSString *, id> *> * _Nullable values, NSError * _Nullable error) {
+                                 completion([MTRDeviceController encodeXPCResponseValues:values], error);
+                             }];
     } else {
         NSLog(@"Failed to get shared controller");
         completion(nil, [NSError errorWithDomain:MTRErrorDomain code:MTRErrorCodeGeneralError userInfo:nil]);
@@ -287,48 +256,28 @@
 {
     __auto_type sharedController = sController;
     if (sharedController) {
-        [sharedController
-            getBaseDevice:nodeId
-                    queue:dispatch_get_main_queue()
-               completion:^(MTRBaseDevice * _Nullable device, NSError * _Nullable error) {
-                   if (error) {
-                       NSLog(@"Failed to get connected device");
-                       establishedHandler();
-                       // Send an error report so that the client knows of the failure
-                       [self.clientProxy handleReportWithController:controller
-                                                             nodeId:nodeId
-                                                             values:nil
-                                                              error:[NSError errorWithDomain:MTRErrorDomain
-                                                                                        code:MTRErrorCodeGeneralError
-                                                                                    userInfo:nil]];
-                   } else {
-                       __auto_type * subscriptionParams = [MTRDeviceController decodeXPCSubscribeParams:params];
-                       if (subscriptionParams == nil) {
-                           subscriptionParams = [[MTRSubscribeParams alloc] initWithMinInterval:minInterval
-                                                                                    maxInterval:maxInterval];
-                       } else {
-                           subscriptionParams.minInterval = minInterval;
-                           subscriptionParams.maxInterval = maxInterval;
-                       }
-                       [device subscribeAttributeWithEndpointId:endpointId
-                                                      clusterId:clusterId
-                                                    attributeId:attributeId
-                                                    minInterval:minInterval
-                                                    maxInterval:maxInterval
-                                                         params:subscriptionParams
-                                                    clientQueue:dispatch_get_main_queue()
-                                                  reportHandler:^(NSArray<NSDictionary<NSString *, id> *> * _Nullable values,
-                                                      NSError * _Nullable error) {
-                                                      [self.clientProxy
-                                                          handleReportWithController:controller
-                                                                              nodeId:nodeId
-                                                                              values:[MTRDeviceController
-                                                                                         encodeXPCResponseValues:values]
-                                                                               error:error];
-                                                  }
-                                        subscriptionEstablished:establishedHandler];
-                   }
-               }];
+        __auto_type * subscriptionParams = [MTRDeviceController decodeXPCSubscribeParams:params];
+        if (subscriptionParams == nil) {
+            subscriptionParams = [[MTRSubscribeParams alloc] initWithMinInterval:minInterval maxInterval:maxInterval];
+        } else {
+            subscriptionParams.minInterval = minInterval;
+            subscriptionParams.maxInterval = maxInterval;
+        }
+        __auto_type device = [MTRBaseDevice deviceWithNodeID:@(nodeId) controller:sharedController];
+        [device subscribeToAttributesWithEndpointID:endpointId
+                                          clusterID:clusterId
+                                        attributeID:attributeId
+                                             params:subscriptionParams
+                                              queue:dispatch_get_main_queue()
+                                      reportHandler:^(
+                                          NSArray<NSDictionary<NSString *, id> *> * _Nullable values, NSError * _Nullable error) {
+                                          [self.clientProxy
+                                              handleReportWithController:controller
+                                                                  nodeId:nodeId
+                                                                  values:[MTRDeviceController encodeXPCResponseValues:values]
+                                                                   error:error];
+                                      }
+                            subscriptionEstablished:establishedHandler];
     } else {
         NSLog(@"Failed to get shared controller");
         establishedHandler();
@@ -346,16 +295,8 @@
 {
     __auto_type sharedController = sController;
     if (sharedController) {
-        [sharedController getBaseDevice:nodeId
-                                  queue:dispatch_get_main_queue()
-                             completion:^(MTRBaseDevice * _Nullable device, NSError * _Nullable error) {
-                                 if (error) {
-                                     NSLog(@"Failed to get connected device");
-                                 } else {
-                                     [device deregisterReportHandlersWithClientQueue:dispatch_get_main_queue()
-                                                                          completion:completion];
-                                 }
-                             }];
+        __auto_type device = [MTRBaseDevice deviceWithNodeID:@(nodeId) controller:sharedController];
+        [device deregisterReportHandlersWithQueue:dispatch_get_main_queue() completion:completion];
     } else {
         NSLog(@"Failed to get shared controller");
         completion();
@@ -377,50 +318,41 @@
             attributeCacheContainer = [[MTRAttributeCacheContainer alloc] init];
         }
 
-        [sharedController getBaseDevice:nodeId
-                                  queue:dispatch_get_main_queue()
-                      completionHandler:^(MTRBaseDevice * _Nullable device, NSError * _Nullable error) {
-                          if (error) {
-                              NSLog(@"Error: Failed to get connected device (%llu) for attribute cache: %@", nodeId, error);
-                              completion(error);
-                              return;
-                          }
-                          NSMutableArray * established = [NSMutableArray arrayWithCapacity:1];
-                          [established addObject:@NO];
-                          __auto_type * subscriptionParams = [MTRDeviceController decodeXPCSubscribeParams:params];
-                          if (subscriptionParams == nil) {
-                              subscriptionParams = [[MTRSubscribeParams alloc] initWithMinInterval:minInterval
-                                                                                       maxInterval:maxInterval];
-                          } else {
-                              subscriptionParams.minInterval = minInterval;
-                              subscriptionParams.maxInterval = maxInterval;
-                          }
-                          [device subscribeWithQueue:dispatch_get_main_queue()
-                              params:subscriptionParams
-                              attributeCacheContainer:attributeCacheContainer
-                              attributeReportHandler:^(NSArray * value) {
-                                  NSLog(@"Received report: %@", value);
-                              }
-                              eventReportHandler:nil
-                              errorHandler:^(NSError * error) {
-                                  NSLog(@"Received report error: %@", error);
-                                  if (![established[0] boolValue]) {
-                                      established[0] = @YES;
-                                      completion(error);
-                                  }
-                              }
-                              subscriptionEstablished:^() {
-                                  NSLog(@"Attribute cache subscription succeeded for device %llu", nodeId);
-                                  if (attributeCacheContainer) {
-                                      [self.attributeCacheDictionary setObject:attributeCacheContainer forKey:@(nodeId)];
-                                  }
-                                  if (![established[0] boolValue]) {
-                                      established[0] = @YES;
-                                      completion(nil);
-                                  }
-                              }
-                              resubscriptionScheduled:nil];
-                      }];
+        __auto_type device = [MTRBaseDevice deviceWithNodeID:@(nodeId) controller:sharedController];
+        NSMutableArray * established = [NSMutableArray arrayWithCapacity:1];
+        [established addObject:@NO];
+        __auto_type * subscriptionParams = [MTRDeviceController decodeXPCSubscribeParams:params];
+        if (subscriptionParams == nil) {
+            subscriptionParams = [[MTRSubscribeParams alloc] initWithMinInterval:minInterval maxInterval:maxInterval];
+        } else {
+            subscriptionParams.minInterval = minInterval;
+            subscriptionParams.maxInterval = maxInterval;
+        }
+        [device subscribeWithQueue:dispatch_get_main_queue()
+            params:subscriptionParams
+            attributeCacheContainer:attributeCacheContainer
+            attributeReportHandler:^(NSArray * value) {
+                NSLog(@"Received report: %@", value);
+            }
+            eventReportHandler:nil
+            errorHandler:^(NSError * error) {
+                NSLog(@"Received report error: %@", error);
+                if (![established[0] boolValue]) {
+                    established[0] = @YES;
+                    completion(error);
+                }
+            }
+            subscriptionEstablished:^() {
+                NSLog(@"Attribute cache subscription succeeded for device %llu", nodeId);
+                if (attributeCacheContainer) {
+                    [self.attributeCacheDictionary setObject:attributeCacheContainer forKey:@(nodeId)];
+                }
+                if (![established[0] boolValue]) {
+                    established[0] = @YES;
+                    completion(nil);
+                }
+            }
+            resubscriptionScheduled:nil];
     } else {
         NSLog(@"Failed to get shared controller");
         completion([NSError errorWithDomain:MTRErrorDomain code:MTRErrorCodeGeneralError userInfo:nil]);
@@ -453,7 +385,6 @@
 @end
 
 static const uint16_t kPairingTimeoutInSeconds = 10;
-static const uint16_t kCASESetupTimeoutInSeconds = 30;
 static const uint16_t kTimeoutInSeconds = 3;
 static const uint64_t kDeviceId = 0x12344321;
 static NSString * kOnboardingPayload = @"MT:-24J0AFN00KA0648G00";
@@ -489,7 +420,9 @@
 {
     XCTAssertEqual(error.code, 0);
     NSError * commissionError = nil;
-    [sController commissionDevice:kDeviceId commissioningParams:[[MTRCommissioningParameters alloc] init] error:&commissionError];
+    [sController commissionNodeWithID:@(kDeviceId)
+                  commissioningParams:[[MTRCommissioningParameters alloc] init]
+                                error:&commissionError];
     XCTAssertNil(commissionError);
 
     // Keep waiting for onCommissioningComplete
@@ -567,17 +500,6 @@
 
     [self waitForExpectationsWithTimeout:kPairingTimeoutInSeconds handler:nil];
 
-    __block XCTestExpectation * connectionExpectation = [self expectationWithDescription:@"CASE established"];
-    [controller getBaseDevice:kDeviceId
-                        queue:dispatch_get_main_queue()
-                   completion:^(MTRBaseDevice * _Nullable device, NSError * _Nullable error) {
-                       XCTAssertEqual(error.code, 0);
-                       sDevice = device;
-                       [connectionExpectation fulfill];
-                       connectionExpectation = nil;
-                   }];
-    [self waitForExpectationsWithTimeout:kCASESetupTimeoutInSeconds handler:nil];
-
     mSampleListener = [[MTRXPCListenerSample alloc] init];
     [mSampleListener start];
 }
@@ -600,9 +522,6 @@
 
 - (void)waitForCommissionee
 {
-    XCTestExpectation * expectation = [self expectationWithDescription:@"Wait for the commissioned device to be retrieved"];
-
-    dispatch_queue_t queue = dispatch_get_main_queue();
     __auto_type remoteController = [MTRDeviceController
         sharedControllerWithID:MTRDeviceControllerId
                xpcConnectBlock:^NSXPCConnection * _Nonnull {
@@ -612,13 +531,7 @@
                    NSLog(@"Listener is not active");
                    return nil;
                }];
-    [remoteController getBaseDevice:kDeviceId
-                              queue:queue
-                         completion:^(MTRBaseDevice * _Nullable device, NSError * _Nullable error) {
-                             mConnectedDevice = device;
-                             [expectation fulfill];
-                         }];
-    [self waitForExpectationsWithTimeout:kTimeoutInSeconds handler:nil];
+    mConnectedDevice = [MTRBaseDevice deviceWithNodeID:@(kDeviceId) controller:remoteController];
     mDeviceController = remoteController;
 }
 
@@ -1910,7 +1823,7 @@
 {
     // Put the device back in the state we found it: open commissioning window, no fabrics commissioned.
     dispatch_queue_t queue = dispatch_get_main_queue();
-    MTRBaseDevice * device = sDevice;
+    __auto_type device = [MTRBaseDevice deviceWithNodeID:@(kDeviceId) controller:sController];
 
     // Get our current fabric index, for later deletion.
     XCTestExpectation * readFabricIndexExpectation = [self expectationWithDescription:@"Fabric index read"];
diff --git a/src/darwin/Framework/CHIPTests/MTRXPCProtocolTests.m b/src/darwin/Framework/CHIPTests/MTRXPCProtocolTests.m
index 1ab8f16..c7ef91d 100644
--- a/src/darwin/Framework/CHIPTests/MTRXPCProtocolTests.m
+++ b/src/darwin/Framework/CHIPTests/MTRXPCProtocolTests.m
@@ -92,49 +92,39 @@
                                 queue:queue
                            completion:(void (^)(NSError * _Nullable error))completion
 {
-    __auto_type workQueue = dispatch_get_main_queue();
     __auto_type completionHandler = ^(NSError * _Nullable error) {
         dispatch_async(queue, ^{
             completion(error);
         });
     };
-    [deviceController
-        getBaseDevice:deviceID.unsignedLongLongValue
-                queue:workQueue
-           completion:^(MTRBaseDevice * _Nullable device, NSError * _Nullable error) {
-               if (error) {
-                   NSLog(@"Error: Failed to get connected device (%llu) for attribute cache: %@", deviceID.unsignedLongLongValue,
-                       error);
-                   completionHandler(error);
-                   return;
-               }
-               __auto_type * subscriptionParams
-                   = (params == nil) ? [[MTRSubscribeParams alloc] initWithMinInterval:@(1) maxInterval:@(43200)] : params;
-               __auto_type established = [NSMutableArray arrayWithCapacity:1];
-               [established addObject:@NO];
-               [device subscribeWithQueue:queue
-                   params:subscriptionParams
-                   attributeCacheContainer:self
-                   attributeReportHandler:^(NSArray * value) {
-                       NSLog(@"Report received for attribute cache: %@", value);
-                   }
-                   eventReportHandler:nil
-                   errorHandler:^(NSError * error) {
-                       NSLog(@"Report error received for attribute cache: %@", error);
-                       if (![established[0] boolValue]) {
-                           established[0] = @YES;
-                           completionHandler(error);
-                       }
-                   }
-                   subscriptionEstablished:^() {
-                       NSLog(@"Attribute cache subscription succeeded for device %llu", deviceID.unsignedLongLongValue);
-                       if (![established[0] boolValue]) {
-                           established[0] = @YES;
-                           completionHandler(nil);
-                       }
-                   }
-                   resubscriptionScheduled:nil];
-           }];
+    __auto_type * device = [MTRBaseDevice deviceWithNodeID:deviceID controller:deviceController];
+
+    __auto_type * subscriptionParams
+        = (params == nil) ? [[MTRSubscribeParams alloc] initWithMinInterval:@(1) maxInterval:@(43200)] : params;
+    __auto_type established = [NSMutableArray arrayWithCapacity:1];
+    [established addObject:@NO];
+    [device subscribeWithQueue:queue
+        params:subscriptionParams
+        attributeCacheContainer:self
+        attributeReportHandler:^(NSArray * value) {
+            NSLog(@"Report received for attribute cache: %@", value);
+        }
+        eventReportHandler:nil
+        errorHandler:^(NSError * error) {
+            NSLog(@"Report error received for attribute cache: %@", error);
+            if (![established[0] boolValue]) {
+                established[0] = @YES;
+                completionHandler(error);
+            }
+        }
+        subscriptionEstablished:^() {
+            NSLog(@"Attribute cache subscription succeeded for device %llu", [deviceID unsignedLongLongValue]);
+            if (![established[0] boolValue]) {
+                established[0] = @YES;
+                completionHandler(nil);
+            }
+        }
+        resubscriptionScheduled:nil];
 }
 @end
 
@@ -383,27 +373,21 @@
         completion([MTRDeviceController encodeXPCResponseValues:myValues], nil);
     };
 
-    [_remoteDeviceController getBaseDevice:myNodeId
-                                     queue:dispatch_get_main_queue()
-                                completion:^(MTRBaseDevice * _Nullable device, NSError * _Nullable error) {
-                                    XCTAssertNotNil(device);
-                                    XCTAssertNil(error);
-                                    NSLog(@"Device acquired. Reading...");
-                                    [device readAttributesWithEndpointID:myEndpointId
-                                                               clusterID:myClusterId
-                                                             attributeID:myAttributeId
-                                                                  params:nil
-                                                                   queue:dispatch_get_main_queue()
-                                                              completion:^(id _Nullable value, NSError * _Nullable error) {
-                                                                  NSLog(@"Read value: %@", value);
-                                                                  XCTAssertNotNil(value);
-                                                                  XCTAssertNil(error);
-                                                                  XCTAssertTrue([myValues isEqual:value]);
-                                                                  [responseExpectation fulfill];
-                                                                  self.xpcDisconnectExpectation =
-                                                                      [self expectationWithDescription:@"XPC Disconnected"];
-                                                              }];
-                                }];
+    __auto_type * device = [MTRBaseDevice deviceWithNodeID:@(myNodeId) controller:_remoteDeviceController];
+    NSLog(@"Device acquired. Reading...");
+    [device readAttributesWithEndpointID:myEndpointId
+                               clusterID:myClusterId
+                             attributeID:myAttributeId
+                                  params:nil
+                                   queue:dispatch_get_main_queue()
+                              completion:^(id _Nullable value, NSError * _Nullable error) {
+                                  NSLog(@"Read value: %@", value);
+                                  XCTAssertNotNil(value);
+                                  XCTAssertNil(error);
+                                  XCTAssertTrue([myValues isEqual:value]);
+                                  [responseExpectation fulfill];
+                                  self.xpcDisconnectExpectation = [self expectationWithDescription:@"XPC Disconnected"];
+                              }];
 
     [self waitForExpectations:[NSArray arrayWithObjects:callExpectation, responseExpectation, nil] timeout:kTimeoutInSeconds];
 
@@ -445,27 +429,21 @@
         completion([MTRDeviceController encodeXPCResponseValues:myValues], nil);
     };
 
-    [_remoteDeviceController getBaseDevice:myNodeId
-                                     queue:dispatch_get_main_queue()
-                                completion:^(MTRBaseDevice * _Nullable device, NSError * _Nullable error) {
-                                    XCTAssertNotNil(device);
-                                    XCTAssertNil(error);
-                                    NSLog(@"Device acquired. Reading...");
-                                    [device readAttributesWithEndpointID:myEndpointId
-                                                               clusterID:myClusterId
-                                                             attributeID:myAttributeId
-                                                                  params:myParams
-                                                                   queue:dispatch_get_main_queue()
-                                                              completion:^(id _Nullable value, NSError * _Nullable error) {
-                                                                  NSLog(@"Read value: %@", value);
-                                                                  XCTAssertNotNil(value);
-                                                                  XCTAssertNil(error);
-                                                                  XCTAssertTrue([myValues isEqual:value]);
-                                                                  [responseExpectation fulfill];
-                                                                  self.xpcDisconnectExpectation =
-                                                                      [self expectationWithDescription:@"XPC Disconnected"];
-                                                              }];
-                                }];
+    __auto_type * device = [MTRBaseDevice deviceWithNodeID:@(myNodeId) controller:_remoteDeviceController];
+    NSLog(@"Device acquired. Reading...");
+    [device readAttributesWithEndpointID:myEndpointId
+                               clusterID:myClusterId
+                             attributeID:myAttributeId
+                                  params:myParams
+                                   queue:dispatch_get_main_queue()
+                              completion:^(id _Nullable value, NSError * _Nullable error) {
+                                  NSLog(@"Read value: %@", value);
+                                  XCTAssertNotNil(value);
+                                  XCTAssertNil(error);
+                                  XCTAssertTrue([myValues isEqual:value]);
+                                  [responseExpectation fulfill];
+                                  self.xpcDisconnectExpectation = [self expectationWithDescription:@"XPC Disconnected"];
+                              }];
 
     [self waitForExpectations:[NSArray arrayWithObjects:callExpectation, responseExpectation, nil] timeout:kTimeoutInSeconds];
 
@@ -498,26 +476,20 @@
         completion(nil, myError);
     };
 
-    [_remoteDeviceController getBaseDevice:myNodeId
-                                     queue:dispatch_get_main_queue()
-                                completion:^(MTRBaseDevice * _Nullable device, NSError * _Nullable error) {
-                                    XCTAssertNotNil(device);
-                                    XCTAssertNil(error);
-                                    NSLog(@"Device acquired. Reading...");
-                                    [device readAttributesWithEndpointID:myEndpointId
-                                                               clusterID:myClusterId
-                                                             attributeID:myAttributeId
-                                                                  params:nil
-                                                                   queue:dispatch_get_main_queue()
-                                                              completion:^(id _Nullable value, NSError * _Nullable error) {
-                                                                  NSLog(@"Read value: %@", value);
-                                                                  XCTAssertNil(value);
-                                                                  XCTAssertNotNil(error);
-                                                                  [responseExpectation fulfill];
-                                                                  self.xpcDisconnectExpectation =
-                                                                      [self expectationWithDescription:@"XPC Disconnected"];
-                                                              }];
-                                }];
+    __auto_type * device = [MTRBaseDevice deviceWithNodeID:@(myNodeId) controller:_remoteDeviceController];
+    NSLog(@"Device acquired. Reading...");
+    [device readAttributesWithEndpointID:myEndpointId
+                               clusterID:myClusterId
+                             attributeID:myAttributeId
+                                  params:nil
+                                   queue:dispatch_get_main_queue()
+                              completion:^(id _Nullable value, NSError * _Nullable error) {
+                                  NSLog(@"Read value: %@", value);
+                                  XCTAssertNil(value);
+                                  XCTAssertNotNil(error);
+                                  [responseExpectation fulfill];
+                                  self.xpcDisconnectExpectation = [self expectationWithDescription:@"XPC Disconnected"];
+                              }];
 
     [self waitForExpectations:[NSArray arrayWithObjects:callExpectation, responseExpectation, nil] timeout:kTimeoutInSeconds];
 
@@ -557,28 +529,22 @@
         completion([MTRDeviceController encodeXPCResponseValues:myResults], nil);
     };
 
-    [_remoteDeviceController getBaseDevice:myNodeId
-                                     queue:dispatch_get_main_queue()
-                                completion:^(MTRBaseDevice * _Nullable device, NSError * _Nullable error) {
-                                    XCTAssertNotNil(device);
-                                    XCTAssertNil(error);
-                                    NSLog(@"Device acquired. Writing...");
-                                    [device writeAttributeWithEndpointID:myEndpointId
-                                                               clusterID:myClusterId
-                                                             attributeID:myAttributeId
-                                                                   value:myValue
-                                                       timedWriteTimeout:nil
-                                                                   queue:dispatch_get_main_queue()
-                                                              completion:^(id _Nullable value, NSError * _Nullable error) {
-                                                                  NSLog(@"Write response: %@", value);
-                                                                  XCTAssertNotNil(value);
-                                                                  XCTAssertNil(error);
-                                                                  XCTAssertTrue([myResults isEqual:value]);
-                                                                  [responseExpectation fulfill];
-                                                                  self.xpcDisconnectExpectation =
-                                                                      [self expectationWithDescription:@"XPC Disconnected"];
-                                                              }];
-                                }];
+    __auto_type * device = [MTRBaseDevice deviceWithNodeID:@(myNodeId) controller:_remoteDeviceController];
+    NSLog(@"Device acquired. Writing...");
+    [device writeAttributeWithEndpointID:myEndpointId
+                               clusterID:myClusterId
+                             attributeID:myAttributeId
+                                   value:myValue
+                       timedWriteTimeout:nil
+                                   queue:dispatch_get_main_queue()
+                              completion:^(id _Nullable value, NSError * _Nullable error) {
+                                  NSLog(@"Write response: %@", value);
+                                  XCTAssertNotNil(value);
+                                  XCTAssertNil(error);
+                                  XCTAssertTrue([myResults isEqual:value]);
+                                  [responseExpectation fulfill];
+                                  self.xpcDisconnectExpectation = [self expectationWithDescription:@"XPC Disconnected"];
+                              }];
 
     [self waitForExpectations:[NSArray arrayWithObjects:callExpectation, responseExpectation, nil] timeout:kTimeoutInSeconds];
 
@@ -620,28 +586,22 @@
         completion([MTRDeviceController encodeXPCResponseValues:myResults], nil);
     };
 
-    [_remoteDeviceController getBaseDevice:myNodeId
-                                     queue:dispatch_get_main_queue()
-                                completion:^(MTRBaseDevice * _Nullable device, NSError * _Nullable error) {
-                                    XCTAssertNotNil(device);
-                                    XCTAssertNil(error);
-                                    NSLog(@"Device acquired. Writing...");
-                                    [device writeAttributeWithEndpointID:myEndpointId
-                                                               clusterID:myClusterId
-                                                             attributeID:myAttributeId
-                                                                   value:myValue
-                                                       timedWriteTimeout:myTimedWriteTimeout
-                                                                   queue:dispatch_get_main_queue()
-                                                              completion:^(id _Nullable value, NSError * _Nullable error) {
-                                                                  NSLog(@"Write response: %@", value);
-                                                                  XCTAssertNotNil(value);
-                                                                  XCTAssertNil(error);
-                                                                  XCTAssertTrue([myResults isEqual:value]);
-                                                                  [responseExpectation fulfill];
-                                                                  self.xpcDisconnectExpectation =
-                                                                      [self expectationWithDescription:@"XPC Disconnected"];
-                                                              }];
-                                }];
+    __auto_type * device = [MTRBaseDevice deviceWithNodeID:@(myNodeId) controller:_remoteDeviceController];
+    NSLog(@"Device acquired. Writing...");
+    [device writeAttributeWithEndpointID:myEndpointId
+                               clusterID:myClusterId
+                             attributeID:myAttributeId
+                                   value:myValue
+                       timedWriteTimeout:myTimedWriteTimeout
+                                   queue:dispatch_get_main_queue()
+                              completion:^(id _Nullable value, NSError * _Nullable error) {
+                                  NSLog(@"Write response: %@", value);
+                                  XCTAssertNotNil(value);
+                                  XCTAssertNil(error);
+                                  XCTAssertTrue([myResults isEqual:value]);
+                                  [responseExpectation fulfill];
+                                  self.xpcDisconnectExpectation = [self expectationWithDescription:@"XPC Disconnected"];
+                              }];
 
     [self waitForExpectations:[NSArray arrayWithObjects:callExpectation, responseExpectation, nil] timeout:kTimeoutInSeconds];
 
@@ -676,27 +636,21 @@
         completion(nil, myError);
     };
 
-    [_remoteDeviceController getBaseDevice:myNodeId
-                                     queue:dispatch_get_main_queue()
-                                completion:^(MTRBaseDevice * _Nullable device, NSError * _Nullable error) {
-                                    XCTAssertNotNil(device);
-                                    XCTAssertNil(error);
-                                    NSLog(@"Device acquired. Writing...");
-                                    [device writeAttributeWithEndpointID:myEndpointId
-                                                               clusterID:myClusterId
-                                                             attributeID:myAttributeId
-                                                                   value:myValue
-                                                       timedWriteTimeout:nil
-                                                                   queue:dispatch_get_main_queue()
-                                                              completion:^(id _Nullable value, NSError * _Nullable error) {
-                                                                  NSLog(@"Write response: %@", value);
-                                                                  XCTAssertNil(value);
-                                                                  XCTAssertNotNil(error);
-                                                                  [responseExpectation fulfill];
-                                                                  self.xpcDisconnectExpectation =
-                                                                      [self expectationWithDescription:@"XPC Disconnected"];
-                                                              }];
-                                }];
+    __auto_type * device = [MTRBaseDevice deviceWithNodeID:@(myNodeId) controller:_remoteDeviceController];
+    NSLog(@"Device acquired. Writing...");
+    [device writeAttributeWithEndpointID:myEndpointId
+                               clusterID:myClusterId
+                             attributeID:myAttributeId
+                                   value:myValue
+                       timedWriteTimeout:nil
+                                   queue:dispatch_get_main_queue()
+                              completion:^(id _Nullable value, NSError * _Nullable error) {
+                                  NSLog(@"Write response: %@", value);
+                                  XCTAssertNil(value);
+                                  XCTAssertNotNil(error);
+                                  [responseExpectation fulfill];
+                                  self.xpcDisconnectExpectation = [self expectationWithDescription:@"XPC Disconnected"];
+                              }];
 
     [self waitForExpectations:[NSArray arrayWithObjects:callExpectation, responseExpectation, nil] timeout:kTimeoutInSeconds];
 
@@ -736,28 +690,22 @@
               completion([MTRDeviceController encodeXPCResponseValues:myResults], nil);
           };
 
-    [_remoteDeviceController getBaseDevice:myNodeId
-                                     queue:dispatch_get_main_queue()
-                                completion:^(MTRBaseDevice * _Nullable device, NSError * _Nullable error) {
-                                    XCTAssertNotNil(device);
-                                    XCTAssertNil(error);
-                                    NSLog(@"Device acquired. Invoking command...");
-                                    [device invokeCommandWithEndpointID:myEndpointId
-                                                              clusterID:myClusterId
-                                                              commandID:myCommandId
-                                                          commandFields:myFields
-                                                     timedInvokeTimeout:nil
-                                                                  queue:dispatch_get_main_queue()
-                                                             completion:^(id _Nullable value, NSError * _Nullable error) {
-                                                                 NSLog(@"Command response: %@", value);
-                                                                 XCTAssertNotNil(value);
-                                                                 XCTAssertNil(error);
-                                                                 XCTAssertTrue([myResults isEqual:value]);
-                                                                 [responseExpectation fulfill];
-                                                                 self.xpcDisconnectExpectation =
-                                                                     [self expectationWithDescription:@"XPC Disconnected"];
-                                                             }];
-                                }];
+    __auto_type * device = [MTRBaseDevice deviceWithNodeID:@(myNodeId) controller:_remoteDeviceController];
+    NSLog(@"Device acquired. Invoking command...");
+    [device invokeCommandWithEndpointID:myEndpointId
+                              clusterID:myClusterId
+                              commandID:myCommandId
+                          commandFields:myFields
+                     timedInvokeTimeout:nil
+                                  queue:dispatch_get_main_queue()
+                             completion:^(id _Nullable value, NSError * _Nullable error) {
+                                 NSLog(@"Command response: %@", value);
+                                 XCTAssertNotNil(value);
+                                 XCTAssertNil(error);
+                                 XCTAssertTrue([myResults isEqual:value]);
+                                 [responseExpectation fulfill];
+                                 self.xpcDisconnectExpectation = [self expectationWithDescription:@"XPC Disconnected"];
+                             }];
 
     [self waitForExpectations:[NSArray arrayWithObjects:callExpectation, responseExpectation, nil] timeout:kTimeoutInSeconds];
 
@@ -799,28 +747,22 @@
               completion([MTRDeviceController encodeXPCResponseValues:myResults], nil);
           };
 
-    [_remoteDeviceController getBaseDevice:myNodeId
-                                     queue:dispatch_get_main_queue()
-                                completion:^(MTRBaseDevice * _Nullable device, NSError * _Nullable error) {
-                                    XCTAssertNotNil(device);
-                                    XCTAssertNil(error);
-                                    NSLog(@"Device acquired. Invoking command...");
-                                    [device invokeCommandWithEndpointID:myEndpointId
-                                                              clusterID:myClusterId
-                                                              commandID:myCommandId
-                                                          commandFields:myFields
-                                                     timedInvokeTimeout:myTimedInvokeTimeout
-                                                                  queue:dispatch_get_main_queue()
-                                                             completion:^(id _Nullable value, NSError * _Nullable error) {
-                                                                 NSLog(@"Command response: %@", value);
-                                                                 XCTAssertNotNil(value);
-                                                                 XCTAssertNil(error);
-                                                                 XCTAssertTrue([myResults isEqual:value]);
-                                                                 [responseExpectation fulfill];
-                                                                 self.xpcDisconnectExpectation =
-                                                                     [self expectationWithDescription:@"XPC Disconnected"];
-                                                             }];
-                                }];
+    __auto_type * device = [MTRBaseDevice deviceWithNodeID:@(myNodeId) controller:_remoteDeviceController];
+    NSLog(@"Device acquired. Invoking command...");
+    [device invokeCommandWithEndpointID:myEndpointId
+                              clusterID:myClusterId
+                              commandID:myCommandId
+                          commandFields:myFields
+                     timedInvokeTimeout:myTimedInvokeTimeout
+                                  queue:dispatch_get_main_queue()
+                             completion:^(id _Nullable value, NSError * _Nullable error) {
+                                 NSLog(@"Command response: %@", value);
+                                 XCTAssertNotNil(value);
+                                 XCTAssertNil(error);
+                                 XCTAssertTrue([myResults isEqual:value]);
+                                 [responseExpectation fulfill];
+                                 self.xpcDisconnectExpectation = [self expectationWithDescription:@"XPC Disconnected"];
+                             }];
 
     [self waitForExpectations:[NSArray arrayWithObjects:callExpectation, responseExpectation, nil] timeout:kTimeoutInSeconds];
 
@@ -858,27 +800,21 @@
               completion(nil, myError);
           };
 
-    [_remoteDeviceController getBaseDevice:myNodeId
-                                     queue:dispatch_get_main_queue()
-                                completion:^(MTRBaseDevice * _Nullable device, NSError * _Nullable error) {
-                                    XCTAssertNotNil(device);
-                                    XCTAssertNil(error);
-                                    NSLog(@"Device acquired. Invoking command...");
-                                    [device invokeCommandWithEndpointID:myEndpointId
-                                                              clusterID:myClusterId
-                                                              commandID:myCommandId
-                                                          commandFields:myFields
-                                                     timedInvokeTimeout:nil
-                                                                  queue:dispatch_get_main_queue()
-                                                             completion:^(id _Nullable value, NSError * _Nullable error) {
-                                                                 NSLog(@"Command response: %@", value);
-                                                                 XCTAssertNil(value);
-                                                                 XCTAssertNotNil(error);
-                                                                 [responseExpectation fulfill];
-                                                                 self.xpcDisconnectExpectation =
-                                                                     [self expectationWithDescription:@"XPC Disconnected"];
-                                                             }];
-                                }];
+    __auto_type * device = [MTRBaseDevice deviceWithNodeID:@(myNodeId) controller:_remoteDeviceController];
+    NSLog(@"Device acquired. Invoking command...");
+    [device invokeCommandWithEndpointID:myEndpointId
+                              clusterID:myClusterId
+                              commandID:myCommandId
+                          commandFields:myFields
+                     timedInvokeTimeout:nil
+                                  queue:dispatch_get_main_queue()
+                             completion:^(id _Nullable value, NSError * _Nullable error) {
+                                 NSLog(@"Command response: %@", value);
+                                 XCTAssertNil(value);
+                                 XCTAssertNotNil(error);
+                                 [responseExpectation fulfill];
+                                 self.xpcDisconnectExpectation = [self expectationWithDescription:@"XPC Disconnected"];
+                             }];
 
     [self waitForExpectations:[NSArray arrayWithObjects:callExpectation, responseExpectation, nil] timeout:kTimeoutInSeconds];
 
@@ -921,30 +857,24 @@
 
     _xpcDisconnectExpectation = [self expectationWithDescription:@"XPC Disconnected"];
 
-    [_remoteDeviceController
-        getBaseDevice:myNodeId
-                queue:dispatch_get_main_queue()
-           completion:^(MTRBaseDevice * _Nullable device, NSError * _Nullable error) {
-               XCTAssertNotNil(device);
-               XCTAssertNil(error);
-               NSLog(@"Device acquired. Subscribing...");
-               __auto_type * params = [[MTRSubscribeParams alloc] initWithMinInterval:myMinInterval maxInterval:myMaxInterval];
-               [device subscribeToAttributesWithEndpointID:myEndpointId
-                   clusterID:myClusterId
-                   attributeID:myAttributeId
-                   params:params
-                   queue:dispatch_get_main_queue()
-                   reportHandler:^(NSArray<NSDictionary<NSString *, id> *> * _Nullable values, NSError * _Nullable error) {
-                       NSLog(@"Report value: %@", values);
-                       XCTAssertNotNil(values);
-                       XCTAssertNil(error);
-                       XCTAssertTrue([myReport isEqual:values]);
-                       [reportExpectation fulfill];
-                   }
-                   subscriptionEstablished:^{
-                       [establishExpectation fulfill];
-                   }];
-           }];
+    __auto_type * params = [[MTRSubscribeParams alloc] initWithMinInterval:myMinInterval maxInterval:myMaxInterval];
+    __auto_type * device = [MTRBaseDevice deviceWithNodeID:@(myNodeId) controller:_remoteDeviceController];
+    NSLog(@"Device acquired. Subscribing...");
+    [device subscribeToAttributesWithEndpointID:myEndpointId
+        clusterID:myClusterId
+        attributeID:myAttributeId
+        params:params
+        queue:dispatch_get_main_queue()
+        reportHandler:^(NSArray<NSDictionary<NSString *, id> *> * _Nullable values, NSError * _Nullable error) {
+            NSLog(@"Report value: %@", values);
+            XCTAssertNotNil(values);
+            XCTAssertNil(error);
+            XCTAssertTrue([myReport isEqual:values]);
+            [reportExpectation fulfill];
+        }
+        subscriptionEstablished:^{
+            [establishExpectation fulfill];
+        }];
 
     [self waitForExpectations:[NSArray arrayWithObjects:callExpectation, establishExpectation, nil] timeout:kTimeoutInSeconds];
 
@@ -984,15 +914,12 @@
     };
 
     // Deregister report handler
-    [_remoteDeviceController getBaseDevice:myNodeId
-                                     queue:dispatch_get_main_queue()
-                                completion:^(MTRBaseDevice * _Nullable device, NSError * _Nullable error) {
-                                    NSLog(@"Device acquired. Deregistering...");
-                                    [device deregisterReportHandlersWithQueue:dispatch_get_main_queue()
-                                                                   completion:^{
-                                                                       NSLog(@"Deregistered");
-                                                                   }];
-                                }];
+    device = [MTRBaseDevice deviceWithNodeID:@(myNodeId) controller:_remoteDeviceController];
+    NSLog(@"Device acquired. Deregistering...");
+    [device deregisterReportHandlersWithQueue:dispatch_get_main_queue()
+                                   completion:^{
+                                       NSLog(@"Deregistered");
+                                   }];
 
     // Wait for disconnection
     [self waitForExpectations:@[ _xpcDisconnectExpectation, stopExpectation ] timeout:kTimeoutInSeconds];
@@ -1038,29 +965,23 @@
 
     _xpcDisconnectExpectation = [self expectationWithDescription:@"XPC Disconnected"];
 
-    [_remoteDeviceController
-        getBaseDevice:myNodeId
-                queue:dispatch_get_main_queue()
-           completion:^(MTRBaseDevice * _Nullable device, NSError * _Nullable error) {
-               XCTAssertNotNil(device);
-               XCTAssertNil(error);
-               NSLog(@"Device acquired. Subscribing...");
-               [device subscribeToAttributesWithEndpointID:myEndpointId
-                   clusterID:myClusterId
-                   attributeID:myAttributeId
-                   params:myParams
-                   queue:dispatch_get_main_queue()
-                   reportHandler:^(NSArray<NSDictionary<NSString *, id> *> * _Nullable values, NSError * _Nullable error) {
-                       NSLog(@"Report value: %@", values);
-                       XCTAssertNotNil(values);
-                       XCTAssertNil(error);
-                       XCTAssertTrue([myReport isEqual:values]);
-                       [reportExpectation fulfill];
-                   }
-                   subscriptionEstablished:^{
-                       [establishExpectation fulfill];
-                   }];
-           }];
+    __auto_type * device = [MTRBaseDevice deviceWithNodeID:@(myNodeId) controller:_remoteDeviceController];
+    NSLog(@"Device acquired. Subscribing...");
+    [device subscribeToAttributesWithEndpointID:myEndpointId
+        clusterID:myClusterId
+        attributeID:myAttributeId
+        params:myParams
+        queue:dispatch_get_main_queue()
+        reportHandler:^(NSArray<NSDictionary<NSString *, id> *> * _Nullable values, NSError * _Nullable error) {
+            NSLog(@"Report value: %@", values);
+            XCTAssertNotNil(values);
+            XCTAssertNil(error);
+            XCTAssertTrue([myReport isEqual:values]);
+            [reportExpectation fulfill];
+        }
+        subscriptionEstablished:^{
+            [establishExpectation fulfill];
+        }];
 
     [self waitForExpectations:[NSArray arrayWithObjects:callExpectation, establishExpectation, nil] timeout:kTimeoutInSeconds];
 
@@ -1100,15 +1021,12 @@
     };
 
     // Deregister report handler
-    [_remoteDeviceController getBaseDevice:myNodeId
-                                     queue:dispatch_get_main_queue()
-                                completion:^(MTRBaseDevice * _Nullable device, NSError * _Nullable error) {
-                                    NSLog(@"Device acquired. Deregistering...");
-                                    [device deregisterReportHandlersWithQueue:dispatch_get_main_queue()
-                                                                   completion:^{
-                                                                       NSLog(@"Deregistered");
-                                                                   }];
-                                }];
+    device = [MTRBaseDevice deviceWithNodeID:@(myNodeId) controller:_remoteDeviceController];
+    NSLog(@"Device acquired. Deregistering...");
+    [device deregisterReportHandlersWithQueue:dispatch_get_main_queue()
+                                   completion:^{
+                                       NSLog(@"Deregistered");
+                                   }];
 
     // Wait for disconnection
     [self waitForExpectations:@[ _xpcDisconnectExpectation, stopExpectation ] timeout:kTimeoutInSeconds];
@@ -1149,30 +1067,24 @@
 
     _xpcDisconnectExpectation = [self expectationWithDescription:@"XPC Disconnected"];
 
-    [_remoteDeviceController
-        getBaseDevice:myNodeId
-                queue:dispatch_get_main_queue()
-           completion:^(MTRBaseDevice * _Nullable device, NSError * _Nullable error) {
-               XCTAssertNotNil(device);
-               XCTAssertNil(error);
-               NSLog(@"Device acquired. Subscribing...");
-               __auto_type * params = [[MTRSubscribeParams alloc] initWithMinInterval:myMinInterval maxInterval:myMaxInterval];
-               [device subscribeToAttributesWithEndpointID:myEndpointId
-                   clusterID:myClusterId
-                   attributeID:myAttributeId
-                   params:params
-                   queue:dispatch_get_main_queue()
-                   reportHandler:^(NSArray<NSDictionary<NSString *, id> *> * _Nullable values, NSError * _Nullable error) {
-                       NSLog(@"Report value: %@", values);
-                       XCTAssertNotNil(values);
-                       XCTAssertNil(error);
-                       XCTAssertTrue([myReport isEqual:values]);
-                       [reportExpectation fulfill];
-                   }
-                   subscriptionEstablished:^{
-                       [establishExpectation fulfill];
-                   }];
-           }];
+    __auto_type * params = [[MTRSubscribeParams alloc] initWithMinInterval:myMinInterval maxInterval:myMaxInterval];
+    __auto_type * device = [MTRBaseDevice deviceWithNodeID:@(myNodeId) controller:_remoteDeviceController];
+    NSLog(@"Device acquired. Subscribing...");
+    [device subscribeToAttributesWithEndpointID:myEndpointId
+        clusterID:myClusterId
+        attributeID:myAttributeId
+        params:params
+        queue:dispatch_get_main_queue()
+        reportHandler:^(NSArray<NSDictionary<NSString *, id> *> * _Nullable values, NSError * _Nullable error) {
+            NSLog(@"Report value: %@", values);
+            XCTAssertNotNil(values);
+            XCTAssertNil(error);
+            XCTAssertTrue([myReport isEqual:values]);
+            [reportExpectation fulfill];
+        }
+        subscriptionEstablished:^{
+            [establishExpectation fulfill];
+        }];
 
     [self waitForExpectations:[NSArray arrayWithObjects:callExpectation, establishExpectation, nil] timeout:kTimeoutInSeconds];
 
@@ -1210,15 +1122,12 @@
 
     // Deregister report handler
     _xpcDisconnectExpectation.inverted = NO;
-    [_remoteDeviceController getBaseDevice:myNodeId
-                                     queue:dispatch_get_main_queue()
-                                completion:^(MTRBaseDevice * _Nullable device, NSError * _Nullable error) {
-                                    NSLog(@"Device acquired. Deregistering...");
-                                    [device deregisterReportHandlersWithQueue:dispatch_get_main_queue()
-                                                                   completion:^{
-                                                                       NSLog(@"Deregistered");
-                                                                   }];
-                                }];
+    device = [MTRBaseDevice deviceWithNodeID:@(myNodeId) controller:_remoteDeviceController];
+    NSLog(@"Device acquired. Deregistering...");
+    [device deregisterReportHandlersWithQueue:dispatch_get_main_queue()
+                                   completion:^{
+                                       NSLog(@"Deregistered");
+                                   }];
 
     // Wait for disconnection
     [self waitForExpectations:@[ _xpcDisconnectExpectation, stopExpectation ] timeout:kTimeoutInSeconds];
@@ -1260,30 +1169,24 @@
 
     _xpcDisconnectExpectation = [self expectationWithDescription:@"XPC Disconnected"];
 
-    [_remoteDeviceController
-        getBaseDevice:myNodeId
-                queue:dispatch_get_main_queue()
-           completion:^(MTRBaseDevice * _Nullable device, NSError * _Nullable error) {
-               XCTAssertNotNil(device);
-               XCTAssertNil(error);
-               NSLog(@"Device acquired. Subscribing...");
-               __auto_type * params = [[MTRSubscribeParams alloc] initWithMinInterval:myMinInterval maxInterval:myMaxInterval];
-               [device subscribeToAttributesWithEndpointID:myEndpointId
-                   clusterID:myClusterId
-                   attributeID:myAttributeId
-                   params:params
-                   queue:dispatch_get_main_queue()
-                   reportHandler:^(NSArray<NSDictionary<NSString *, id> *> * _Nullable values, NSError * _Nullable error) {
-                       NSLog(@"Report value: %@", values);
-                       XCTAssertNotNil(values);
-                       XCTAssertNil(error);
-                       XCTAssertTrue([myReport isEqual:values]);
-                       [reportExpectation fulfill];
-                   }
-                   subscriptionEstablished:^{
-                       [establishExpectation fulfill];
-                   }];
-           }];
+    __auto_type * params = [[MTRSubscribeParams alloc] initWithMinInterval:myMinInterval maxInterval:myMaxInterval];
+    __auto_type * device = [MTRBaseDevice deviceWithNodeID:@(myNodeId) controller:_remoteDeviceController];
+    NSLog(@"Device acquired. Subscribing...");
+    [device subscribeToAttributesWithEndpointID:myEndpointId
+        clusterID:myClusterId
+        attributeID:myAttributeId
+        params:params
+        queue:dispatch_get_main_queue()
+        reportHandler:^(NSArray<NSDictionary<NSString *, id> *> * _Nullable values, NSError * _Nullable error) {
+            NSLog(@"Report value: %@", values);
+            XCTAssertNotNil(values);
+            XCTAssertNil(error);
+            XCTAssertTrue([myReport isEqual:values]);
+            [reportExpectation fulfill];
+        }
+        subscriptionEstablished:^{
+            [establishExpectation fulfill];
+        }];
 
     [self waitForExpectations:[NSArray arrayWithObjects:callExpectation, establishExpectation, nil] timeout:kTimeoutInSeconds];
 
@@ -1323,15 +1226,12 @@
     };
 
     // Deregister report handler
-    [_remoteDeviceController getBaseDevice:myNodeId
-                                     queue:dispatch_get_main_queue()
-                                completion:^(MTRBaseDevice * _Nullable device, NSError * _Nullable error) {
-                                    NSLog(@"Device acquired. Deregistering...");
-                                    [device deregisterReportHandlersWithQueue:dispatch_get_main_queue()
-                                                                   completion:^{
-                                                                       NSLog(@"Deregistered");
-                                                                   }];
-                                }];
+    device = [MTRBaseDevice deviceWithNodeID:@(myNodeId) controller:_remoteDeviceController];
+    NSLog(@"Device acquired. Deregistering...");
+    [device deregisterReportHandlersWithQueue:dispatch_get_main_queue()
+                                   completion:^{
+                                       NSLog(@"Deregistered");
+                                   }];
 
     // Wait for disconnection
     [self waitForExpectations:@[ _xpcDisconnectExpectation, stopExpectation ] timeout:kTimeoutInSeconds];
@@ -1373,30 +1273,24 @@
 
     _xpcDisconnectExpectation = [self expectationWithDescription:@"XPC Disconnected"];
 
-    [_remoteDeviceController
-        getBaseDevice:myNodeId
-                queue:dispatch_get_main_queue()
-           completion:^(MTRBaseDevice * _Nullable device, NSError * _Nullable error) {
-               XCTAssertNotNil(device);
-               XCTAssertNil(error);
-               NSLog(@"Device acquired. Subscribing...");
-               __auto_type * params = [[MTRSubscribeParams alloc] initWithMinInterval:myMinInterval maxInterval:myMaxInterval];
-               [device subscribeToAttributesWithEndpointID:myEndpointId
-                   clusterID:myClusterId
-                   attributeID:myAttributeId
-                   params:params
-                   queue:dispatch_get_main_queue()
-                   reportHandler:^(NSArray<NSDictionary<NSString *, id> *> * _Nullable values, NSError * _Nullable error) {
-                       NSLog(@"Report value: %@", values);
-                       XCTAssertNotNil(values);
-                       XCTAssertNil(error);
-                       XCTAssertTrue([myReport isEqual:values]);
-                       [reportExpectation fulfill];
-                   }
-                   subscriptionEstablished:^{
-                       [establishExpectation fulfill];
-                   }];
-           }];
+    __auto_type * params = [[MTRSubscribeParams alloc] initWithMinInterval:myMinInterval maxInterval:myMaxInterval];
+    __auto_type * device = [MTRBaseDevice deviceWithNodeID:@(myNodeId) controller:_remoteDeviceController];
+    NSLog(@"Device acquired. Subscribing...");
+    [device subscribeToAttributesWithEndpointID:myEndpointId
+        clusterID:myClusterId
+        attributeID:myAttributeId
+        params:params
+        queue:dispatch_get_main_queue()
+        reportHandler:^(NSArray<NSDictionary<NSString *, id> *> * _Nullable values, NSError * _Nullable error) {
+            NSLog(@"Report value: %@", values);
+            XCTAssertNotNil(values);
+            XCTAssertNil(error);
+            XCTAssertTrue([myReport isEqual:values]);
+            [reportExpectation fulfill];
+        }
+        subscriptionEstablished:^{
+            [establishExpectation fulfill];
+        }];
 
     [self waitForExpectations:[NSArray arrayWithObjects:callExpectation, establishExpectation, nil] timeout:kTimeoutInSeconds];
 
@@ -1436,15 +1330,12 @@
     };
 
     // Deregister report handler
-    [_remoteDeviceController getBaseDevice:myNodeId
-                                     queue:dispatch_get_main_queue()
-                                completion:^(MTRBaseDevice * _Nullable device, NSError * _Nullable error) {
-                                    NSLog(@"Device acquired. Deregistering...");
-                                    [device deregisterReportHandlersWithQueue:dispatch_get_main_queue()
-                                                                   completion:^{
-                                                                       NSLog(@"Deregistered");
-                                                                   }];
-                                }];
+    device = [MTRBaseDevice deviceWithNodeID:@(myNodeId) controller:_remoteDeviceController];
+    NSLog(@"Device acquired. Deregistering...");
+    [device deregisterReportHandlersWithQueue:dispatch_get_main_queue()
+                                   completion:^{
+                                       NSLog(@"Deregistered");
+                                   }];
 
     // Wait for disconnection
     [self waitForExpectations:@[ _xpcDisconnectExpectation, stopExpectation ] timeout:kTimeoutInSeconds];
@@ -1486,30 +1377,24 @@
 
     _xpcDisconnectExpectation = [self expectationWithDescription:@"XPC Disconnected"];
 
-    [_remoteDeviceController
-        getBaseDevice:myNodeId
-                queue:dispatch_get_main_queue()
-           completion:^(MTRBaseDevice * _Nullable device, NSError * _Nullable error) {
-               XCTAssertNotNil(device);
-               XCTAssertNil(error);
-               NSLog(@"Device acquired. Subscribing...");
-               __auto_type * params = [[MTRSubscribeParams alloc] initWithMinInterval:myMinInterval maxInterval:myMaxInterval];
-               [device subscribeToAttributesWithEndpointID:myEndpointId
-                   clusterID:myClusterId
-                   attributeID:myAttributeId
-                   params:params
-                   queue:dispatch_get_main_queue()
-                   reportHandler:^(NSArray<NSDictionary<NSString *, id> *> * _Nullable values, NSError * _Nullable error) {
-                       NSLog(@"Report value: %@", values);
-                       XCTAssertNotNil(values);
-                       XCTAssertNil(error);
-                       XCTAssertTrue([myReport isEqual:values]);
-                       [reportExpectation fulfill];
-                   }
-                   subscriptionEstablished:^{
-                       [establishExpectation fulfill];
-                   }];
-           }];
+    __auto_type * params = [[MTRSubscribeParams alloc] initWithMinInterval:myMinInterval maxInterval:myMaxInterval];
+    __auto_type * device = [MTRBaseDevice deviceWithNodeID:@(myNodeId) controller:_remoteDeviceController];
+    NSLog(@"Device acquired. Subscribing...");
+    [device subscribeToAttributesWithEndpointID:myEndpointId
+        clusterID:myClusterId
+        attributeID:myAttributeId
+        params:params
+        queue:dispatch_get_main_queue()
+        reportHandler:^(NSArray<NSDictionary<NSString *, id> *> * _Nullable values, NSError * _Nullable error) {
+            NSLog(@"Report value: %@", values);
+            XCTAssertNotNil(values);
+            XCTAssertNil(error);
+            XCTAssertTrue([myReport isEqual:values]);
+            [reportExpectation fulfill];
+        }
+        subscriptionEstablished:^{
+            [establishExpectation fulfill];
+        }];
 
     [self waitForExpectations:[NSArray arrayWithObjects:callExpectation, establishExpectation, nil] timeout:kTimeoutInSeconds];
 
@@ -1549,15 +1434,12 @@
     };
 
     // Deregister report handler
-    [_remoteDeviceController getBaseDevice:myNodeId
-                                     queue:dispatch_get_main_queue()
-                                completion:^(MTRBaseDevice * _Nullable device, NSError * _Nullable error) {
-                                    NSLog(@"Device acquired. Deregistering...");
-                                    [device deregisterReportHandlersWithQueue:dispatch_get_main_queue()
-                                                                   completion:^{
-                                                                       NSLog(@"Deregistered");
-                                                                   }];
-                                }];
+    device = [MTRBaseDevice deviceWithNodeID:@(myNodeId) controller:_remoteDeviceController];
+    NSLog(@"Device acquired. Deregistering...");
+    [device deregisterReportHandlersWithQueue:dispatch_get_main_queue()
+                                   completion:^{
+                                       NSLog(@"Deregistered");
+                                   }];
 
     // Wait for disconnection
     [self waitForExpectations:@[ _xpcDisconnectExpectation, stopExpectation ] timeout:kTimeoutInSeconds];
@@ -1599,30 +1481,24 @@
 
     _xpcDisconnectExpectation = [self expectationWithDescription:@"XPC Disconnected"];
 
-    [_remoteDeviceController
-        getBaseDevice:myNodeId
-                queue:dispatch_get_main_queue()
-           completion:^(MTRBaseDevice * _Nullable device, NSError * _Nullable error) {
-               XCTAssertNotNil(device);
-               XCTAssertNil(error);
-               NSLog(@"Device acquired. Subscribing...");
-               __auto_type * params = [[MTRSubscribeParams alloc] initWithMinInterval:myMinInterval maxInterval:myMaxInterval];
-               [device subscribeToAttributesWithEndpointID:myEndpointId
-                   clusterID:myClusterId
-                   attributeID:myAttributeId
-                   params:params
-                   queue:dispatch_get_main_queue()
-                   reportHandler:^(NSArray<NSDictionary<NSString *, id> *> * _Nullable values, NSError * _Nullable error) {
-                       NSLog(@"Report value: %@", values);
-                       XCTAssertNotNil(values);
-                       XCTAssertNil(error);
-                       XCTAssertTrue([myReport isEqual:values]);
-                       [reportExpectation fulfill];
-                   }
-                   subscriptionEstablished:^{
-                       [establishExpectation fulfill];
-                   }];
-           }];
+    __auto_type * params = [[MTRSubscribeParams alloc] initWithMinInterval:myMinInterval maxInterval:myMaxInterval];
+    __auto_type * device = [MTRBaseDevice deviceWithNodeID:@(myNodeId) controller:_remoteDeviceController];
+    NSLog(@"Device acquired. Subscribing...");
+    [device subscribeToAttributesWithEndpointID:myEndpointId
+        clusterID:myClusterId
+        attributeID:myAttributeId
+        params:params
+        queue:dispatch_get_main_queue()
+        reportHandler:^(NSArray<NSDictionary<NSString *, id> *> * _Nullable values, NSError * _Nullable error) {
+            NSLog(@"Report value: %@", values);
+            XCTAssertNotNil(values);
+            XCTAssertNil(error);
+            XCTAssertTrue([myReport isEqual:values]);
+            [reportExpectation fulfill];
+        }
+        subscriptionEstablished:^{
+            [establishExpectation fulfill];
+        }];
 
     [self waitForExpectations:[NSArray arrayWithObjects:callExpectation, establishExpectation, nil] timeout:kTimeoutInSeconds];
 
@@ -1662,15 +1538,12 @@
     };
 
     // Deregister report handler
-    [_remoteDeviceController getBaseDevice:myNodeId
-                                     queue:dispatch_get_main_queue()
-                                completion:^(MTRBaseDevice * _Nullable device, NSError * _Nullable error) {
-                                    NSLog(@"Device acquired. Deregistering...");
-                                    [device deregisterReportHandlersWithQueue:dispatch_get_main_queue()
-                                                                   completion:^{
-                                                                       NSLog(@"Deregistered");
-                                                                   }];
-                                }];
+    device = [MTRBaseDevice deviceWithNodeID:@(myNodeId) controller:_remoteDeviceController];
+    NSLog(@"Device acquired. Deregistering...");
+    [device deregisterReportHandlersWithQueue:dispatch_get_main_queue()
+                                   completion:^{
+                                       NSLog(@"Deregistered");
+                                   }];
 
     // Wait for disconnection
     [self waitForExpectations:@[ _xpcDisconnectExpectation, stopExpectation ] timeout:kTimeoutInSeconds];
@@ -1711,30 +1584,24 @@
 
     _xpcDisconnectExpectation = [self expectationWithDescription:@"XPC Disconnected"];
 
-    [_remoteDeviceController
-        getBaseDevice:myNodeId
-                queue:dispatch_get_main_queue()
-           completion:^(MTRBaseDevice * _Nullable device, NSError * _Nullable error) {
-               XCTAssertNotNil(device);
-               XCTAssertNil(error);
-               NSLog(@"Device acquired. Subscribing...");
-               __auto_type * params = [[MTRSubscribeParams alloc] initWithMinInterval:myMinInterval maxInterval:myMaxInterval];
-               [device subscribeToAttributesWithEndpointID:nil
-                   clusterID:myClusterId
-                   attributeID:myAttributeId
-                   params:params
-                   queue:dispatch_get_main_queue()
-                   reportHandler:^(NSArray<NSDictionary<NSString *, id> *> * _Nullable values, NSError * _Nullable error) {
-                       NSLog(@"Report value: %@", values);
-                       XCTAssertNotNil(values);
-                       XCTAssertNil(error);
-                       XCTAssertTrue([myReport isEqual:values]);
-                       [reportExpectation fulfill];
-                   }
-                   subscriptionEstablished:^{
-                       [establishExpectation fulfill];
-                   }];
-           }];
+    __auto_type * params = [[MTRSubscribeParams alloc] initWithMinInterval:myMinInterval maxInterval:myMaxInterval];
+    __auto_type * device = [MTRBaseDevice deviceWithNodeID:@(myNodeId) controller:_remoteDeviceController];
+    NSLog(@"Device acquired. Subscribing...");
+    [device subscribeToAttributesWithEndpointID:nil
+        clusterID:myClusterId
+        attributeID:myAttributeId
+        params:params
+        queue:dispatch_get_main_queue()
+        reportHandler:^(NSArray<NSDictionary<NSString *, id> *> * _Nullable values, NSError * _Nullable error) {
+            NSLog(@"Report value: %@", values);
+            XCTAssertNotNil(values);
+            XCTAssertNil(error);
+            XCTAssertTrue([myReport isEqual:values]);
+            [reportExpectation fulfill];
+        }
+        subscriptionEstablished:^{
+            [establishExpectation fulfill];
+        }];
 
     [self waitForExpectations:[NSArray arrayWithObjects:callExpectation, establishExpectation, nil] timeout:kTimeoutInSeconds];
 
@@ -1774,15 +1641,12 @@
     };
 
     // Deregister report handler
-    [_remoteDeviceController getBaseDevice:myNodeId
-                                     queue:dispatch_get_main_queue()
-                                completion:^(MTRBaseDevice * _Nullable device, NSError * _Nullable error) {
-                                    NSLog(@"Device acquired. Deregistering...");
-                                    [device deregisterReportHandlersWithQueue:dispatch_get_main_queue()
-                                                                   completion:^{
-                                                                       NSLog(@"Deregistered");
-                                                                   }];
-                                }];
+    device = [MTRBaseDevice deviceWithNodeID:@(myNodeId) controller:_remoteDeviceController];
+    NSLog(@"Device acquired. Deregistering...");
+    [device deregisterReportHandlersWithQueue:dispatch_get_main_queue()
+                                   completion:^{
+                                       NSLog(@"Deregistered");
+                                   }];
 
     // Wait for disconnection
     [self waitForExpectations:@[ _xpcDisconnectExpectation, stopExpectation ] timeout:kTimeoutInSeconds];
@@ -1823,30 +1687,24 @@
 
     _xpcDisconnectExpectation = [self expectationWithDescription:@"XPC Disconnected"];
 
-    [_remoteDeviceController
-        getBaseDevice:myNodeId
-                queue:dispatch_get_main_queue()
-           completion:^(MTRBaseDevice * _Nullable device, NSError * _Nullable error) {
-               XCTAssertNotNil(device);
-               XCTAssertNil(error);
-               NSLog(@"Device acquired. Subscribing...");
-               __auto_type * params = [[MTRSubscribeParams alloc] initWithMinInterval:myMinInterval maxInterval:myMaxInterval];
-               [device subscribeToAttributesWithEndpointID:myEndpointId
-                   clusterID:nil
-                   attributeID:myAttributeId
-                   params:params
-                   queue:dispatch_get_main_queue()
-                   reportHandler:^(NSArray<NSDictionary<NSString *, id> *> * _Nullable values, NSError * _Nullable error) {
-                       NSLog(@"Report value: %@", values);
-                       XCTAssertNotNil(values);
-                       XCTAssertNil(error);
-                       XCTAssertTrue([myReport isEqual:values]);
-                       [reportExpectation fulfill];
-                   }
-                   subscriptionEstablished:^{
-                       [establishExpectation fulfill];
-                   }];
-           }];
+    __auto_type * params = [[MTRSubscribeParams alloc] initWithMinInterval:myMinInterval maxInterval:myMaxInterval];
+    __auto_type * device = [MTRBaseDevice deviceWithNodeID:@(myNodeId) controller:_remoteDeviceController];
+    NSLog(@"Device acquired. Subscribing...");
+    [device subscribeToAttributesWithEndpointID:myEndpointId
+        clusterID:nil
+        attributeID:myAttributeId
+        params:params
+        queue:dispatch_get_main_queue()
+        reportHandler:^(NSArray<NSDictionary<NSString *, id> *> * _Nullable values, NSError * _Nullable error) {
+            NSLog(@"Report value: %@", values);
+            XCTAssertNotNil(values);
+            XCTAssertNil(error);
+            XCTAssertTrue([myReport isEqual:values]);
+            [reportExpectation fulfill];
+        }
+        subscriptionEstablished:^{
+            [establishExpectation fulfill];
+        }];
 
     [self waitForExpectations:[NSArray arrayWithObjects:callExpectation, establishExpectation, nil] timeout:kTimeoutInSeconds];
 
@@ -1886,15 +1744,12 @@
     };
 
     // Deregister report handler
-    [_remoteDeviceController getBaseDevice:myNodeId
-                                     queue:dispatch_get_main_queue()
-                                completion:^(MTRBaseDevice * _Nullable device, NSError * _Nullable error) {
-                                    NSLog(@"Device acquired. Deregistering...");
-                                    [device deregisterReportHandlersWithQueue:dispatch_get_main_queue()
-                                                                   completion:^{
-                                                                       NSLog(@"Deregistered");
-                                                                   }];
-                                }];
+    device = [MTRBaseDevice deviceWithNodeID:@(myNodeId) controller:_remoteDeviceController];
+    NSLog(@"Device acquired. Deregistering...");
+    [device deregisterReportHandlersWithQueue:dispatch_get_main_queue()
+                                   completion:^{
+                                       NSLog(@"Deregistered");
+                                   }];
 
     // Wait for disconnection
     [self waitForExpectations:@[ _xpcDisconnectExpectation, stopExpectation ] timeout:kTimeoutInSeconds];
@@ -1935,30 +1790,24 @@
 
     _xpcDisconnectExpectation = [self expectationWithDescription:@"XPC Disconnected"];
 
-    [_remoteDeviceController
-        getBaseDevice:myNodeId
-                queue:dispatch_get_main_queue()
-           completion:^(MTRBaseDevice * _Nullable device, NSError * _Nullable error) {
-               XCTAssertNotNil(device);
-               XCTAssertNil(error);
-               NSLog(@"Device acquired. Subscribing...");
-               __auto_type * params = [[MTRSubscribeParams alloc] initWithMinInterval:myMinInterval maxInterval:myMaxInterval];
-               [device subscribeToAttributesWithEndpointID:myEndpointId
-                   clusterID:myClusterId
-                   attributeID:nil
-                   params:params
-                   queue:dispatch_get_main_queue()
-                   reportHandler:^(NSArray<NSDictionary<NSString *, id> *> * _Nullable values, NSError * _Nullable error) {
-                       NSLog(@"Report value: %@", values);
-                       XCTAssertNotNil(values);
-                       XCTAssertNil(error);
-                       XCTAssertTrue([myReport isEqual:values]);
-                       [reportExpectation fulfill];
-                   }
-                   subscriptionEstablished:^{
-                       [establishExpectation fulfill];
-                   }];
-           }];
+    __auto_type * params = [[MTRSubscribeParams alloc] initWithMinInterval:myMinInterval maxInterval:myMaxInterval];
+    __auto_type * device = [MTRBaseDevice deviceWithNodeID:@(myNodeId) controller:_remoteDeviceController];
+    NSLog(@"Device acquired. Subscribing...");
+    [device subscribeToAttributesWithEndpointID:myEndpointId
+        clusterID:myClusterId
+        attributeID:nil
+        params:params
+        queue:dispatch_get_main_queue()
+        reportHandler:^(NSArray<NSDictionary<NSString *, id> *> * _Nullable values, NSError * _Nullable error) {
+            NSLog(@"Report value: %@", values);
+            XCTAssertNotNil(values);
+            XCTAssertNil(error);
+            XCTAssertTrue([myReport isEqual:values]);
+            [reportExpectation fulfill];
+        }
+        subscriptionEstablished:^{
+            [establishExpectation fulfill];
+        }];
 
     [self waitForExpectations:[NSArray arrayWithObjects:callExpectation, establishExpectation, nil] timeout:kTimeoutInSeconds];
 
@@ -1998,15 +1847,12 @@
     };
 
     // Deregister report handler
-    [_remoteDeviceController getBaseDevice:myNodeId
-                                     queue:dispatch_get_main_queue()
-                                completion:^(MTRBaseDevice * _Nullable device, NSError * _Nullable error) {
-                                    NSLog(@"Device acquired. Deregistering...");
-                                    [device deregisterReportHandlersWithQueue:dispatch_get_main_queue()
-                                                                   completion:^{
-                                                                       NSLog(@"Deregistered");
-                                                                   }];
-                                }];
+    device = [MTRBaseDevice deviceWithNodeID:@(myNodeId) controller:_remoteDeviceController];
+    NSLog(@"Device acquired. Deregistering...");
+    [device deregisterReportHandlersWithQueue:dispatch_get_main_queue()
+                                   completion:^{
+                                       NSLog(@"Deregistered");
+                                   }];
 
     // Wait for disconnection
     [self waitForExpectations:@[ _xpcDisconnectExpectation, stopExpectation ] timeout:kTimeoutInSeconds];
@@ -2058,30 +1904,24 @@
         myMaxInterval = maxIntervals[i];
         callExpectation = [self expectationWithDescription:[NSString stringWithFormat:@"XPC call (%u) received", i]];
         establishExpectation = [self expectationWithDescription:[NSString stringWithFormat:@"Established (%u) called", i]];
-        [_remoteDeviceController
-            getBaseDevice:myNodeId
-                    queue:dispatch_get_main_queue()
-               completion:^(MTRBaseDevice * _Nullable device, NSError * _Nullable error) {
-                   XCTAssertNotNil(device);
-                   XCTAssertNil(error);
-                   NSLog(@"Device acquired. Subscribing...");
-                   __auto_type * params = [[MTRSubscribeParams alloc] initWithMinInterval:myMinInterval maxInterval:myMaxInterval];
-                   [device subscribeToAttributesWithEndpointID:myEndpointId
-                       clusterID:myClusterId
-                       attributeID:myAttributeId
-                       params:params
-                       queue:dispatch_get_main_queue()
-                       reportHandler:^(NSArray<NSDictionary<NSString *, id> *> * _Nullable values, NSError * _Nullable error) {
-                           NSLog(@"Subscriber [%d] report value: %@", i, values);
-                           XCTAssertNotNil(values);
-                           XCTAssertNil(error);
-                           XCTAssertTrue([myReports[i] isEqual:values]);
-                           [reportExpectations[i] fulfill];
-                       }
-                       subscriptionEstablished:^{
-                           [establishExpectation fulfill];
-                       }];
-               }];
+        __auto_type * params = [[MTRSubscribeParams alloc] initWithMinInterval:myMinInterval maxInterval:myMaxInterval];
+        __auto_type * device = [MTRBaseDevice deviceWithNodeID:@(myNodeId) controller:_remoteDeviceController];
+        NSLog(@"Device acquired. Subscribing...");
+        [device subscribeToAttributesWithEndpointID:myEndpointId
+            clusterID:myClusterId
+            attributeID:myAttributeId
+            params:params
+            queue:dispatch_get_main_queue()
+            reportHandler:^(NSArray<NSDictionary<NSString *, id> *> * _Nullable values, NSError * _Nullable error) {
+                NSLog(@"Subscriber [%d] report value: %@", i, values);
+                XCTAssertNotNil(values);
+                XCTAssertNil(error);
+                XCTAssertTrue([myReports[i] isEqual:values]);
+                [reportExpectations[i] fulfill];
+            }
+            subscriptionEstablished:^{
+                [establishExpectation fulfill];
+            }];
 
         [self waitForExpectations:[NSArray arrayWithObjects:callExpectation, establishExpectation, nil] timeout:kTimeoutInSeconds];
     }
@@ -2132,16 +1972,12 @@
 
     // Deregister report handler for first subscriber
     __auto_type deregisterExpectation = [self expectationWithDescription:@"First subscriber deregistered"];
-    [_remoteDeviceController getBaseDevice:nodeToStop
-                                     queue:dispatch_get_main_queue()
-                                completion:^(MTRBaseDevice * _Nullable device, NSError * _Nullable error) {
-                                    NSLog(@"Device acquired. Deregistering...");
-                                    [device deregisterReportHandlersWithQueue:dispatch_get_main_queue()
-                                                                   completion:^{
-                                                                       NSLog(@"Deregistered");
-                                                                       [deregisterExpectation fulfill];
-                                                                   }];
-                                }];
+    __auto_type * device = [MTRBaseDevice deviceWithNodeID:@(nodeToStop) controller:_remoteDeviceController];
+    [device deregisterReportHandlersWithQueue:dispatch_get_main_queue()
+                                   completion:^{
+                                       NSLog(@"Deregistered");
+                                       [deregisterExpectation fulfill];
+                                   }];
 
     [self waitForExpectations:@[ stopExpectation, deregisterExpectation ] timeout:kTimeoutInSeconds];
 
@@ -2190,16 +2026,13 @@
 
     // Deregister report handler for second subscriber
     __auto_type secondDeregisterExpectation = [self expectationWithDescription:@"Second subscriber deregistered"];
-    [_remoteDeviceController getBaseDevice:nodeToStop
-                                     queue:dispatch_get_main_queue()
-                                completion:^(MTRBaseDevice * _Nullable device, NSError * _Nullable error) {
-                                    NSLog(@"Device acquired. Deregistering...");
-                                    [device deregisterReportHandlersWithQueue:dispatch_get_main_queue()
-                                                                   completion:^{
-                                                                       NSLog(@"Deregistered");
-                                                                       [secondDeregisterExpectation fulfill];
-                                                                   }];
-                                }];
+    device = [MTRBaseDevice deviceWithNodeID:@(nodeToStop) controller:_remoteDeviceController];
+    NSLog(@"Device acquired. Deregistering...");
+    [device deregisterReportHandlersWithQueue:dispatch_get_main_queue()
+                                   completion:^{
+                                       NSLog(@"Deregistered");
+                                       [secondDeregisterExpectation fulfill];
+                                   }];
 
     // Wait for deregistration and disconnection
     [self waitForExpectations:@[ secondDeregisterExpectation, _xpcDisconnectExpectation, stopExpectation ]
@@ -2245,6 +2078,31 @@
 {
     NSString * myUUID = [[NSUUID UUID] UUIDString];
     uint64_t myNodeId = 9876543210;
+    NSNumber * myEndpointId = @100;
+    NSNumber * myClusterId = @200;
+    NSNumber * myAttributeId = @300;
+    NSArray * myValues = @[ @{
+        @"attributePath" : [MTRAttributePath attributePathWithEndpointID:myEndpointId
+                                                               clusterID:myClusterId
+                                                             attributeID:myAttributeId],
+        @"data" : @ { @"type" : @"SignedInteger", @"value" : @123456 }
+    } ];
+
+    XCTestExpectation * callExpectation = [self expectationWithDescription:@"XPC call received"];
+    XCTestExpectation * responseExpectation = [self expectationWithDescription:@"XPC response received"];
+
+    _handleReadAttribute = ^(id controller, NSNumber * nodeId, NSNumber * _Nullable endpointId, NSNumber * _Nullable clusterId,
+        NSNumber * _Nullable attributeId, MTRReadParams * _Nullable params,
+        void (^completion)(id _Nullable values, NSError * _Nullable error)) {
+        XCTAssertTrue([controller isEqualToString:myUUID]);
+        XCTAssertEqual([nodeId unsignedLongLongValue], myNodeId);
+        XCTAssertEqual([endpointId unsignedShortValue], [myEndpointId unsignedShortValue]);
+        XCTAssertEqual([clusterId unsignedLongValue], [myClusterId unsignedLongValue]);
+        XCTAssertEqual([attributeId unsignedLongValue], [myAttributeId unsignedLongValue]);
+        XCTAssertNil(params);
+        [callExpectation fulfill];
+        completion([MTRDeviceController encodeXPCResponseValues:myValues], nil);
+    };
 
     __auto_type unspecifiedRemoteDeviceController =
         [MTRDeviceController sharedControllerWithID:nil
@@ -2259,17 +2117,30 @@
         [anySharedRemoteControllerCallExpectation fulfill];
     };
 
-    __auto_type deviceAcquired = [self expectationWithDescription:@"Connected device was acquired"];
-    [unspecifiedRemoteDeviceController getBaseDevice:myNodeId
-                                               queue:dispatch_get_main_queue()
-                                          completion:^(MTRBaseDevice * _Nullable device, NSError * _Nullable error) {
-                                              XCTAssertNotNil(device);
-                                              XCTAssertNil(error);
-                                              [deviceAcquired fulfill];
-                                          }];
+    __auto_type * device = [MTRBaseDevice deviceWithNodeID:@(myNodeId) controller:unspecifiedRemoteDeviceController];
+    // Do a read to exercise the device.
+    NSLog(@"Device acquired. Reading...");
+    [device readAttributesWithEndpointID:myEndpointId
+                               clusterID:myClusterId
+                             attributeID:myAttributeId
+                                  params:nil
+                                   queue:dispatch_get_main_queue()
+                              completion:^(id _Nullable value, NSError * _Nullable error) {
+                                  NSLog(@"Read value: %@", value);
+                                  XCTAssertNotNil(value);
+                                  XCTAssertNil(error);
+                                  XCTAssertTrue([myValues isEqual:value]);
+                                  [responseExpectation fulfill];
+                                  self.xpcDisconnectExpectation = [self expectationWithDescription:@"XPC Disconnected"];
+                              }];
 
-    [self waitForExpectations:[NSArray arrayWithObjects:anySharedRemoteControllerCallExpectation, deviceAcquired, nil]
+    [self waitForExpectations:[NSArray arrayWithObjects:anySharedRemoteControllerCallExpectation, callExpectation,
+                                       responseExpectation, nil]
                       timeout:kTimeoutInSeconds];
+
+    // When read is done, connection should have been released
+    [self waitForExpectations:[NSArray arrayWithObject:_xpcDisconnectExpectation] timeout:kTimeoutInSeconds];
+    XCTAssertNil(_xpcConnection);
 }
 
 - (void)testSubscribeAttributeCacheSuccess
@@ -2517,24 +2388,19 @@
                                                                           return nil;
                                                                       }];
 
-    [failingDeviceController getBaseDevice:myNodeId
-                                     queue:dispatch_get_main_queue()
-                                completion:^(MTRBaseDevice * _Nullable device, NSError * _Nullable error) {
-                                    XCTAssertNotNil(device);
-                                    XCTAssertNil(error);
-                                    NSLog(@"Device acquired. Reading...");
-                                    [device readAttributesWithEndpointID:myEndpointId
-                                                               clusterID:myClusterId
-                                                             attributeID:myAttributeId
-                                                                  params:nil
-                                                                   queue:dispatch_get_main_queue()
-                                                              completion:^(id _Nullable value, NSError * _Nullable error) {
-                                                                  NSLog(@"Read value: %@", value);
-                                                                  XCTAssertNil(value);
-                                                                  XCTAssertNotNil(error);
-                                                                  [responseExpectation fulfill];
-                                                              }];
-                                }];
+    __auto_type * device = [MTRBaseDevice deviceWithNodeID:@(myNodeId) controller:failingDeviceController];
+    NSLog(@"Device acquired. Reading...");
+    [device readAttributesWithEndpointID:myEndpointId
+                               clusterID:myClusterId
+                             attributeID:myAttributeId
+                                  params:nil
+                                   queue:dispatch_get_main_queue()
+                              completion:^(id _Nullable value, NSError * _Nullable error) {
+                                  NSLog(@"Read value: %@", value);
+                                  XCTAssertNil(value);
+                                  XCTAssertNotNil(error);
+                                  [responseExpectation fulfill];
+                              }];
 
     [self waitForExpectations:@[ responseExpectation ] timeout:kTimeoutInSeconds];
 }