From 1092516b4f70681d5845cf441cd0fecac6c8d3dc Mon Sep 17 00:00:00 2001 From: Boris Zbarsky Date: Thu, 20 Apr 2023 18:51:40 -0400 Subject: [PATCH] Include event header information in results from multi-path read/subscribe. (#26173) Fixes https://github.com/project-chip/connectedhomeip/issues/26075 --- src/darwin/Framework/CHIP/MTRBaseDevice.h | 19 +++ src/darwin/Framework/CHIP/MTRBaseDevice.mm | 120 ++++++++++++++---- .../Framework/CHIP/MTRBaseDevice_Internal.h | 8 ++ src/darwin/Framework/CHIP/MTRDevice.h | 6 - src/darwin/Framework/CHIP/MTRDevice.mm | 84 +----------- .../Framework/CHIP/MTRDevice_Internal.h | 7 - .../Framework/CHIPTests/MTRDeviceTests.m | 92 +++++++++++++- 7 files changed, 223 insertions(+), 113 deletions(-) diff --git a/src/darwin/Framework/CHIP/MTRBaseDevice.h b/src/darwin/Framework/CHIP/MTRBaseDevice.h index 65050131d2fa12..e9caa66561f484 100644 --- a/src/darwin/Framework/CHIP/MTRBaseDevice.h +++ b/src/darwin/Framework/CHIP/MTRBaseDevice.h @@ -41,6 +41,20 @@ NS_ASSUME_NONNULL_BEGIN * MTRDataKey: Data-value NSDictionary object. * Included when there is data and when there is no error. * The data-value is described below. + * MTREventNumberKey : NSNumber-wrapped uint64_t value. Monotonically increasing, and consecutive event reports + * should have consecutive numbers unless device reboots, or if events are lost. + * Only present when both MTREventPathKey and MTRDataKey are present. + * MTREventPriorityKey : NSNumber-wrapped MTREventPriority value. + * Only present when both MTREventPathKey and MTRDataKey are present. + * MTREventTimeTypeKey : NSNumber-wrapped MTREventTimeType value. + * Only present when both MTREventPathKey and MTRDataKey are present. + * MTREventSystemUpTimeKey : NSNumber-wrapped NSTimeInterval value. + * Only present when MTREventTimeTypeKey is MTREventTimeTypeSystemUpTime. + * MTREventTimestampDateKey : NSDate object. + * Only present when MTREventTimeTypeKey is MTREventTimeTypeTimestampDate. + * + * Only one of MTREventTimestampDateKey and MTREventSystemUpTimeKey will be present, depending on the value for + * MTREventTimeTypeKey. * * A data-value is an NSDictionary object with the following key values: * @@ -117,6 +131,11 @@ extern NSString * const MTRDoubleValueType; extern NSString * const MTRNullValueType; extern NSString * const MTRStructureValueType; extern NSString * const MTRArrayValueType; +extern NSString * const MTREventNumberKey API_AVAILABLE(ios(16.5), macos(13.4), watchos(9.5), tvos(16.5)); +extern NSString * const MTREventPriorityKey API_AVAILABLE(ios(16.5), macos(13.4), watchos(9.5), tvos(16.5)); +extern NSString * const MTREventTimeTypeKey API_AVAILABLE(ios(16.5), macos(13.4), watchos(9.5), tvos(16.5)); +extern NSString * const MTREventSystemUpTimeKey API_AVAILABLE(ios(16.5), macos(13.4), watchos(9.5), tvos(16.5)); +extern NSString * const MTREventTimestampDateKey API_AVAILABLE(ios(16.5), macos(13.4), watchos(9.5), tvos(16.5)); @class MTRClusterStateCacheContainer; @class MTRAttributeCacheContainer; diff --git a/src/darwin/Framework/CHIP/MTRBaseDevice.mm b/src/darwin/Framework/CHIP/MTRBaseDevice.mm index a966a34dde8cf1..4522c63d7989c1 100644 --- a/src/darwin/Framework/CHIP/MTRBaseDevice.mm +++ b/src/darwin/Framework/CHIP/MTRBaseDevice.mm @@ -74,6 +74,11 @@ NSString * const MTRNullValueType = @"Null"; NSString * const MTRStructureValueType = @"Structure"; NSString * const MTRArrayValueType = @"Array"; +NSString * const MTREventNumberKey = @"eventNumber"; +NSString * const MTREventPriorityKey = @"eventPriority"; +NSString * const MTREventTimeTypeKey = @"eventTimeType"; +NSString * const MTREventSystemUpTimeKey = @"eventSystemUpTime"; +NSString * const MTREventTimestampDateKey = @"eventTimestampDate"; class MTRDataValueDictionaryCallbackBridge; @@ -752,7 +757,7 @@ CHIP_ERROR Encode(chip::TLV::TLVWriter & writer, chip::TLV::Tag tag) const public: using OnSuccessAttributeCallbackType = std::function; - using OnSuccessEventCallbackType = std::function; + using OnSuccessEventCallbackType = std::function; using OnErrorCallbackType = std::function; using OnDoneCallbackType = std::function; @@ -844,7 +849,7 @@ void OnEventData(const EventHeader & aEventHeader, TLV::TLVReader * apData, cons SuccessOrExit(err = app::DataModel::Decode(*apData, value)); - mOnEventSuccess(aEventHeader.mPath, value); + mOnEventSuccess(aEventHeader, value); exit: if (err != CHIP_NO_ERROR) { @@ -944,31 +949,28 @@ - (void)readAttributePaths:(NSArray * _Nullable)attri auto resultArray = [[NSMutableArray alloc] init]; auto onAttributeSuccessCb - = [resultArray](const ConcreteAttributePath & attributePath, const MTRDataValueDictionaryDecodableType & aData) { + = [resultArray](const ConcreteAttributePath & aAttributePath, const MTRDataValueDictionaryDecodableType & aData) { [resultArray addObject:@ { - MTRAttributePathKey : [[MTRAttributePath alloc] initWithPath:attributePath], + MTRAttributePathKey : [[MTRAttributePath alloc] initWithPath:aAttributePath], MTRDataKey : aData.GetDecodedObject() }]; }; auto onEventSuccessCb - = [resultArray](const ConcreteEventPath & eventPath, const MTRDataValueDictionaryDecodableType & aData) { - [resultArray addObject:@ { - MTREventPathKey : [[MTREventPath alloc] initWithPath:eventPath], - MTRDataKey : aData.GetDecodedObject() - }]; + = [resultArray](const EventHeader & aEventHeader, const MTRDataValueDictionaryDecodableType & aData) { + [resultArray addObject:[MTRBaseDevice eventReportForHeader:aEventHeader andData:aData.GetDecodedObject()]]; }; - auto onFailureCb = [resultArray, interactionStatus](const app::ConcreteAttributePath * attributePath, - const app::ConcreteEventPath * eventPath, CHIP_ERROR aError) { - if (attributePath != nullptr) { + auto onFailureCb = [resultArray, interactionStatus](const app::ConcreteAttributePath * aAttributePath, + const app::ConcreteEventPath * aEventPath, CHIP_ERROR aError) { + if (aAttributePath != nullptr) { [resultArray addObject:@ { - MTRAttributePathKey : [[MTRAttributePath alloc] initWithPath:*attributePath], + MTRAttributePathKey : [[MTRAttributePath alloc] initWithPath:*aAttributePath], MTRErrorKey : [MTRError errorForCHIPErrorCode:aError] }]; - } else if (eventPath != nullptr) { + } else if (aEventPath != nullptr) { [resultArray addObject:@ { - MTREventPathKey : [[MTREventPath alloc] initWithPath:*eventPath], + MTREventPathKey : [[MTREventPath alloc] initWithPath:*aEventPath], MTRErrorKey : [MTRError errorForCHIPErrorCode:aError] }]; } else { @@ -1343,14 +1345,11 @@ - (void)subscribeToAttributePaths:(NSArray * _Nullabl }); }; - auto onEventReportCb = [queue, reportHandler](const ConcreteEventPath & eventPath, - const MTRDataValueDictionaryDecodableType & data) { - id valueObject = data.GetDecodedObject(); - ConcreteEventPath pathCopy(eventPath); + auto onEventReportCb = [queue, reportHandler]( + const EventHeader & eventHeader, const MTRDataValueDictionaryDecodableType & data) { + NSDictionary * report = [MTRBaseDevice eventReportForHeader:eventHeader andData:data.GetDecodedObject()]; dispatch_async(queue, ^{ - reportHandler( - @[ @ { MTREventPathKey : [[MTREventPath alloc] initWithPath:pathCopy], MTRDataKey : valueObject } ], - nil); + reportHandler(@[ report ], nil); }); }; @@ -1540,6 +1539,43 @@ static CHIP_ERROR OpenCommissioningWindow(Controller::DeviceController * control delete self; } +#pragma mark - Utility for time conversion +NSTimeInterval MTRTimeIntervalForEventTimestampValue(uint64_t timeValue) +{ + // Note: The event timestamp value as written in the spec is in microseconds, but the released 1.0 SDK implemented it in + // milliseconds. The following issue was filed to address the inconsistency: + // https://github.com/CHIP-Specifications/connectedhomeip-spec/issues/6236 + // For consistency with the released behavior, calculations here will be done in milliseconds. + + // First convert the event timestamp value (in milliseconds) to NSTimeInterval - to minimize potential loss of precision + // of uint64 => NSTimeInterval (double), convert whole seconds and remainder separately and then combine + uint64_t eventTimestampValueSeconds = timeValue / chip::kMillisecondsPerSecond; + uint64_t eventTimestampValueRemainderMilliseconds = timeValue % chip::kMillisecondsPerSecond; + NSTimeInterval eventTimestampValueRemainder + = NSTimeInterval(eventTimestampValueRemainderMilliseconds) / chip::kMillisecondsPerSecond; + NSTimeInterval eventTimestampValue = eventTimestampValueSeconds + eventTimestampValueRemainder; + + return eventTimestampValue; +} + +#pragma mark - Utility for event priority conversion +BOOL MTRPriorityLevelIsValid(chip::app::PriorityLevel priorityLevel) +{ + return (priorityLevel >= chip::app::PriorityLevel::Debug) && (priorityLevel <= chip::app::PriorityLevel::Critical); +} + +MTREventPriority MTREventPriorityForValidPriorityLevel(chip::app::PriorityLevel priorityLevel) +{ + switch (priorityLevel) { + case chip::app::PriorityLevel::Debug: + return MTREventPriorityDebug; + case chip::app::PriorityLevel::Info: + return MTREventPriorityInfo; + default: + return MTREventPriorityCritical; + } +} + } // anonymous namespace - (void)_openCommissioningWindowWithSetupPasscode:(nullable NSNumber *)setupPasscode @@ -1738,6 +1774,46 @@ - (void)subscribeToEventsWithEndpointID:(NSNumber * _Nullable)endpointID subscriptionEstablished:subscriptionEstablished resubscriptionScheduled:nil]; } + ++ (NSDictionary *)eventReportForHeader:(const chip::app::EventHeader &)header andData:(id _Nullable)data +{ + MTREventPath * eventPath = [[MTREventPath alloc] initWithPath:header.mPath]; + if (data == nil) { + MTR_LOG_ERROR("%@ could not decode event data", eventPath); + return @{ MTREventPathKey : eventPath, MTRErrorKey : [MTRError errorForCHIPErrorCode:CHIP_ERROR_INVALID_ARGUMENT] }; + } + + // Construct the right type, and key/value depending on the type + NSNumber * eventTimeType; + NSString * timestampKey; + id timestampValue; + if (header.mTimestamp.mType == Timestamp::Type::kSystem) { + eventTimeType = @(MTREventTimeTypeSystemUpTime); + timestampKey = MTREventSystemUpTimeKey; + timestampValue = @(MTRTimeIntervalForEventTimestampValue(header.mTimestamp.mValue)); + } else if (header.mTimestamp.mType == Timestamp::Type::kEpoch) { + eventTimeType = @(MTREventTimeTypeTimestampDate); + timestampKey = MTREventTimestampDateKey; + timestampValue = [NSDate dateWithTimeIntervalSince1970:MTRTimeIntervalForEventTimestampValue(header.mTimestamp.mValue)]; + } else { + MTR_LOG_ERROR("%@ Unsupported event timestamp type %u - ignoring", eventPath, (unsigned int) header.mTimestamp.mType); + return @{ MTREventPathKey : eventPath, MTRErrorKey : [MTRError errorForCHIPErrorCode:CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE] }; + } + + if (!MTRPriorityLevelIsValid(header.mPriorityLevel)) { + MTR_LOG_ERROR("%@ Unsupported event priority %u - ignoring", eventPath, (unsigned int) header.mPriorityLevel); + return @{ MTREventPathKey : eventPath, MTRErrorKey : [MTRError errorForCHIPErrorCode:CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE] }; + } + + return @{ + MTREventPathKey : eventPath, + MTRDataKey : data, + MTREventNumberKey : @(header.mEventNumber), + MTREventPriorityKey : @(MTREventPriorityForValidPriorityLevel(header.mPriorityLevel)), + MTREventTimeTypeKey : eventTimeType, + timestampKey : timestampValue + }; +} @end @implementation MTRBaseDevice (Deprecated) diff --git a/src/darwin/Framework/CHIP/MTRBaseDevice_Internal.h b/src/darwin/Framework/CHIP/MTRBaseDevice_Internal.h index cf5944a0a66a5d..d6be73f96375c2 100644 --- a/src/darwin/Framework/CHIP/MTRBaseDevice_Internal.h +++ b/src/darwin/Framework/CHIP/MTRBaseDevice_Internal.h @@ -23,6 +23,7 @@ #include #include #include +#include #include #include @@ -75,6 +76,13 @@ static inline MTRTransportType MTRMakeTransportType(chip::Transport::Type type) */ - (instancetype)initWithNodeID:(NSNumber *)nodeID controller:(MTRDeviceController *)controller; +/** + * Create a report, suitable in including in the sort of data structure that + * gets passed to MTRDeviceResponseHandler, from a given event header and + * already-decoded event data. The data is allowed to be nil in error cases + * (e.g. when TLV decoding failed). + */ ++ (NSDictionary *)eventReportForHeader:(const chip::app::EventHeader &)header andData:(id _Nullable)data; @end @interface MTRClusterPath () diff --git a/src/darwin/Framework/CHIP/MTRDevice.h b/src/darwin/Framework/CHIP/MTRDevice.h index 6f8bda9703e0bc..71f24c1e33528a 100644 --- a/src/darwin/Framework/CHIP/MTRDevice.h +++ b/src/darwin/Framework/CHIP/MTRDevice.h @@ -202,12 +202,6 @@ typedef NS_ENUM(NSUInteger, MTRDeviceState) { @end -extern NSString * const MTREventNumberKey API_AVAILABLE(ios(16.5), macos(13.4), watchos(9.5), tvos(16.5)); -extern NSString * const MTREventPriorityKey API_AVAILABLE(ios(16.5), macos(13.4), watchos(9.5), tvos(16.5)); -extern NSString * const MTREventTimeTypeKey API_AVAILABLE(ios(16.5), macos(13.4), watchos(9.5), tvos(16.5)); -extern NSString * const MTREventSystemUpTimeKey API_AVAILABLE(ios(16.5), macos(13.4), watchos(9.5), tvos(16.5)); -extern NSString * const MTREventTimestampDateKey API_AVAILABLE(ios(16.5), macos(13.4), watchos(9.5), tvos(16.5)); - @protocol MTRDeviceDelegate @required /** diff --git a/src/darwin/Framework/CHIP/MTRDevice.mm b/src/darwin/Framework/CHIP/MTRDevice.mm index 5b8486fa4b2079..da828545025156 100644 --- a/src/darwin/Framework/CHIP/MTRDevice.mm +++ b/src/darwin/Framework/CHIP/MTRDevice.mm @@ -38,12 +38,6 @@ #include #include -NSString * const MTREventNumberKey = @"eventNumber"; -NSString * const MTREventPriorityKey = @"eventPriority"; -NSString * const MTREventTimeTypeKey = @"eventTimeType"; -NSString * const MTREventSystemUpTimeKey = @"eventSystemUpTime"; -NSString * const MTREventTimestampDateKey = @"eventTimestampDate"; - typedef void (^MTRDeviceAttributeReportHandler)(NSArray * _Nonnull); // Consider moving utility classes to their own file @@ -89,41 +83,6 @@ - (id)strongObject return aNumber; } -NSTimeInterval MTRTimeIntervalForEventTimestampValue(uint64_t timeValue) -{ - // Note: The event timestamp value as written in the spec is in microseconds, but the released 1.0 SDK implemented it in - // milliseconds. The following issue was filed to address the inconsistency: - // https://github.com/CHIP-Specifications/connectedhomeip-spec/issues/6236 - // For consistency with the released behavior, calculations here will be done in milliseconds. - - // First convert the event timestamp value (in milliseconds) to NSTimeInterval - to minimize potential loss of precision - // of uint64 => NSTimeInterval (double), convert whole seconds and remainder separately and then combine - uint64_t eventTimestampValueSeconds = timeValue / chip::kMillisecondsPerSecond; - uint64_t eventTimestampValueRemainderMilliseconds = timeValue % chip::kMillisecondsPerSecond; - NSTimeInterval eventTimestampValueRemainder - = NSTimeInterval(eventTimestampValueRemainderMilliseconds) / chip::kMillisecondsPerSecond; - NSTimeInterval eventTimestampValue = eventTimestampValueSeconds + eventTimestampValueRemainder; - - return eventTimestampValue; -} - -BOOL MTRPriorityLevelIsValid(chip::app::PriorityLevel priorityLevel) -{ - return (priorityLevel >= chip::app::PriorityLevel::Debug) && (priorityLevel <= chip::app::PriorityLevel::Critical); -} - -MTREventPriority MTREventPriorityForValidPriorityLevel(chip::app::PriorityLevel priorityLevel) -{ - switch (priorityLevel) { - case chip::app::PriorityLevel::Debug: - return MTREventPriorityDebug; - case chip::app::PriorityLevel::Info: - return MTREventPriorityInfo; - default: - return MTREventPriorityCritical; - } -} - #pragma mark - SubscriptionCallback class declaration using namespace chip; using namespace chip::app; @@ -1214,43 +1173,14 @@ - (void)invokeCommandWithEndpointID:(NSNumber *)endpointID MTRErrorKey : [MTRError errorForCHIPErrorCode:CHIP_ERROR_INVALID_ARGUMENT] }]; } else { - id value = MTRDecodeDataValueDictionaryFromCHIPTLV(apData); - if (value) { - // Construct the right type, and key/value depending on the type - NSNumber * eventTimeType; - NSString * timestampKey; - id timestampValue; - if (aEventHeader.mTimestamp.mType == Timestamp::Type::kSystem) { - eventTimeType = @(MTREventTimeTypeSystemUpTime); - timestampKey = MTREventSystemUpTimeKey; - timestampValue = @(MTRTimeIntervalForEventTimestampValue(aEventHeader.mTimestamp.mValue)); - } else if (aEventHeader.mTimestamp.mType == Timestamp::Type::kEpoch) { - eventTimeType = @(MTREventTimeTypeTimestampDate); - timestampKey = MTREventTimestampDateKey; - timestampValue = - [NSDate dateWithTimeIntervalSince1970:MTRTimeIntervalForEventTimestampValue(aEventHeader.mTimestamp.mValue)]; - } else { - MTR_LOG_INFO( - "%@ Unsupported event timestamp type %u - ignoring", eventPath, (unsigned int) aEventHeader.mTimestamp.mType); - ReportError(CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE); - return; - } - - if (!MTRPriorityLevelIsValid(aEventHeader.mPriorityLevel)) { - MTR_LOG_INFO("%@ Unsupported event priority %u - ignoring", eventPath, (unsigned int) aEventHeader.mPriorityLevel); - ReportError(CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE); - return; - } - - [mEventReports addObject:@{ - MTREventPathKey : eventPath, - MTRDataKey : value, - MTREventNumberKey : @(aEventHeader.mEventNumber), - MTREventPriorityKey : @(MTREventPriorityForValidPriorityLevel(aEventHeader.mPriorityLevel)), - MTREventTimeTypeKey : eventTimeType, - timestampKey : timestampValue - }]; + id value; + if (apData == nullptr) { + value = nil; + } else { + value = MTRDecodeDataValueDictionaryFromCHIPTLV(apData); } + + [mEventReports addObject:[MTRBaseDevice eventReportForHeader:aEventHeader andData:value]]; } } diff --git a/src/darwin/Framework/CHIP/MTRDevice_Internal.h b/src/darwin/Framework/CHIP/MTRDevice_Internal.h index cccff5027e2cb2..8da4cecb60f7bd 100644 --- a/src/darwin/Framework/CHIP/MTRDevice_Internal.h +++ b/src/darwin/Framework/CHIP/MTRDevice_Internal.h @@ -56,11 +56,4 @@ typedef void (^MTRDevicePerformAsyncBlock)(MTRBaseDevice * baseDevice); // Returns min or max, if it is below or above, respectively. NSNumber * MTRClampedNumber(NSNumber * aNumber, NSNumber * min, NSNumber * max); -#pragma mark - Utility for time conversion -NSTimeInterval MTRTimeIntervalForEventTimestampValue(uint64_t timeValue); - -#pragma mark - Utility for event priority conversion -BOOL MTRPriorityLevelIsValid(chip::app::PriorityLevel priorityLevel); -MTREventPriority MTREventPriorityForValidPriorityLevel(chip::app::PriorityLevel); - NS_ASSUME_NONNULL_END diff --git a/src/darwin/Framework/CHIPTests/MTRDeviceTests.m b/src/darwin/Framework/CHIPTests/MTRDeviceTests.m index a71f3878d7515a..2a444a85ce0e81 100644 --- a/src/darwin/Framework/CHIPTests/MTRDeviceTests.m +++ b/src/darwin/Framework/CHIPTests/MTRDeviceTests.m @@ -1659,9 +1659,27 @@ - (void)test020_ReadMultipleAttributes XCTAssertEqualObjects(path.endpoint, @0); XCTAssertEqualObjects(path.cluster, @40); XCTAssertEqualObjects(path.event, @0); - XCTAssertNotNil(result[@"data"]); XCTAssertNil(result[@"error"]); + + XCTAssertNotNil(result[@"data"]); XCTAssertTrue([result[@"data"] isKindOfClass:[NSDictionary class]]); + + XCTAssertNotNil(result[@"eventNumber"]); + XCTAssertTrue([result[@"eventNumber"] isKindOfClass:[NSNumber class]]); + + XCTAssertNotNil(result[@"eventPriority"]); + XCTAssertTrue([result[@"eventPriority"] isKindOfClass:[NSNumber class]]); + XCTAssertEqualObjects(result[@"eventPriority"], @(MTREventPriorityCritical)); + + XCTAssertNotNil(result[@"eventTimeType"]); + XCTAssertTrue([result[@"eventTimeType"] isKindOfClass:[NSNumber class]]); + + XCTAssertTrue(result[@"eventSystemUpTime"] != nil || result[@"eventTimestampDate"] != nil); + if (result[@"eventSystemUpTime"] != nil) { + XCTAssertTrue([result[@"eventSystemUpTime"] isKindOfClass:[NSNumber class]]); + } else { + XCTAssertTrue([result[@"eventTimestampDate"] isKindOfClass:[NSDate class]]); + } } else if ([result objectForKey:@"attributePath"]) { ++attributeResultCount; MTRAttributePath * path = result[@"attributePath"]; @@ -2079,6 +2097,78 @@ - (void)test024_SubscribeMultipleAttributesAllErrors [self waitForExpectations:@[ errorExpectation ] timeout:kTimeoutInSeconds]; } +- (void)test025_SubscribeMultipleEvents +{ + MTRBaseDevice * device = GetConnectedDevice(); + dispatch_queue_t queue = dispatch_get_main_queue(); + + // Subscribe + XCTestExpectation * expectation = [self expectationWithDescription:@"subscribe multiple events"]; + __auto_type * params = [[MTRSubscribeParams alloc] initWithMinInterval:@(1) maxInterval:@(10)]; + + NSArray * eventPaths = @[ + // Startup event. + [MTREventRequestPath requestPathWithEndpointID:@0 clusterID:@40 eventID:@0], + // Shutdown event. + [MTREventRequestPath requestPathWithEndpointID:@0 clusterID:@40 eventID:@1], + ]; + + XCTestExpectation * startupEventExpectation = [self expectationWithDescription:@"report startup event"]; + __auto_type reportHandler = ^(id _Nullable values, NSError * _Nullable error) { + XCTAssertNil(error); + XCTAssertEqual([MTRErrorTestUtils errorToZCLErrorCode:error], 0); + XCTAssertTrue([values isKindOfClass:[NSArray class]]); + + for (NSDictionary * result in values) { + XCTAssertNotNil(result[@"eventPath"]); + + MTREventPath * path = result[@"eventPath"]; + // We only expect to see a Startup event here. + XCTAssertEqualObjects(path.endpoint, @0); + XCTAssertEqualObjects(path.cluster, @40); + XCTAssertEqualObjects(path.event, @0); + + XCTAssertNil(result[@"error"]); + + XCTAssertNotNil(result[@"data"]); + XCTAssertTrue([result[@"data"] isKindOfClass:[NSDictionary class]]); + + XCTAssertNotNil(result[@"eventNumber"]); + XCTAssertTrue([result[@"eventNumber"] isKindOfClass:[NSNumber class]]); + + XCTAssertNotNil(result[@"eventPriority"]); + XCTAssertTrue([result[@"eventPriority"] isKindOfClass:[NSNumber class]]); + XCTAssertEqualObjects(result[@"eventPriority"], @(MTREventPriorityCritical)); + + XCTAssertNotNil(result[@"eventTimeType"]); + XCTAssertTrue([result[@"eventTimeType"] isKindOfClass:[NSNumber class]]); + + XCTAssertTrue(result[@"eventSystemUpTime"] != nil || result[@"eventTimestampDate"] != nil); + if (result[@"eventSystemUpTime"] != nil) { + XCTAssertTrue([result[@"eventSystemUpTime"] isKindOfClass:[NSNumber class]]); + } else { + XCTAssertTrue([result[@"eventTimestampDate"] isKindOfClass:[NSDate class]]); + } + + [startupEventExpectation fulfill]; + } + }; + + [device subscribeToAttributePaths:nil + eventPaths:eventPaths + params:params + queue:queue + reportHandler:reportHandler + subscriptionEstablished:^{ + NSLog(@"subscribe complete"); + [expectation fulfill]; + } + resubscriptionScheduled:nil]; + + // Wait till establishment + [self waitForExpectations:@[ startupEventExpectation, expectation ] timeout:kTimeoutInSeconds]; +} + - (void)test900_SubscribeAllAttributes { MTRBaseDevice * device = GetConnectedDevice();