diff --git a/src/app/EventPathParams.h b/src/app/EventPathParams.h index 0a8cea3ee1acaa..29af405c60b515 100644 --- a/src/app/EventPathParams.h +++ b/src/app/EventPathParams.h @@ -43,6 +43,9 @@ struct EventPathParams 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 a0ff37f5d682c8..fecf8757befdc3 100644 --- a/src/darwin/Framework/CHIP/MTRBaseDevice.h +++ b/src/darwin/Framework/CHIP/MTRBaseDevice.h @@ -130,6 +130,38 @@ typedef NS_ENUM(uint8_t, MTRTransportType) { 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 +@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 +@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; @@ -229,6 +261,26 @@ typedef NS_ENUM(uint8_t, MTRTransportType) { completion:(MTRDeviceResponseHandler)completion 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 * _Nullable)attributePaths + eventPaths:(NSArray * _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 * @@ -305,6 +357,27 @@ typedef NS_ENUM(uint8_t, MTRTransportType) { subscriptionEstablished:(MTRSubscriptionEstablishedHandler _Nullable)subscriptionEstablished 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 * _Nullable)attributePaths + eventPaths:(NSArray * _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 * diff --git a/src/darwin/Framework/CHIP/MTRBaseDevice.mm b/src/darwin/Framework/CHIP/MTRBaseDevice.mm index 09f5ea582cc9e2..a8e585d6bf33a9 100644 --- a/src/darwin/Framework/CHIP/MTRBaseDevice.mm +++ b/src/darwin/Framework/CHIP/MTRBaseDevice.mm @@ -147,9 +147,17 @@ static void PurgeReadClientContainers( container.readClientPtr = nullptr; } if (container.pathParams) { - Platform::Delete(container.pathParams); + static_assert(std::is_trivially_destructible::value, + "AttributePathParams destructors won't get run"); + Platform::MemoryFree(container.pathParams); container.pathParams = nullptr; } + if (container.eventPathParams) { + static_assert( + std::is_trivially_destructible::value, "EventPathParams destructors won't get run"); + Platform::MemoryFree(container.eventPathParams); + container.eventPathParams = nullptr; + } } [listToDelete removeAllObjects]; if (completion) { @@ -223,9 +231,15 @@ - (void)onDone _readClientPtr = nullptr; } if (_pathParams) { - Platform::Delete(_pathParams); + static_assert(std::is_trivially_destructible::value, "AttributePathParams destructors won't get run"); + Platform::MemoryFree(_pathParams); _pathParams = nullptr; } + if (_eventPathParams) { + static_assert(std::is_trivially_destructible::value, "EventPathParams destructors won't get run"); + Platform::MemoryFree(_eventPathParams); + _eventPathParams = nullptr; + } PurgeCompletedReadClientContainers(_deviceID); } @@ -236,9 +250,15 @@ - (void)dealloc _readClientPtr = nullptr; } if (_pathParams) { - Platform::Delete(_pathParams); + static_assert(std::is_trivially_destructible::value, "AttributePathParams destructors won't get run"); + Platform::MemoryFree(_pathParams); _pathParams = nullptr; } + if (_eventPathParams) { + static_assert(std::is_trivially_destructible::value, "EventPathParams destructors won't get run"); + Platform::MemoryFree(_eventPathParams); + _eventPathParams = nullptr; + } } @end @@ -730,22 +750,31 @@ CHIP_ERROR Encode(chip::TLV::TLVWriter & writer, chip::TLV::Tag tag) const template class BufferedReadClientCallback final : public app::ReadClient::Callback { public: - using OnSuccessCallbackType - = std::function; - using OnErrorCallbackType - = std::function; + using OnSuccessAttributeCallbackType + = std::function; + using OnSuccessEventCallbackType = std::function; + using OnErrorCallbackType = std::function; using OnDoneCallbackType = std::function; using OnSubscriptionEstablishedCallbackType = std::function; + using OnDeviceResubscriptionScheduledCallbackType = std::function; - 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 @@ void OnAttributeData( 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 @@ void OnAttributeData( 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 @@ void OnEventData(const EventHeader & aEventHeader, TLV::TLVReader * apData, cons 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 @@ void OnSubscriptionEstablished(SubscriptionId aSubscriptionId) override } } + 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 mReadClient; + app::AttributePathParams * mAttributePathParamsList; + app::EventPathParams * mEventPathParamsList; + size_t mAttributePathParamsSize; + size_t mEventPathParamsSize; }; - (void)readAttributesWithEndpointID:(NSNumber * _Nullable)endpointID @@ -840,9 +900,39 @@ - (void)readAttributesWithEndpointID:(NSNumber * _Nullable)endpointID 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 * attributePaths = [NSArray + arrayWithObject:[MTRAttributeRequestPath requestPathWithEndpointID:endpointID clusterID:clusterID attributeID:attributeID]]; + [self readAttributePaths:attributePaths eventPaths:nil params:params queue:queue completion:completion]; +} + +- (void)readAttributePaths:(NSArray * _Nullable)attributePaths + eventPaths:(NSArray * _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 * attributes = nil; + if (attributePaths != nil) { + attributes = [[NSMutableArray alloc] init]; + for (MTRAttributeRequestPath * attributePath in attributePaths) { + [attributes addObject:[attributePath copy]]; + } + } + + NSMutableArray * 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 @@ - (void)readAttributesWithEndpointID:(NSNumber * _Nullable)endpointID auto interactionStatus = std::make_shared(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 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 onAttributeSuccessCb + = [resultArray](const ConcreteAttributePath & attributePath, const MTRDataValueDictionaryDecodableType & aData) { + [resultArray addObject:@ { + MTRAttributePathKey : [[MTRAttributePath alloc] initWithPath:attributePath], + MTRDataKey : aData.GetDecodedObject() + }]; + }; + + 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 @@ - (void)readAttributesWithEndpointID:(NSNumber * _Nullable)endpointID } }; - app::AttributePathParams attributePath; - if (endpointID) { - attributePath.mEndpointId = static_cast([endpointID unsignedShortValue]); - } - if (clusterID) { - attributePath.mClusterId = static_cast([clusterID unsignedLongValue]); + Platform::ScopedMemoryBuffer attributePathParamsList; + Platform::ScopedMemoryBuffer 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 (attributeID) { - attributePath.mAttributeId = static_cast([attributeID 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++]]; + } } + 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; - - auto onDone = [resultArray, interactionStatus, bridge, successCb, failureCb]( - BufferedReadClientCallback * callback) { - if (*interactionStatus != CHIP_NO_ERROR) { - // Failure - failureCb(bridge, *interactionStatus); - } else { - // Success - successCb(bridge, resultArray); - } - chip::Platform::Delete(callback); - }; + readParams.mpAttributePathParamsList = attributePathParamsList.Get(); + readParams.mAttributePathParamsListSize = [attributePaths count]; + readParams.mpEventPathParamsList = eventPathParamsList.Get(); + readParams.mEventPathParamsListSize = [eventPaths count]; + + AttributePathParams * attributePathParamsListToFree = attributePathParamsList.Get(); + EventPathParams * eventPathParamsListToFree = eventPathParamsList.Get(); + + auto onDone + = [resultArray, interactionStatus, bridge, successCb, failureCb, attributePathParamsListToFree, + eventPathParamsListToFree](BufferedReadClientCallback * 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>( - 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( @@ -929,6 +1052,8 @@ - (void)readAttributesWithEndpointID:(NSNumber * _Nullable)endpointID // callback->AdoptReadClient(std::move(readClient)); callback.release(); + attributePathParamsList.Release(); + eventPathParamsList.Release(); return err; }); std::move(*bridge).DispatchAction(self); @@ -1141,6 +1266,32 @@ - (void)subscribeToAttributesWithEndpointID:(NSNumber * _Nullable)endpointID reportHandler:(MTRDeviceResponseHandler)reportHandler subscriptionEstablished:(MTRSubscriptionEstablishedHandler)subscriptionEstablished { + NSArray * 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 * _Nullable)attributePaths + eventPaths:(NSArray * _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 @@ - (void)subscribeToAttributesWithEndpointID:(NSNumber * _Nullable)endpointID } // Copy params before going async. - endpointID = (endpointID == nil) ? nil : [endpointID copy]; - clusterID = (clusterID == nil) ? nil : [clusterID copy]; - attributeID = (attributeID == nil) ? nil : [attributeID copy]; + NSMutableArray * attributes = nil; + if (attributePaths != nil) { + attributes = [[NSMutableArray alloc] init]; + for (MTRAttributeRequestPath * attributePath in attributePaths) { + [attributes addObject:[attributePath copy]]; + } + } + + NSMutableArray * 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 @@ - (void)subscribeToAttributesWithEndpointID:(NSNumber * _Nullable)endpointID 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 @@ - (void)subscribeToAttributesWithEndpointID:(NSNumber * _Nullable)endpointID }); }; + 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(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 @@ - (void)subscribeToAttributesWithEndpointID:(NSNumber * _Nullable)endpointID } }; + 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(); - if (endpointID) { - container.pathParams->mEndpointId = static_cast([endpointID unsignedShortValue]); - } - if (clusterID) { - container.pathParams->mClusterId = static_cast([clusterID unsignedLongValue]); + + size_t attributePathSize = 0; + if (attributes != nil) { + container.pathParams = static_cast( + 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 (attributeID) { - container.pathParams->mAttributeId = static_cast([attributeID unsignedLongValue]); + size_t eventPathSize = 0; + if (events != nil) { + container.eventPathParams + = static_cast(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 @@ - (void)subscribeToAttributesWithEndpointID:(NSNumber * _Nullable)endpointID 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 * callback) { [container onDone]; @@ -1236,8 +1443,8 @@ - (void)subscribeToAttributesWithEndpointID:(NSNumber * _Nullable)endpointID }; auto callback = chip::Platform::MakeUnique>( - 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( engine, exchangeManager, callback->GetBufferedCallback(), chip::app::ReadClient::InteractionType::Subscribe); @@ -1255,8 +1462,15 @@ - (void)subscribeToAttributesWithEndpointID:(NSNumber * _Nullable)endpointID }); } 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 @@ - (void)readEventsWithEndpointID:(NSNumber * _Nullable)endpointID 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_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([endpointID unsignedShortValue]); - } - if (clusterID) { - eventPath.mClusterId = static_cast([clusterID unsignedLongValue]); - } - if (eventID) { - eventPath.mEventId = static_cast([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 * callback) { - if (*interactionStatus != CHIP_NO_ERROR) { - // Failure - failureCb(bridge, *interactionStatus); - } else { - successCb(bridge, resultArray); - } - chip::Platform::Delete(callback); - }; - - auto callback = chip::Platform::MakeUnique>( - eventPath.mClusterId, eventPath.mEventId, onSuccessCb, onFailureCb, onDone, nullptr); - VerifyOrReturnError(callback != nullptr, CHIP_ERROR_NO_MEMORY); - - auto readClient = chip::Platform::MakeUnique( - 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 * 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 @@ - (void)subscribeToEventsWithEndpointID:(NSNumber * _Nullable)endpointID 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 & 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(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(); - if (endpointID) { - container.eventPathParams->mEndpointId = static_cast([endpointID unsignedShortValue]); - } - if (clusterID) { - container.eventPathParams->mClusterId = static_cast([clusterID unsignedLongValue]); - } - if (eventID) { - container.eventPathParams->mEventId = static_cast([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 * 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>( - container.eventPathParams->mClusterId, container.eventPathParams->mEventId, onReportCb, onFailureCb, onDone, - onEstablishedCb); - - auto readClient = Platform::New( - 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 * 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 @@ - (void)deregisterReportHandlersWithClientQueue:(dispatch_queue_t)queue completi @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:@" 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(_endpoint.unsignedShortValue); + } else { + params.SetWildcardEndpointId(); + } + + if (_cluster != nil) { + params.mClusterId = static_cast(_cluster.unsignedLongValue); + } else { + params.SetWildcardClusterId(); + } + + if (_attribute != nil) { + params.mAttributeId = static_cast(_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:@" 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(_endpoint.unsignedShortValue); + } else { + params.SetWildcardEndpointId(); + } + + if (_cluster != nil) { + params.mClusterId = static_cast(_cluster.unsignedLongValue); + } else { + params.SetWildcardClusterId(); + } + + if (_event != nil) { + params.mEventId = static_cast(_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 9fb062a90cceb2..cf5944a0a66a5d 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 +#include #include #include #include #include #include +#include @class MTRDeviceController; @@ -106,6 +108,14 @@ static inline MTRTransportType MTRMakeTransportType(chip::Transport::Type type) 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 80757382ba11d1..f649a04dff13e8 100644 --- a/src/darwin/Framework/CHIPTests/MTRDeviceTests.m +++ b/src/darwin/Framework/CHIPTests/MTRDeviceTests.m @@ -1617,6 +1617,280 @@ - (void)test019_MTRDeviceMultipleCommands 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 * 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 * 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 * 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 * 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 @@ - (void)test900_SubscribeAllAttributes params.resubscribeAutomatically = NO; [device subscribeToAttributesWithEndpointID:@1 clusterID:@6 - attributeID:@0xffffffff + attributeID:nil params:params queue:queue reportHandler:^(id _Nullable values, NSError * _Nullable error) {