diff --git a/src/darwin/Framework/CHIP/MTRDevice.h b/src/darwin/Framework/CHIP/MTRDevice.h index 23a1ecf51ad56c..ee7b1608d8246c 100644 --- a/src/darwin/Framework/CHIP/MTRDevice.h +++ b/src/darwin/Framework/CHIP/MTRDevice.h @@ -381,6 +381,15 @@ 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 + * + * This is called when the MTRDevice object goes from not knowing the device to having cached the first attribute reports that include basic mandatory information, e.g. Descriptor clusters. + * + * The intention is that after this is called, the client should be able to call read for mandatory attributes and likely expect non-nil values. + */ +- (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 dbf1785c9bb6ba..0f304f42b7a741 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 @@ -502,6 +503,11 @@ - (void)setDelegate:(id)delegate queue:(dispatch_queue_t)queu _weakDelegate = [MTRWeakReference weakReferenceWithObject:delegate]; _delegateQueue = queue; + // If Check if cache is already primed and client hasn't been informed yet, call the -deviceCachePrimed: callback + if (!_delegateDeviceCachePrimedCalled && [self _isCachePrimedWithInitialConfigurationData]) { + [self _callDelegateDeviceCachePrimed]; + } + if (setUpSubscription) { [self _setupSubscription]; } @@ -574,6 +580,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 { @@ -611,6 +640,11 @@ - (void)_handleSubscriptionEstablished // reset subscription attempt wait time when subscription succeeds _lastSubscriptionAttemptWait = 0; + // As subscription is established, check if the delegate needs to be informed + if (!_delegateDeviceCachePrimedCalled) { + [self _callDelegateDeviceCachePrimed]; + } + [self _changeState:MTRDeviceStateReachable]; os_unfair_lock_unlock(&self->_lock); @@ -741,6 +775,7 @@ - (void)_handleReportEnd _receivingReport = NO; _receivingPrimingReport = NO; _estimatedStartTimeFromGeneralDiagnosticsUpTime = nil; + // For unit testing only #ifdef DEBUG id delegate = _weakDelegate.strongObject; @@ -1948,17 +1983,24 @@ - (NSArray *)_getAttributesToReportWithReportedValues:(NSArray *)attributeValues reportChanges:(BOOL)reportChanges { + os_unfair_lock_lock(&self->_lock); + if (reportChanges) { - [self _handleAttributeReport:attributeValues]; + [self _reportAttributes:[self _getAttributesToReportWithReportedValues:attributeValues]]; } else { - os_unfair_lock_lock(&self->_lock); for (NSDictionary * responseValue in attributeValues) { MTRAttributePath * path = responseValue[MTRAttributePathKey]; NSDictionary * dataValue = responseValue[MTRDataKey]; _readCache[path] = dataValue; } - os_unfair_lock_unlock(&self->_lock); } + + // If cache is set from storage and is primed with initial configuration data, then assume the client had beeen informed in the past, and mark that the callback has been called + if ([self _isCachePrimedWithInitialConfigurationData]) { + _delegateDeviceCachePrimedCalled = YES; + } + + os_unfair_lock_unlock(&self->_lock); } // If value is non-nil, associate with expectedValueID @@ -2135,6 +2177,42 @@ - (void)_removeExpectedValueForAttributePath:(MTRAttributePath *)attributePath e } } +// This method checks if there is a need to inform delegate that the attribute cache has been "primed" +- (BOOL)_isCachePrimedWithInitialConfigurationData +{ + os_unfair_lock_assert_owner(&self->_lock); + + // Check if root node descriptor exists + NSDictionary * rootDescriptorPartsListDataValue = _readCache[[MTRAttributePath attributePathWithEndpointID:@(kRootEndpointId) clusterID:@(MTRClusterIDTypeDescriptorID) attributeID:@(MTRAttributeIDTypeClusterDescriptorAttributePartsListID)]]; + if (!rootDescriptorPartsListDataValue || ![MTRArrayValueType isEqualToString:rootDescriptorPartsListDataValue[MTRTypeKey]]) { + return NO; + } + NSArray * partsList = rootDescriptorPartsListDataValue[MTRValueKey]; + if (![partsList isKindOfClass:[NSArray class]] || !partsList.count) { + MTR_LOG_ERROR("%@ unexpected type %@ for parts list %@", self, [partsList class], partsList); + return NO; + } + + // 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 NO; + } + } + + return YES; +} + - (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..e5539f4f75d993 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]; @@ -2879,10 +2883,17 @@ - (void)test031_MTRDeviceAttributeCacheLocalTestStorage __strong __auto_type strongDelegate = weakDelegate; strongDelegate.onReportEnd = nil; }; + __block BOOL onDeviceCachePrimedCalled = NO; + delegate.onDeviceCachePrimed = ^{ + onDeviceCachePrimedCalled = YES; + }; [device setDelegate:delegate queue:queue]; [self waitForExpectations:@[ resubGotReportsExpectation ] timeout:60]; + // Make sure that the new callback is only ever called once, the first time subscription was primed + XCTAssertFalse(onDeviceCachePrimedCalled); + NSUInteger attributesReportedWithSecondSubscription = [device unitTestAttributesReportedSinceLastCheck]; XCTAssertTrue(attributesReportedWithSecondSubscription < attributesReportedWithFirstSubscription); 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