From b18b0a82fa45ab9c7315aea4508bc3a66b353aa0 Mon Sep 17 00:00:00 2001 From: Jeff Tung <100387939+jtung-apple@users.noreply.github.com> Date: Mon, 9 Oct 2023 17:11:38 -0700 Subject: [PATCH] [Darwin] MTRDevice cache make use of controller storage for persistent cache Co-authored-by: Boris Zbarsky --- src/darwin/Framework/CHIP/MTRBaseDevice.h | 4 +- src/darwin/Framework/CHIP/MTRBaseDevice.mm | 65 +++ src/darwin/Framework/CHIP/MTRDevice.mm | 56 ++- .../Framework/CHIP/MTRDeviceController.mm | 6 + .../CHIP/MTRDeviceControllerDataStore.h | 8 + .../CHIP/MTRDeviceControllerDataStore.mm | 408 ++++++++++++++++++ .../Framework/CHIP/MTRDevice_Internal.h | 5 + .../Framework/CHIPTests/MTRDeviceTests.m | 53 +-- .../CHIPTests/MTRPerControllerStorageTests.m | 210 +++++++++ .../TestHelpers/MTRDeviceTestDelegate.h | 32 ++ .../TestHelpers/MTRDeviceTestDelegate.m | 56 +++ .../Matter.xcodeproj/project.pbxproj | 6 + 12 files changed, 843 insertions(+), 66 deletions(-) create mode 100644 src/darwin/Framework/CHIPTests/TestHelpers/MTRDeviceTestDelegate.h create mode 100644 src/darwin/Framework/CHIPTests/TestHelpers/MTRDeviceTestDelegate.m diff --git a/src/darwin/Framework/CHIP/MTRBaseDevice.h b/src/darwin/Framework/CHIP/MTRBaseDevice.h index 64c8fd99981595..af0a15bd1a6857 100644 --- a/src/darwin/Framework/CHIP/MTRBaseDevice.h +++ b/src/darwin/Framework/CHIP/MTRBaseDevice.h @@ -549,7 +549,7 @@ MTR_AVAILABLE(ios(16.1), macos(13.0), watchos(9.1), tvos(16.1)) * wildcards). */ MTR_AVAILABLE(ios(16.4), macos(13.3), watchos(9.4), tvos(16.4)) -@interface MTRClusterPath : NSObject +@interface MTRClusterPath : NSObject @property (nonatomic, readonly, copy) NSNumber * endpoint; @property (nonatomic, readonly, copy) NSNumber * cluster; @@ -565,7 +565,7 @@ MTR_AVAILABLE(ios(16.4), macos(13.3), watchos(9.4), tvos(16.4)) * wildcards). */ MTR_AVAILABLE(ios(16.1), macos(13.0), watchos(9.1), tvos(16.1)) -@interface MTRAttributePath : MTRClusterPath +@interface MTRAttributePath : MTRClusterPath @property (nonatomic, readonly, copy) NSNumber * attribute; diff --git a/src/darwin/Framework/CHIP/MTRBaseDevice.mm b/src/darwin/Framework/CHIP/MTRBaseDevice.mm index a14986d2e24f5a..41de3b4646f870 100644 --- a/src/darwin/Framework/CHIP/MTRBaseDevice.mm +++ b/src/darwin/Framework/CHIP/MTRBaseDevice.mm @@ -2425,6 +2425,42 @@ - (id)copyWithZone:(NSZone *)zone return [MTRClusterPath clusterPathWithEndpointID:_endpoint clusterID:_cluster]; } +static NSString * const sEndpointKey = @"endpointKey"; +static NSString * const sClusterKey = @"clusterKey"; + ++ (BOOL)supportsSecureCoding +{ + return YES; +} + +- (nullable instancetype)initWithCoder:(NSCoder *)decoder +{ + self = [super init]; + if (self == nil) { + return nil; + } + + _endpoint = [decoder decodeObjectOfClass:[NSNumber class] forKey:sEndpointKey]; + if (_endpoint && ![_endpoint isKindOfClass:[NSNumber class]]) { + MTR_LOG_ERROR("MTRClusterPath decoded %@ for endpoint, not NSNumber.", _endpoint); + return nil; + } + + _cluster = [decoder decodeObjectOfClass:[NSNumber class] forKey:sClusterKey]; + if (_cluster && ![_cluster isKindOfClass:[NSNumber class]]) { + MTR_LOG_ERROR("MTRClusterPath decoded %@ for cluster, not NSNumber.", _cluster); + return nil; + } + + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder +{ + [coder encodeObject:_endpoint forKey:sEndpointKey]; + [coder encodeObject:_cluster forKey:sClusterKey]; +} + @end @implementation MTRAttributePath @@ -2482,6 +2518,35 @@ - (ConcreteAttributePath)_asConcretePath return ConcreteAttributePath([self.endpoint unsignedShortValue], static_cast([self.cluster unsignedLongValue]), static_cast([self.attribute unsignedLongValue])); } + +static NSString * const sAttributeKey = @"attributeKey"; + ++ (BOOL)supportsSecureCoding +{ + return YES; +} + +- (nullable instancetype)initWithCoder:(NSCoder *)decoder +{ + self = [super initWithCoder:decoder]; + if (self == nil) { + return nil; + } + + _attribute = [decoder decodeObjectOfClass:[NSNumber class] forKey:sAttributeKey]; + if (_attribute && ![_attribute isKindOfClass:[NSNumber class]]) { + MTR_LOG_ERROR("MTRAttributePath decoded %@ for attribute, not NSNumber.", _attribute); + return nil; + } + + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder +{ + [coder encodeObject:_attribute forKey:sAttributeKey]; +} + @end @implementation MTRAttributePath (Deprecated) diff --git a/src/darwin/Framework/CHIP/MTRDevice.mm b/src/darwin/Framework/CHIP/MTRDevice.mm index 22f1de7e4c18a0..a7aa7645185a7c 100644 --- a/src/darwin/Framework/CHIP/MTRDevice.mm +++ b/src/darwin/Framework/CHIP/MTRDevice.mm @@ -1503,15 +1503,20 @@ - (NSArray *)_getAttributesToReportWithReportedValues:(NSArray * attributeReponseValue in reportedAttributeValues) { - MTRAttributePath * attributePath = attributeReponseValue[MTRAttributePathKey]; - NSDictionary * attributeDataValue = attributeReponseValue[MTRDataKey]; - NSError * attributeError = attributeReponseValue[MTRErrorKey]; + BOOL dataStoreExists = _deviceController.controllerDataStore != nil; + NSMutableArray * attributesToPersist; + if (dataStoreExists) { + attributesToPersist = [NSMutableArray array]; + } + for (NSDictionary * attributeResponseValue in reportedAttributeValues) { + MTRAttributePath * attributePath = attributeResponseValue[MTRAttributePathKey]; + NSDictionary * attributeDataValue = attributeResponseValue[MTRDataKey]; + NSError * attributeError = attributeResponseValue[MTRErrorKey]; NSDictionary * previousValue; // sanity check either data value or error must exist if (!attributeDataValue && !attributeError) { - MTR_LOG_INFO("%@ report %@ no data value or error: %@", self, attributePath, attributeReponseValue); + MTR_LOG_INFO("%@ report %@ no data value or error: %@", self, attributePath, attributeResponseValue); continue; } @@ -1532,6 +1537,12 @@ - (NSArray *)_getAttributesToReportWithReportedValues:(NSArray *)attributeValues reportChanges:(BOOL)reportChanges +{ + if (reportChanges) { + [self _handleAttributeReport: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 value is non-nil, associate with expectedValueID // If value is nil, remove only if expectedValueID matches // previousValue is an out parameter @@ -1662,9 +1692,9 @@ - (NSArray *)_getAttributesToReportWithNewExpectedValues:(NSArray * attributeReponseValue in expectedAttributeValues) { - MTRAttributePath * attributePath = attributeReponseValue[MTRAttributePathKey]; - NSDictionary * attributeDataValue = attributeReponseValue[MTRDataKey]; + for (NSDictionary * attributeResponseValue in expectedAttributeValues) { + MTRAttributePath * attributePath = attributeResponseValue[MTRAttributePathKey]; + NSDictionary * attributeDataValue = attributeResponseValue[MTRDataKey]; BOOL shouldReportValue = NO; NSDictionary * attributeValueToReport; diff --git a/src/darwin/Framework/CHIP/MTRDeviceController.mm b/src/darwin/Framework/CHIP/MTRDeviceController.mm index 9621b447fb49b1..bddfd7814114c1 100644 --- a/src/darwin/Framework/CHIP/MTRDeviceController.mm +++ b/src/darwin/Framework/CHIP/MTRDeviceController.mm @@ -864,6 +864,12 @@ - (MTRDevice *)deviceForNodeID:(NSNumber *)nodeID if ([self isRunning]) { _nodeIDToDeviceMap[nodeID] = deviceToReturn; } + + // Load persisted attributes if they exist. + NSArray * attributesFromCache = [_controllerDataStore getStoredAttributesForNodeID:nodeID]; + if (attributesFromCache) { + [deviceToReturn setAttributeValues:attributesFromCache reportChanges:NO]; + } } os_unfair_lock_unlock(&_deviceMapLock); diff --git a/src/darwin/Framework/CHIP/MTRDeviceControllerDataStore.h b/src/darwin/Framework/CHIP/MTRDeviceControllerDataStore.h index 21adb966b80997..8df10f63e1d27d 100644 --- a/src/darwin/Framework/CHIP/MTRDeviceControllerDataStore.h +++ b/src/darwin/Framework/CHIP/MTRDeviceControllerDataStore.h @@ -63,6 +63,14 @@ NS_ASSUME_NONNULL_BEGIN - (CHIP_ERROR)storeLastLocallyUsedNOC:(MTRCertificateTLVBytes)noc; - (MTRCertificateTLVBytes _Nullable)fetchLastLocallyUsedNOC; +/** + * Storage for MTRDevice attribute read cache. This is local-only storage as an optimization. New controller devices using MTRDevice API can prime their own local cache from devices directly. + */ +- (nullable NSArray *)getStoredAttributesForNodeID:(NSNumber *)nodeID; +- (void)storeAttributeValues:(NSArray *)dataValues forNodeID:(NSNumber *)nodeID; +- (void)clearStoredAttributesForNodeID:(NSNumber *)nodeID; +- (void)clearAllStoredAttributes; + @end NS_ASSUME_NONNULL_END diff --git a/src/darwin/Framework/CHIP/MTRDeviceControllerDataStore.mm b/src/darwin/Framework/CHIP/MTRDeviceControllerDataStore.mm index 7336b27f225cee..12acafb4db0097 100644 --- a/src/darwin/Framework/CHIP/MTRDeviceControllerDataStore.mm +++ b/src/darwin/Framework/CHIP/MTRDeviceControllerDataStore.mm @@ -15,6 +15,9 @@ */ #include "MTRDeviceControllerDataStore.h" + +// Importing MTRBaseDevice.h for the MTRAttributePath class. Needs to change when https://github.com/project-chip/connectedhomeip/issues/31247 is fixed. +#import "MTRBaseDevice.h" #import "MTRLogging_Internal.h" #include @@ -273,6 +276,408 @@ - (nullable MTRCASESessionResumptionInfo *)_findResumptionInfoWithKey:(nullable return resumptionInfo; } +#pragma - Attribute Cache utility + +/** MTRDevice cache storage + * + * Per controller: + * NodeID index + * key: "attrCacheNodeIndex" + * value: list of nodeIDs + * EndpointID index + * key: "attrCacheEndpointIndex::endpointID" + * value: list of endpoint IDs + * ClusterID index + * key: " clusters" + * value: list of cluster IDs + * AttributeID index + * key: " attributes" + * value: list of attribute IDs + * Attribute data entry: + * key: " attribute data" + * value: serialized dictionary of attribute data + * + * Attribute data dictionary + * Additional value "serial number" + */ + +- (id)_fetchAttributeCacheValueForKey:(NSString *)key expectedClass:(Class)expectedClass; +{ + __block id data; + dispatch_sync(_storageDelegateQueue, ^{ + data = [_storageDelegate controller:_controller + valueForKey:key + securityLevel:MTRStorageSecurityLevelSecure + sharingType:MTRStorageSharingTypeNotShared]; + }); + + if (data == nil) { + return nil; + } + + if (![data isKindOfClass:expectedClass]) { + return nil; + } + + return data; +} + +- (BOOL)_storeAttributeCacheValue:(id)value forKey:(NSString *)key +{ + return [_storageDelegate controller:_controller + storeValue:value + forKey:key + securityLevel:MTRStorageSecurityLevelSecure + sharingType:MTRStorageSharingTypeNotShared]; +} + +- (void)_removeAttributeCacheValueForKey:(NSString *)key +{ + [_storageDelegate controller:_controller + removeValueForKey:key + securityLevel:MTRStorageSecurityLevelSecure + sharingType:MTRStorageSharingTypeNotShared]; +} + +static NSString * sAttributeCacheNodeIndexKey = @"attrCacheNodeIndex"; + +- (nullable NSArray *)_fetchNodeIndex +{ + return [self _fetchAttributeCacheValueForKey:sAttributeCacheNodeIndexKey expectedClass:[NSArray class]]; +} + +- (BOOL)_storeNodeIndex:(NSArray *)nodeIndex +{ + return [self _storeAttributeCacheValue:nodeIndex forKey:sAttributeCacheNodeIndexKey]; +} + +- (void)_deleteNodeIndex +{ + [self _removeAttributeCacheValueForKey:sAttributeCacheNodeIndexKey]; +} + +static NSString * sAttributeCacheEndpointIndexKeyPrefix = @"attrCacheEndpointIndex"; + +- (NSString *)_endpointIndexKeyForNodeID:(NSNumber *)nodeID +{ + return [sAttributeCacheEndpointIndexKeyPrefix stringByAppendingFormat:@":0x%016llX", nodeID.unsignedLongLongValue]; +} + +- (nullable NSArray *)_fetchEndpointIndexForNodeID:(NSNumber *)nodeID +{ + return [self _fetchAttributeCacheValueForKey:[self _endpointIndexKeyForNodeID:nodeID] expectedClass:[NSArray class]]; +} + +- (BOOL)_storeEndpointIndex:(NSArray *)endpointIndex forNodeID:(NSNumber *)nodeID +{ + return [self _storeAttributeCacheValue:endpointIndex forKey:[self _endpointIndexKeyForNodeID:nodeID]]; +} + +- (void)_deleteEndpointIndexForNodeID:(NSNumber *)nodeID +{ + [self _removeAttributeCacheValueForKey:[self _endpointIndexKeyForNodeID:nodeID]]; +} + +static NSString * sAttributeCacheClusterIndexKeyPrefix = @"attrCacheClusterIndex"; + +- (NSString *)_clusterIndexKeyForNodeID:(NSNumber *)nodeID endpointID:(NSNumber *)endpointID +{ + return [sAttributeCacheClusterIndexKeyPrefix stringByAppendingFormat:@":0x%016llX:%0x04X", nodeID.unsignedLongLongValue, endpointID.unsignedShortValue]; +} + +- (nullable NSArray *)_fetchClusterIndexForNodeID:(NSNumber *)nodeID endpointID:(NSNumber *)endpointID +{ + return [self _fetchAttributeCacheValueForKey:[self _clusterIndexKeyForNodeID:nodeID endpointID:endpointID] expectedClass:[NSArray class]]; +} + +- (BOOL)_storeClusterIndex:(NSArray *)clusterIndex forNodeID:(NSNumber *)nodeID endpointID:(NSNumber *)endpointID +{ + return [self _storeAttributeCacheValue:clusterIndex forKey:[self _clusterIndexKeyForNodeID:nodeID endpointID:endpointID]]; +} + +- (void)_deleteClusterIndexForNodeID:(NSNumber *)nodeID endpointID:(NSNumber *)endpointID +{ + [self _removeAttributeCacheValueForKey:[self _clusterIndexKeyForNodeID:nodeID endpointID:endpointID]]; +} + +static NSString * sAttributeCacheAttributeIndexKeyPrefix = @"attrCacheAttributeIndex"; + +- (NSString *)_attributeIndexKeyForNodeID:(NSNumber *)nodeID endpointID:(NSNumber *)endpointID clusterID:(NSNumber *)clusterID +{ + return [sAttributeCacheAttributeIndexKeyPrefix stringByAppendingFormat:@":0x%016llX:0x%04X:0x%08lX", nodeID.unsignedLongLongValue, endpointID.unsignedShortValue, clusterID.unsignedLongValue]; +} + +- (nullable NSArray *)_fetchAttributeIndexForNodeID:(NSNumber *)nodeID endpointID:(NSNumber *)endpointID clusterID:(NSNumber *)clusterID +{ + return [self _fetchAttributeCacheValueForKey:[self _attributeIndexKeyForNodeID:nodeID endpointID:endpointID clusterID:clusterID] expectedClass:[NSArray class]]; +} + +- (BOOL)_storeAttributeIndex:(NSArray *)attributeIndex forNodeID:(NSNumber *)nodeID endpointID:(NSNumber *)endpointID clusterID:(NSNumber *)clusterID +{ + return [self _storeAttributeCacheValue:attributeIndex forKey:[self _attributeIndexKeyForNodeID:nodeID endpointID:endpointID clusterID:clusterID]]; +} + +- (void)_deleteAttributeIndexForNodeID:(NSNumber *)nodeID endpointID:(NSNumber *)endpointID clusterID:(NSNumber *)clusterID +{ + [self _removeAttributeCacheValueForKey:[self _attributeIndexKeyForNodeID:nodeID endpointID:endpointID clusterID:clusterID]]; +} + +static NSString * sAttributeCacheAttributeValueKeyPrefix = @"attrCacheAttributeValue"; + +- (NSString *)_attributeValueKeyForNodeID:(NSNumber *)nodeID endpointID:(NSNumber *)endpointID clusterID:(NSNumber *)clusterID attributeID:(NSNumber *)attributeID +{ + return [sAttributeCacheAttributeValueKeyPrefix stringByAppendingFormat:@":0x%016llX:0x%04X:0x%08lX:0x%08lX", nodeID.unsignedLongLongValue, endpointID.unsignedShortValue, clusterID.unsignedLongValue, attributeID.unsignedLongValue]; +} + +- (nullable NSDictionary *)_fetchAttributeValueForNodeID:(NSNumber *)nodeID endpointID:(NSNumber *)endpointID clusterID:(NSNumber *)clusterID attributeID:(NSNumber *)attributeID +{ + return [self _fetchAttributeCacheValueForKey:[self _attributeValueKeyForNodeID:nodeID endpointID:endpointID clusterID:clusterID attributeID:attributeID] expectedClass:[NSDictionary class]]; +} + +- (BOOL)_storeAttributeValue:(NSDictionary *)value forNodeID:(NSNumber *)nodeID endpointID:(NSNumber *)endpointID clusterID:(NSNumber *)clusterID attributeID:(NSNumber *)attributeID +{ + return [self _storeAttributeCacheValue:value forKey:[self _attributeValueKeyForNodeID:nodeID endpointID:endpointID clusterID:clusterID attributeID:attributeID]]; +} + +- (void)_deleteAttributeValueForNodeID:(NSNumber *)nodeID endpointID:(NSNumber *)endpointID clusterID:(NSNumber *)clusterID attributeID:(NSNumber *)attributeID +{ + [self _removeAttributeCacheValueForKey:[self _attributeValueKeyForNodeID:nodeID endpointID:endpointID clusterID:clusterID attributeID:attributeID]]; +} + +#pragma - Attribute Cache management + +- (nullable NSArray *)getStoredAttributesForNodeID:(NSNumber *)nodeID +{ + NSMutableArray * attributesToReturn = nil; + + // Fetch node index + NSArray * nodeIndex = [self _fetchNodeIndex]; + + if (![nodeIndex containsObject:nodeID]) { + // Sanity check and delete if nodeID exists in index + NSArray * endpointIndex = [self _fetchEndpointIndexForNodeID:nodeID]; + if (endpointIndex) { + MTR_LOG_ERROR("Persistent attribute cache contains orphaned entry for nodeID %@ - deleting", nodeID); + [self clearStoredAttributesForNodeID:nodeID]; + } + return nil; + } + + // Fetch endpoint index + NSArray * endpointIndex = [self _fetchEndpointIndexForNodeID:nodeID]; + + for (NSNumber * endpointID in endpointIndex) { + // Fetch endpoint index + NSArray * clusterIndex = [self _fetchClusterIndexForNodeID:nodeID endpointID:endpointID]; + + for (NSNumber * clusterID in clusterIndex) { + // Fetch endpoint index + NSArray * attributeIndex = [self _fetchAttributeIndexForNodeID:nodeID endpointID:endpointID clusterID:clusterID]; + + for (NSNumber * attributeID in attributeIndex) { + NSDictionary * value = [self _fetchAttributeValueForNodeID:nodeID endpointID:endpointID clusterID:clusterID attributeID:attributeID]; + + if (value) { + if (!attributesToReturn) { + attributesToReturn = [NSMutableArray array]; + } + + // Construct data-value dictionary and add to array + MTRAttributePath * path = [MTRAttributePath attributePathWithEndpointID:endpointID clusterID:clusterID attributeID:attributeID]; + [attributesToReturn addObject:@{ MTRAttributePathKey : path, MTRDataKey : value }]; + } + } + + // TODO: Add per-cluster integrity check verification + } + } + + return attributesToReturn; +} + +- (void)_pruneEmptyStoredAttributesBranches +{ + // Fetch node index + NSMutableArray * nodeIndex = [self _fetchNodeIndex].mutableCopy; + NSUInteger nodeIndexCount = nodeIndex.count; + + for (NSNumber * nodeID in nodeIndex) { + // Fetch endpoint index + NSMutableArray * endpointIndex = [self _fetchEndpointIndexForNodeID:nodeID].mutableCopy; + NSUInteger endpointIndexCount = endpointIndex.count; + + for (NSNumber * endpointID in endpointIndex) { + // Fetch endpoint index + NSMutableArray * clusterIndex = [self _fetchClusterIndexForNodeID:nodeID endpointID:endpointID].mutableCopy; + NSUInteger clusterIndexCount = clusterIndex.count; + + for (NSNumber * clusterID in clusterIndex) { + // Fetch endpoint index + NSMutableArray * attributeIndex = [self _fetchAttributeIndexForNodeID:nodeID endpointID:endpointID clusterID:clusterID].mutableCopy; + NSUInteger attributeIndexCount = attributeIndex.count; + + for (NSNumber * attributeID in attributeIndex) { + NSDictionary * value = [self _fetchAttributeValueForNodeID:nodeID endpointID:endpointID clusterID:clusterID attributeID:attributeID]; + + if (!value) { + [attributeIndex removeObject:attributeID]; + } + } + + if (!attributeIndex.count) { + [clusterIndex removeObject:clusterID]; + } else if (attributeIndex.count != attributeIndexCount) { + [self _storeAttributeIndex:attributeIndex forNodeID:nodeID endpointID:endpointID clusterID:clusterID]; + } + } + + if (!clusterIndex.count) { + [endpointIndex removeObject:endpointID]; + } else if (clusterIndex.count != clusterIndexCount) { + [self _storeClusterIndex:clusterIndex forNodeID:nodeID endpointID:endpointID]; + } + } + + if (!endpointIndex.count) { + [nodeIndex removeObject:nodeID]; + } else if (endpointIndex.count != endpointIndexCount) { + [self _storeEndpointIndex:endpointIndex forNodeID:nodeID]; + } + } + + if (!nodeIndex.count) { + [self _deleteNodeIndex]; + } else if (nodeIndex.count != nodeIndexCount) { + [self _storeNodeIndex:nodeIndex]; + } +} + +- (void)storeAttributeValues:(NSArray *)dataValues forNodeID:(NSNumber *)nodeID +{ + dispatch_async(_storageDelegateQueue, ^{ + BOOL anyStoreFailed = NO; + + for (NSDictionary * dataValue in dataValues) { + MTRAttributePath * path = dataValue[MTRAttributePathKey]; + NSDictionary * value = dataValue[MTRDataKey]; + + BOOL storeFailed = NO; + // Ensure node index exists + NSArray * nodeIndex = [self _fetchNodeIndex]; + if (!nodeIndex) { + nodeIndex = [NSArray arrayWithObject:nodeID]; + storeFailed = ![self _storeNodeIndex:nodeIndex]; + } else if (![nodeIndex containsObject:nodeID]) { + storeFailed = ![self _storeNodeIndex:[nodeIndex arrayByAddingObject:nodeID]]; + } + if (storeFailed) { + anyStoreFailed = YES; + continue; + } + + // Ensure endpoint index exists + NSArray * endpointIndex = [self _fetchEndpointIndexForNodeID:nodeID]; + if (!endpointIndex) { + endpointIndex = [NSArray arrayWithObject:path.endpoint]; + storeFailed = ![self _storeEndpointIndex:endpointIndex forNodeID:nodeID]; + } else if (![endpointIndex containsObject:path.endpoint]) { + storeFailed = ![self _storeEndpointIndex:[endpointIndex arrayByAddingObject:path.endpoint] forNodeID:nodeID]; + } + if (storeFailed) { + anyStoreFailed = YES; + continue; + } + + // Ensure cluster index exists + NSArray * clusterIndex = [self _fetchClusterIndexForNodeID:nodeID endpointID:path.endpoint]; + if (!clusterIndex) { + clusterIndex = [NSArray arrayWithObject:path.cluster]; + storeFailed = ![self _storeClusterIndex:clusterIndex forNodeID:nodeID endpointID:path.endpoint]; + } else if (![clusterIndex containsObject:path.cluster]) { + storeFailed = ![self _storeClusterIndex:[clusterIndex arrayByAddingObject:path.cluster] forNodeID:nodeID endpointID:path.endpoint]; + } + if (storeFailed) { + anyStoreFailed = YES; + continue; + } + + // TODO: Add per-cluster integrity check calculation and store with cluster + // TODO: Think about adding more integrity check for endpoint and node levels as well + + // Ensure attribute index exists + NSArray * attributeIndex = [self _fetchAttributeIndexForNodeID:nodeID endpointID:path.endpoint clusterID:path.cluster]; + if (!attributeIndex) { + attributeIndex = [NSArray arrayWithObject:path.attribute]; + storeFailed = ![self _storeAttributeIndex:attributeIndex forNodeID:nodeID endpointID:path.endpoint clusterID:path.cluster]; + } else if (![attributeIndex containsObject:path.attribute]) { + storeFailed = ![self _storeAttributeIndex:[attributeIndex arrayByAddingObject:path.attribute] forNodeID:nodeID endpointID:path.endpoint clusterID:path.cluster]; + } + if (storeFailed) { + anyStoreFailed = YES; + continue; + } + + // Store value + storeFailed = [self _storeAttributeValue:value forNodeID:nodeID endpointID:path.endpoint clusterID:path.cluster attributeID:path.attribute]; + if (storeFailed) { + anyStoreFailed = YES; + } + } + + // In the rare event that store fails, allow all attribute store attempts to go through and prune empty branches at the end altogether. + if (anyStoreFailed) { + [self _pruneEmptyStoredAttributesBranches]; + } + }); +} + +- (void)_clearStoredAttributesForNodeID:(NSNumber *)nodeID +{ + // Fetch endpoint index + NSArray * endpointIndex = [self _fetchEndpointIndexForNodeID:nodeID]; + + for (NSNumber * endpointID in endpointIndex) { + // Fetch cluster index + NSArray * clusterIndex = [self _fetchClusterIndexForNodeID:nodeID endpointID:endpointID]; + + for (NSNumber * clusterID in clusterIndex) { + // Fetch attribute index + NSArray * attributeIndex = [self _fetchAttributeIndexForNodeID:nodeID endpointID:endpointID clusterID:clusterID]; + + for (NSNumber * attributeID in attributeIndex) { + [self _deleteAttributeValueForNodeID:nodeID endpointID:endpointID clusterID:clusterID attributeID:attributeID]; + } + + [self _deleteAttributeIndexForNodeID:nodeID endpointID:endpointID clusterID:clusterID]; + } + + [self _deleteClusterIndexForNodeID:nodeID endpointID:endpointID]; + } + + [self _deleteEndpointIndexForNodeID:nodeID]; +} + +- (void)clearStoredAttributesForNodeID:(NSNumber *)nodeID +{ + dispatch_async(_storageDelegateQueue, ^{ + [self _clearStoredAttributesForNodeID:nodeID]; + }); +} + +- (void)clearAllStoredAttributes +{ + dispatch_async(_storageDelegateQueue, ^{ + // Fetch node index + NSArray * nodeIndex = [self _fetchNodeIndex]; + + for (NSNumber * nodeID in nodeIndex) { + [self _clearStoredAttributesForNodeID:nodeID]; + } + + [self _deleteNodeIndex]; + }); +} + @end @implementation MTRCASESessionResumptionInfo @@ -356,6 +761,9 @@ - (void)encodeWithCoder:(NSCoder *)coder [NSData class], [NSArray class], [MTRCASESessionResumptionInfo class], + [NSDictionary class], + [NSString class], + [MTRAttributePath class], ]]; return sStorageClasses; } diff --git a/src/darwin/Framework/CHIP/MTRDevice_Internal.h b/src/darwin/Framework/CHIP/MTRDevice_Internal.h index cad9af9e79cf81..bda6cceaace1c2 100644 --- a/src/darwin/Framework/CHIP/MTRDevice_Internal.h +++ b/src/darwin/Framework/CHIP/MTRDevice_Internal.h @@ -68,6 +68,11 @@ typedef void (^MTRDevicePerformAsyncBlock)(MTRBaseDevice * baseDevice); @property (nonatomic) dispatch_queue_t queue; @property (nonatomic, readonly) MTRAsyncWorkQueue * asyncWorkQueue; +// Method to insert attribute values +// attributeValues : array of response-value dictionaries with non-null MTRAttributePathKey value +// reportChanges : if set to YES, attribute reports will also sent to the delegate if new values are different +- (void)setAttributeValues:(NSArray *)attributeValues reportChanges:(BOOL)reportChanges; + @end #pragma mark - Utility for clamping numbers diff --git a/src/darwin/Framework/CHIPTests/MTRDeviceTests.m b/src/darwin/Framework/CHIPTests/MTRDeviceTests.m index ffe7fa989cd50e..4fbe535e46a942 100644 --- a/src/darwin/Framework/CHIPTests/MTRDeviceTests.m +++ b/src/darwin/Framework/CHIPTests/MTRDeviceTests.m @@ -25,6 +25,7 @@ #import #import "MTRCommandPayloadExtensions_Internal.h" +#import "MTRDeviceTestDelegate.h" #import "MTRErrorTestUtils.h" #import "MTRTestKeys.h" #import "MTRTestResetCommissioneeHelper.h" @@ -123,55 +124,6 @@ - (void)controller:(MTRDeviceController *)controller commissioningComplete:(NSEr @end -typedef void (^MTRDeviceTestDelegateDataHandler)(NSArray *> *); - -@interface MTRDeviceTestDelegate : NSObject -@property (nonatomic) dispatch_block_t onReachable; -@property (nonatomic, nullable) dispatch_block_t onNotReachable; -@property (nonatomic, nullable) MTRDeviceTestDelegateDataHandler onAttributeDataReceived; -@property (nonatomic, nullable) MTRDeviceTestDelegateDataHandler onEventDataReceived; -@property (nonatomic, nullable) dispatch_block_t onReportEnd; -@end - -@implementation MTRDeviceTestDelegate -- (void)device:(MTRDevice *)device stateChanged:(MTRDeviceState)state -{ - if (state == MTRDeviceStateReachable) { - self.onReachable(); - } else if (state != MTRDeviceStateReachable && self.onNotReachable != nil) { - self.onNotReachable(); - } -} - -- (void)device:(MTRDevice *)device receivedAttributeReport:(NSArray *> *)attributeReport -{ - if (self.onAttributeDataReceived != nil) { - self.onAttributeDataReceived(attributeReport); - } -} - -- (void)device:(MTRDevice *)device receivedEventReport:(NSArray *> *)eventReport -{ - if (self.onEventDataReceived != nil) { - self.onEventDataReceived(eventReport); - } -} - -- (void)unitTestReportEndForDevice:(MTRDevice *)device -{ - if (self.onReportEnd != nil) { - self.onReportEnd(); - } -} - -- (NSNumber *)unitTestMaxIntervalOverrideForSubscription:(MTRDevice *)device -{ - // Make sure our subscriptions time out in finite time. - return @(2); // seconds -} - -@end - @interface MTRDeviceTests : XCTestCase @end @@ -1673,8 +1625,7 @@ - (void)test017_TestMTRDeviceBasics // Now make sure we ignore later tests. Ideally we would just unsubscribe // or remove the delegate, but there's no good way to do that. - delegate.onReachable = ^() { - }; + delegate.onReachable = nil; delegate.onNotReachable = nil; delegate.onAttributeDataReceived = nil; delegate.onEventDataReceived = nil; diff --git a/src/darwin/Framework/CHIPTests/MTRPerControllerStorageTests.m b/src/darwin/Framework/CHIPTests/MTRPerControllerStorageTests.m index effce2ef5b3403..02c0da77daf774 100644 --- a/src/darwin/Framework/CHIPTests/MTRPerControllerStorageTests.m +++ b/src/darwin/Framework/CHIPTests/MTRPerControllerStorageTests.m @@ -19,6 +19,7 @@ // system dependencies #import +#import "MTRDeviceTestDelegate.h" #import "MTRErrorTestUtils.h" #import "MTRFabricInfoChecker.h" #import "MTRTestKeys.h" @@ -33,8 +34,23 @@ static const uint16_t kTestVendorId = 0xFFF1u; #ifdef DEBUG +// MTRDeviceControllerDataStore.h includes C++ header, and so we need to declare the methods separately +@protocol MTRDeviceControllerDataStoreAttributeStoreMethods +- (nullable NSArray *)getStoredAttributesForNodeID:(NSNumber *)nodeID; +- (void)storeAttributeValues:(NSArray *)dataValues forNodeID:(NSNumber *)nodeID; +- (void)clearStoredAttributesForNodeID:(NSNumber *)nodeID; +- (void)clearAllStoredAttributes; +@end + +// Declare internal methods for testing @interface MTRDeviceController (Test) + (void)forceLocalhostAdvertisingOnly; +- (void)removeDevice:(MTRDevice *)device; +@property (nonatomic, readonly, nullable) id controllerDataStore; +@end + +@interface MTRDevice (Test) +- (BOOL)_attributeDataValue:(NSDictionary *)one isEqualToDataValue:(NSDictionary *)theOther; @end #endif // DEBUG @@ -1044,6 +1060,200 @@ - (void)test007_TestMultipleControllers XCTAssertFalse([controller3 isRunning]); } +- (void)test008_TestDataStoreDirect +{ + __auto_type * factory = [MTRDeviceControllerFactory sharedInstance]; + XCTAssertNotNil(factory); + + __auto_type * rootKeys = [[MTRTestKeys alloc] init]; + XCTAssertNotNil(rootKeys); + + __auto_type * operationalKeys = [[MTRTestKeys alloc] init]; + XCTAssertNotNil(operationalKeys); + + __auto_type * storageDelegate = [[MTRTestPerControllerStorage alloc] initWithControllerID:[NSUUID UUID]]; + + NSNumber * nodeID = @(123); + NSNumber * fabricID = @(456); + + NSError * error; + MTRDeviceController * controller = [self startControllerWithRootKeys:rootKeys + operationalKeys:operationalKeys + fabricID:fabricID + nodeID:nodeID + storage:storageDelegate + error:&error]; + XCTAssertNil(error); + XCTAssertNotNil(controller); + XCTAssertTrue([controller isRunning]); + + XCTAssertEqualObjects(controller.controllerNodeID, nodeID); + + NSArray * testAttributes = @[ + @{ MTRAttributePathKey : [MTRAttributePath attributePathWithEndpointID:@(1) clusterID:@(1) attributeID:@(1)], MTRDataKey : @ { MTRTypeKey : MTRUnsignedIntegerValueType, MTRValueKey : @(111) } }, + @{ MTRAttributePathKey : [MTRAttributePath attributePathWithEndpointID:@(1) clusterID:@(1) attributeID:@(2)], MTRDataKey : @ { MTRTypeKey : MTRUnsignedIntegerValueType, MTRValueKey : @(112) } }, + @{ MTRAttributePathKey : [MTRAttributePath attributePathWithEndpointID:@(1) clusterID:@(1) attributeID:@(3)], MTRDataKey : @ { MTRTypeKey : MTRUnsignedIntegerValueType, MTRValueKey : @(113) } }, + + @{ MTRAttributePathKey : [MTRAttributePath attributePathWithEndpointID:@(1) clusterID:@(2) attributeID:@(1)], MTRDataKey : @ { MTRTypeKey : MTRUnsignedIntegerValueType, MTRValueKey : @(121) } }, + @{ MTRAttributePathKey : [MTRAttributePath attributePathWithEndpointID:@(1) clusterID:@(2) attributeID:@(2)], MTRDataKey : @ { MTRTypeKey : MTRUnsignedIntegerValueType, MTRValueKey : @(122) } }, + @{ MTRAttributePathKey : [MTRAttributePath attributePathWithEndpointID:@(1) clusterID:@(2) attributeID:@(3)], MTRDataKey : @ { MTRTypeKey : MTRUnsignedIntegerValueType, MTRValueKey : @(123) } }, + + @{ MTRAttributePathKey : [MTRAttributePath attributePathWithEndpointID:@(2) clusterID:@(1) attributeID:@(1)], MTRDataKey : @ { MTRTypeKey : MTRUnsignedIntegerValueType, MTRValueKey : @(211) } }, + @{ MTRAttributePathKey : [MTRAttributePath attributePathWithEndpointID:@(2) clusterID:@(1) attributeID:@(2)], MTRDataKey : @ { MTRTypeKey : MTRUnsignedIntegerValueType, MTRValueKey : @(212) } }, + @{ MTRAttributePathKey : [MTRAttributePath attributePathWithEndpointID:@(2) clusterID:@(1) attributeID:@(3)], MTRDataKey : @ { MTRTypeKey : MTRUnsignedIntegerValueType, MTRValueKey : @(213) } }, + ]; + [controller.controllerDataStore storeAttributeValues:testAttributes forNodeID:@(1001)]; + [controller.controllerDataStore storeAttributeValues:testAttributes forNodeID:@(1002)]; + [controller.controllerDataStore storeAttributeValues:testAttributes forNodeID:@(1003)]; + + // Check values are written and can be fetched + NSArray * dataStoreValues = [controller.controllerDataStore getStoredAttributesForNodeID:@(1001)]; + XCTAssertEqual(dataStoreValues.count, 9); + dataStoreValues = [controller.controllerDataStore getStoredAttributesForNodeID:@(1002)]; + XCTAssertEqual(dataStoreValues.count, 9); + dataStoreValues = [controller.controllerDataStore getStoredAttributesForNodeID:@(1003)]; + XCTAssertEqual(dataStoreValues.count, 9); + + // Check values + for (NSDictionary * responseValue in dataStoreValues) { + MTRAttributePath * path = responseValue[MTRAttributePathKey]; + XCTAssertNotNil(path); + NSDictionary * dataValue = responseValue[MTRDataKey]; + XCTAssertNotNil(dataValue); + NSString * type = dataValue[MTRTypeKey]; + XCTAssertNotNil(type); + XCTAssertEqualObjects(type, MTRUnsignedIntegerValueType); + NSNumber * value = dataValue[MTRValueKey]; + XCTAssertNotNil(value); + + if ([path.endpoint isEqualToNumber:@(1)] && [path.cluster isEqualToNumber:@(1)] && [path.attribute isEqualToNumber:@(1)]) { + XCTAssertEqualObjects(value, @(111)); + } else if ([path.endpoint isEqualToNumber:@(1)] && [path.cluster isEqualToNumber:@(1)] && [path.attribute isEqualToNumber:@(2)]) { + XCTAssertEqualObjects(value, @(112)); + } else if ([path.endpoint isEqualToNumber:@(1)] && [path.cluster isEqualToNumber:@(1)] && [path.attribute isEqualToNumber:@(3)]) { + XCTAssertEqualObjects(value, @(113)); + } else if ([path.endpoint isEqualToNumber:@(1)] && [path.cluster isEqualToNumber:@(2)] && [path.attribute isEqualToNumber:@(1)]) { + XCTAssertEqualObjects(value, @(121)); + } else if ([path.endpoint isEqualToNumber:@(1)] && [path.cluster isEqualToNumber:@(2)] && [path.attribute isEqualToNumber:@(2)]) { + XCTAssertEqualObjects(value, @(122)); + } else if ([path.endpoint isEqualToNumber:@(1)] && [path.cluster isEqualToNumber:@(2)] && [path.attribute isEqualToNumber:@(3)]) { + XCTAssertEqualObjects(value, @(123)); + } else if ([path.endpoint isEqualToNumber:@(2)] && [path.cluster isEqualToNumber:@(1)] && [path.attribute isEqualToNumber:@(1)]) { + XCTAssertEqualObjects(value, @(211)); + } else if ([path.endpoint isEqualToNumber:@(2)] && [path.cluster isEqualToNumber:@(1)] && [path.attribute isEqualToNumber:@(2)]) { + XCTAssertEqualObjects(value, @(212)); + } else if ([path.endpoint isEqualToNumber:@(2)] && [path.cluster isEqualToNumber:@(1)] && [path.attribute isEqualToNumber:@(3)]) { + XCTAssertEqualObjects(value, @(213)); + } + } + + [controller.controllerDataStore clearStoredAttributesForNodeID:@(1001)]; + dataStoreValues = [controller.controllerDataStore getStoredAttributesForNodeID:@(1001)]; + XCTAssertEqual(dataStoreValues.count, 0); + dataStoreValues = [controller.controllerDataStore getStoredAttributesForNodeID:@(1002)]; + XCTAssertEqual(dataStoreValues.count, 9); + dataStoreValues = [controller.controllerDataStore getStoredAttributesForNodeID:@(1003)]; + XCTAssertEqual(dataStoreValues.count, 9); + + [controller.controllerDataStore clearAllStoredAttributes]; + dataStoreValues = [controller.controllerDataStore getStoredAttributesForNodeID:@(1001)]; + XCTAssertEqual(dataStoreValues.count, 0); + dataStoreValues = [controller.controllerDataStore getStoredAttributesForNodeID:@(1002)]; + XCTAssertEqual(dataStoreValues.count, 0); + dataStoreValues = [controller.controllerDataStore getStoredAttributesForNodeID:@(1003)]; + XCTAssertEqual(dataStoreValues.count, 0); + + [controller shutdown]; + XCTAssertFalse([controller isRunning]); +} + +- (void)test009_TestDataStoreMTRDevice +{ + __auto_type * factory = [MTRDeviceControllerFactory sharedInstance]; + XCTAssertNotNil(factory); + + __auto_type queue = dispatch_get_main_queue(); + + __auto_type * rootKeys = [[MTRTestKeys alloc] init]; + XCTAssertNotNil(rootKeys); + + __auto_type * operationalKeys = [[MTRTestKeys alloc] init]; + XCTAssertNotNil(operationalKeys); + + __auto_type * storageDelegate = [[MTRTestPerControllerStorage alloc] initWithControllerID:[NSUUID UUID]]; + + NSNumber * nodeID = @(123); + NSNumber * fabricID = @(456); + + NSError * error; + + MTRPerControllerStorageTestsCertificateIssuer * certificateIssuer; + MTRDeviceController * controller = [self startControllerWithRootKeys:rootKeys + operationalKeys:operationalKeys + fabricID:fabricID + nodeID:nodeID + storage:storageDelegate + error:&error + certificateIssuer:&certificateIssuer]; + XCTAssertNil(error); + XCTAssertNotNil(controller); + XCTAssertTrue([controller isRunning]); + + XCTAssertEqualObjects(controller.controllerNodeID, nodeID); + + // Now commission the device, to test that that works. + NSNumber * deviceID = @(17); + certificateIssuer.nextNodeID = deviceID; + [self commissionWithController:controller newNodeID:deviceID]; + + // We should have established CASE using our operational key. + XCTAssertEqual(operationalKeys.signatureCount, 1); + + __auto_type * device = [MTRDevice deviceWithNodeID:deviceID controller:controller]; + __auto_type * delegate = [[MTRDeviceTestDelegate alloc] init]; + + XCTestExpectation * subscriptionExpectation = [self expectationWithDescription:@"Subscription has been set up"]; + + delegate.onReportEnd = ^{ + [subscriptionExpectation fulfill]; + }; + + [device setDelegate:delegate queue:queue]; + + [self waitForExpectations:@[ subscriptionExpectation ] timeout:60]; + + NSArray * dataStoreValues = [controller.controllerDataStore getStoredAttributesForNodeID:deviceID]; + + // Verify all values are stored into storage + for (NSDictionary * responseValue in dataStoreValues) { + MTRAttributePath * path = responseValue[MTRAttributePathKey]; + XCTAssertNotNil(path); + NSDictionary * dataValue = responseValue[MTRDataKey]; + XCTAssertNotNil(dataValue); + + NSDictionary * dataValueFromMTRDevice = [device readAttributeWithEndpointID:path.endpoint clusterID:path.cluster attributeID:path.attribute params:nil]; + XCTAssertTrue([device _attributeDataValue:dataValue isEqualToDataValue:dataValueFromMTRDevice]); + } + + // Now force the removal of the object from controller to test reloading read cache from storage + [controller removeDevice:device]; + + // Verify the new device is initialized with the same values + __auto_type * deviceNew = [MTRDevice deviceWithNodeID:deviceID controller:controller]; + for (NSDictionary * responseValue in dataStoreValues) { + MTRAttributePath * path = responseValue[MTRAttributePathKey]; + XCTAssertNotNil(path); + NSDictionary * dataValue = responseValue[MTRDataKey]; + XCTAssertNotNil(dataValue); + + NSDictionary * dataValueFromMTRDevice = [deviceNew readAttributeWithEndpointID:path.endpoint clusterID:path.cluster attributeID:path.attribute params:nil]; + XCTAssertTrue([deviceNew _attributeDataValue:dataValue isEqualToDataValue:dataValueFromMTRDevice]); + } + + [controller shutdown]; + XCTAssertFalse([controller isRunning]); +} + // TODO: This might want to go in a separate test file, with some shared setup // across multiple tests, maybe. Would need to factor out // startControllerWithRootKeys into a test helper. diff --git a/src/darwin/Framework/CHIPTests/TestHelpers/MTRDeviceTestDelegate.h b/src/darwin/Framework/CHIPTests/TestHelpers/MTRDeviceTestDelegate.h new file mode 100644 index 00000000000000..0f7fce14226525 --- /dev/null +++ b/src/darwin/Framework/CHIPTests/TestHelpers/MTRDeviceTestDelegate.h @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2023 Project CHIP Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +typedef void (^MTRDeviceTestDelegateDataHandler)(NSArray *> *); + +@interface MTRDeviceTestDelegate : NSObject +@property (nonatomic, nullable) dispatch_block_t onReachable; +@property (nonatomic, nullable) dispatch_block_t onNotReachable; +@property (nonatomic, nullable) MTRDeviceTestDelegateDataHandler onAttributeDataReceived; +@property (nonatomic, nullable) MTRDeviceTestDelegateDataHandler onEventDataReceived; +@property (nonatomic, nullable) dispatch_block_t onReportEnd; +@end + +NS_ASSUME_NONNULL_END diff --git a/src/darwin/Framework/CHIPTests/TestHelpers/MTRDeviceTestDelegate.m b/src/darwin/Framework/CHIPTests/TestHelpers/MTRDeviceTestDelegate.m new file mode 100644 index 00000000000000..9e09aab6d96dd5 --- /dev/null +++ b/src/darwin/Framework/CHIPTests/TestHelpers/MTRDeviceTestDelegate.m @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2023 Project CHIP Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "MTRDeviceTestDelegate.h" + +@implementation MTRDeviceTestDelegate +- (void)device:(MTRDevice *)device stateChanged:(MTRDeviceState)state +{ + if (state == MTRDeviceStateReachable && self.onReachable != nil) { + self.onReachable(); + } else if (state != MTRDeviceStateReachable && self.onNotReachable != nil) { + self.onNotReachable(); + } +} + +- (void)device:(MTRDevice *)device receivedAttributeReport:(NSArray *> *)attributeReport +{ + if (self.onAttributeDataReceived != nil) { + self.onAttributeDataReceived(attributeReport); + } +} + +- (void)device:(MTRDevice *)device receivedEventReport:(NSArray *> *)eventReport +{ + if (self.onEventDataReceived != nil) { + self.onEventDataReceived(eventReport); + } +} + +- (void)unitTestReportEndForDevice:(MTRDevice *)device +{ + if (self.onReportEnd != nil) { + self.onReportEnd(); + } +} + +- (NSNumber *)unitTestMaxIntervalOverrideForSubscription:(MTRDevice *)device +{ + // Make sure our subscriptions time out in finite time. + return @(2); // seconds +} + +@end diff --git a/src/darwin/Framework/Matter.xcodeproj/project.pbxproj b/src/darwin/Framework/Matter.xcodeproj/project.pbxproj index 552a797d34a08f..06838384c04e27 100644 --- a/src/darwin/Framework/Matter.xcodeproj/project.pbxproj +++ b/src/darwin/Framework/Matter.xcodeproj/project.pbxproj @@ -257,6 +257,7 @@ 7596A8512878709F004DAE0E /* MTRAsyncCallbackQueueTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 7596A8502878709F004DAE0E /* MTRAsyncCallbackQueueTests.m */; }; 7596A85528788557004DAE0E /* MTRClusters.mm in Sources */ = {isa = PBXBuildFile; fileRef = 7596A85228788557004DAE0E /* MTRClusters.mm */; }; 7596A85728788557004DAE0E /* MTRClusters.h in Headers */ = {isa = PBXBuildFile; fileRef = 7596A85428788557004DAE0E /* MTRClusters.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 75B0D01E2B71B47F002074DD /* MTRDeviceTestDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 75B0D01D2B71B47F002074DD /* MTRDeviceTestDelegate.m */; }; 75B765C12A1D71BC0014719B /* MTRAttributeSpecifiedCheck.h in Headers */ = {isa = PBXBuildFile; fileRef = 75B765C02A1D71BC0014719B /* MTRAttributeSpecifiedCheck.h */; }; 75B765C32A1D82D30014719B /* MTRAttributeSpecifiedCheck.mm in Sources */ = {isa = PBXBuildFile; fileRef = 75B765C22A1D82D30014719B /* MTRAttributeSpecifiedCheck.mm */; }; 8874C1322B69C7060084BEFD /* MTRMetricsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8874C1312B69C7060084BEFD /* MTRMetricsTests.m */; }; @@ -658,6 +659,8 @@ 7596A8502878709F004DAE0E /* MTRAsyncCallbackQueueTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MTRAsyncCallbackQueueTests.m; sourceTree = ""; }; 7596A85228788557004DAE0E /* MTRClusters.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MTRClusters.mm; sourceTree = ""; }; 7596A85428788557004DAE0E /* MTRClusters.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTRClusters.h; sourceTree = ""; }; + 75B0D01C2B71B46F002074DD /* MTRDeviceTestDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MTRDeviceTestDelegate.h; sourceTree = ""; }; + 75B0D01D2B71B47F002074DD /* MTRDeviceTestDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MTRDeviceTestDelegate.m; sourceTree = ""; }; 75B765BF2A1D70F80014719B /* MTRAttributeSpecifiedCheck-src.zapt */ = {isa = PBXFileReference; lastKnownFileType = text; path = "MTRAttributeSpecifiedCheck-src.zapt"; sourceTree = ""; }; 75B765C02A1D71BC0014719B /* MTRAttributeSpecifiedCheck.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MTRAttributeSpecifiedCheck.h; sourceTree = ""; }; 75B765C22A1D82D30014719B /* MTRAttributeSpecifiedCheck.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MTRAttributeSpecifiedCheck.mm; sourceTree = ""; }; @@ -1161,6 +1164,8 @@ 518D3F822AA132DC008E0007 /* MTRTestPerControllerStorage.h */, 518D3F812AA132DC008E0007 /* MTRTestPerControllerStorage.m */, 51C984602A61CE2A00B0AD9A /* MTRFabricInfoChecker.m */, + 75B0D01C2B71B46F002074DD /* MTRDeviceTestDelegate.h */, + 75B0D01D2B71B47F002074DD /* MTRDeviceTestDelegate.m */, ); path = TestHelpers; sourceTree = ""; @@ -1974,6 +1979,7 @@ 51339B1F2A0DA64D00C798C1 /* MTRCertificateValidityTests.m in Sources */, 5173A47929C0E82300F67F48 /* MTRFabricInfoTests.m in Sources */, 5143851E2A65885500EDC8E6 /* MTRSwiftPairingTests.swift in Sources */, + 75B0D01E2B71B47F002074DD /* MTRDeviceTestDelegate.m in Sources */, 3D0C484B29DA4FA0006D811F /* MTRErrorTests.m in Sources */, 3DA1A3582ABABF6A004F0BB9 /* MTRAsyncWorkQueueTests.m in Sources */, 51742B4A29CB5FC1009974FE /* MTRTestResetCommissioneeHelper.m in Sources */,