diff --git a/src/darwin/Framework/CHIP/MTRDevice.h b/src/darwin/Framework/CHIP/MTRDevice.h index d4641d5dd321b3..39a9c126536467 100644 --- a/src/darwin/Framework/CHIP/MTRDevice.h +++ b/src/darwin/Framework/CHIP/MTRDevice.h @@ -403,6 +403,11 @@ MTR_EXTERN NSString * const MTRDataVersionKey MTR_NEWLY_AVAILABLE; */ - (void)deviceBecameActive:(MTRDevice *)device MTR_AVAILABLE(ios(16.4), macos(13.3), watchos(9.4), tvos(16.4)); +/** + * Notifies delegate when the device attribute cache has been primed with initial configuration data of the device + */ +- (void)deviceCachePrimed:(MTRDevice *)device MTR_NEWLY_AVAILABLE; + @end @interface MTRDevice (Deprecated) diff --git a/src/darwin/Framework/CHIP/MTRDevice.mm b/src/darwin/Framework/CHIP/MTRDevice.mm index 1b42caecf80c76..3c8a822b368969 100644 --- a/src/darwin/Framework/CHIP/MTRDevice.mm +++ b/src/darwin/Framework/CHIP/MTRDevice.mm @@ -225,6 +225,7 @@ @implementation MTRDevice { #ifdef DEBUG NSUInteger _unitTestAttributesReportedSinceLastCheck; #endif + BOOL _delegateDeviceCachePrimedCalled; } - (instancetype)initWithNodeID:(NSNumber *)nodeID controller:(MTRDeviceController *)controller @@ -506,6 +507,9 @@ - (void)setDelegate:(id)delegate queue:(dispatch_queue_t)queu [self _setupSubscription]; } + // Check if cache is already primed from storage + [self _checkIfCacheIsPrimed]; + os_unfair_lock_unlock(&self->_lock); } @@ -574,6 +578,29 @@ - (BOOL)_subscriptionAbleToReport return (delegate != nil) && (state == MTRDeviceStateReachable); } +- (BOOL)_callDelegateWithBlock:(void (^)(id))block +{ + os_unfair_lock_assert_owner(&self->_lock); + id delegate = _weakDelegate.strongObject; + if (delegate) { + dispatch_async(_delegateQueue, ^{ + block(delegate); + }); + return YES; + } + return NO; +} + +- (void)_callDelegateDeviceCachePrimed +{ + os_unfair_lock_assert_owner(&self->_lock); + _delegateDeviceCachePrimedCalled = [self _callDelegateWithBlock:^(id delegate) { + if ([delegate respondsToSelector:@selector(deviceCachePrimed:)]) { + [delegate deviceCachePrimed:self]; + } + }]; +} + // assume lock is held - (void)_changeState:(MTRDeviceState)state { @@ -741,6 +768,12 @@ - (void)_handleReportEnd _receivingReport = NO; _receivingPrimingReport = NO; _estimatedStartTimeFromGeneralDiagnosticsUpTime = nil; + + // First subscription report is priming report + if (!_delegateDeviceCachePrimedCalled) { + [self _callDelegateDeviceCachePrimed]; + } + // For unit testing only #ifdef DEBUG id delegate = _weakDelegate.strongObject; @@ -2147,6 +2180,48 @@ - (void)_removeExpectedValueForAttributePath:(MTRAttributePath *)attributePath e } } +// This method checks if there is a need to inform delegate that the attribute cache has been "primed" +// - The delegate callback is only called once +- (void)_checkIfCacheIsPrimed +{ + os_unfair_lock_assert_owner(&self->_lock); + + // Only send the callback once per lifetime of MTRDevice + if (_delegateDeviceCachePrimedCalled) { + return; + } + + // Check if root node descriptor exists + NSDictionary * rootDescriptorPartsListDataValue = _readCache[[MTRAttributePath attributePathWithEndpointID:@(0) clusterID:@(MTRClusterIDTypeDescriptorID) attributeID:@(MTRAttributeIDTypeClusterDescriptorAttributePartsListID)]]; + if (!rootDescriptorPartsListDataValue || ![MTRArrayValueType isEqualToString:rootDescriptorPartsListDataValue[MTRTypeKey]]) { + return; + } + NSArray * partsList = rootDescriptorPartsListDataValue[MTRValueKey]; + if (![partsList isKindOfClass:[NSArray class]] || !partsList.count) { + MTR_LOG_ERROR("%@ unexpected type %@ for parts list %@", self, [partsList class], partsList); + return; + } + + // Check if we have cached descriptor clusters for each listed endpoint + for (NSDictionary * endpointDataValue in partsList) { + if (![MTRUnsignedIntegerValueType isEqual:endpointDataValue[MTRTypeKey]]) { + MTR_LOG_ERROR("%@ unexpected type for parts list item %@", self, endpointDataValue); + continue; + } + NSNumber * endpoint = endpointDataValue[MTRValueKey]; + if (![endpoint isKindOfClass:[NSNumber class]]) { + MTR_LOG_ERROR("%@ unexpected type for parts list item %@", self, endpointDataValue); + continue; + } + NSDictionary * descriptorDeviceTypeListDataValue = _readCache[[MTRAttributePath attributePathWithEndpointID:endpoint clusterID:@(MTRClusterIDTypeDescriptorID) attributeID:@(MTRAttributeIDTypeClusterDescriptorAttributeDeviceTypeListID)]]; + if (![MTRArrayValueType isEqualToString:descriptorDeviceTypeListDataValue[MTRTypeKey]] || !descriptorDeviceTypeListDataValue[MTRValueKey]) { + return; + } + } + + [self _callDelegateDeviceCachePrimed]; +} + - (MTRBaseDevice *)newBaseDevice { return [MTRBaseDevice deviceWithNodeID:self.nodeID controller:self.deviceController]; diff --git a/src/darwin/Framework/CHIPTests/MTRDeviceTests.m b/src/darwin/Framework/CHIPTests/MTRDeviceTests.m index e4628bd475b5ae..ac514415518e01 100644 --- a/src/darwin/Framework/CHIPTests/MTRDeviceTests.m +++ b/src/darwin/Framework/CHIPTests/MTRDeviceTests.m @@ -2843,7 +2843,7 @@ - (void)test031_MTRDeviceAttributeCacheLocalTestStorage { dispatch_queue_t queue = dispatch_get_main_queue(); - // First start with clean slate and + // First start with clean slate by removing the MTRDevice and clearing the persisted cache __auto_type * device = [MTRDevice deviceWithNodeID:@(kDeviceId) controller:sController]; [sController removeDevice:device]; [sController.controllerDataStore clearAllStoredAttributes]; @@ -2853,6 +2853,7 @@ - (void)test031_MTRDeviceAttributeCacheLocalTestStorage // Now recreate device and get subscription primed device = [MTRDevice deviceWithNodeID:@(kDeviceId) controller:sController]; XCTestExpectation * gotReportsExpectation = [self expectationWithDescription:@"Attribute and Event reports have been received"]; + XCTestExpectation * gotDeviceCachePrimed = [self expectationWithDescription:@"Device cache primed for the first time"]; __auto_type * delegate = [[MTRDeviceTestDelegate alloc] init]; __weak __auto_type weakDelegate = delegate; delegate.onReportEnd = ^{ @@ -2860,9 +2861,12 @@ - (void)test031_MTRDeviceAttributeCacheLocalTestStorage __strong __auto_type strongDelegate = weakDelegate; strongDelegate.onReportEnd = nil; }; + delegate.onDeviceCachePrimed = ^{ + [gotDeviceCachePrimed fulfill]; + }; [device setDelegate:delegate queue:queue]; - [self waitForExpectations:@[ gotReportsExpectation ] timeout:60]; + [self waitForExpectations:@[ gotReportsExpectation, gotDeviceCachePrimed ] timeout:60]; NSUInteger attributesReportedWithFirstSubscription = [device unitTestAttributesReportedSinceLastCheck]; @@ -2874,14 +2878,18 @@ - (void)test031_MTRDeviceAttributeCacheLocalTestStorage device = [MTRDevice deviceWithNodeID:@(kDeviceId) controller:sController]; XCTestExpectation * resubGotReportsExpectation = [self expectationWithDescription:@"Attribute and Event reports have been received for resubscription"]; + XCTestExpectation * gotDeviceCachePrimedAgain = [self expectationWithDescription:@"Device cache primed upon load from persistence"]; delegate.onReportEnd = ^{ [resubGotReportsExpectation fulfill]; __strong __auto_type strongDelegate = weakDelegate; strongDelegate.onReportEnd = nil; }; + delegate.onDeviceCachePrimed = ^{ + [gotDeviceCachePrimedAgain fulfill]; + }; [device setDelegate:delegate queue:queue]; - [self waitForExpectations:@[ resubGotReportsExpectation ] timeout:60]; + [self waitForExpectations:@[ gotDeviceCachePrimedAgain, resubGotReportsExpectation ] timeout:60]; NSUInteger attributesReportedWithSecondSubscription = [device unitTestAttributesReportedSinceLastCheck]; diff --git a/src/darwin/Framework/CHIPTests/TestHelpers/MTRDeviceTestDelegate.h b/src/darwin/Framework/CHIPTests/TestHelpers/MTRDeviceTestDelegate.h index 0f7fce14226525..e8fd8f969b2aeb 100644 --- a/src/darwin/Framework/CHIPTests/TestHelpers/MTRDeviceTestDelegate.h +++ b/src/darwin/Framework/CHIPTests/TestHelpers/MTRDeviceTestDelegate.h @@ -27,6 +27,7 @@ typedef void (^MTRDeviceTestDelegateDataHandler)(NSArray