diff --git a/src/darwin/Framework/CHIP/MTRBaseDevice.h b/src/darwin/Framework/CHIP/MTRBaseDevice.h index c63d7e81bca2e5..ed6a12873755f4 100644 --- a/src/darwin/Framework/CHIP/MTRBaseDevice.h +++ b/src/darwin/Framework/CHIP/MTRBaseDevice.h @@ -429,11 +429,27 @@ API_AVAILABLE(ios(16.4), macos(13.3), watchos(9.4), tvos(16.4)) @property (nonatomic, readonly, copy, nullable) NSError * error; @end +typedef NS_ENUM(NSUInteger, MTREventTimeType) { + MTREventTimeTypeSystemUpTime = 0, + MTREventTimeTypeTimestampDate +} MTR_NEWLY_AVAILABLE; + +typedef NS_ENUM(NSUInteger, MTREventPriority) { + MTREventPriorityDebug = 0, + MTREventPriorityInfo = 1, + MTREventPriorityCritical = 2 +} MTR_NEWLY_AVAILABLE; + @interface MTREventReport : NSObject @property (nonatomic, readonly, copy) MTREventPath * path; @property (nonatomic, readonly, copy) NSNumber * eventNumber; // EventNumber type (uint64_t) -@property (nonatomic, readonly, copy) NSNumber * priority; // PriorityLevel type (uint8_t) -@property (nonatomic, readonly, copy) NSNumber * timestamp; // Timestamp type (uint64_t) +@property (nonatomic, readonly, copy) NSNumber * priority; // PriorityLevel type (MTREventPriority) + +// Either systemUpTime or timestampDate will be valid depending on eventTimeType +@property (nonatomic, readonly) MTREventTimeType eventTimeType MTR_NEWLY_AVAILABLE; +@property (nonatomic, readonly) NSTimeInterval systemUpTime MTR_NEWLY_AVAILABLE; +@property (nonatomic, readonly, copy, nullable) NSDate * timestampDate MTR_NEWLY_AVAILABLE; + // An instance of one of the event payload interfaces. @property (nonatomic, readonly, copy) id value; @@ -543,4 +559,8 @@ API_AVAILABLE(ios(16.4), macos(13.3), watchos(9.4), tvos(16.4)) @end +@interface MTREventReport (Deprecated) +@property (nonatomic, readonly, copy) NSNumber * timestamp MTR_NEWLY_DEPRECATED("Please use timestampDate and systemUpTime"); +@end + NS_ASSUME_NONNULL_END diff --git a/src/darwin/Framework/CHIP/MTRBaseDevice.mm b/src/darwin/Framework/CHIP/MTRBaseDevice.mm index 0657c656d1ede4..98760e8c4fff86 100644 --- a/src/darwin/Framework/CHIP/MTRBaseDevice.mm +++ b/src/darwin/Framework/CHIP/MTRBaseDevice.mm @@ -2042,19 +2042,36 @@ - (instancetype)initWithPath:(const ConcreteDataAttributePath &)path value:(id _ } @end +@interface MTREventReport () { + NSNumber * _timestampValue; +} +@end + @implementation MTREventReport -- (instancetype)initWithPath:(const ConcreteEventPath &)path +- (instancetype)initWithPath:(const chip::app::ConcreteEventPath &)path eventNumber:(NSNumber *)eventNumber - priority:(NSNumber *)priority - timestamp:(NSNumber *)timestamp + priority:(PriorityLevel)priority + timestamp:(const Timestamp &)timestamp value:(id _Nullable)value error:(NSError * _Nullable)error { if (self = [super init]) { _path = [[MTREventPath alloc] initWithPath:path]; _eventNumber = eventNumber; - _priority = priority; - _timestamp = timestamp; + if (!MTRPriorityLevelIsValid(priority)) { + return nil; + } + _priority = @(MTREventPriorityForValidPriorityLevel(priority)); + _timestampValue = @(timestamp.mValue); + if (timestamp.IsSystem()) { + _eventTimeType = MTREventTimeTypeSystemUpTime; + _systemUpTime = MTRTimeIntervalForEventTimestampValue(timestamp.mValue); + } else if (timestamp.IsEpoch()) { + _eventTimeType = MTREventTimeTypeTimestampDate; + _timestampDate = [NSDate dateWithTimeIntervalSince1970:MTRTimeIntervalForEventTimestampValue(timestamp.mValue)]; + } else { + return nil; + } _value = value; _error = error; } @@ -2062,6 +2079,13 @@ - (instancetype)initWithPath:(const ConcreteEventPath &)path } @end +@implementation MTREventReport (Deprecated) +- (NSNumber *)timestamp +{ + return _timestampValue; +} +@end + namespace { void SubscriptionCallback::OnEventData(const EventHeader & aEventHeader, TLV::TLVReader * apData, const StatusIB * apStatus) { @@ -2093,8 +2117,8 @@ - (instancetype)initWithPath:(const ConcreteEventPath &)path [mEventReports addObject:[[MTREventReport alloc] initWithPath:aEventHeader.mPath eventNumber:@(aEventHeader.mEventNumber) - priority:@((uint8_t) aEventHeader.mPriorityLevel) - timestamp:@(aEventHeader.mTimestamp.mValue) + priority:aEventHeader.mPriorityLevel + timestamp:aEventHeader.mTimestamp value:value error:error]]; } diff --git a/src/darwin/Framework/CHIP/MTRBaseDevice_Internal.h b/src/darwin/Framework/CHIP/MTRBaseDevice_Internal.h index 9e943d2327b690..9fb062a90cceb2 100644 --- a/src/darwin/Framework/CHIP/MTRBaseDevice_Internal.h +++ b/src/darwin/Framework/CHIP/MTRBaseDevice_Internal.h @@ -22,6 +22,7 @@ #include #include #include +#include @class MTRDeviceController; @@ -99,8 +100,8 @@ static inline MTRTransportType MTRMakeTransportType(chip::Transport::Type type) @interface MTREventReport () - (instancetype)initWithPath:(const chip::app::ConcreteEventPath &)path eventNumber:(NSNumber *)eventNumber - priority:(NSNumber *)priority - timestamp:(NSNumber *)timestamp + priority:(chip::app::PriorityLevel)priority + timestamp:(const chip::app::Timestamp &)timestamp value:(id _Nullable)value error:(NSError * _Nullable)error; @end diff --git a/src/darwin/Framework/CHIP/MTRDevice.h b/src/darwin/Framework/CHIP/MTRDevice.h index 313fdb779ebb1a..e3cd859b6f816d 100644 --- a/src/darwin/Framework/CHIP/MTRDevice.h +++ b/src/darwin/Framework/CHIP/MTRDevice.h @@ -62,6 +62,20 @@ typedef NS_ENUM(NSUInteger, MTRDeviceState) { */ @property (nonatomic, readonly) MTRDeviceState state; +/** + * The estimated device system start time. + * + * A device can report its events with either calendar time or time since system start time. When events are reported with time + * since system start time, this property will return an estimation of the device system start time. Because a device may report + * timestamps this way due to the lack of a wall clock, system start time can only be estimated based on event receive time and the + * timestamp value, and this estimation may change over time. + * + * Device reboots may also cause the estimated device start time to jump forward. + * + * If events are always reported with calendar time, then this property will return nil. + */ +@property (nonatomic, readonly, nullable) NSDate * estimatedStartTime MTR_NEWLY_AVAILABLE; + /** * Set the delegate to receive asynchronous callbacks about the device. * @@ -168,6 +182,12 @@ typedef NS_ENUM(NSUInteger, MTRDeviceState) { @end +extern NSString * const MTREventNumberKey MTR_NEWLY_AVAILABLE; +extern NSString * const MTREventPriorityKey MTR_NEWLY_AVAILABLE; +extern NSString * const MTREventTimeTypeKey MTR_NEWLY_AVAILABLE; +extern NSString * const MTREventSystemUpTimeKey MTR_NEWLY_AVAILABLE; +extern NSString * const MTREventTimestampDateKey MTR_NEWLY_AVAILABLE; + @protocol MTRDeviceDelegate @required /** @@ -186,6 +206,19 @@ typedef NS_ENUM(NSUInteger, MTRDeviceState) { * Notifies delegate of event reports from the MTRDevice * * @param eventReport An array of response-value objects as described in MTRDeviceResponseHandler + * + * In addition to the MTREventPathKey and MTRDataKey containing the path and event values, eventReport also contains + * these keys: + * + * MTREventNumberKey : NSNumber-wrapped uint64_t value. Monotonically increasing, and consecutive event reports + * should have consecutive numbers unless device reboots, or if events are lost. + * MTREventPriorityKey : NSNumber-wrapped MTREventPriority value. + * MTREventTimeTypeKey : NSNumber-wrapped MTREventTimeType value. + * MTREventSystemUpTimeKey : NSNumber-wrapped NSTimeInterval value. + * MTREventTimestampDateKey : NSDate object. + * + * Only one of MTREventTimestampDateKey and MTREventSystemUpTimeKey will be present, depending on the value for + * MTREventTimeTypeKey. */ - (void)device:(MTRDevice *)device receivedEventReport:(NSArray *> *)eventReport; diff --git a/src/darwin/Framework/CHIP/MTRDevice.mm b/src/darwin/Framework/CHIP/MTRDevice.mm index 672bdbef0b1fff..cf949a46e5e795 100644 --- a/src/darwin/Framework/CHIP/MTRDevice.mm +++ b/src/darwin/Framework/CHIP/MTRDevice.mm @@ -21,6 +21,7 @@ #import "MTRBaseDevice_Internal.h" #import "MTRBaseSubscriptionCallback.h" #import "MTRCluster.h" +#import "MTRClusterConstants.h" #import "MTRDeviceController_Internal.h" #import "MTRDevice_Internal.h" #import "MTRError_Internal.h" @@ -37,6 +38,12 @@ #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 @@ -102,6 +109,41 @@ - (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; @@ -217,11 +259,16 @@ - (void)_changeState:(MTRDeviceState)state { MTRDeviceState lastState = _state; _state = state; - id delegate = _weakDelegate.strongObject; - if (delegate && (lastState != state)) { - dispatch_async(_delegateQueue, ^{ - [delegate device:self stateChanged:state]; - }); + if (lastState != state) { + if (state != MTRDeviceStateReachable) { + _estimatedStartTime = nil; + } + id delegate = _weakDelegate.strongObject; + if (delegate) { + dispatch_async(_delegateQueue, ^{ + [delegate device:self stateChanged:state]; + }); + } } } @@ -347,7 +394,41 @@ - (void)_handleEventReport:(NSArray *> *)eventRepor { os_unfair_lock_lock(&self->_lock); - // first combine with previous unreported events, if they exist + NSDate * oldEstimatedStartTime = _estimatedStartTime; + for (NSDictionary * eventDict in eventReport) { + // Whenever a StartUp event is received, reset the estimated start time + MTREventPath * eventPath = eventDict[MTREventPathKey]; + BOOL isStartUpEvent = (eventPath.cluster.unsignedLongValue == MTRClusterIDTypeBasicInformationID) + && (eventPath.event.unsignedLongValue == MTREventIDTypeClusterBasicInformationEventStartUpID); + if (isStartUpEvent) { + _estimatedStartTime = nil; + } + + // If event time is of MTREventTimeTypeSystemUpTime type, then update estimated start time as needed + NSNumber * eventTimeTypeNumber = eventDict[MTREventTimeTypeKey]; + if (!eventTimeTypeNumber) { + MTR_LOG_ERROR("Event %@ missing event time type", eventDict); + continue; + } + MTREventTimeType eventTimeType = (MTREventTimeType) eventTimeTypeNumber.unsignedIntegerValue; + if (eventTimeType == MTREventTimeTypeSystemUpTime) { + NSNumber * eventTimeValueNumber = eventDict[MTREventSystemUpTimeKey]; + if (!eventTimeValueNumber) { + MTR_LOG_ERROR("Event %@ missing event time value", eventDict); + continue; + } + NSTimeInterval eventTimeValue = eventTimeValueNumber.doubleValue; + NSDate * potentialSystemStartTime = [NSDate dateWithTimeIntervalSinceNow:-eventTimeValue]; + if (!_estimatedStartTime || ([potentialSystemStartTime compare:_estimatedStartTime] == NSOrderedAscending)) { + _estimatedStartTime = potentialSystemStartTime; + } + } + } + if (oldEstimatedStartTime != _estimatedStartTime) { + MTR_LOG_INFO("%@ updated estimated start time to %@", self, _estimatedStartTime); + } + + // Combine with previous unreported events, if they exist if (_unreportedEvents) { eventReport = [_unreportedEvents arrayByAddingObjectsFromArray:eventReport]; _unreportedEvents = nil; @@ -950,7 +1031,40 @@ - (void)invokeCommandWithEndpointID:(NSNumber *)endpointID } else { id value = MTRDecodeDataValueDictionaryFromCHIPTLV(apData); if (value) { - [mEventReports addObject:@ { MTREventPathKey : eventPath, MTRDataKey : 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 + }]; } } } diff --git a/src/darwin/Framework/CHIP/MTRDevice_Internal.h b/src/darwin/Framework/CHIP/MTRDevice_Internal.h index c1611acaa39e8f..1cb11e83df748f 100644 --- a/src/darwin/Framework/CHIP/MTRDevice_Internal.h +++ b/src/darwin/Framework/CHIP/MTRDevice_Internal.h @@ -51,4 +51,11 @@ 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 2bf77149a576d5..1f9f0bae6121d0 100644 --- a/src/darwin/Framework/CHIPTests/MTRDeviceTests.m +++ b/src/darwin/Framework/CHIPTests/MTRDeviceTests.m @@ -1388,8 +1388,21 @@ - (void)test017_TestMTRDeviceBasics }; __block unsigned eventReportsReceived = 0; - delegate.onEventDataReceived = ^(NSArray *> * data) { - eventReportsReceived += data.count; + delegate.onEventDataReceived = ^(NSArray *> * eventReport) { + eventReportsReceived += eventReport.count; + + for (NSDictionary * eventDict in eventReport) { + NSNumber * eventTimeTypeNumber = eventDict[MTREventTimeTypeKey]; + XCTAssertNotNil(eventTimeTypeNumber); + MTREventTimeType eventTimeType = (MTREventTimeType) eventTimeTypeNumber.unsignedIntegerValue; + XCTAssert((eventTimeType == MTREventTimeTypeSystemUpTime) || (eventTimeType == MTREventTimeTypeTimestampDate)); + if (eventTimeType == MTREventTimeTypeSystemUpTime) { + XCTAssertNotNil(eventDict[MTREventSystemUpTimeKey]); + XCTAssertNotNil(device.estimatedStartTime); + } else if (eventTimeType == MTREventTimeTypeTimestampDate) { + XCTAssertNotNil(eventDict[MTREventTimestampDateKey]); + } + } }; [device setDelegate:delegate queue:queue]; @@ -1444,6 +1457,9 @@ - (void)test017_TestMTRDeviceBasics // with data versions) during the resubscribe. XCTAssertEqual(attributeReportsReceived, 0); XCTAssertEqual(eventReportsReceived, 0); + + // Check that device resets start time on subscription drop + XCTAssertNil(device.estimatedStartTime); } - (void)test018_SubscriptionErrorWhenNotResubscribing