diff --git a/src/darwin/Framework/CHIP/MTRBaseDevice.h b/src/darwin/Framework/CHIP/MTRBaseDevice.h index d3a1b0833f11fd..71f6a9cec43962 100644 --- a/src/darwin/Framework/CHIP/MTRBaseDevice.h +++ b/src/darwin/Framework/CHIP/MTRBaseDevice.h @@ -166,7 +166,7 @@ typedef NS_ENUM(uint8_t, MTRTransportType) { */ NS_SWIFT_SENDABLE MTR_AVAILABLE(ios(17.0), macos(14.0), watchos(10.0), tvos(17.0)) -@interface MTRAttributeRequestPath : NSObject +@interface MTRAttributeRequestPath : NSObject @property (nonatomic, readonly, copy, nullable) NSNumber * endpoint MTR_AVAILABLE(ios(17.0), macos(14.0), watchos(10.0), tvos(17.0)); @property (nonatomic, readonly, copy, nullable) NSNumber * cluster MTR_AVAILABLE(ios(17.0), macos(14.0), watchos(10.0), tvos(17.0)); @property (nonatomic, readonly, copy, nullable) @@ -185,7 +185,7 @@ MTR_AVAILABLE(ios(17.0), macos(14.0), watchos(10.0), tvos(17.0)) */ NS_SWIFT_SENDABLE MTR_AVAILABLE(ios(17.0), macos(14.0), watchos(10.0), tvos(17.0)) -@interface MTREventRequestPath : NSObject +@interface MTREventRequestPath : NSObject @property (nonatomic, readonly, copy, nullable) NSNumber * endpoint MTR_AVAILABLE(ios(17.0), macos(14.0), watchos(10.0), tvos(17.0)); @property (nonatomic, readonly, copy, nullable) NSNumber * cluster MTR_AVAILABLE(ios(17.0), macos(14.0), watchos(10.0), tvos(17.0)); @property (nonatomic, readonly, copy, nullable) NSNumber * event MTR_AVAILABLE(ios(17.0), macos(14.0), watchos(10.0), tvos(17.0)); diff --git a/src/darwin/Framework/CHIP/MTRBaseDevice.mm b/src/darwin/Framework/CHIP/MTRBaseDevice.mm index 7f03a9690b8e61..8a1feefea20607 100644 --- a/src/darwin/Framework/CHIP/MTRBaseDevice.mm +++ b/src/darwin/Framework/CHIP/MTRBaseDevice.mm @@ -2333,6 +2333,57 @@ - (void)convertToAttributePathParams:(chip::app::AttributePathParams &)params params.SetWildcardAttributeId(); } } + +static NSString * const sEndpointIDKey = @"endpointIDKey"; +static NSString * const sClusterIDKey = @"clusterIDKey"; +static NSString * const sAttributeIDKey = @"attributeIDKey"; + ++ (BOOL)supportsSecureCoding +{ + return YES; +} + +- (nullable instancetype)initWithCoder:(NSCoder *)decoder +{ + self = [super init]; + if (self == nil) { + return nil; + } + + _endpoint = [decoder decodeObjectOfClass:[NSNumber class] forKey:sEndpointIDKey]; + if (_endpoint && ![_endpoint isKindOfClass:[NSNumber class]]) { + MTR_LOG_ERROR("MTRAttributeRequestPath decoded %@ for endpoint, not NSNumber.", _attribute); + return nil; + } + + _cluster = [decoder decodeObjectOfClass:[NSNumber class] forKey:sClusterIDKey]; + if (_cluster && ![_cluster isKindOfClass:[NSNumber class]]) { + MTR_LOG_ERROR("MTRAttributeRequestPath decoded %@ for cluster, not NSNumber.", _attribute); + return nil; + } + + _attribute = [decoder decodeObjectOfClass:[NSNumber class] forKey:sAttributeIDKey]; + if (_attribute && ![_attribute isKindOfClass:[NSNumber class]]) { + MTR_LOG_ERROR("MTRAttributeRequestPath decoded %@ for attribute, not NSNumber.", _attribute); + return nil; + } + + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder +{ + if (_endpoint) { + [coder encodeObject:_endpoint forKey:sEndpointIDKey]; + } + if (_cluster) { + [coder encodeObject:_cluster forKey:sClusterIDKey]; + } + if (_attribute) { + [coder encodeObject:_attribute forKey:sAttributeIDKey]; + } +} + @end @implementation MTREventRequestPath @@ -2406,6 +2457,56 @@ - (void)convertToEventPathParams:(chip::app::EventPathParams &)params params.SetWildcardEventId(); } } +static NSString * const sEventEndpointIDKey = @"endpointIDKey"; +static NSString * const sEventClusterIDKey = @"clusterIDKey"; +static NSString * const sEventAttributeIDKey = @"attributeIDKey"; + ++ (BOOL)supportsSecureCoding +{ + return YES; +} + +- (nullable instancetype)initWithCoder:(NSCoder *)decoder +{ + self = [super init]; + if (self == nil) { + return nil; + } + + _endpoint = [decoder decodeObjectOfClass:[NSNumber class] forKey:sEventEndpointIDKey]; + if (_endpoint && ![_endpoint isKindOfClass:[NSNumber class]]) { + MTR_LOG_ERROR("MTREventRequestPath decoded %@ for endpoint, not NSNumber.", _endpoint); + return nil; + } + + _cluster = [decoder decodeObjectOfClass:[NSNumber class] forKey:sEventClusterIDKey]; + if (_cluster && ![_cluster isKindOfClass:[NSNumber class]]) { + MTR_LOG_ERROR("MTREventRequestPath decoded %@ for cluster, not NSNumber.", _cluster); + return nil; + } + + _event = [decoder decodeObjectOfClass:[NSNumber class] forKey:sEventAttributeIDKey]; + if (_event && ![_event isKindOfClass:[NSNumber class]]) { + MTR_LOG_ERROR("MTREventRequestPath decoded %@ for event, not NSNumber.", _event); + return nil; + } + + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder +{ + if (_endpoint) { + [coder encodeObject:_endpoint forKey:sEventEndpointIDKey]; + } + if (_cluster) { + [coder encodeObject:_cluster forKey:sEventClusterIDKey]; + } + if (_event) { + [coder encodeObject:_event forKey:sEventAttributeIDKey]; + } +} + @end @implementation MTRClusterPath 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 cb0d402dd5dc44..46fc8faa40ee1d 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/MTRDeviceController_XPC.mm b/src/darwin/Framework/CHIP/MTRDeviceController_XPC.mm index 29860fd41f1039..4cf30e1a34dd80 100644 --- a/src/darwin/Framework/CHIP/MTRDeviceController_XPC.mm +++ b/src/darwin/Framework/CHIP/MTRDeviceController_XPC.mm @@ -66,7 +66,7 @@ - (NSXPCInterface *)_interfaceForServerProtocol NSMutableSet * allowedClasses = [MTRDeviceController_XPC _allowedClasses]; [allowedClasses addObjectsFromArray:@[ [MTRCommandPath class], - [MTRAttributePath class] + [MTRAttributePath class], ]]; [interface setClasses:allowedClasses @@ -74,6 +74,27 @@ - (NSXPCInterface *)_interfaceForServerProtocol argumentIndex:0 ofReply:YES]; + // readAttributePaths: gets handed an array of MTRAttributeRequestPath. + allowedClasses = [MTRDeviceController_XPC _allowedClasses]; + [allowedClasses addObjectsFromArray:@[ + [MTRAttributeRequestPath class], + ]]; + [interface setClasses:allowedClasses + forSelector:@selector(deviceController:nodeID:readAttributePaths:withReply:) + argumentIndex:2 + ofReply:NO]; + + // readAttributePaths: returns response-value dictionaries that have + // attribute paths and values. + allowedClasses = [MTRDeviceController_XPC _allowedClasses]; + [allowedClasses addObjectsFromArray:@[ + [MTRAttributePath class], + ]]; + [interface setClasses:allowedClasses + forSelector:@selector(deviceController:nodeID:readAttributePaths:withReply:) + argumentIndex:0 + ofReply:YES]; + return interface; } @@ -82,7 +103,7 @@ - (NSXPCInterface *)_interfaceForClientProtocol NSXPCInterface * interface = [NSXPCInterface interfaceWithProtocol:@protocol(MTRXPCClientProtocol)]; NSMutableSet * allowedClasses = [MTRDeviceController_XPC _allowedClasses]; [allowedClasses addObjectsFromArray:@[ - [MTRAttributePath class] + [MTRAttributePath class], ]]; [interface setClasses:allowedClasses @@ -92,7 +113,7 @@ - (NSXPCInterface *)_interfaceForClientProtocol allowedClasses = [MTRDeviceController_XPC _allowedClasses]; [allowedClasses addObjectsFromArray:@[ - [MTREventPath class] + [MTREventPath class], ]]; [interface setClasses:allowedClasses diff --git a/src/darwin/Framework/CHIP/MTRDevice_Concrete.mm b/src/darwin/Framework/CHIP/MTRDevice_Concrete.mm index 28c198bd941861..62f6a5f1769208 100644 --- a/src/darwin/Framework/CHIP/MTRDevice_Concrete.mm +++ b/src/darwin/Framework/CHIP/MTRDevice_Concrete.mm @@ -2963,6 +2963,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 @@ -3968,19 +4041,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 18764080cbab32..a8d9a48e9cbb35 100644 --- a/src/darwin/Framework/CHIP/MTRDevice_XPC.mm +++ b/src/darwin/Framework/CHIP/MTRDevice_XPC.mm @@ -203,13 +203,13 @@ - (oneway void)device:(NSNumber *)nodeID internalStateUpdated:(NSDictionary *)di MTR_DEVICE_SIMPLE_REMOTE_XPC_GETTER(estimatedStartTime, NSDate * _Nullable, nil, getEstimatedStartTimeWithReply) MTR_DEVICE_SIMPLE_REMOTE_XPC_GETTER(estimatedSubscriptionLatency, NSNumber * _Nullable, nil, getEstimatedSubscriptionLatencyWithReply) -typedef NSDictionary * _Nullable readAttributeResponseType; +typedef NSDictionary * _Nullable ReadAttributeResponseType; MTR_DEVICE_COMPLEX_REMOTE_XPC_GETTER(readAttributeWithEndpointID : (NSNumber *) endpointID clusterID : (NSNumber *) clusterID attributeID : (NSNumber *) attributeID params : (MTRReadParams * _Nullable) params, - readAttributeResponseType, + ReadAttributeResponseType, nil, readAttributeWithEndpointID : endpointID clusterID @@ -231,6 +231,14 @@ - (oneway void)device:(NSNumber *)nodeID internalStateUpdated:(NSDictionary *)di : expectedValueInterval timedWriteTimeout : timeout) +typedef NSArray *> * ReadAttributePathsResponseType; +MTR_DEVICE_COMPLEX_REMOTE_XPC_GETTER(readAttributePaths + : (NSArray *) attributePaths, + ReadAttributePathsResponseType, + [NSArray array], // Default return value + readAttributePaths + : attributePaths withReply) + - (void)_invokeCommandWithEndpointID:(NSNumber *)endpointID clusterID:(NSNumber *)clusterID commandID:(NSNumber *)commandID diff --git a/src/darwin/Framework/CHIP/XPC Protocol/MTRXPCServerProtocol.h b/src/darwin/Framework/CHIP/XPC Protocol/MTRXPCServerProtocol.h index eaaa4b655e18e9..bd7d0e6df0e99c 100644 --- a/src/darwin/Framework/CHIP/XPC Protocol/MTRXPCServerProtocol.h +++ b/src/darwin/Framework/CHIP/XPC Protocol/MTRXPCServerProtocol.h @@ -29,6 +29,8 @@ MTR_NEWLY_AVAILABLE - (oneway void)deviceController:(NSUUID *)controller nodeID:(NSNumber *)nodeID readAttributeWithEndpointID:(NSNumber *)endpointID clusterID:(NSNumber *)clusterID attributeID:(NSNumber *)attributeID params:(MTRReadParams * _Nullable)params withReply:(void (^)(NSDictionary * _Nullable))reply; - (oneway void)deviceController:(NSUUID *)controller nodeID:(NSNumber *)nodeID writeAttributeWithEndpointID:(NSNumber *)endpointID clusterID:(NSNumber *)clusterID attributeID:(NSNumber *)attributeID value:(id)value expectedValueInterval:(NSNumber * _Nullable)expectedValueInterval timedWriteTimeout:(NSNumber * _Nullable)timeout; +- (oneway void)deviceController:(NSUUID *)controller nodeID:(NSNumber *)nodeID readAttributePaths:(NSArray *)attributePaths withReply:(void (^)(NSArray *> *))reply; + - (oneway void)deviceController:(NSUUID *)controller nodeID:(NSNumber *)nodeID invokeCommandWithEndpointID:(NSNumber *)endpointID clusterID:(NSNumber *)clusterID commandID:(NSNumber *)commandID commandFields:(id)commandFields expectedValues:(NSArray *> * _Nullable)expectedValues expectedValueInterval:(NSNumber * _Nullable)expectedValueInterval timedInvokeTimeout:(NSNumber * _Nullable)timeout serverSideProcessingTimeout:(NSNumber * _Nullable)serverSideProcessingTimeout completion:(MTRDeviceResponseHandler)completion; diff --git a/src/darwin/Framework/CHIPTests/MTRDeviceTests.m b/src/darwin/Framework/CHIPTests/MTRDeviceTests.m index 615ff63996a27e..459eec5919ceab 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); @@ -3117,48 +3165,49 @@ - (void)test031_MTRDeviceAttributeCacheLocalTestStorage XCTAssertTrue(storedAttributeCountDifferenceFromMTRDeviceReport > 300); } -- (void)test032_MTRPathClassesEncoding +- (void)doEncodeDecodeRoundTrip:(id)encodable { + // We know all our encodables are in fact NSObject. + NSObject * obj = (NSObject *) encodable; + NSError * encodeError; - NSData * encodedData; - NSError * decodeError; - id decodedValue; + NSData * encodedData = [NSKeyedArchiver archivedDataWithRootObject:encodable requiringSecureCoding:YES error:&encodeError]; + XCTAssertNil(encodeError, @"Failed to encode %@", NSStringFromClass(obj.class)); - // Test attribute path encode / decode - MTRAttributePath * originalAttributePath = [MTRAttributePath attributePathWithEndpointID:@(101) clusterID:@(102) attributeID:@(103)]; - encodedData = [NSKeyedArchiver archivedDataWithRootObject:originalAttributePath requiringSecureCoding:YES error:&encodeError]; - XCTAssertNil(encodeError); + NSError * decodeError; + id decodedValue = [NSKeyedUnarchiver unarchivedObjectOfClasses:[NSSet setWithObject:obj.class] fromData:encodedData error:&decodeError]; + XCTAssertNil(decodeError, @"Failed to decode %@", NSStringFromClass([obj class])); + XCTAssertTrue([decodedValue isKindOfClass:obj.class], @"Expected %@ but got %@", NSStringFromClass(obj.class), NSStringFromClass([decodedValue class])); - decodedValue = [NSKeyedUnarchiver unarchivedObjectOfClasses:[NSSet setWithObject:[MTRAttributePath class]] fromData:encodedData error:&decodeError]; - XCTAssertNil(decodeError); - XCTAssertTrue([decodedValue isKindOfClass:[MTRAttributePath class]]); + XCTAssertEqualObjects(obj, decodedValue, @"Decoding for %@ did not round-trip correctly", NSStringFromClass([obj class])); +} - MTRAttributePath * decodedAttributePath = decodedValue; - XCTAssertEqualObjects(originalAttributePath, decodedAttributePath); +- (void)test032_MTRPathClassesEncoding +{ + // Test attribute path encode / decode + MTRAttributePath * attributePath = [MTRAttributePath attributePathWithEndpointID:@(101) clusterID:@(102) attributeID:@(103)]; + [self doEncodeDecodeRoundTrip:attributePath]; // Test event path encode / decode - MTREventPath * originalEventPath = [MTREventPath eventPathWithEndpointID:@(201) clusterID:@(202) eventID:@(203)]; - encodedData = [NSKeyedArchiver archivedDataWithRootObject:originalEventPath requiringSecureCoding:YES error:&encodeError]; - XCTAssertNil(encodeError); + MTREventPath * eventPath = [MTREventPath eventPathWithEndpointID:@(201) clusterID:@(202) eventID:@(203)]; + [self doEncodeDecodeRoundTrip:eventPath]; - decodedValue = [NSKeyedUnarchiver unarchivedObjectOfClasses:[NSSet setWithObject:[MTREventPath class]] fromData:encodedData error:&decodeError]; - XCTAssertNil(decodeError); - XCTAssertTrue([decodedValue isKindOfClass:[MTREventPath class]]); + // Test command path encode / decode + MTRCommandPath * commandPath = [MTRCommandPath commandPathWithEndpointID:@(301) clusterID:@(302) commandID:@(303)]; + [self doEncodeDecodeRoundTrip:commandPath]; - MTREventPath * decodedEventPath = decodedValue; - XCTAssertEqualObjects(originalEventPath, decodedEventPath); + // Test attribute request path encode/decode + MTRAttributeRequestPath * attributeRequestPath = [MTRAttributeRequestPath requestPathWithEndpointID:nil clusterID:nil attributeID:nil]; + [self doEncodeDecodeRoundTrip:attributeRequestPath]; - // Test command path encode / decode - MTRCommandPath * originalCommandPath = [MTRCommandPath commandPathWithEndpointID:@(301) clusterID:@(302) commandID:@(303)]; - encodedData = [NSKeyedArchiver archivedDataWithRootObject:originalCommandPath requiringSecureCoding:YES error:&encodeError]; - XCTAssertNil(encodeError); + attributeRequestPath = [MTRAttributeRequestPath requestPathWithEndpointID:@(101) clusterID:@(102) attributeID:@(103)]; + [self doEncodeDecodeRoundTrip:attributeRequestPath]; - decodedValue = [NSKeyedUnarchiver unarchivedObjectOfClasses:[NSSet setWithObject:[MTRCommandPath class]] fromData:encodedData error:&decodeError]; - XCTAssertNil(decodeError); - XCTAssertTrue([decodedValue isKindOfClass:[MTRCommandPath class]]); + attributeRequestPath = [MTRAttributeRequestPath requestPathWithEndpointID:nil clusterID:@(105) attributeID:@(106)]; + [self doEncodeDecodeRoundTrip:attributeRequestPath]; - MTRCommandPath * decodedCommandPath = decodedValue; - XCTAssertEqualObjects(originalCommandPath, decodedCommandPath); + attributeRequestPath = [MTRAttributeRequestPath requestPathWithEndpointID:@(107) clusterID:nil attributeID:@(109)]; + [self doEncodeDecodeRoundTrip:attributeRequestPath]; } // Helper API to test if changes in an attribute with a path specified by endpointId, clusterId and attributeId trigger