Skip to content

Commit

Permalink
Darwin: Pass event timestamp to MTRDeviceDelegate (#24858)
Browse files Browse the repository at this point in the history
* Darwin: Pass event timestamp to MTRDeviceDelegate

Co-authored-by: Boris Zbarsky <[email protected]>
  • Loading branch information
jtung-apple and bzbarsky-apple authored Feb 10, 2023
1 parent 377e8e8 commit 02bc67e
Show file tree
Hide file tree
Showing 7 changed files with 235 additions and 20 deletions.
24 changes: 22 additions & 2 deletions src/darwin/Framework/CHIP/MTRBaseDevice.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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
38 changes: 31 additions & 7 deletions src/darwin/Framework/CHIP/MTRBaseDevice.mm
Original file line number Diff line number Diff line change
Expand Up @@ -2042,26 +2042,50 @@ - (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;
}
return self;
}
@end

@implementation MTREventReport (Deprecated)
- (NSNumber *)timestamp
{
return _timestampValue;
}
@end

namespace {
void SubscriptionCallback::OnEventData(const EventHeader & aEventHeader, TLV::TLVReader * apData, const StatusIB * apStatus)
{
Expand Down Expand Up @@ -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]];
}
Expand Down
5 changes: 3 additions & 2 deletions src/darwin/Framework/CHIP/MTRBaseDevice_Internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
#include <app/ConcreteCommandPath.h>
#include <app/ConcreteEventPath.h>
#include <app/DeviceProxy.h>
#include <app/EventLoggingTypes.h>

@class MTRDeviceController;

Expand Down Expand Up @@ -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
Expand Down
33 changes: 33 additions & 0 deletions src/darwin/Framework/CHIP/MTRDevice.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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 <NSObject>
@required
/**
Expand All @@ -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<NSDictionary<NSString *, id> *> *)eventReport;

Expand Down
128 changes: 121 additions & 7 deletions src/darwin/Framework/CHIP/MTRDevice.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -37,6 +38,12 @@
#include <app/InteractionModelEngine.h>
#include <platform/PlatformManager.h>

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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -217,11 +259,16 @@ - (void)_changeState:(MTRDeviceState)state
{
MTRDeviceState lastState = _state;
_state = state;
id<MTRDeviceDelegate> delegate = _weakDelegate.strongObject;
if (delegate && (lastState != state)) {
dispatch_async(_delegateQueue, ^{
[delegate device:self stateChanged:state];
});
if (lastState != state) {
if (state != MTRDeviceStateReachable) {
_estimatedStartTime = nil;
}
id<MTRDeviceDelegate> delegate = _weakDelegate.strongObject;
if (delegate) {
dispatch_async(_delegateQueue, ^{
[delegate device:self stateChanged:state];
});
}
}
}

Expand Down Expand Up @@ -347,7 +394,41 @@ - (void)_handleEventReport:(NSArray<NSDictionary<NSString *, id> *> *)eventRepor
{
os_unfair_lock_lock(&self->_lock);

// first combine with previous unreported events, if they exist
NSDate * oldEstimatedStartTime = _estimatedStartTime;
for (NSDictionary<NSString *, id> * 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;
Expand Down Expand Up @@ -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
}];
}
}
}
Expand Down
7 changes: 7 additions & 0 deletions src/darwin/Framework/CHIP/MTRDevice_Internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
20 changes: 18 additions & 2 deletions src/darwin/Framework/CHIPTests/MTRDeviceTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -1388,8 +1388,21 @@ - (void)test017_TestMTRDeviceBasics
};

__block unsigned eventReportsReceived = 0;
delegate.onEventDataReceived = ^(NSArray<NSDictionary<NSString *, id> *> * data) {
eventReportsReceived += data.count;
delegate.onEventDataReceived = ^(NSArray<NSDictionary<NSString *, id> *> * eventReport) {
eventReportsReceived += eventReport.count;

for (NSDictionary<NSString *, id> * 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];
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 02bc67e

Please sign in to comment.