diff --git a/src/darwin/Framework/CHIP/MTRDevice.h b/src/darwin/Framework/CHIP/MTRDevice.h index 2858ae56924af9..5a8dcb8de08355 100644 --- a/src/darwin/Framework/CHIP/MTRDevice.h +++ b/src/darwin/Framework/CHIP/MTRDevice.h @@ -186,6 +186,20 @@ MTR_AVAILABLE(ios(16.1), macos(13.0), watchos(9.1), tvos(16.1)) expectedValueInterval:(NSNumber *)expectedValueInterval timedWriteTimeout:(NSNumber * _Nullable)timeout; +/** + * Read the attributes identified by the provided attribute paths. The paths + * can include wildcards. + * + * Paths that do not correspond to any existing attributes, or that the + * MTRDevice does not have attribute values for, will not be present in the + * return value from this function. + * + * @return an array of response-value dictionaries as described in the + * documentation for MTRDeviceResponseHandler. Each one will have an + * MTRAttributePathKey and an MTRDataKey. + */ +- (NSArray *> *)readAttributePaths:(NSArray *)attributePaths MTR_NEWLY_AVAILABLE; + /** * Invoke a command with a designated command path * diff --git a/src/darwin/Framework/CHIP/MTRDevice.mm b/src/darwin/Framework/CHIP/MTRDevice.mm index f323c220ae118e..98c0e5f39b2c3a 100644 --- a/src/darwin/Framework/CHIP/MTRDevice.mm +++ b/src/darwin/Framework/CHIP/MTRDevice.mm @@ -1272,6 +1272,17 @@ - (void)writeAttributeWithEndpointID:(NSNumber *)endpointID #undef MTRDeviceErrorStr } +- (NSArray *> *)readAttributePaths:(NSArray *)attributePaths +{ +#define MTRDeviceErrorStr "MTRDevice readAttributePaths: must be handled by subclasses" + MTR_LOG_ERROR(MTRDeviceErrorStr); +#ifdef DEBUG + NSAssert(NO, @MTRDeviceErrorStr); +#endif // DEBUG +#undef MTRDeviceErrorStr + return [NSArray array]; +} + - (void)invokeCommandWithEndpointID:(NSNumber *)endpointID clusterID:(NSNumber *)clusterID commandID:(NSNumber *)commandID diff --git a/src/darwin/Framework/CHIP/MTRDevice_Concrete.mm b/src/darwin/Framework/CHIP/MTRDevice_Concrete.mm index 9beacdfd046bbc..b5f542a9b7fa93 100644 --- a/src/darwin/Framework/CHIP/MTRDevice_Concrete.mm +++ b/src/darwin/Framework/CHIP/MTRDevice_Concrete.mm @@ -2949,6 +2949,79 @@ - (void)writeAttributeWithEndpointID:(NSNumber *)endpointID [_asyncWorkQueue enqueueWorkItem:workItem descriptionWithFormat:@"write %@ 0x%llx 0x%llx", endpointID, clusterID.unsignedLongLongValue, attributeID.unsignedLongLongValue]; } +- (NSArray *> *)readAttributePaths:(NSArray *)attributePaths +{ + // Determine the set of what the spec calls "existent paths" that correspond + // to the request paths. Building the whole set in-memory is OK, because + // we're going to need all those paths for our return value anyway. + NSMutableSet * existentPaths = [[NSMutableSet alloc] init]; + { + std::lock_guard lock(_lock); + for (MTRAttributeRequestPath * path in attributePaths) { + [self _addExistentPathsFor:path to:existentPaths]; + } + } + + NSMutableArray *> * result = [NSMutableArray arrayWithCapacity:existentPaths.count]; + for (MTRAttributePath * path in existentPaths) { + auto * value = [self readAttributeWithEndpointID:path.endpoint clusterID:path.cluster attributeID:path.attribute params:nil]; + if (!value) { + continue; + } + [result addObject:@{ + MTRAttributePathKey : path, + MTRDataKey : value, + }]; + } + + return result; +} + +- (void)_addExistentPathsFor:(MTRAttributeRequestPath *)path to:(NSMutableSet *)set +{ + os_unfair_lock_assert_owner(&_lock); + + if (path.endpoint != nil) { + [self _addExistentPathsForEndpoint:path.endpoint path:path to:set]; + return; + } + + NSArray * endpointList = [self _endpointList]; + for (NSNumber * endpoint in endpointList) { + [self _addExistentPathsForEndpoint:endpoint path:path to:set]; + } +} + +- (void)_addExistentPathsForEndpoint:(NSNumber *)endpoint path:(MTRAttributeRequestPath *)path to:(NSMutableSet *)set +{ + os_unfair_lock_assert_owner(&_lock); + + if (path.cluster != nil) { + [self _addExistentPathsForEndpoint:endpoint cluster:path.cluster attribute:path.attribute to:set]; + return; + } + + auto * clusterList = [self _serverListForEndpointID:endpoint]; + for (NSNumber * cluster in clusterList) { + [self _addExistentPathsForEndpoint:endpoint cluster:cluster attribute:path.attribute to:set]; + } +} + +- (void)_addExistentPathsForEndpoint:(NSNumber *)endpoint cluster:(NSNumber *)cluster attribute:(NSNumber * _Nullable)attribute to:(NSMutableSet *)set +{ + os_unfair_lock_assert_owner(&_lock); + + if (attribute != nil) { + [set addObject:[MTRAttributePath attributePathWithEndpointID:endpoint clusterID:cluster attributeID:attribute]]; + return; + } + + auto * attributeList = [self _attributeListForEndpointID:endpoint clusterID:cluster]; + for (NSNumber * existentAttribute in attributeList) { + [set addObject:[MTRAttributePath attributePathWithEndpointID:endpoint clusterID:cluster attributeID:existentAttribute]]; + } +} + - (void)_invokeCommandWithEndpointID:(NSNumber *)endpointID clusterID:(NSNumber *)clusterID commandID:(NSNumber *)commandID @@ -3954,19 +4027,55 @@ - (void)_updateAttributeDependentDescriptionData { os_unfair_lock_assert_owner(&_lock); - auto * partsListPath = [MTRAttributePath attributePathWithEndpointID:@(kRootEndpointId) - clusterID:@(MTRClusterIDTypeDescriptorID) - attributeID:@(MTRAttributeIDTypeClusterDescriptorAttributePartsListID)]; - auto * partsList = [self _cachedAttributeValueForPath:partsListPath]; - NSMutableArray * endpointsOnDevice = [self arrayOfNumbersFromAttributeValue:partsList]; - if (!endpointsOnDevice) { - endpointsOnDevice = [[NSMutableArray alloc] init]; - } - // Add Root node! - [endpointsOnDevice addObject:@(0)]; + auto * partsList = [self _cachedListOfNumbersValueForEndpointID:@(kRootEndpointId) + clusterID:@(MTRClusterIDTypeDescriptorID) + attributeID:@(MTRAttributeIDTypeClusterDescriptorAttributePartsListID)]; + NSMutableArray * endpointsOnDevice = [partsList mutableCopy]; + // Add Root Node endpoint. + [endpointsOnDevice addObject:@(kRootEndpointId)]; return endpointsOnDevice; } +/** + * Returns the cached value of the relevant attribute as a list of numbers. + * Returns an empty list if the value does not exist or can't be converted to a + * list of numbers. + */ +- (NSArray *)_cachedListOfNumbersValueForEndpointID:(NSNumber *)endpointID + clusterID:(NSNumber *)clusterID + attributeID:(NSNumber *)attributeID +{ + os_unfair_lock_assert_owner(&_lock); + + auto * path = [MTRAttributePath attributePathWithEndpointID:endpointID + clusterID:clusterID + attributeID:attributeID]; + auto * value = [self _cachedAttributeValueForPath:path]; + NSArray * arrayValue = [self arrayOfNumbersFromAttributeValue:value]; + if (arrayValue) { + return arrayValue; + } + return [NSArray array]; +} + +- (NSArray *)_serverListForEndpointID:(NSNumber *)endpointID +{ + os_unfair_lock_assert_owner(&_lock); + + return [self _cachedListOfNumbersValueForEndpointID:endpointID + clusterID:@(MTRClusterIDTypeDescriptorID) + attributeID:@(MTRAttributeIDTypeClusterDescriptorAttributeServerListID)]; +} + +- (NSArray *)_attributeListForEndpointID:(NSNumber *)endpointID clusterID:(NSNumber *)clusterID +{ + os_unfair_lock_assert_owner(&_lock); + + return [self _cachedListOfNumbersValueForEndpointID:endpointID + clusterID:clusterID + attributeID:@(MTRAttributeIDTypeGlobalAttributeAttributeListID)]; +} + - (NSNumber * _Nullable)_networkFeatures { NSNumber * _Nullable result = nil; diff --git a/src/darwin/Framework/CHIP/MTRDevice_XPC.mm b/src/darwin/Framework/CHIP/MTRDevice_XPC.mm index 46f143e4bc189e..d24d5cd47677bd 100644 --- a/src/darwin/Framework/CHIP/MTRDevice_XPC.mm +++ b/src/darwin/Framework/CHIP/MTRDevice_XPC.mm @@ -231,6 +231,12 @@ - (oneway void)device:(NSNumber *)nodeID internalStateUpdated:(NSDictionary *)di : expectedValueInterval timedWriteTimeout : timeout) +- (NSArray *> *)readAttributePaths:(NSArray *)attributePaths +{ + // TODO: Implement + return [NSArray array]; +} + - (void)_invokeCommandWithEndpointID:(NSNumber *)endpointID clusterID:(NSNumber *)clusterID commandID:(NSNumber *)commandID diff --git a/src/darwin/Framework/CHIPTests/MTRDeviceTests.m b/src/darwin/Framework/CHIPTests/MTRDeviceTests.m index 615ff63996a27e..870764cd52c76e 100644 --- a/src/darwin/Framework/CHIPTests/MTRDeviceTests.m +++ b/src/darwin/Framework/CHIPTests/MTRDeviceTests.m @@ -1451,8 +1451,23 @@ - (void)test017_TestMTRDeviceBasics [subscriptionExpectation fulfill]; }; + NSMutableSet * endpoints = [[NSMutableSet alloc] init]; + NSMutableSet * clusters = [[NSMutableSet alloc] init]; + NSMutableSet * rootBasicInformationAttributes = [[NSMutableSet alloc] init]; __block unsigned attributeReportsReceived = 0; delegate.onAttributeDataReceived = ^(NSArray *> * data) { + for (NSDictionary * dataItem in data) { + MTRAttributePath * path = dataItem[MTRAttributePathKey]; + XCTAssertNotNil(path); + if (dataItem[MTRDataKey]) { + [endpoints addObject:path.endpoint]; + [clusters addObject:[MTRClusterPath clusterPathWithEndpointID:path.endpoint clusterID:path.cluster]]; + if (path.endpoint.unsignedLongValue == 0 && path.cluster.unsignedLongValue == MTRClusterIDTypeBasicInformationID) { + [rootBasicInformationAttributes addObject:path.attribute]; + } + } + } + attributeReportsReceived += data.count; }; @@ -1539,6 +1554,39 @@ - (void)test017_TestMTRDeviceBasics XCTAssertNotEqual(attributeReportsReceived, 0); XCTAssertNotEqual(eventReportsReceived, 0); + // Test readAttributePaths. First, try DeviceTypeList across all endpoints. + __auto_type * deviceTypeListPath = [MTRAttributeRequestPath requestPathWithEndpointID:nil clusterID:@(MTRClusterIDTypeDescriptorID) attributeID:@(MTRAttributeIDTypeClusterDescriptorAttributeDeviceTypeListID)]; + __auto_type * deviceTypes = [device readAttributePaths:@[ deviceTypeListPath ]]; + XCTAssertEqual(deviceTypes.count, endpoints.count); + + // Now try ClusterRevision across all clusters. + __auto_type * clusterRevisionsPath = [MTRAttributeRequestPath requestPathWithEndpointID:nil clusterID:nil attributeID:@(MTRAttributeIDTypeGlobalAttributeClusterRevisionID)]; + __auto_type * clusterRevisions = [device readAttributePaths:@[ clusterRevisionsPath ]]; + XCTAssertEqual(clusterRevisions.count, clusters.count); + + // Now try BasicInformation in a few different ways: + __auto_type * basicInformationAllAttributesPath = [MTRAttributeRequestPath requestPathWithEndpointID:nil clusterID:@(MTRClusterIDTypeBasicInformationID) attributeID:nil]; + __auto_type * basicInformationAllAttributes = [device readAttributePaths:@[ basicInformationAllAttributesPath ]]; + + __auto_type * basicInformationAllRootAttributesPath = [MTRAttributeRequestPath requestPathWithEndpointID:@(0) clusterID:@(MTRClusterIDTypeBasicInformationID) attributeID:nil]; + __auto_type * basicInformationAllRootAttributes = [device readAttributePaths:@[ basicInformationAllRootAttributesPath ]]; + // Should have gotten the same things, because Basic Information only exists + // on the root endpoint. + XCTAssertEqualObjects([NSSet setWithArray:basicInformationAllAttributes], [NSSet setWithArray:basicInformationAllRootAttributes]); + XCTAssertEqual(basicInformationAllAttributes.count, rootBasicInformationAttributes.count); + + // Now try multiple paths. Should just get the union of all the things for + // each path. + __auto_type * variousThings = [device readAttributePaths:@[ deviceTypeListPath, basicInformationAllRootAttributesPath ]]; + XCTAssertEqualObjects([NSSet setWithArray:variousThings], + [[NSSet setWithArray:deviceTypes] setByAddingObjectsFromSet:[NSSet setWithArray:basicInformationAllRootAttributes]]); + + // And similar if the paths expand to overlapping sets of existent paths + // (e.g. because Basic Information has a ClusterRevision). + variousThings = [device readAttributePaths:@[ clusterRevisionsPath, basicInformationAllRootAttributesPath ]]; + XCTAssertEqualObjects([NSSet setWithArray:variousThings], + [[NSSet setWithArray:clusterRevisions] setByAddingObjectsFromSet:[NSSet setWithArray:basicInformationAllRootAttributes]]); + // Before resubscribe, first test write failure and expected value effects NSNumber * testEndpointID = @(1); NSNumber * testClusterID = @(8);