[Darwin] Implement multiple read and subscribe (#25840)

* Implement darwin multiple attribute, event read / subscribe

* restyle

* Add Exception check

* Remove unused code

* Restyle

* Modify from comment

* Restyle

* Add MTRAttributeRequestPath, EventRequestPath

* restyle

* Update src/darwin/Framework/CHIP/MTRBaseDevice.h

Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>

* Update src/darwin/Framework/CHIP/MTRBaseDevice.h

Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>

* Update src/darwin/Framework/CHIP/MTRBaseDevice.h

Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>

* Update src/darwin/Framework/CHIP/MTRBaseDevice.h

Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>

* Update src/darwin/Framework/CHIP/MTRBaseDevice.h

Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>

* Update src/darwin/Framework/CHIP/MTRBaseDevice.h

Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>

* Update src/darwin/Framework/CHIP/MTRBaseDevice.h

Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>

* Update src/darwin/Framework/CHIP/MTRBaseDevice.h

Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>

* Update src/darwin/Framework/CHIP/MTRBaseDevice.h

Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>

* Update src/darwin/Framework/CHIP/MTRBaseDevice.h

Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>

* Update src/darwin/Framework/CHIP/MTRBaseDevice.h

Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>

* Update src/darwin/Framework/CHIP/MTRBaseDevice.h

Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>

* Update src/darwin/Framework/CHIP/MTRBaseDevice.h

Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>

* Update src/darwin/Framework/CHIP/MTRBaseDevice.h

Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>

* Update src/darwin/Framework/CHIP/MTRBaseDevice.h

Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>

* Update src/darwin/Framework/CHIP/MTRBaseDevice.mm

Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>

* Update src/darwin/Framework/CHIP/MTRBaseDevice.h

Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>

* Update src/darwin/Framework/CHIP/MTRBaseDevice.h

Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>

* Update src/darwin/Framework/CHIP/MTRBaseDevice.h

Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>

* Update src/darwin/Framework/CHIP/MTRBaseDevice.mm

Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>

* Update src/darwin/Framework/CHIP/MTRBaseDevice.mm

Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>

* Update src/darwin/Framework/CHIP/MTRBaseDevice.mm

Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>

* Update src/darwin/Framework/CHIP/MTRBaseDevice.mm

Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>

* Update src/darwin/Framework/CHIP/MTRBaseDevice.mm

Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>

* Update src/darwin/Framework/CHIP/MTRBaseDevice.mm

Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>

* Update src/darwin/Framework/CHIP/MTRBaseDevice.mm

Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>

* Update src/darwin/Framework/CHIP/MTRBaseDevice.mm

Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>

* Update src/darwin/Framework/CHIP/MTRBaseDevice.mm

Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>

* fix build error

* restyle

* Add set null eventPath

* Modify memort allocation

* restyle

* Modify delete -> memoryfree

* Update code

* remove unused code

* restyle

* Fix crash in Test

* Add TestCases, modify some issues

* restyle

* Add comment, modify testcase

* Update src/darwin/Framework/CHIP/MTRBaseDevice.h

Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>

* Update src/darwin/Framework/CHIP/MTRBaseDevice.mm

Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>

* Update src/darwin/Framework/CHIP/MTRBaseDevice.mm

Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>

* Update src/darwin/Framework/CHIP/MTRBaseDevice.h

Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>

* Update src/darwin/Framework/CHIP/MTRBaseDevice.h

Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>

* Update src/darwin/Framework/CHIP/MTRBaseDevice.h

Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>

* Update src/darwin/Framework/CHIP/MTRBaseDevice.h

Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>

* modify code location, add some patch

* modify typo

* Add assert code

* restyle

* Update src/darwin/Framework/CHIP/MTRBaseDevice.mm

Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>

* Update src/darwin/Framework/CHIP/MTRBaseDevice.mm

Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>

* Add subscribe null check

* modify

* Modify using uniqur_ptr

* restyle

* Revert "restyle"

This reverts commit f855ea573ccbfa8b136b6fc349ee235ab00cc91c.

* Revert "Modify using uniqur_ptr"

This reverts commit 40eca604e7d71c71580aa67c89902f6abab21e1d.

* modify using scopeBuffer

* restyle

* Dispatch resubscription callback to the right queue.

---------

Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>
diff --git a/src/app/EventPathParams.h b/src/app/EventPathParams.h
index 0a8cea3..29af405 100644
--- a/src/app/EventPathParams.h
+++ b/src/app/EventPathParams.h
@@ -43,6 +43,9 @@
     inline bool HasWildcardEndpointId() const { return mEndpointId == kInvalidEndpointId; }
     inline bool HasWildcardClusterId() const { return mClusterId == kInvalidClusterId; }
     inline bool HasWildcardEventId() const { return mEventId == kInvalidEventId; }
+    inline void SetWildcardEndpointId() { mEndpointId = kInvalidEndpointId; }
+    inline void SetWildcardClusterId() { mClusterId = kInvalidClusterId; }
+    inline void SetWildcardEventId() { mEventId = kInvalidEventId; }
 
     bool IsEventPathSupersetOf(const ConcreteEventPath & other) const
     {
diff --git a/src/darwin/Framework/CHIP/MTRBaseDevice.h b/src/darwin/Framework/CHIP/MTRBaseDevice.h
index a0ff37f..fecf875 100644
--- a/src/darwin/Framework/CHIP/MTRBaseDevice.h
+++ b/src/darwin/Framework/CHIP/MTRBaseDevice.h
@@ -130,6 +130,38 @@
     MTRTransportTypeTCP,
 } API_AVAILABLE(ios(16.4), macos(13.3), watchos(9.4), tvos(16.4));
 
+/**
+ * A path indicating an attribute being requested (for read or subscribe).
+ *
+ * nil is used to represent wildcards.
+ */
+MTR_NEWLY_AVAILABLE
+@interface MTRAttributeRequestPath : NSObject <NSCopying>
+@property (nonatomic, readonly, copy, nullable) NSNumber * endpoint;
+@property (nonatomic, readonly, copy, nullable) NSNumber * cluster;
+@property (nonatomic, readonly, copy, nullable) NSNumber * attribute;
+
++ (MTRAttributeRequestPath *)requestPathWithEndpointID:(NSNumber * _Nullable)endpointID
+                                             clusterID:(NSNumber * _Nullable)clusterID
+                                           attributeID:(NSNumber * _Nullable)attributeID MTR_NEWLY_AVAILABLE;
+@end
+
+/**
+ * A path indicating an event being requested (for read or subscribe).
+ *
+ * nil is used to represent wildcards.
+ */
+MTR_NEWLY_AVAILABLE
+@interface MTREventRequestPath : NSObject <NSCopying>
+@property (nonatomic, readonly, copy, nullable) NSNumber * endpoint;
+@property (nonatomic, readonly, copy, nullable) NSNumber * cluster;
+@property (nonatomic, readonly, copy, nullable) NSNumber * event;
+
++ (MTREventRequestPath *)requestPathWithEndpointID:(NSNumber * _Nullable)endpointID
+                                         clusterID:(NSNumber * _Nullable)clusterID
+                                           eventID:(NSNumber * _Nullable)eventID MTR_NEWLY_AVAILABLE;
+@end
+
 @interface MTRBaseDevice : NSObject
 
 - (instancetype)init NS_UNAVAILABLE;
@@ -230,6 +262,26 @@
     API_AVAILABLE(ios(16.4), macos(13.3), watchos(9.4), tvos(16.4));
 
 /**
+ * Reads multiple attribute or event paths from the device.
+ *
+ * Nil is treated as an empty array for attributePaths and eventPaths.
+ *
+ * Lists of attribute and event paths to read can be provided via attributePaths and eventPaths.
+ *
+ * The completion will be called with an error if the input parameters are invalid (e.g., both attributePaths and eventPaths are
+ * empty.) or the entire read interaction fails. Otherwise it will be called with values, which may be empty (e.g. if no paths
+ * matched the wildcard paths passed in) or may include per-path errors if particular paths failed.
+ *
+ * If the sum of the lengths of attributePaths and eventPaths exceeds 9, the read may fail due to the device not supporting that
+ * many read paths.
+ */
+- (void)readAttributePaths:(NSArray<MTRAttributeRequestPath *> * _Nullable)attributePaths
+                eventPaths:(NSArray<MTREventRequestPath *> * _Nullable)eventPaths
+                    params:(MTRReadParams * _Nullable)params
+                     queue:(dispatch_queue_t)queue
+                completion:(MTRDeviceResponseHandler)completion MTR_NEWLY_AVAILABLE;
+
+/**
  * Write to attribute in a designated attribute path
  *
  * @param value       A data-value NSDictionary object as described in
@@ -306,6 +358,27 @@
     API_AVAILABLE(ios(16.4), macos(13.3), watchos(9.4), tvos(16.4));
 
 /**
+ * Subscribes to multiple attribute or event paths.
+ *
+ * Nil is treated as an empty array for attributePaths and eventPaths.
+ *
+ * Lists of attribute and event paths to subscribe to can be provided via attributePaths and eventPaths.
+ *
+ * The reportHandler will be called with an error if the inputs are invalid (e.g., both attributePaths and eventPaths are
+ * empty), or if the subscription fails entirely.
+ *
+ * If the sum of the lengths of attributePaths and eventPaths exceeds 3, the subscribe may fail due to the device not supporting
+ * that many paths for a subscription.
+ */
+- (void)subscribeToAttributePaths:(NSArray<MTRAttributeRequestPath *> * _Nullable)attributePaths
+                       eventPaths:(NSArray<MTREventRequestPath *> * _Nullable)eventPaths
+                           params:(MTRSubscribeParams * _Nullable)params
+                            queue:(dispatch_queue_t)queue
+                    reportHandler:(MTRDeviceResponseHandler)reportHandler
+          subscriptionEstablished:(MTRSubscriptionEstablishedHandler _Nullable)subscriptionEstablished
+          resubscriptionScheduled:(MTRDeviceResubscriptionScheduledHandler _Nullable)resubscriptionScheduled MTR_NEWLY_AVAILABLE;
+
+/**
  * Deregister all local report handlers for a remote device
  *
  * This method is applicable only for a remote device. For a local device, the stack has to be shutdown to stop report handlers.
diff --git a/src/darwin/Framework/CHIP/MTRBaseDevice.mm b/src/darwin/Framework/CHIP/MTRBaseDevice.mm
index 09f5ea5..a8e585d 100644
--- a/src/darwin/Framework/CHIP/MTRBaseDevice.mm
+++ b/src/darwin/Framework/CHIP/MTRBaseDevice.mm
@@ -147,9 +147,17 @@
                     container.readClientPtr = nullptr;
                 }
                 if (container.pathParams) {
-                    Platform::Delete(container.pathParams);
+                    static_assert(std::is_trivially_destructible<AttributePathParams>::value,
+                        "AttributePathParams destructors won't get run");
+                    Platform::MemoryFree(container.pathParams);
                     container.pathParams = nullptr;
                 }
+                if (container.eventPathParams) {
+                    static_assert(
+                        std::is_trivially_destructible<EventPathParams>::value, "EventPathParams destructors won't get run");
+                    Platform::MemoryFree(container.eventPathParams);
+                    container.eventPathParams = nullptr;
+                }
             }
             [listToDelete removeAllObjects];
             if (completion) {
@@ -223,9 +231,15 @@
         _readClientPtr = nullptr;
     }
     if (_pathParams) {
-        Platform::Delete(_pathParams);
+        static_assert(std::is_trivially_destructible<AttributePathParams>::value, "AttributePathParams destructors won't get run");
+        Platform::MemoryFree(_pathParams);
         _pathParams = nullptr;
     }
+    if (_eventPathParams) {
+        static_assert(std::is_trivially_destructible<EventPathParams>::value, "EventPathParams destructors won't get run");
+        Platform::MemoryFree(_eventPathParams);
+        _eventPathParams = nullptr;
+    }
     PurgeCompletedReadClientContainers(_deviceID);
 }
 
@@ -236,9 +250,15 @@
         _readClientPtr = nullptr;
     }
     if (_pathParams) {
-        Platform::Delete(_pathParams);
+        static_assert(std::is_trivially_destructible<AttributePathParams>::value, "AttributePathParams destructors won't get run");
+        Platform::MemoryFree(_pathParams);
         _pathParams = nullptr;
     }
+    if (_eventPathParams) {
+        static_assert(std::is_trivially_destructible<EventPathParams>::value, "EventPathParams destructors won't get run");
+        Platform::MemoryFree(_eventPathParams);
+        _eventPathParams = nullptr;
+    }
 }
 @end
 
@@ -730,22 +750,31 @@
 
 template <typename DecodableValueType> class BufferedReadClientCallback final : public app::ReadClient::Callback {
 public:
-    using OnSuccessCallbackType
-        = std::function<void(const app::ConcreteClusterPath & aPath, const uint32_t aValueId, const DecodableValueType & aData)>;
-    using OnErrorCallbackType
-        = std::function<void(const app::ConcreteClusterPath * aPath, const uint32_t aValueId, CHIP_ERROR aError)>;
+    using OnSuccessAttributeCallbackType
+        = std::function<void(const ConcreteAttributePath & aPath, const DecodableValueType & aData)>;
+    using OnSuccessEventCallbackType = std::function<void(const ConcreteEventPath & aPath, const DecodableValueType & aData)>;
+    using OnErrorCallbackType = std::function<void(
+        const app::ConcreteAttributePath * attributePath, const app::ConcreteEventPath * eventPath, CHIP_ERROR aError)>;
     using OnDoneCallbackType = std::function<void(BufferedReadClientCallback * callback)>;
     using OnSubscriptionEstablishedCallbackType = std::function<void()>;
+    using OnDeviceResubscriptionScheduledCallbackType = std::function<void(NSError * error, NSNumber * resubscriptionDelay)>;
 
-    BufferedReadClientCallback(ClusterId aClusterId, uint32_t aValueId, OnSuccessCallbackType aOnSuccess,
+    BufferedReadClientCallback(app::AttributePathParams * aAttributePathParamsList, size_t aAttributePathParamsSize,
+        app::EventPathParams * aEventPathParamsList, size_t aEventPathParamsSize,
+        OnSuccessAttributeCallbackType aOnAttributeSuccess, OnSuccessEventCallbackType aOnEventSuccess,
         OnErrorCallbackType aOnError, OnDoneCallbackType aOnDone,
-        OnSubscriptionEstablishedCallbackType aOnSubscriptionEstablished = nullptr)
-        : mClusterId(aClusterId)
-        , mValueId(aValueId)
-        , mOnSuccess(aOnSuccess)
+        OnSubscriptionEstablishedCallbackType aOnSubscriptionEstablished = nullptr,
+        OnDeviceResubscriptionScheduledCallbackType aOnDeviceResubscriptionScheduled = nullptr)
+        : mAttributePathParamsList(aAttributePathParamsList)
+        , mAttributePathParamsSize(aAttributePathParamsSize)
+        , mEventPathParamsList(aEventPathParamsList)
+        , mEventPathParamsSize(aEventPathParamsSize)
+        , mOnAttributeSuccess(aOnAttributeSuccess)
+        , mOnEventSuccess(aOnEventSuccess)
         , mOnError(aOnError)
         , mOnDone(aOnDone)
         , mOnSubscriptionEstablished(aOnSubscriptionEstablished)
+        , mOnDeviceResubscriptionScheduled(aOnDeviceResubscriptionScheduled)
         , mBufferedReadAdapter(*this)
     {
     }
@@ -768,6 +797,10 @@
         CHIP_ERROR err = CHIP_NO_ERROR;
         DecodableValueType value;
 
+        VerifyOrExit(mOnAttributeSuccess != nullptr, err = CHIP_ERROR_INVALID_ARGUMENT);
+
+        VerifyOrExit(mAttributePathParamsList != nullptr, err = CHIP_ERROR_INVALID_ARGUMENT);
+
         //
         // We shouldn't be getting list item operations in the provided path since that should be handled by the buffered read
         // callback. If we do, that's a bug.
@@ -775,18 +808,20 @@
         VerifyOrDie(!aPath.IsListItemOperation());
 
         VerifyOrExit(aStatus.IsSuccess(), err = aStatus.ToChipError());
-        VerifyOrExit((aPath.mClusterId == mClusterId || mClusterId == kInvalidClusterId)
-                && (aPath.mAttributeId == mValueId || mValueId == kInvalidAttributeId),
+        VerifyOrExit(
+            std::find_if(mAttributePathParamsList, mAttributePathParamsList + mAttributePathParamsSize,
+                [aPath](app::AttributePathParams & pathParam) -> bool { return pathParam.IsAttributePathSupersetOf(aPath); })
+                != mAttributePathParamsList + mAttributePathParamsSize,
             err = CHIP_ERROR_SCHEMA_MISMATCH);
         VerifyOrExit(apData != nullptr, err = CHIP_ERROR_INVALID_ARGUMENT);
 
         SuccessOrExit(err = app::DataModel::Decode(*apData, value));
 
-        mOnSuccess(aPath, aPath.mAttributeId, value);
+        mOnAttributeSuccess(aPath, value);
 
     exit:
         if (err != CHIP_NO_ERROR) {
-            mOnError(&aPath, aPath.mAttributeId, err);
+            mOnError(&aPath, nullptr, err);
         }
     }
 
@@ -795,22 +830,29 @@
         CHIP_ERROR err = CHIP_NO_ERROR;
         DecodableValueType value;
 
-        VerifyOrExit((aEventHeader.mPath.mClusterId == mClusterId || mClusterId == kInvalidClusterId)
-                && (aEventHeader.mPath.mEventId == mValueId || mValueId == kInvalidEventId),
+        VerifyOrExit(mOnEventSuccess != nullptr, err = CHIP_ERROR_INVALID_ARGUMENT);
+
+        VerifyOrExit(mEventPathParamsList != nullptr, err = CHIP_ERROR_INVALID_ARGUMENT);
+
+        VerifyOrExit(std::find_if(mEventPathParamsList, mEventPathParamsList + mEventPathParamsSize,
+                         [aEventHeader](app::EventPathParams & pathParam) -> bool {
+                             return pathParam.IsEventPathSupersetOf(aEventHeader.mPath);
+                         })
+                != mEventPathParamsList + mEventPathParamsSize,
             err = CHIP_ERROR_SCHEMA_MISMATCH);
         VerifyOrExit(apData != nullptr, err = CHIP_ERROR_INVALID_ARGUMENT);
 
         SuccessOrExit(err = app::DataModel::Decode(*apData, value));
 
-        mOnSuccess(aEventHeader.mPath, aEventHeader.mPath.mEventId, value);
+        mOnEventSuccess(aEventHeader.mPath, value);
 
     exit:
         if (err != CHIP_NO_ERROR) {
-            mOnError(&aEventHeader.mPath, aEventHeader.mPath.mEventId, err);
+            mOnError(nullptr, &aEventHeader.mPath, err);
         }
     }
 
-    void OnError(CHIP_ERROR aError) override { mOnError(nullptr, kInvalidAttributeId, aError); }
+    void OnError(CHIP_ERROR aError) override { mOnError(nullptr, nullptr, aError); }
 
     void OnDone(ReadClient *) override { mOnDone(this); }
 
@@ -821,16 +863,34 @@
         }
     }
 
+    CHIP_ERROR OnResubscriptionNeeded(ReadClient * apReadClient, CHIP_ERROR aTerminationCause) override
+    {
+        CHIP_ERROR err = ReadClient::Callback::OnResubscriptionNeeded(apReadClient, aTerminationCause);
+        ReturnErrorOnFailure(err);
+
+        if (mOnDeviceResubscriptionScheduled != nullptr) {
+            auto callback = mOnDeviceResubscriptionScheduled;
+            auto error = [MTRError errorForCHIPErrorCode:aTerminationCause];
+            auto delayMs = @(apReadClient->ComputeTimeTillNextSubscription());
+            callback(error, delayMs);
+        }
+        return CHIP_NO_ERROR;
+    }
+
     void OnDeallocatePaths(chip::app::ReadPrepareParams && aReadPrepareParams) override {}
 
-    ClusterId mClusterId;
-    uint32_t mValueId;
-    OnSuccessCallbackType mOnSuccess;
+    OnSuccessAttributeCallbackType mOnAttributeSuccess;
+    OnSuccessEventCallbackType mOnEventSuccess;
     OnErrorCallbackType mOnError;
     OnDoneCallbackType mOnDone;
     OnSubscriptionEstablishedCallbackType mOnSubscriptionEstablished;
+    OnDeviceResubscriptionScheduledCallbackType mOnDeviceResubscriptionScheduled;
     app::BufferedReadCallback mBufferedReadAdapter;
     Platform::UniquePtr<app::ReadClient> mReadClient;
+    app::AttributePathParams * mAttributePathParamsList;
+    app::EventPathParams * mEventPathParamsList;
+    size_t mAttributePathParamsSize;
+    size_t mEventPathParamsSize;
 };
 
 - (void)readAttributesWithEndpointID:(NSNumber * _Nullable)endpointID
@@ -840,9 +900,39 @@
                                queue:(dispatch_queue_t)queue
                           completion:(MTRDeviceResponseHandler)completion
 {
-    endpointID = (endpointID == nil) ? nil : [endpointID copy];
-    clusterID = (clusterID == nil) ? nil : [clusterID copy];
-    attributeID = (attributeID == nil) ? nil : [attributeID copy];
+    NSArray<MTRAttributeRequestPath *> * attributePaths = [NSArray
+        arrayWithObject:[MTRAttributeRequestPath requestPathWithEndpointID:endpointID clusterID:clusterID attributeID:attributeID]];
+    [self readAttributePaths:attributePaths eventPaths:nil params:params queue:queue completion:completion];
+}
+
+- (void)readAttributePaths:(NSArray<MTRAttributeRequestPath *> * _Nullable)attributePaths
+                eventPaths:(NSArray<MTREventRequestPath *> * _Nullable)eventPaths
+                    params:(MTRReadParams * _Nullable)params
+                     queue:(dispatch_queue_t)queue
+                completion:(MTRDeviceResponseHandler)completion
+{
+    if ((attributePaths == nil || [attributePaths count] == 0) && (eventPaths == nil || [eventPaths count] == 0)) {
+        dispatch_async(queue, ^{
+            completion(nil, [MTRError errorForCHIPErrorCode:CHIP_ERROR_INVALID_ARGUMENT]);
+        });
+        return;
+    }
+
+    NSMutableArray<MTRAttributeRequestPath *> * attributes = nil;
+    if (attributePaths != nil) {
+        attributes = [[NSMutableArray alloc] init];
+        for (MTRAttributeRequestPath * attributePath in attributePaths) {
+            [attributes addObject:[attributePath copy]];
+        }
+    }
+
+    NSMutableArray<MTREventRequestPath *> * events = nil;
+    if (eventPaths != nil) {
+        events = [[NSMutableArray alloc] init];
+        for (MTRAttributeRequestPath * eventPath in eventPaths) {
+            [events addObject:[eventPath copy]];
+        }
+    }
     params = (params == nil) ? nil : [params copy];
     auto * bridge = new MTRDataValueDictionaryCallbackBridge(queue, completion,
         ^(ExchangeManager & exchangeManager, const SessionHandle & session, MTRDataValueDictionaryCallback successCb,
@@ -853,21 +943,32 @@
             auto interactionStatus = std::make_shared<CHIP_ERROR>(CHIP_NO_ERROR);
 
             auto resultArray = [[NSMutableArray alloc] init];
-            auto onSuccessCb = [resultArray](const app::ConcreteClusterPath & clusterPath, const uint32_t aValueId,
-                                   const MTRDataValueDictionaryDecodableType & aData) {
-                app::ConcreteAttributePath attribPath(clusterPath.mEndpointId, clusterPath.mClusterId, aValueId);
-                [resultArray addObject:@ {
-                    MTRAttributePathKey : [[MTRAttributePath alloc] initWithPath:attribPath],
-                    MTRDataKey : aData.GetDecodedObject()
-                }];
-            };
+            auto onAttributeSuccessCb
+                = [resultArray](const ConcreteAttributePath & attributePath, const MTRDataValueDictionaryDecodableType & aData) {
+                      [resultArray addObject:@ {
+                          MTRAttributePathKey : [[MTRAttributePath alloc] initWithPath:attributePath],
+                          MTRDataKey : aData.GetDecodedObject()
+                      }];
+                  };
 
-            auto onFailureCb = [resultArray, interactionStatus](
-                                   const app::ConcreteClusterPath * clusterPath, const uint32_t aValueId, CHIP_ERROR aError) {
-                if (clusterPath) {
-                    app::ConcreteAttributePath attribPath(clusterPath->mEndpointId, clusterPath->mClusterId, aValueId);
+            auto onEventSuccessCb
+                = [resultArray](const ConcreteEventPath & eventPath, const MTRDataValueDictionaryDecodableType & aData) {
+                      [resultArray addObject:@ {
+                          MTREventPathKey : [[MTREventPath alloc] initWithPath:eventPath],
+                          MTRDataKey : aData.GetDecodedObject()
+                      }];
+                  };
+
+            auto onFailureCb = [resultArray, interactionStatus](const app::ConcreteAttributePath * attributePath,
+                                   const app::ConcreteEventPath * eventPath, CHIP_ERROR aError) {
+                if (attributePath != nullptr) {
                     [resultArray addObject:@ {
-                        MTRAttributePathKey : [[MTRAttributePath alloc] initWithPath:attribPath],
+                        MTRAttributePathKey : [[MTRAttributePath alloc] initWithPath:*attributePath],
+                        MTRErrorKey : [MTRError errorForCHIPErrorCode:aError]
+                    }];
+                } else if (eventPath != nullptr) {
+                    [resultArray addObject:@ {
+                        MTREventPathKey : [[MTREventPath alloc] initWithPath:*eventPath],
                         MTRErrorKey : [MTRError errorForCHIPErrorCode:aError]
                     }];
                 } else {
@@ -878,38 +979,60 @@
                 }
             };
 
-            app::AttributePathParams attributePath;
-            if (endpointID) {
-                attributePath.mEndpointId = static_cast<chip::EndpointId>([endpointID unsignedShortValue]);
+            Platform::ScopedMemoryBuffer<AttributePathParams> attributePathParamsList;
+            Platform::ScopedMemoryBuffer<EventPathParams> eventPathParamsList;
+
+            if (attributes != nil) {
+                size_t count = 0;
+                VerifyOrReturnError(attributePathParamsList.Calloc([attributes count]), CHIP_ERROR_NO_MEMORY);
+                for (MTRAttributeRequestPath * attribute in attributes) {
+                    [attribute convertToAttributePathParams:attributePathParamsList[count++]];
+                }
             }
-            if (clusterID) {
-                attributePath.mClusterId = static_cast<chip::ClusterId>([clusterID unsignedLongValue]);
+
+            if (events != nil) {
+                size_t count = 0;
+                VerifyOrReturnError(eventPathParamsList.Calloc([events count]), CHIP_ERROR_NO_MEMORY);
+                for (MTREventRequestPath * event in events) {
+                    [event convertToEventPathParams:eventPathParamsList[count++]];
+                }
             }
-            if (attributeID) {
-                attributePath.mAttributeId = static_cast<chip::AttributeId>([attributeID unsignedLongValue]);
-            }
+
             app::InteractionModelEngine * engine = app::InteractionModelEngine::GetInstance();
             CHIP_ERROR err = CHIP_NO_ERROR;
 
             chip::app::ReadPrepareParams readParams(session);
             [params toReadPrepareParams:readParams];
-            readParams.mpAttributePathParamsList = &attributePath;
-            readParams.mAttributePathParamsListSize = 1;
+            readParams.mpAttributePathParamsList = attributePathParamsList.Get();
+            readParams.mAttributePathParamsListSize = [attributePaths count];
+            readParams.mpEventPathParamsList = eventPathParamsList.Get();
+            readParams.mEventPathParamsListSize = [eventPaths count];
 
-            auto onDone = [resultArray, interactionStatus, bridge, successCb, failureCb](
-                              BufferedReadClientCallback<MTRDataValueDictionaryDecodableType> * callback) {
-                if (*interactionStatus != CHIP_NO_ERROR) {
-                    // Failure
-                    failureCb(bridge, *interactionStatus);
-                } else {
-                    // Success
-                    successCb(bridge, resultArray);
-                }
-                chip::Platform::Delete(callback);
-            };
+            AttributePathParams * attributePathParamsListToFree = attributePathParamsList.Get();
+            EventPathParams * eventPathParamsListToFree = eventPathParamsList.Get();
+
+            auto onDone
+                = [resultArray, interactionStatus, bridge, successCb, failureCb, attributePathParamsListToFree,
+                      eventPathParamsListToFree](BufferedReadClientCallback<MTRDataValueDictionaryDecodableType> * callback) {
+                      if (*interactionStatus != CHIP_NO_ERROR) {
+                          // Failure
+                          failureCb(bridge, *interactionStatus);
+                      } else {
+                          // Success
+                          successCb(bridge, resultArray);
+                      }
+                      if (attributePathParamsListToFree != nullptr) {
+                          Platform::MemoryFree(attributePathParamsListToFree);
+                      }
+                      if (eventPathParamsListToFree != nullptr) {
+                          Platform::MemoryFree(eventPathParamsListToFree);
+                      }
+                      chip::Platform::Delete(callback);
+                  };
 
             auto callback = chip::Platform::MakeUnique<BufferedReadClientCallback<MTRDataValueDictionaryDecodableType>>(
-                attributePath.mClusterId, attributePath.mAttributeId, onSuccessCb, onFailureCb, onDone, nullptr);
+                attributePathParamsList.Get(), readParams.mAttributePathParamsListSize, eventPathParamsList.Get(),
+                readParams.mEventPathParamsListSize, onAttributeSuccessCb, onEventSuccessCb, onFailureCb, onDone, nullptr);
             VerifyOrReturnError(callback != nullptr, CHIP_ERROR_NO_MEMORY);
 
             auto readClient = chip::Platform::MakeUnique<app::ReadClient>(
@@ -929,6 +1052,8 @@
             //
             callback->AdoptReadClient(std::move(readClient));
             callback.release();
+            attributePathParamsList.Release();
+            eventPathParamsList.Release();
             return err;
         });
     std::move(*bridge).DispatchAction(self);
@@ -1141,6 +1266,32 @@
                               reportHandler:(MTRDeviceResponseHandler)reportHandler
                     subscriptionEstablished:(MTRSubscriptionEstablishedHandler)subscriptionEstablished
 {
+    NSArray<MTRAttributeRequestPath *> * attributePaths = [NSArray
+        arrayWithObject:[MTRAttributeRequestPath requestPathWithEndpointID:endpointID clusterID:clusterID attributeID:attributeID]];
+    [self subscribeToAttributePaths:attributePaths
+                         eventPaths:nil
+                             params:params
+                              queue:queue
+                      reportHandler:reportHandler
+            subscriptionEstablished:subscriptionEstablished
+            resubscriptionScheduled:nil];
+}
+
+- (void)subscribeToAttributePaths:(NSArray<MTRAttributeRequestPath *> * _Nullable)attributePaths
+                       eventPaths:(NSArray<MTREventRequestPath *> * _Nullable)eventPaths
+                           params:(MTRSubscribeParams * _Nullable)params
+                            queue:(dispatch_queue_t)queue
+                    reportHandler:(MTRDeviceResponseHandler)reportHandler
+          subscriptionEstablished:(MTRSubscriptionEstablishedHandler _Nullable)subscriptionEstablished
+          resubscriptionScheduled:(MTRDeviceResubscriptionScheduledHandler _Nullable)resubscriptionScheduled
+{
+    if ((attributePaths == nil || [attributePaths count] == 0) && (eventPaths == nil || [eventPaths count] == 0)) {
+        dispatch_async(queue, ^{
+            reportHandler(nil, [MTRError errorForCHIPErrorCode:CHIP_ERROR_INVALID_ARGUMENT]);
+        });
+        return;
+    }
+
     if (self.isPASEDevice) {
         // We don't support subscriptions over PASE.
         dispatch_async(queue, ^{
@@ -1150,9 +1301,22 @@
     }
 
     // Copy params before going async.
-    endpointID = (endpointID == nil) ? nil : [endpointID copy];
-    clusterID = (clusterID == nil) ? nil : [clusterID copy];
-    attributeID = (attributeID == nil) ? nil : [attributeID copy];
+    NSMutableArray<MTRAttributeRequestPath *> * attributes = nil;
+    if (attributePaths != nil) {
+        attributes = [[NSMutableArray alloc] init];
+        for (MTRAttributeRequestPath * attributePath in attributePaths) {
+            [attributes addObject:[attributePath copy]];
+        }
+    }
+
+    NSMutableArray<MTREventRequestPath *> * events = nil;
+    if (eventPaths != nil) {
+        events = [[NSMutableArray alloc] init];
+        for (MTRAttributeRequestPath * eventPath in eventPaths) {
+            [events addObject:[eventPath copy]];
+        }
+    }
+
     params = (params == nil) ? nil : [params copy];
 
     [self.deviceController
@@ -1168,10 +1332,10 @@
                        return;
                    }
 
-                   auto onReportCb = [queue, reportHandler](const app::ConcreteClusterPath & clusterPath, const uint32_t aValueId,
-                                         const MTRDataValueDictionaryDecodableType & data) {
+                   auto onAttributeReportCb = [queue, reportHandler](const ConcreteAttributePath & attributePath,
+                                                  const MTRDataValueDictionaryDecodableType & data) {
                        id valueObject = data.GetDecodedObject();
-                       app::ConcreteAttributePath pathCopy(clusterPath.mEndpointId, clusterPath.mClusterId, aValueId);
+                       ConcreteAttributePath pathCopy(attributePath);
                        dispatch_async(queue, ^{
                            reportHandler(@[ @ {
                                MTRAttributePathKey : [[MTRAttributePath alloc] initWithPath:pathCopy],
@@ -1181,9 +1345,22 @@
                        });
                    };
 
+                   auto onEventReportCb = [queue, reportHandler](const ConcreteEventPath & eventPath,
+                                              const MTRDataValueDictionaryDecodableType & data) {
+                       id valueObject = data.GetDecodedObject();
+                       ConcreteEventPath pathCopy(eventPath);
+                       dispatch_async(queue, ^{
+                           reportHandler(
+                               @[ @ { MTREventPathKey : [[MTREventPath alloc] initWithPath:pathCopy], MTRDataKey : valueObject } ],
+                               nil);
+                       });
+                   };
+
                    auto establishedOrFailed = chip::Platform::MakeShared<BOOL>(NO);
                    auto onFailureCb = [establishedOrFailed, queue, subscriptionEstablished, reportHandler](
-                                          const app::ConcreteClusterPath * clusterPath, const uint32_t aValueId, CHIP_ERROR error) {
+                                          const app::ConcreteAttributePath * attributePath,
+                                          const app::ConcreteEventPath * eventPath, CHIP_ERROR error) {
+                       // TODO, Requires additional logic if attributePath or eventPath is not null
                        if (!(*establishedOrFailed)) {
                            *establishedOrFailed = YES;
                            if (subscriptionEstablished) {
@@ -1207,17 +1384,45 @@
                        }
                    };
 
+                   auto onResubscriptionScheduledCb
+                       = [queue, resubscriptionScheduled](NSError * error, NSNumber * resubscriptionDelay) {
+                             if (resubscriptionScheduled) {
+                                 dispatch_async(queue, ^{
+                                     resubscriptionScheduled(error, resubscriptionDelay);
+                                 });
+                             }
+                         };
+
                    MTRReadClientContainer * container = [[MTRReadClientContainer alloc] init];
                    container.deviceID = self.nodeID;
-                   container.pathParams = Platform::New<app::AttributePathParams>();
-                   if (endpointID) {
-                       container.pathParams->mEndpointId = static_cast<chip::EndpointId>([endpointID unsignedShortValue]);
+
+                   size_t attributePathSize = 0;
+                   if (attributes != nil) {
+                       container.pathParams = static_cast<AttributePathParams *>(
+                           Platform::MemoryCalloc([attributes count], sizeof(AttributePathParams)));
+                       if (container.pathParams == nullptr) {
+                           dispatch_async(queue, ^{
+                               reportHandler(nil, [MTRError errorForCHIPErrorCode:CHIP_ERROR_NO_MEMORY]);
+                           });
+                           return;
+                       }
+                       for (MTRAttributeRequestPath * attribute in attributes) {
+                           [attribute convertToAttributePathParams:container.pathParams[attributePathSize++]];
+                       }
                    }
-                   if (clusterID) {
-                       container.pathParams->mClusterId = static_cast<chip::ClusterId>([clusterID unsignedLongValue]);
-                   }
-                   if (attributeID) {
-                       container.pathParams->mAttributeId = static_cast<chip::AttributeId>([attributeID unsignedLongValue]);
+                   size_t eventPathSize = 0;
+                   if (events != nil) {
+                       container.eventPathParams
+                           = static_cast<EventPathParams *>(Platform::MemoryCalloc([events count], sizeof(EventPathParams)));
+                       if (container.eventPathParams == nullptr) {
+                           dispatch_async(queue, ^{
+                               reportHandler(nil, [MTRError errorForCHIPErrorCode:CHIP_ERROR_NO_MEMORY]);
+                           });
+                           return;
+                       }
+                       for (MTREventRequestPath * event in events) {
+                           [event convertToEventPathParams:container.eventPathParams[eventPathSize++]];
+                       }
                    }
 
                    app::InteractionModelEngine * engine = app::InteractionModelEngine::GetInstance();
@@ -1226,7 +1431,9 @@
                    chip::app::ReadPrepareParams readParams(session.Value());
                    [params toReadPrepareParams:readParams];
                    readParams.mpAttributePathParamsList = container.pathParams;
-                   readParams.mAttributePathParamsListSize = 1;
+                   readParams.mAttributePathParamsListSize = attributePathSize;
+                   readParams.mpEventPathParamsList = container.eventPathParams;
+                   readParams.mEventPathParamsListSize = eventPathSize;
 
                    auto onDone = [container](BufferedReadClientCallback<MTRDataValueDictionaryDecodableType> * callback) {
                        [container onDone];
@@ -1236,8 +1443,8 @@
                    };
 
                    auto callback = chip::Platform::MakeUnique<BufferedReadClientCallback<MTRDataValueDictionaryDecodableType>>(
-                       container.pathParams->mClusterId, container.pathParams->mAttributeId, onReportCb, onFailureCb, onDone,
-                       onEstablishedCb);
+                       container.pathParams, attributePathSize, container.eventPathParams, eventPathSize, onAttributeReportCb,
+                       onEventReportCb, onFailureCb, onDone, onEstablishedCb, onResubscriptionScheduledCb);
 
                    auto readClient = Platform::New<app::ReadClient>(
                        engine, exchangeManager, callback->GetBufferedCallback(), chip::app::ReadClient::InteractionType::Subscribe);
@@ -1255,8 +1462,15 @@
                            });
                        }
                        Platform::Delete(readClient);
-                       Platform::Delete(container.pathParams);
+                       if (container.pathParams != nullptr) {
+                           Platform::MemoryFree(container.pathParams);
+                       }
+
+                       if (container.eventPathParams != nullptr) {
+                           Platform::MemoryFree(container.eventPathParams);
+                       }
                        container.pathParams = nullptr;
+                       container.eventPathParams = nullptr;
                        return;
                    }
 
@@ -1498,97 +1712,10 @@
                            queue:(dispatch_queue_t)queue
                       completion:(MTRDeviceResponseHandler)completion
 {
-    endpointID = (endpointID == nil) ? nil : [endpointID copy];
-    clusterID = (clusterID == nil) ? nil : [clusterID copy];
-    eventID = (eventID == nil) ? nil : [eventID copy];
-    params = (params == nil) ? nil : [params copy];
-    auto * bridge = new MTRDataValueDictionaryCallbackBridge(queue, completion,
-        ^(ExchangeManager & exchangeManager, const SessionHandle & session, MTRDataValueDictionaryCallback successCb,
-            MTRErrorCallback failureCb, MTRCallbackBridgeBase * bridge) {
-            // interactionStatus tracks whether the whole read interaction has failed.
-            //
-            // Make sure interactionStatus survives even if this block scope is destroyed.
-            auto interactionStatus = std::make_shared<CHIP_ERROR>(CHIP_NO_ERROR);
-
-            auto resultArray = [[NSMutableArray alloc] init];
-            auto onSuccessCb = [resultArray](const app::ConcreteClusterPath & clusterPath, const uint32_t aValueId,
-                                   const MTRDataValueDictionaryDecodableType & aData) {
-                app::ConcreteEventPath eventPath(clusterPath.mEndpointId, clusterPath.mClusterId, aValueId);
-                [resultArray addObject:@ {
-                    MTREventPathKey : [[MTREventPath alloc] initWithPath:eventPath],
-                    MTRDataKey : aData.GetDecodedObject()
-                }];
-            };
-
-            auto onFailureCb = [resultArray, interactionStatus](
-                                   const app::ConcreteClusterPath * clusterPath, const uint32_t aValueId, CHIP_ERROR aError) {
-                if (clusterPath) {
-                    app::ConcreteEventPath eventPath(clusterPath->mEndpointId, clusterPath->mClusterId, aValueId);
-                    [resultArray addObject:@ {
-                        MTREventPathKey : [[MTREventPath alloc] initWithPath:eventPath],
-                        MTRErrorKey : [MTRError errorForCHIPErrorCode:aError]
-                    }];
-                } else {
-                    // This will only happen once per read interaction, and
-                    // after that there will be no more calls to onFailureCb or
-                    // onSuccessCb.
-                    *interactionStatus = aError;
-                }
-            };
-
-            app::EventPathParams eventPath;
-            if (endpointID) {
-                eventPath.mEndpointId = static_cast<chip::EndpointId>([endpointID unsignedShortValue]);
-            }
-            if (clusterID) {
-                eventPath.mClusterId = static_cast<chip::ClusterId>([clusterID unsignedLongValue]);
-            }
-            if (eventID) {
-                eventPath.mEventId = static_cast<chip::EventId>([eventID unsignedLongValue]);
-            }
-            app::InteractionModelEngine * engine = app::InteractionModelEngine::GetInstance();
-            CHIP_ERROR err = CHIP_NO_ERROR;
-
-            chip::app::ReadPrepareParams readParams(session);
-            [params toReadPrepareParams:readParams];
-            readParams.mpEventPathParamsList = &eventPath;
-            readParams.mEventPathParamsListSize = 1;
-
-            auto onDone = [resultArray, interactionStatus, bridge, successCb, failureCb](
-                              BufferedReadClientCallback<MTRDataValueDictionaryDecodableType> * callback) {
-                if (*interactionStatus != CHIP_NO_ERROR) {
-                    // Failure
-                    failureCb(bridge, *interactionStatus);
-                } else {
-                    successCb(bridge, resultArray);
-                }
-                chip::Platform::Delete(callback);
-            };
-
-            auto callback = chip::Platform::MakeUnique<BufferedReadClientCallback<MTRDataValueDictionaryDecodableType>>(
-                eventPath.mClusterId, eventPath.mEventId, onSuccessCb, onFailureCb, onDone, nullptr);
-            VerifyOrReturnError(callback != nullptr, CHIP_ERROR_NO_MEMORY);
-
-            auto readClient = chip::Platform::MakeUnique<app::ReadClient>(
-                engine, &exchangeManager, callback->GetBufferedCallback(), chip::app::ReadClient::InteractionType::Read);
-            VerifyOrReturnError(readClient != nullptr, CHIP_ERROR_NO_MEMORY);
-
-            err = readClient->SendRequest(readParams);
-
-            if (err != CHIP_NO_ERROR) {
-                return err;
-            }
-
-            //
-            // At this point, we'll get a callback through the OnDone callback above regardless of success or failure
-            // of the read operation to permit us to free up the callback object. So, release ownership of the callback
-            // object now to prevent it from being reclaimed at the end of this scoped block.
-            //
-            callback->AdoptReadClient(std::move(readClient));
-            callback.release();
-            return err;
-        });
-    std::move(*bridge).DispatchAction(self);
+    NSArray<MTREventRequestPath *> * eventPaths = [NSArray arrayWithObject:[MTREventRequestPath requestPathWithEndpointID:endpointID
+                                                                                                                clusterID:clusterID
+                                                                                                                  eventID:eventID]];
+    [self readAttributePaths:nil eventPaths:eventPaths params:params queue:queue completion:completion];
 }
 
 - (void)subscribeToEventsWithEndpointID:(NSNumber * _Nullable)endpointID
@@ -1599,129 +1726,16 @@
                           reportHandler:(MTRDeviceResponseHandler)reportHandler
                 subscriptionEstablished:(MTRSubscriptionEstablishedHandler)subscriptionEstablished
 {
-    if (self.isPASEDevice) {
-        // We don't support subscriptions over PASE.
-        dispatch_async(queue, ^{
-            reportHandler(nil, [MTRError errorForCHIPErrorCode:CHIP_ERROR_INCORRECT_STATE]);
-        });
-        return;
-    }
-
-    // Copy params before going async.
-    endpointID = (endpointID == nil) ? nil : [endpointID copy];
-    clusterID = (clusterID == nil) ? nil : [clusterID copy];
-    eventID = (eventID == nil) ? nil : [eventID copy];
-    params = (params == nil) ? nil : [params copy];
-
-    [self.deviceController
-        getSessionForNode:self.nodeID
-               completion:^(ExchangeManager * _Nullable exchangeManager, const Optional<SessionHandle> & session,
-                   NSError * _Nullable error) {
-                   if (error != nil) {
-                       if (reportHandler) {
-                           dispatch_async(queue, ^{
-                               reportHandler(nil, error);
-                           });
-                       }
-                       return;
-                   }
-
-                   auto onReportCb = [queue, reportHandler](const app::ConcreteClusterPath & clusterPath, const uint32_t aValueId,
-                                         const MTRDataValueDictionaryDecodableType & data) {
-                       id valueObject = data.GetDecodedObject();
-                       app::ConcreteEventPath pathCopy(clusterPath.mEndpointId, clusterPath.mClusterId, aValueId);
-                       dispatch_async(queue, ^{
-                           reportHandler(
-                               @[ @ { MTREventPathKey : [[MTREventPath alloc] initWithPath:pathCopy], MTRDataKey : valueObject } ],
-                               nil);
-                       });
-                   };
-
-                   auto establishedOrFailed = chip::Platform::MakeShared<BOOL>(NO);
-                   auto onFailureCb = [establishedOrFailed, queue, subscriptionEstablished, reportHandler](
-                                          const app::ConcreteClusterPath * clusterPath, const uint32_t aValueId, CHIP_ERROR error) {
-                       if (!(*establishedOrFailed)) {
-                           *establishedOrFailed = YES;
-                           if (subscriptionEstablished) {
-                               dispatch_async(queue, subscriptionEstablished);
-                           }
-                       }
-                       if (reportHandler) {
-                           dispatch_async(queue, ^{
-                               reportHandler(nil, [MTRError errorForCHIPErrorCode:error]);
-                           });
-                       }
-                   };
-
-                   auto onEstablishedCb = [establishedOrFailed, queue, subscriptionEstablished]() {
-                       if (*establishedOrFailed) {
-                           return;
-                       }
-                       *establishedOrFailed = YES;
-                       if (subscriptionEstablished) {
-                           dispatch_async(queue, subscriptionEstablished);
-                       }
-                   };
-
-                   MTRReadClientContainer * container = [[MTRReadClientContainer alloc] init];
-                   container.deviceID = self.nodeID;
-                   container.eventPathParams = Platform::New<app::EventPathParams>();
-                   if (endpointID) {
-                       container.eventPathParams->mEndpointId = static_cast<chip::EndpointId>([endpointID unsignedShortValue]);
-                   }
-                   if (clusterID) {
-                       container.eventPathParams->mClusterId = static_cast<chip::ClusterId>([clusterID unsignedLongValue]);
-                   }
-                   if (eventID) {
-                       container.eventPathParams->mEventId = static_cast<chip::EventId>([eventID unsignedLongValue]);
-                   }
-                   container.eventPathParams->mIsUrgentEvent = params.reportEventsUrgently;
-
-                   app::InteractionModelEngine * engine = app::InteractionModelEngine::GetInstance();
-                   CHIP_ERROR err = CHIP_NO_ERROR;
-
-                   chip::app::ReadPrepareParams readParams(session.Value());
-                   [params toReadPrepareParams:readParams];
-                   readParams.mpEventPathParamsList = container.eventPathParams;
-                   readParams.mEventPathParamsListSize = 1;
-
-                   auto onDone = [container](BufferedReadClientCallback<MTRDataValueDictionaryDecodableType> * callback) {
-                       [container onDone];
-                       // Make sure we delete callback last, because doing that actually destroys our
-                       // lambda, so we can't access captured values after that.
-                       chip::Platform::Delete(callback);
-                   };
-
-                   auto callback = chip::Platform::MakeUnique<BufferedReadClientCallback<MTRDataValueDictionaryDecodableType>>(
-                       container.eventPathParams->mClusterId, container.eventPathParams->mEventId, onReportCb, onFailureCb, onDone,
-                       onEstablishedCb);
-
-                   auto readClient = Platform::New<app::ReadClient>(
-                       engine, exchangeManager, callback->GetBufferedCallback(), chip::app::ReadClient::InteractionType::Subscribe);
-
-                   if (!params.resubscribeAutomatically) {
-                       err = readClient->SendRequest(readParams);
-                   } else {
-                       err = readClient->SendAutoResubscribeRequest(std::move(readParams));
-                   }
-
-                   if (err != CHIP_NO_ERROR) {
-                       if (reportHandler) {
-                           dispatch_async(queue, ^{
-                               reportHandler(nil, [MTRError errorForCHIPErrorCode:err]);
-                           });
-                       }
-                       Platform::Delete(readClient);
-                       Platform::Delete(container.eventPathParams);
-                       container.eventPathParams = nullptr;
-                       return;
-                   }
-
-                   // Read clients will be purged when deregistered.
-                   container.readClientPtr = readClient;
-                   AddReadClientContainer(container.deviceID, container);
-                   callback.release();
-               }];
+    NSArray<MTREventRequestPath *> * eventPaths = [NSArray arrayWithObject:[MTREventRequestPath requestPathWithEndpointID:endpointID
+                                                                                                                clusterID:clusterID
+                                                                                                                  eventID:eventID]];
+    [self subscribeToAttributePaths:nil
+                         eventPaths:eventPaths
+                             params:params
+                              queue:queue
+                      reportHandler:reportHandler
+            subscriptionEstablished:subscriptionEstablished
+            resubscriptionScheduled:nil];
 }
 @end
 
@@ -1837,6 +1851,150 @@
 
 @end
 
+@implementation MTRAttributeRequestPath
+- (instancetype)initWithEndpointID:(NSNumber * _Nullable)endpointID
+                         clusterID:(NSNumber * _Nullable)clusterID
+                       attributeID:(NSNumber * _Nullable)attributeID
+{
+    _endpoint = [endpointID copy];
+    _cluster = [clusterID copy];
+    _attribute = [attributeID copy];
+    return self;
+}
+
+- (NSString *)description
+{
+    return [NSString stringWithFormat:@"<MTRAttributeRequestPath> endpoint %u cluster %u attribute %u",
+                     (uint16_t) _endpoint.unsignedShortValue, (uint32_t) _cluster.unsignedLongValue,
+                     (uint32_t) _attribute.unsignedLongValue];
+}
+
++ (MTRAttributeRequestPath *)requestPathWithEndpointID:(NSNumber * _Nullable)endpointID
+                                             clusterID:(NSNumber * _Nullable)clusterID
+                                           attributeID:(NSNumber * _Nullable)attributeID
+{
+
+    return [[MTRAttributeRequestPath alloc] initWithEndpointID:endpointID clusterID:clusterID attributeID:attributeID];
+}
+
+- (BOOL)isEqualToAttributeRequestPath:(MTRAttributeRequestPath *)path
+{
+    return [_endpoint isEqualToNumber:path.endpoint] && [_cluster isEqualToNumber:path.cluster] &&
+        [_attribute isEqualToNumber:path.attribute];
+}
+
+- (BOOL)isEqual:(id)object
+{
+    if (![object isKindOfClass:[self class]]) {
+        return NO;
+    }
+    return [self isEqualToAttributeRequestPath:object];
+}
+
+- (NSUInteger)hash
+{
+    return _endpoint.unsignedShortValue ^ _cluster.unsignedLongValue ^ _attribute.unsignedLongValue;
+}
+
+- (id)copyWithZone:(NSZone *)zone
+{
+    return [MTRAttributeRequestPath requestPathWithEndpointID:_endpoint clusterID:_cluster attributeID:_attribute];
+}
+
+- (void)convertToAttributePathParams:(chip::app::AttributePathParams &)params
+{
+    if (_endpoint != nil) {
+        params.mEndpointId = static_cast<chip::EndpointId>(_endpoint.unsignedShortValue);
+    } else {
+        params.SetWildcardEndpointId();
+    }
+
+    if (_cluster != nil) {
+        params.mClusterId = static_cast<chip::ClusterId>(_cluster.unsignedLongValue);
+    } else {
+        params.SetWildcardClusterId();
+    }
+
+    if (_attribute != nil) {
+        params.mAttributeId = static_cast<chip::AttributeId>(_attribute.unsignedLongValue);
+    } else {
+        params.SetWildcardAttributeId();
+    }
+}
+@end
+
+@implementation MTREventRequestPath
+- (instancetype)initWithEndpointID:(NSNumber * _Nullable)endpointID
+                         clusterID:(NSNumber * _Nullable)clusterID
+                           eventID:(NSNumber * _Nullable)eventID
+{
+    _endpoint = [endpointID copy];
+    _cluster = [clusterID copy];
+    _event = [eventID copy];
+    return self;
+}
+
+- (NSString *)description
+{
+    return [NSString stringWithFormat:@"<MTREventRequestPath> endpoint %u cluster %u event %u",
+                     (uint16_t) _endpoint.unsignedShortValue, (uint32_t) _cluster.unsignedLongValue,
+                     (uint32_t) _event.unsignedLongValue];
+}
+
++ (MTREventRequestPath *)requestPathWithEndpointID:(NSNumber * _Nullable)endpointID
+                                         clusterID:(NSNumber * _Nullable)clusterID
+                                           eventID:(NSNumber * _Nullable)eventID
+{
+
+    return [[MTREventRequestPath alloc] initWithEndpointID:endpointID clusterID:clusterID eventID:eventID];
+}
+
+- (BOOL)isEqualToEventRequestPath:(MTREventRequestPath *)path
+{
+    return
+        [_endpoint isEqualToNumber:path.endpoint] && [_cluster isEqualToNumber:path.cluster] && [_event isEqualToNumber:path.event];
+}
+
+- (BOOL)isEqual:(id)object
+{
+    if (![object isKindOfClass:[self class]]) {
+        return NO;
+    }
+    return [self isEqualToEventRequestPath:object];
+}
+
+- (NSUInteger)hash
+{
+    return _endpoint.unsignedShortValue ^ _cluster.unsignedLongValue ^ _event.unsignedLongValue;
+}
+
+- (id)copyWithZone:(NSZone *)zone
+{
+    return [MTREventRequestPath requestPathWithEndpointID:_endpoint clusterID:_cluster eventID:_event];
+}
+
+- (void)convertToEventPathParams:(chip::app::EventPathParams &)params
+{
+    if (_endpoint != nil) {
+        params.mEndpointId = static_cast<chip::EndpointId>(_endpoint.unsignedShortValue);
+    } else {
+        params.SetWildcardEndpointId();
+    }
+
+    if (_cluster != nil) {
+        params.mClusterId = static_cast<chip::ClusterId>(_cluster.unsignedLongValue);
+    } else {
+        params.SetWildcardClusterId();
+    }
+
+    if (_event != nil) {
+        params.mEventId = static_cast<chip::EventId>(_event.unsignedLongValue);
+    } else {
+        params.SetWildcardEventId();
+    }
+}
+@end
+
 @implementation MTRClusterPath
 - (instancetype)initWithPath:(const ConcreteClusterPath &)path
 {
diff --git a/src/darwin/Framework/CHIP/MTRBaseDevice_Internal.h b/src/darwin/Framework/CHIP/MTRBaseDevice_Internal.h
index 9fb062a..cf5944a 100644
--- a/src/darwin/Framework/CHIP/MTRBaseDevice_Internal.h
+++ b/src/darwin/Framework/CHIP/MTRBaseDevice_Internal.h
@@ -18,11 +18,13 @@
 #import "MTRBaseDevice.h"
 #import <Foundation/Foundation.h>
 
+#include <app/AttributePathParams.h>
 #include <app/ConcreteAttributePath.h>
 #include <app/ConcreteCommandPath.h>
 #include <app/ConcreteEventPath.h>
 #include <app/DeviceProxy.h>
 #include <app/EventLoggingTypes.h>
+#include <app/EventPathParams.h>
 
 @class MTRDeviceController;
 
@@ -106,6 +108,14 @@
                        error:(NSError * _Nullable)error;
 @end
 
+@interface MTRAttributeRequestPath ()
+- (void)convertToAttributePathParams:(chip::app::AttributePathParams &)params;
+@end
+
+@interface MTREventRequestPath ()
+- (void)convertToEventPathParams:(chip::app::EventPathParams &)params;
+@end
+
 // Exported utility function
 // Convert TLV data into data-value dictionary as described in MTRDeviceResponseHandler
 id _Nullable MTRDecodeDataValueDictionaryFromCHIPTLV(chip::TLV::TLVReader * data);
diff --git a/src/darwin/Framework/CHIPTests/MTRDeviceTests.m b/src/darwin/Framework/CHIPTests/MTRDeviceTests.m
index 8075738..f649a04 100644
--- a/src/darwin/Framework/CHIPTests/MTRDeviceTests.m
+++ b/src/darwin/Framework/CHIPTests/MTRDeviceTests.m
@@ -1617,6 +1617,280 @@
                  enforceOrder:YES];
 }
 
+- (void)test020_ReadMultipleAttributes
+{
+    XCTestExpectation * expectation =
+        [self expectationWithDescription:@"read Multiple Attributes (Descriptor, Basic Information Cluster) for all endpoints"];
+
+    MTRBaseDevice * device = GetConnectedDevice();
+    dispatch_queue_t queue = dispatch_get_main_queue();
+
+    NSArray<MTRAttributeRequestPath *> * attributePaths =
+        [NSArray arrayWithObjects:[MTRAttributeRequestPath requestPathWithEndpointID:nil clusterID:@29 attributeID:@0],
+                 [MTRAttributeRequestPath requestPathWithEndpointID:nil clusterID:@29 attributeID:@1],
+                 [MTRAttributeRequestPath requestPathWithEndpointID:nil clusterID:@29 attributeID:@2],
+                 [MTRAttributeRequestPath requestPathWithEndpointID:nil clusterID:@29 attributeID:@3],
+                 [MTRAttributeRequestPath requestPathWithEndpointID:nil clusterID:@29 attributeID:@4],
+                 [MTRAttributeRequestPath requestPathWithEndpointID:nil clusterID:@40 attributeID:@5],
+                 [MTRAttributeRequestPath requestPathWithEndpointID:nil clusterID:@40 attributeID:@6],
+                 [MTRAttributeRequestPath requestPathWithEndpointID:nil clusterID:@40 attributeID:@7], nil];
+
+    NSArray<MTREventRequestPath *> * eventPaths =
+        [NSArray arrayWithObjects:[MTREventRequestPath requestPathWithEndpointID:nil clusterID:@40 eventID:@0], nil];
+
+    [device readAttributePaths:attributePaths
+                    eventPaths:eventPaths
+                        params:nil
+                         queue:queue
+                    completion:^(id _Nullable values, NSError * _Nullable error) {
+                        NSLog(@"read attribute: DeviceType values: %@, error: %@", values, error);
+
+                        XCTAssertNil(error);
+                        XCTAssertEqual([MTRErrorTestUtils errorToZCLErrorCode:error], 0);
+
+                        {
+                            XCTAssertTrue([values isKindOfClass:[NSArray class]]);
+                            NSArray * resultArray = values;
+                            BOOL includeEventPath = NO;
+                            for (NSDictionary * result in resultArray) {
+                                if ([result objectForKey:@"eventPath"]) {
+                                    MTREventPath * path = result[@"eventPath"];
+                                    XCTAssertEqual([path.cluster unsignedIntegerValue], 40);
+                                    XCTAssertEqual([path.event unsignedIntegerValue], 0);
+                                    XCTAssertNotNil(result[@"data"]);
+                                    XCTAssertNil(result[@"error"]);
+                                    XCTAssertTrue([result[@"data"] isKindOfClass:[NSDictionary class]]);
+                                    includeEventPath = YES;
+                                } else if ([result objectForKey:@"attributePath"]) {
+                                    MTRAttributePath * path = result[@"attributePath"];
+                                    if ([path.attribute unsignedIntegerValue] < 5) {
+                                        XCTAssertEqual([path.cluster unsignedIntegerValue], 29);
+                                    } else {
+                                        XCTAssertEqual([path.cluster unsignedIntegerValue], 40);
+                                    }
+                                    XCTAssertNotNil(result[@"data"]);
+                                    XCTAssertNil(result[@"error"]);
+                                    XCTAssertTrue([result[@"data"] isKindOfClass:[NSDictionary class]]);
+                                }
+                            }
+                            XCTAssertTrue(includeEventPath);
+                            XCTAssertTrue([resultArray count] > 0);
+                        }
+
+                        [expectation fulfill];
+                    }];
+
+    [self waitForExpectationsWithTimeout:kTimeoutInSeconds handler:nil];
+}
+
+- (void)test021_ReadMultipleAttributesIncludeUnsupportedAttribute
+{
+    XCTestExpectation * expectation =
+        [self expectationWithDescription:@"read Basic Information Cluster's attributes and include 1 unsupported attribute"];
+
+    MTRBaseDevice * device = GetConnectedDevice();
+    dispatch_queue_t queue = dispatch_get_main_queue();
+
+    NSNumber * failAttributeID = @10000;
+
+    NSArray<MTRAttributeRequestPath *> * attributePaths =
+        [NSArray arrayWithObjects:[MTRAttributeRequestPath requestPathWithEndpointID:nil clusterID:@40 attributeID:@0],
+                 [MTRAttributeRequestPath requestPathWithEndpointID:nil clusterID:@40 attributeID:@1],
+                 [MTRAttributeRequestPath requestPathWithEndpointID:nil clusterID:@40 attributeID:@2],
+                 [MTRAttributeRequestPath requestPathWithEndpointID:nil clusterID:@40 attributeID:@3],
+                 [MTRAttributeRequestPath requestPathWithEndpointID:nil clusterID:@40 attributeID:@4],
+                 [MTRAttributeRequestPath requestPathWithEndpointID:nil clusterID:@40 attributeID:failAttributeID] // Fail Case
+                 ,
+                 [MTRAttributeRequestPath requestPathWithEndpointID:nil clusterID:@40 attributeID:@5],
+                 [MTRAttributeRequestPath requestPathWithEndpointID:nil clusterID:@40 attributeID:@6],
+                 [MTRAttributeRequestPath requestPathWithEndpointID:nil clusterID:@40 attributeID:@7], nil];
+
+    [device readAttributePaths:attributePaths
+                    eventPaths:nil
+                        params:nil
+                         queue:queue
+                    completion:^(id _Nullable values, NSError * _Nullable error) {
+                        NSLog(@"read attribute: DeviceType values: %@, error: %@", values, error);
+
+                        XCTAssertNil(error);
+                        XCTAssertEqual([MTRErrorTestUtils errorToZCLErrorCode:error], 0);
+
+                        {
+                            XCTAssertTrue([values isKindOfClass:[NSArray class]]);
+                            NSArray * resultArray = values;
+                            for (NSDictionary * result in resultArray) {
+                                MTRAttributePath * path = result[@"attributePath"];
+                                XCTAssertEqual([path.cluster unsignedIntegerValue], 40);
+                                if (path.attribute.unsignedIntegerValue != failAttributeID.unsignedIntegerValue) {
+                                    XCTAssertNotNil(result[@"data"]);
+                                    XCTAssertNil(result[@"error"]);
+                                    XCTAssertTrue([result[@"data"] isKindOfClass:[NSDictionary class]]);
+                                } else {
+                                    XCTAssertNil(result[@"data"]);
+                                    XCTAssertNotNil(result[@"error"]);
+                                }
+                            }
+                            XCTAssertTrue([resultArray count] > 0);
+                        }
+
+                        [expectation fulfill];
+                    }];
+
+    [self waitForExpectationsWithTimeout:kTimeoutInSeconds handler:nil];
+}
+
+- (void)test022_SubscribeMultipleAttributes
+{
+    MTRBaseDevice * device = GetConnectedDevice();
+    dispatch_queue_t queue = dispatch_get_main_queue();
+
+    // Subscribe
+    XCTestExpectation * expectation = [self expectationWithDescription:@"subscribe OnOff attribute"];
+    __auto_type * params = [[MTRSubscribeParams alloc] initWithMinInterval:@(1) maxInterval:@(10)];
+
+    NSArray<MTRAttributeRequestPath *> * attributePaths =
+        [NSArray arrayWithObjects:[MTRAttributeRequestPath requestPathWithEndpointID:@1 clusterID:@6 attributeID:@0],
+                 [MTRAttributeRequestPath requestPathWithEndpointID:@0 clusterID:@40 attributeID:@5], nil];
+
+    [device subscribeToAttributePaths:attributePaths
+        eventPaths:nil
+        params:params
+        queue:queue
+        reportHandler:^(id _Nullable values, NSError * _Nullable error) {
+            NSLog(@"report attributes: values: %@, error: %@", values, error);
+
+            if (globalReportHandler) {
+                __auto_type callback = globalReportHandler;
+                callback(values, error);
+            }
+        }
+        subscriptionEstablished:^{
+            NSLog(@"subscribe attribute");
+            [expectation fulfill];
+        }
+        resubscriptionScheduled:nil];
+
+    // Wait till establishment
+    [self waitForExpectations:[NSArray arrayWithObject:expectation] timeout:kTimeoutInSeconds];
+
+    // Set up expectation for report
+    XCTestExpectation * reportExpectation = [self expectationWithDescription:@"report received"];
+    globalReportHandler = ^(id _Nullable values, NSError * _Nullable error) {
+        XCTAssertEqual([MTRErrorTestUtils errorToZCLErrorCode:error], 0);
+        XCTAssertTrue([values isKindOfClass:[NSArray class]]);
+        NSDictionary * result = values[0];
+        MTRAttributePath * path = result[@"attributePath"];
+        if (path.endpoint.unsignedShortValue == 1) {
+            XCTAssertEqual([path.cluster unsignedIntegerValue], 6);
+            XCTAssertEqual([path.attribute unsignedIntegerValue], 0);
+            XCTAssertTrue([result[@"data"] isKindOfClass:[NSDictionary class]]);
+            XCTAssertTrue([result[@"data"][@"type"] isEqualToString:@"Boolean"]);
+            if ([result[@"data"][@"value"] boolValue] == YES) {
+                [reportExpectation fulfill];
+                globalReportHandler = nil;
+            }
+        } else if (path.endpoint.unsignedShortValue == 0) {
+            XCTAssertEqual([path.cluster unsignedIntegerValue], 40);
+            XCTAssertEqual([path.attribute unsignedIntegerValue], 5);
+            XCTAssertTrue([result[@"data"] isKindOfClass:[NSDictionary class]]);
+            XCTAssertTrue([result[@"data"][@"type"] isEqualToString:@"UTF8String"]);
+        } else {
+            XCTAssertTrue(NO);
+        }
+    };
+
+    // Send commands to trigger attribute change
+    XCTestExpectation * commandExpectation = [self expectationWithDescription:@"command responded"];
+    NSDictionary * fields = @{ @"type" : @"Structure", @"value" : [NSArray array] };
+    [device invokeCommandWithEndpointID:@1
+                              clusterID:@6
+                              commandID:@1
+                          commandFields:fields
+                     timedInvokeTimeout:nil
+                                  queue:queue
+                             completion:^(id _Nullable values, NSError * _Nullable error) {
+                                 NSLog(@"invoke command: On values: %@, error: %@", values, error);
+
+                                 XCTAssertNil(error);
+                                 XCTAssertEqual([MTRErrorTestUtils errorToZCLErrorCode:error], 0);
+
+                                 {
+                                     XCTAssertTrue([values isKindOfClass:[NSArray class]]);
+                                     NSArray * resultArray = values;
+                                     for (NSDictionary * result in resultArray) {
+                                         MTRCommandPath * path = result[@"commandPath"];
+                                         XCTAssertEqual([path.endpoint unsignedIntegerValue], 1);
+                                         XCTAssertEqual([path.cluster unsignedIntegerValue], 6);
+                                         XCTAssertEqual([path.command unsignedIntegerValue], 1);
+                                         XCTAssertNil(result[@"error"]);
+                                     }
+                                     XCTAssertEqual([resultArray count], 1);
+                                 }
+                                 [commandExpectation fulfill];
+                             }];
+
+    [self waitForExpectations:[NSArray arrayWithObject:commandExpectation] timeout:kTimeoutInSeconds];
+
+    // Wait for report
+    [self waitForExpectations:[NSArray arrayWithObject:reportExpectation] timeout:kTimeoutInSeconds];
+
+    // Set up expectation for 2nd report
+    reportExpectation = [self expectationWithDescription:@"receive OnOff attribute report"];
+    globalReportHandler = ^(id _Nullable values, NSError * _Nullable error) {
+        XCTAssertEqual([MTRErrorTestUtils errorToZCLErrorCode:error], 0);
+        XCTAssertTrue([values isKindOfClass:[NSArray class]]);
+        NSDictionary * result = values[0];
+        MTRAttributePath * path = result[@"attributePath"];
+        XCTAssertEqual([path.endpoint unsignedIntegerValue], 1);
+        XCTAssertEqual([path.cluster unsignedIntegerValue], 6);
+        XCTAssertEqual([path.attribute unsignedIntegerValue], 0);
+        XCTAssertTrue([result[@"data"] isKindOfClass:[NSDictionary class]]);
+        XCTAssertTrue([result[@"data"][@"type"] isEqualToString:@"Boolean"]);
+        if ([result[@"data"][@"value"] boolValue] == NO) {
+            [reportExpectation fulfill];
+            globalReportHandler = nil;
+        }
+    };
+
+    // Send command to trigger attribute change
+    fields = [NSDictionary dictionaryWithObjectsAndKeys:@"Structure", @"type", [NSArray array], @"value", nil];
+    [device invokeCommandWithEndpointID:@1
+                              clusterID:@6
+                              commandID:@0
+                          commandFields:fields
+                     timedInvokeTimeout:nil
+                                  queue:queue
+                             completion:^(id _Nullable values, NSError * _Nullable error) {
+                                 NSLog(@"invoke command: On values: %@, error: %@", values, error);
+
+                                 XCTAssertNil(error);
+                                 XCTAssertEqual([MTRErrorTestUtils errorToZCLErrorCode:error], 0);
+
+                                 {
+                                     XCTAssertTrue([values isKindOfClass:[NSArray class]]);
+                                     NSArray * resultArray = values;
+                                     for (NSDictionary * result in resultArray) {
+                                         MTRCommandPath * path = result[@"commandPath"];
+                                         XCTAssertEqual([path.endpoint unsignedIntegerValue], 1);
+                                         XCTAssertEqual([path.cluster unsignedIntegerValue], 6);
+                                         XCTAssertEqual([path.command unsignedIntegerValue], 0);
+                                         XCTAssertNil(result[@"error"]);
+                                     }
+                                     XCTAssertEqual([resultArray count], 1);
+                                 }
+                             }];
+
+    // Wait for report
+    [self waitForExpectations:[NSArray arrayWithObject:reportExpectation] timeout:kTimeoutInSeconds];
+
+    expectation = [self expectationWithDescription:@"Report handler deregistered"];
+    [device deregisterReportHandlersWithQueue:queue
+                                   completion:^{
+                                       [expectation fulfill];
+                                   }];
+    [self waitForExpectations:@[ expectation ] timeout:kTimeoutInSeconds];
+}
+
 - (void)test900_SubscribeAllAttributes
 {
     MTRBaseDevice * device = GetConnectedDevice();
@@ -1637,7 +1911,7 @@
     params.resubscribeAutomatically = NO;
     [device subscribeToAttributesWithEndpointID:@1
         clusterID:@6
-        attributeID:@0xffffffff
+        attributeID:nil
         params:params
         queue:queue
         reportHandler:^(id _Nullable values, NSError * _Nullable error) {