Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Darwin] MTRDevice event report should indicate if received during priming. #30717

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/darwin/Framework/CHIP/MTRBaseDevice.h
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ NS_ASSUME_NONNULL_BEGIN
* Only present when MTREventTimeTypeKey is MTREventTimeTypeSystemUpTime.
* MTREventTimestampDateKey : NSDate object.
* Only present when MTREventTimeTypeKey is MTREventTimeTypeTimestampDate.
* MTREventIsHistoricalKey : NSNumber-wrapped BOOL value.
* Value is YES if the event is in the far past or not realtime.
* Only present when MTREventPathKey is present.
*
* Only one of MTREventTimestampDateKey and MTREventSystemUpTimeKey will be present, depending on the value for
* MTREventTimeTypeKey.
Expand Down Expand Up @@ -141,6 +144,7 @@ extern NSString * const MTREventPriorityKey MTR_AVAILABLE(ios(16.5), macos(13.4)
extern NSString * const MTREventTimeTypeKey MTR_AVAILABLE(ios(16.5), macos(13.4), watchos(9.5), tvos(16.5));
extern NSString * const MTREventSystemUpTimeKey MTR_AVAILABLE(ios(16.5), macos(13.4), watchos(9.5), tvos(16.5));
extern NSString * const MTREventTimestampDateKey MTR_AVAILABLE(ios(16.5), macos(13.4), watchos(9.5), tvos(16.5));
extern NSString * const MTREventIsHistoricalKey MTR_AVAILABLE(ios(17.3), macos(14.3), watchos(10.3), tvos(17.3));

@class MTRClusterStateCacheContainer;
@class MTRAttributeCacheContainer;
Expand Down
1 change: 1 addition & 0 deletions src/darwin/Framework/CHIP/MTRBaseDevice.mm
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
NSString * const MTREventTimeTypeKey = @"eventTimeType";
NSString * const MTREventSystemUpTimeKey = @"eventSystemUpTime";
NSString * const MTREventTimestampDateKey = @"eventTimestampDate";
NSString * const MTREventIsHistoricalKey = @"eventIsHistorical";

class MTRDataValueDictionaryCallbackBridge;

Expand Down
62 changes: 52 additions & 10 deletions src/darwin/Framework/CHIP/MTRDevice.mm
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,16 @@ @interface MTRDevice ()
@property (nonatomic) chip::FabricIndex fabricIndex;
@property (nonatomic) MTRWeakReference<id<MTRDeviceDelegate>> * weakDelegate;
@property (nonatomic) dispatch_queue_t delegateQueue;
@property (nonatomic) NSArray<NSDictionary<NSString *, id> *> * unreportedEvents;
@property (nonatomic) NSMutableArray<NSDictionary<NSString *, id> *> * unreportedEvents;
@property (nonatomic) BOOL receivingReport;
@property (nonatomic) BOOL receivingPrimingReport;

// TODO: instead of all the BOOL properties that are some facet of the state, move to internal state machine that has (at least):
// Unsubscribed (not attemping)
// Attempting subscription
// Subscribed (gotten subscription response / in steady state with no OnError/OnDone)
// Actively receiving report
// Actively receiving priming report

/**
* If subscriptionActive is true that means that either we are in the middle of
Expand Down Expand Up @@ -453,13 +462,21 @@ - (void)_handleUnsolicitedMessageFromPublisher
- (void)_handleReportBegin
{
os_unfair_lock_lock(&self->_lock);
[self _changeState:MTRDeviceStateReachable];
_receivingReport = YES;
if (_state != MTRDeviceStateReachable) {
_receivingPrimingReport = YES;
[self _changeState:MTRDeviceStateReachable];
} else {
_receivingPrimingReport = NO;
}
os_unfair_lock_unlock(&self->_lock);
}

- (void)_handleReportEnd
{
os_unfair_lock_lock(&self->_lock);
_receivingReport = NO;
_receivingPrimingReport = NO;
_estimatedStartTimeFromGeneralDiagnosticsUpTime = nil;
// For unit testing only
#ifdef DEBUG
Expand Down Expand Up @@ -499,11 +516,27 @@ - (void)_handleAttributeReport:(NSArray<NSDictionary<NSString *, id> *> *)attrib
os_unfair_lock_unlock(&self->_lock);
}

#ifdef DEBUG
- (void)unitTestInjectEventReport:(NSArray<NSDictionary<NSString *, id> *> *)eventReport
{
dispatch_async(self.queue, ^{
[self _handleEventReport:eventReport];
});
}
#endif

- (void)_handleEventReport:(NSArray<NSDictionary<NSString *, id> *> *)eventReport
{
os_unfair_lock_lock(&self->_lock);

NSDate * oldEstimatedStartTime = _estimatedStartTime;
// Combine with previous unreported events, if they exist
NSMutableArray * reportToReturn;
if (_unreportedEvents) {
reportToReturn = _unreportedEvents;
} else {
reportToReturn = [NSMutableArray array];
}
for (NSDictionary<NSString *, id> * eventDict in eventReport) {
// Whenever a StartUp event is received, reset the estimated start time
// New subscription case
Expand Down Expand Up @@ -564,25 +597,29 @@ - (void)_handleEventReport:(NSArray<NSDictionary<NSString *, id> *> *)eventRepor
_estimatedStartTime = potentialSystemStartTime;
}
}

NSMutableDictionary * eventToReturn = eventDict.mutableCopy;
if (_receivingPrimingReport) {
eventToReturn[MTREventIsHistoricalKey] = @(YES);
} else {
eventToReturn[MTREventIsHistoricalKey] = @(NO);
}

[reportToReturn addObject:eventToReturn];
}
if (oldEstimatedStartTime != _estimatedStartTime) {
MTR_LOG_DEFAULT("%@ updated estimated start time to %@", self, _estimatedStartTime);
}

// Combine with previous unreported events, if they exist
if (_unreportedEvents) {
eventReport = [_unreportedEvents arrayByAddingObjectsFromArray:eventReport];
_unreportedEvents = nil;
}

id<MTRDeviceDelegate> delegate = _weakDelegate.strongObject;
if (delegate) {
_unreportedEvents = nil;
dispatch_async(_delegateQueue, ^{
[delegate device:self receivedEventReport:eventReport];
[delegate device:self receivedEventReport:reportToReturn];
});
} else {
// save unreported events
_unreportedEvents = eventReport;
_unreportedEvents = reportToReturn;
}

os_unfair_lock_unlock(&self->_lock);
Expand Down Expand Up @@ -1450,6 +1487,11 @@ - (NSArray *)_getAttributesToReportWithReportedValues:(NSArray<NSDictionary<NSSt
continue;
}

// Additional signal to help mark events as being received during priming report in the event the device rebooted and we get a subscription resumption priming report without noticing it became unreachable first
if (_receivingReport && AttributeHasChangesOmittedQuality(attributePath)) {
_receivingPrimingReport = YES;
}

// check if value is different than cache, and report if needed
BOOL shouldReportAttribute = NO;

Expand Down
29 changes: 28 additions & 1 deletion src/darwin/Framework/CHIPTests/MTRDeviceTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ - (void)failSubscribers:(dispatch_queue_t)queue completion:(void (^)(void))compl
// Test function for whitebox testing
+ (id)CHIPEncodeAndDecodeNSObject:(id)object;
@end

@interface MTRDevice (Test)
- (void)unitTestInjectEventReport:(NSArray<NSDictionary<NSString *, id> *> *)eventReport;
@end
#endif

@interface MTRDeviceTestDeviceControllerDelegate : NSObject <MTRDeviceControllerDelegate>
Expand Down Expand Up @@ -1458,6 +1462,9 @@ - (void)test017_TestMTRDeviceBasics
// can satisfy the test below.
XCTestExpectation * gotReportsExpectation = [self expectationWithDescription:@"Attribute and Event reports have been received"];
__block unsigned eventReportsReceived = 0;
__block BOOL reportEnded = NO;
__block BOOL gotOneNonPrimingEvent = NO;
XCTestExpectation * gotNonPrimingEventExpectation = [self expectationWithDescription:@"Received event outside of priming report"];
delegate.onEventDataReceived = ^(NSArray<NSDictionary<NSString *, id> *> * eventReport) {
eventReportsReceived += eventReport.count;

Expand All @@ -1472,10 +1479,30 @@ - (void)test017_TestMTRDeviceBasics
} else if (eventTimeType == MTREventTimeTypeTimestampDate) {
XCTAssertNotNil(eventDict[MTREventTimestampDateKey]);
}

if (!reportEnded) {
NSNumber * reportIsHistorical = eventDict[MTREventIsHistoricalKey];
XCTAssertTrue(reportIsHistorical.boolValue);
} else {
if (!gotOneNonPrimingEvent) {
NSNumber * reportIsHistorical = eventDict[MTREventIsHistoricalKey];
XCTAssertFalse(reportIsHistorical.boolValue);
gotOneNonPrimingEvent = YES;
[gotNonPrimingEventExpectation fulfill];
}
}
}
};
delegate.onReportEnd = ^() {
reportEnded = YES;
[gotReportsExpectation fulfill];
#ifdef DEBUG
[device unitTestInjectEventReport:@[ @{
MTREventPathKey : [MTREventPath eventPathWithEndpointID:@(1) clusterID:@(1) eventID:@(1)],
MTREventTimeTypeKey : @(MTREventTimeTypeTimestampDate),
MTREventTimestampDateKey : [NSDate date]
} ]];
#endif
};

[device setDelegate:delegate queue:queue];
Expand Down Expand Up @@ -1506,7 +1533,7 @@ - (void)test017_TestMTRDeviceBasics
[device readAttributeWithEndpointID:@(1) clusterID:@(MTRClusterIDTypeLevelControlID) attributeID:@(4) params:nil];
[device readAttributeWithEndpointID:@(1) clusterID:@(MTRClusterIDTypeLevelControlID) attributeID:@(4) params:nil];

[self waitForExpectations:@[ subscriptionExpectation, gotReportsExpectation ] timeout:60];
[self waitForExpectations:@[ subscriptionExpectation, gotReportsExpectation, gotNonPrimingEventExpectation ] timeout:60];

delegate.onReportEnd = nil;

Expand Down
Loading