blob: a905044c44aa20968c9409a046024fc885cbb40e [file] [log] [blame]
/**
*
* Copyright (c) 2022-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 <Matter/Matter.h>
#import <os/lock.h>
#import "MTRAttributeValueWaiter_Internal.h"
#import "MTRBaseClusters.h"
#import "MTRBaseDevice_Internal.h"
#import "MTRCluster.h"
#import "MTRClusterConstants.h"
#import "MTRConversion.h"
#import "MTRDefines_Internal.h"
#import "MTRDeviceController_Internal.h"
#import "MTRDeviceDataValidation.h"
#import "MTRDevice_Internal.h"
#import "MTRError_Internal.h"
#import "MTRLogging_Internal.h"
#import "MTRMetricKeys.h"
#import "MTRMetricsCollector.h"
#import "MTRTimeUtils.h"
#import "MTRUnfairLock.h"
#import "MTRUtilities.h"
#import "zap-generated/MTRCommandPayloads_Internal.h"
#import "lib/core/CHIPError.h"
@implementation MTRDeviceDelegateInfo
- (instancetype)initWithDelegate:(id<MTRDeviceDelegate>)delegate queue:(dispatch_queue_t)queue interestedPathsForAttributes:(NSArray * _Nullable)interestedPathsForAttributes interestedPathsForEvents:(NSArray * _Nullable)interestedPathsForEvents
{
if (self = [super initWithDelegate:delegate queue:queue]) {
_interestedPathsForAttributes = [interestedPathsForAttributes copy];
_interestedPathsForEvents = [interestedPathsForEvents copy];
}
return self;
}
- (NSString *)description
{
return [NSString stringWithFormat:@"<MTRDeviceDelegateInfo: %p delegate value %p interested attribute paths count %lu event paths count %lu>", self, self.delegatePointerValue, static_cast<unsigned long>(_interestedPathsForAttributes.count), static_cast<unsigned long>(_interestedPathsForEvents.count)];
}
@end
#pragma mark - MTRDevice
// Declaring selector so compiler won't complain about testing and calling it in _handleReportEnd
#ifdef DEBUG
@protocol MTRDeviceUnitTestDelegate <MTRDeviceDelegate>
- (void)unitTestReportEndForDevice:(MTRDevice *)device;
- (BOOL)unitTestShouldSetUpSubscriptionForDevice:(MTRDevice *)device;
- (BOOL)unitTestShouldSkipExpectedValuesForWrite:(MTRDevice *)device;
- (NSNumber *)unitTestMaxIntervalOverrideForSubscription:(MTRDevice *)device;
- (BOOL)unitTestForceAttributeReportsIfMatchingCache:(MTRDevice *)device;
- (BOOL)unitTestPretendThreadEnabled:(MTRDevice *)device;
- (void)unitTestSubscriptionPoolDequeue:(MTRDevice *)device;
- (void)unitTestSubscriptionPoolWorkComplete:(MTRDevice *)device;
- (void)unitTestClusterDataPersisted:(MTRDevice *)device;
- (BOOL)unitTestSuppressTimeBasedReachabilityChanges:(MTRDevice *)device;
@end
#endif
MTR_DIRECT_MEMBERS
@interface MTRDevice ()
// nil until the first time we need it. Access guarded by our lock.
@property (nonatomic, readwrite, nullable) NSHashTable<MTRAttributeValueWaiter *> * attributeValueWaiters;
@end
@implementation MTRDevice
- (instancetype)initForSubclassesWithNodeID:(NSNumber *)nodeID controller:(MTRDeviceController *)controller
{
if (self = [super init]) {
_lock = OS_UNFAIR_LOCK_INIT;
_delegateManager = [[MTRDelegateManager alloc] initWithOwner:self];
_deviceController = controller;
_nodeID = nodeID;
_state = MTRDeviceStateUnknown;
}
return self;
}
// For now, implement an initWithNodeID in case some sub-class outside the
// framework called it (by manually declaring it, even though it's not public
// API). Ideally we would not have this thing, since its signature does not
// match the initWithNodeID signatures of our subclasses.
- (instancetype)initWithNodeID:(NSNumber *)nodeID controller:(MTRDeviceController *)controller
{
return [self initForSubclassesWithNodeID:nodeID controller:controller];
}
- (void)dealloc
{
// TODO: retain cycle and clean up https://github.com/project-chip/connectedhomeip/issues/34267
MTR_LOG("MTRDevice dealloc: %p", self);
[_deviceController deviceDeallocated];
// Locking because _cancelAllAttributeValueWaiters has os_unfair_lock_assert_owner(&_lock)
std::lock_guard lock(_lock);
[self _cancelAllAttributeValueWaiters];
}
+ (MTRDevice *)deviceWithNodeID:(NSNumber *)nodeID controller:(MTRDeviceController *)controller
{
if (nodeID == nil || controller == nil) {
// These are not nullable in our API, but clearly someone is not
// actually turning on the relevant compiler checks (or is doing dynamic
// dispatch with bad values). While we promise to not return nil from
// this method, if the caller is ignoring the nullability API contract,
// there's not much we can do here.
MTR_LOG_ERROR("Can't create device with nodeID: %@, controller: %@",
nodeID, controller);
return nil;
}
return [controller deviceForNodeID:nodeID];
}
#pragma mark Delegate handling
- (void)setDelegate:(id<MTRDeviceDelegate>)delegate queue:(dispatch_queue_t)queue
{
MTR_LOG("%@ setDelegate %@", self, delegate);
[self _addDelegate:delegate queue:queue interestedPathsForAttributes:nil interestedPathsForEvents:nil];
}
- (void)addDelegate:(id<MTRDeviceDelegate>)delegate queue:(dispatch_queue_t)queue
{
MTR_LOG("%@ addDelegate %@", self, delegate);
[self _addDelegate:delegate queue:queue interestedPathsForAttributes:nil interestedPathsForEvents:nil];
}
- (void)addDelegate:(id<MTRDeviceDelegate>)delegate queue:(dispatch_queue_t)queue interestedPathsForAttributes:(NSArray * _Nullable)interestedPathsForAttributes interestedPathsForEvents:(NSArray * _Nullable)interestedPathsForEvents
{
MTR_LOG("%@ addDelegate %@ with interested attribute paths %@ event paths %@", self, delegate, interestedPathsForAttributes, interestedPathsForEvents);
[self _addDelegate:delegate queue:queue interestedPathsForAttributes:interestedPathsForAttributes interestedPathsForEvents:interestedPathsForEvents];
}
- (void)_addDelegate:(id<MTRDeviceDelegate>)delegate queue:(dispatch_queue_t)queue interestedPathsForAttributes:(NSArray * _Nullable)interestedPathsForAttributes interestedPathsForEvents:(NSArray * _Nullable)interestedPathsForEvents
{
std::lock_guard lock(_lock);
MTRDeviceDelegateInfo * newDelegateInfo = [[MTRDeviceDelegateInfo alloc] initWithDelegate:delegate queue:queue interestedPathsForAttributes:interestedPathsForAttributes interestedPathsForEvents:interestedPathsForEvents];
[_delegateManager addDelegateInfo:newDelegateInfo];
// Call hook to allow subclasses to act on delegate addition.
// TODO: We're calling this hook even if we just updated our existing
// delegate info, not actually added a new delegate. That might be OK, but
// worth checking.
[self _delegateAdded:delegate];
}
- (void)_delegateAdded:(id<MTRDeviceDelegate>)delegate
{
os_unfair_lock_assert_owner(&self->_lock);
// Nothing to do for now. At the moment this is a hook for subclasses.
}
- (void)removeDelegate:(id<MTRDeviceDelegate>)delegate
{
std::lock_guard lock(_lock);
[_delegateManager removeDelegate:delegate];
// Call hook to allow subclasses to act on delegate removal.
[self _delegateRemoved:delegate];
}
- (void)_delegateRemoved:(id<MTRDeviceDelegate>)delegate
{
os_unfair_lock_assert_owner(&self->_lock);
// Nothing to do for now. At the moment this is a hook for subclasses.
}
- (void)invalidate
{
std::lock_guard lock(_lock);
[_delegateManager removeAllDelegates];
[self _cancelAllAttributeValueWaiters];
}
- (BOOL)delegateExists
{
std::lock_guard lock(_lock);
return [self _delegateExists];
}
- (BOOL)_delegateExists
{
os_unfair_lock_assert_owner(&self->_lock);
return [self _iterateDelegatesWithBlock:nil];
}
- (BOOL)_iterateDelegatesWithBlock:(void(NS_NOESCAPE ^ _Nullable)(MTRDeviceDelegateInfo * delegateInfo))block
{
os_unfair_lock_assert_owner(&self->_lock);
return [_delegateManager iterateDelegatesWithBlock:block] > 0;
}
- (BOOL)_callDelegatesWithBlock:(void (^)(id<MTRDeviceDelegate> delegate))block
{
os_unfair_lock_assert_owner(&self->_lock);
return [_delegateManager callDelegatesWithBlock:block];
}
- (BOOL)_lockAndCallDelegatesWithBlock:(void (^)(id<MTRDeviceDelegate> delegate))block
{
std::lock_guard lock(self->_lock);
return [self _callDelegatesWithBlock:block];
}
#ifdef DEBUG
// Only used for unit test purposes - normal delegate should not expect or handle being called back synchronously
- (void)_callFirstDelegateSynchronouslyWithBlock:(void (^)(id<MTRDeviceDelegate> delegate))block
{
os_unfair_lock_assert_owner(&self->_lock);
[_delegateManager callFirstDelegateSynchronouslyWithBlock:block];
}
#endif
#ifdef DEBUG
- (NSUInteger)unitTestNonnullDelegateCount
{
std::lock_guard lock(self->_lock);
return [_delegateManager unitTestNonnullDelegateCount];
}
#endif
#pragma mark Device Interactions
- (NSDictionary<NSString *, id> * _Nullable)readAttributeWithEndpointID:(NSNumber *)endpointID
clusterID:(NSNumber *)clusterID
attributeID:(NSNumber *)attributeID
params:(MTRReadParams * _Nullable)params
{
MTR_ABSTRACT_METHOD();
return nil;
}
- (void)writeAttributeWithEndpointID:(NSNumber *)endpointID
clusterID:(NSNumber *)clusterID
attributeID:(NSNumber *)attributeID
value:(id)value
expectedValueInterval:(NSNumber *)expectedValueInterval
timedWriteTimeout:(NSNumber * _Nullable)timeout
{
MTR_ABSTRACT_METHOD();
}
- (NSArray<NSDictionary<NSString *, id> *> *)readAttributePaths:(NSArray<MTRAttributeRequestPath *> *)attributePaths
{
MTR_ABSTRACT_METHOD();
return [NSArray array];
}
- (NSDictionary<MTRAttributePath *, NSDictionary<NSString *, id> *> *)descriptorClusters
{
@autoreleasepool {
// For now, we have a temp array that we should make sure dies as soon
// as possible.
//
// TODO: We should have a version of readAttributePaths that returns a
// dictionary in the format we want here.
auto path = [MTRAttributeRequestPath requestPathWithEndpointID:nil
clusterID:@(MTRClusterIDTypeDescriptorID)
attributeID:nil];
auto * data = [self readAttributePaths:@[ path ]];
auto * retval = [NSMutableDictionary dictionaryWithCapacity:data.count];
for (NSDictionary * item in data) {
// We double-check that the things in our dictionaries are the right
// thing, because XPC has no way to check that for us.
if (MTR_SAFE_CAST(item[MTRAttributePathKey], MTRAttributePath) && MTR_SAFE_CAST(item[MTRDataKey], NSDictionary)) {
retval[item[MTRAttributePathKey]] = item[MTRDataKey];
}
}
return retval;
}
}
- (void)invokeCommandWithEndpointID:(NSNumber *)endpointID
clusterID:(NSNumber *)clusterID
commandID:(NSNumber *)commandID
commandFields:(NSDictionary<NSString *, id> * _Nullable)commandFields
expectedValues:(NSArray<NSDictionary<NSString *, id> *> * _Nullable)expectedValues
expectedValueInterval:(NSNumber * _Nullable)expectedValueInterval
queue:(dispatch_queue_t)queue
completion:(MTRDeviceResponseHandler)completion
{
if (commandFields == nil) {
commandFields = @{
MTRTypeKey : MTRStructureValueType,
MTRValueKey : @[],
};
}
[self invokeCommandWithEndpointID:endpointID
clusterID:clusterID
commandID:commandID
commandFields:commandFields
expectedValues:expectedValues
expectedValueInterval:expectedValueInterval
timedInvokeTimeout:nil
queue:queue
completion:completion];
}
- (void)invokeCommandWithEndpointID:(NSNumber *)endpointID
clusterID:(NSNumber *)clusterID
commandID:(NSNumber *)commandID
commandFields:(id)commandFields
expectedValues:(NSArray<NSDictionary<NSString *, id> *> * _Nullable)expectedValues
expectedValueInterval:(NSNumber * _Nullable)expectedValueInterval
timedInvokeTimeout:(NSNumber * _Nullable)timeout
queue:(dispatch_queue_t)queue
completion:(MTRDeviceResponseHandler)completion
{
// We don't have a way to communicate a non-default invoke timeout
// here for now.
// TODO: https://github.com/project-chip/connectedhomeip/issues/24563
if (![commandFields isKindOfClass:NSDictionary.class]) {
MTR_LOG_ERROR("%@ invokeCommandWithEndpointID passed a commandFields (%@) that is not a data-value NSDictionary object",
self, commandFields);
completion(nil, [MTRError errorForCHIPErrorCode:CHIP_ERROR_INVALID_ARGUMENT]);
return;
}
MTRDeviceDataValueDictionary fieldsDataValue = commandFields;
if (![MTRStructureValueType isEqual:fieldsDataValue[MTRTypeKey]]) {
MTR_LOG_ERROR("%@ invokeCommandWithEndpointID passed a commandFields (%@) that is not a structure-typed data-value object",
self, commandFields);
completion(nil, [MTRError errorForCHIPErrorCode:CHIP_ERROR_INVALID_ARGUMENT]);
return;
}
[self _invokeCommandWithEndpointID:endpointID
clusterID:clusterID
commandID:commandID
commandFields:commandFields
expectedValues:expectedValues
expectedValueInterval:expectedValueInterval
timedInvokeTimeout:timeout
serverSideProcessingTimeout:nil
queue:queue
completion:completion];
}
- (void)_invokeCommandWithEndpointID:(NSNumber *)endpointID
clusterID:(NSNumber *)clusterID
commandID:(NSNumber *)commandID
commandFields:(MTRDeviceDataValueDictionary)commandFields
expectedValues:(NSArray<NSDictionary<NSString *, id> *> * _Nullable)expectedValues
expectedValueInterval:(NSNumber * _Nullable)expectedValueInterval
timedInvokeTimeout:(NSNumber * _Nullable)timeout
serverSideProcessingTimeout:(NSNumber * _Nullable)serverSideProcessingTimeout
queue:(dispatch_queue_t)queue
completion:(MTRDeviceResponseHandler)completion
{
MTR_ABSTRACT_METHOD();
}
- (void)_invokeKnownCommandWithEndpointID:(NSNumber *)endpointID
clusterID:(NSNumber *)clusterID
commandID:(NSNumber *)commandID
commandPayload:(id)commandPayload
expectedValues:(NSArray<NSDictionary<NSString *, id> *> * _Nullable)expectedValues
expectedValueInterval:(NSNumber * _Nullable)expectedValueInterval
timedInvokeTimeout:(NSNumber * _Nullable)timeout
serverSideProcessingTimeout:(NSNumber * _Nullable)serverSideProcessingTimeout
responseClass:(Class _Nullable)responseClass
queue:(dispatch_queue_t)queue
completion:(void (^)(id _Nullable response, NSError * _Nullable error))completion
{
if (![commandPayload respondsToSelector:@selector(_encodeAsDataValue:)]) {
dispatch_async(queue, ^{
completion(nil, [MTRError errorForCHIPErrorCode:CHIP_ERROR_INVALID_ARGUMENT]);
});
return;
}
NSError * encodingError;
auto * commandFields = [commandPayload _encodeAsDataValue:&encodingError];
if (commandFields == nil) {
dispatch_async(queue, ^{
completion(nil, encodingError);
});
return;
}
auto responseHandler = ^(NSArray<NSDictionary<NSString *, id> *> * _Nullable values, NSError * _Nullable error) {
id _Nullable response = nil;
if (error == nil) {
if (values.count != 1) {
error = [NSError errorWithDomain:MTRErrorDomain code:MTRErrorCodeSchemaMismatch userInfo:nil];
} else if (responseClass != nil) {
response = [[responseClass alloc] initWithResponseValue:values[0] error:&error];
}
}
completion(response, error);
};
[self _invokeCommandWithEndpointID:endpointID
clusterID:clusterID
commandID:commandID
commandFields:commandFields
expectedValues:expectedValues
expectedValueInterval:expectedValueInterval
timedInvokeTimeout:timeout
serverSideProcessingTimeout:serverSideProcessingTimeout
queue:queue
completion:responseHandler];
}
- (void)invokeCommands:(NSArray<NSArray<MTRCommandWithRequiredResponse *> *> *)commands
queue:(dispatch_queue_t)queue
completion:(MTRDeviceResponseHandler)completion
{
MTR_ABSTRACT_METHOD();
dispatch_async(queue, ^{
completion(nil, [MTRError errorForCHIPErrorCode:CHIP_ERROR_INCORRECT_STATE]);
});
}
- (void)openCommissioningWindowWithSetupPasscode:(NSNumber *)setupPasscode
discriminator:(NSNumber *)discriminator
duration:(NSNumber *)duration
queue:(dispatch_queue_t)queue
completion:(MTRDeviceOpenCommissioningWindowHandler)completion
{
MTR_ABSTRACT_METHOD();
dispatch_async(queue, ^{
completion(nil, [MTRError errorForCHIPErrorCode:CHIP_ERROR_INCORRECT_STATE]);
});
}
- (void)openCommissioningWindowWithDiscriminator:(NSNumber *)discriminator
duration:(NSNumber *)duration
queue:(dispatch_queue_t)queue
completion:(MTRDeviceOpenCommissioningWindowHandler)completion
{
MTR_ABSTRACT_METHOD();
dispatch_async(queue, ^{
completion(nil, [MTRError errorForCHIPErrorCode:CHIP_ERROR_INCORRECT_STATE]);
});
}
- (void)downloadLogOfType:(MTRDiagnosticLogType)type
timeout:(NSTimeInterval)timeout
queue:(dispatch_queue_t)queue
completion:(void (^)(NSURL * _Nullable url, NSError * _Nullable error))completion
{
MTR_ABSTRACT_METHOD();
dispatch_async(queue, ^{
completion(nil, [MTRError errorForCHIPErrorCode:CHIP_ERROR_INCORRECT_STATE]);
});
}
- (nullable NSNumber *)estimatedSubscriptionLatency
{
MTR_ABSTRACT_METHOD();
return nil;
}
#pragma mark - Cache management
- (NSArray<NSDictionary<NSString *, id> *> *)getAllAttributesReport
{
MTR_ABSTRACT_METHOD();
return nil;
}
- (BOOL)deviceCachePrimed
{
MTR_ABSTRACT_METHOD();
return NO;
}
- (BOOL)diagnosticLogTransferInProgress
{
MTR_ABSTRACT_METHOD();
return NO;
}
#pragma mark - Suspend/resume management
- (void)controllerSuspended
{
// Nothing to do for now.
}
- (void)controllerResumed
{
// Nothing to do for now.
}
#pragma mark - Value comparisons
- (BOOL)_attributeDataValue:(MTRDeviceDataValueDictionary)one isEqualToDataValue:(MTRDeviceDataValueDictionary)theOther
{
// Sanity check for nil cases
if (!one && !theOther) {
MTR_LOG_ERROR("%@ attribute data-value comparison does not expect comparing two nil dictionaries", self);
return YES;
}
if (!one || !theOther) {
// Comparing against nil is expected, and should return NO quietly
return NO;
}
// Attribute data-value dictionaries are equal if type and value are equal, and specifically, this should return true if values are both nil
return [one[MTRTypeKey] isEqual:theOther[MTRTypeKey]] && ((one[MTRValueKey] == theOther[MTRValueKey]) || [one[MTRValueKey] isEqual:theOther[MTRValueKey]]);
}
// _attributeDataValue:satisfiesExpectedDataValue: checks whether the newly
// received attribute data value satisfies the expectation we have.
//
// For now, a value is considered to satisfy the expectation if it's equal to
// the expected value, though we allow the fields of structs to be in a
// different order than expected: while in theory the spec does require a
// specific ordering for struct fields, in practice we should not force certain
// API consumers to deal with knowing what that ordering is.
//
// Things to consider for future:
//
// 1) Should a value that has _extra_ fields in a struct compared to the expected
// value be considered as satisfying the expectation? Arguably, yes.
//
// 2) Should lists actually enforce order (as now), or should they allow
// reordering entries?
//
// 3) For fabric-scoped lists, should we have a way to check for just "our
// fabric's" entries?
- (BOOL)_attributeDataValue:(MTRDeviceDataValueDictionary)observed satisfiesValueExpectation:(MTRDeviceDataValueDictionary)expected
{
// Sanity check for nil cases (which really should not happen!)
if (!observed && !expected) {
MTR_LOG_ERROR("%@ observed to expected attribute data-value comparison does not expect comparing two nil dictionaries", self);
return YES;
}
if (!observed || !expected) {
// Again, not expected here. But clearly the expectation is not really
// satisfied, in some sense.
MTR_LOG_ERROR("@ observed to expected attribute data-value comparison does not expect a nil %s", observed ? "expected" : "observed");
return NO;
}
if (![observed[MTRTypeKey] isEqual:expected[MTRTypeKey]]) {
// Different types, does not satisfy expectation.
return NO;
}
if ([MTRArrayValueType isEqual:expected[MTRTypeKey]]) {
// For array-values, check that sizes are same and entries satisfy expectations.
if (![observed[MTRValueKey] isKindOfClass:NSArray.class] || ![expected[MTRValueKey] isKindOfClass:NSArray.class]) {
// Malformed data, just claim expectation is not satisfied.
MTR_LOG_ERROR("%@ at least one of observed and expected value is not an NSArrray: %@, %@", self, observed, expected);
return NO;
}
NSArray<NSDictionary<NSString *, MTRDeviceDataValueDictionary> *> * observedArray = observed[MTRValueKey];
NSArray<NSDictionary<NSString *, MTRDeviceDataValueDictionary> *> * expectedArray = expected[MTRValueKey];
if (observedArray.count != expectedArray.count) {
return NO;
}
for (NSUInteger i = 0; i < observedArray.count; ++i) {
NSDictionary<NSString *, MTRDeviceDataValueDictionary> * observedEntry = observedArray[i];
NSDictionary<NSString *, MTRDeviceDataValueDictionary> * expectedEntry = expectedArray[i];
if (![observedEntry isKindOfClass:NSDictionary.class] || ![expectedEntry isKindOfClass:NSDictionary.class]) {
MTR_LOG_ERROR("%@ expected or observed array-value contains entries that are not NSDictionary: %@, %@", self, observedEntry, expectedEntry);
return NO;
}
MTRDeviceDataValueDictionary observedDataValue = observedEntry[MTRDataKey];
if (!MTR_SAFE_CAST(observedDataValue, NSDictionary)) {
MTR_LOG_ERROR("%@ observed data-value is not an NSDictionary: %@", self, observedDataValue);
return NO;
}
MTRDeviceDataValueDictionary expectedDataValue = expectedEntry[MTRDataKey];
if (!MTR_SAFE_CAST(expectedDataValue, NSDictionary)) {
MTR_LOG_ERROR("%@ expected data-value is not an NSDictionary: %@", self, expectedDataValue);
return NO;
}
if (![self _attributeDataValue:observedDataValue satisfiesValueExpectation:expectedDataValue]) {
return NO;
}
}
return YES;
}
if (![MTRStructureValueType isEqual:expected[MTRTypeKey]]) {
// For everything except arrays and structs, expectation is satisfied
// exactly when the values are equal.
return [self _attributeDataValue:observed isEqualToDataValue:expected];
}
// Now we have two structure-values. Make sure they have the same number of fields
// in them.
if (![observed[MTRValueKey] isKindOfClass:NSArray.class] || ![expected[MTRValueKey] isKindOfClass:NSArray.class]) {
// Malformed data, just claim not equivalent.
MTR_LOG_ERROR("%@ at least one of observed and expected value is not an NSArrray: %@, %@", self, observed, expected);
return NO;
}
NSArray<NSDictionary<NSString *, id> *> * observedArray = observed[MTRValueKey];
NSArray<NSDictionary<NSString *, id> *> * expectedArray = expected[MTRValueKey];
if (observedArray.count != expectedArray.count) {
return NO;
}
for (NSDictionary<NSString *, id> * expectedField in expectedArray) {
if (!MTR_SAFE_CAST(expectedField, NSDictionary) || !MTR_SAFE_CAST(expectedField[MTRContextTagKey], NSNumber) || !MTR_SAFE_CAST(expectedField[MTRDataKey], NSDictionary)) {
MTR_LOG_ERROR("%@ expected structure-value contains invalid field %@", self, expectedField);
return NO;
}
NSNumber * expectedContextTag = expectedField[MTRContextTagKey];
// Make sure it's present in the other array. In practice, these are
// pretty small arrays, so the O(N^2) behavior here is ok.
BOOL found = NO;
for (NSDictionary<NSString *, id> * observedField in observedArray) {
if (!MTR_SAFE_CAST(observedField, NSDictionary) || !MTR_SAFE_CAST(observedField[MTRContextTagKey], NSNumber) || !MTR_SAFE_CAST(observedField[MTRDataKey], NSDictionary)) {
MTR_LOG_ERROR("%@ observed structure-value contains invalid field %@", self, observedField);
return NO;
}
NSNumber * observedContextTag = observedField[MTRContextTagKey];
if ([expectedContextTag isEqual:observedContextTag]) {
found = YES;
// Compare the data.
if (![self _attributeDataValue:observedField[MTRDataKey] satisfiesValueExpectation:expectedField[MTRDataKey]]) {
return NO;
}
// Found a match for the context tag, stop looking.
break;
}
}
if (!found) {
// Context tag present in expected but not observed.
return NO;
}
}
// All entries in the first field array matched entries in the second field
// array. Since the lengths are equal, the two arrays must match, as long
// as all the context tags listed are distinct. If someone produces invalid
// TLV with the same context tag set in it multiple times, this method could
// claim two structure-values are equivalent when the first has two fields
// with context tag N and the second has a field with context tag N and
// another field with context tag M. That should be ok, in practice, but if
// we discover it's not we will need a better algorithm here. It's not
// clear what "equivalent" should mean for such malformed TLV, expecially if
// the same context tag maps to different values in one of the structs.
return YES;
}
#pragma mark - Handling of waits for attribute values
- (MTRAttributeValueWaiter *)waitForAttributeValues:(NSDictionary<MTRAttributePath *, MTRDeviceDataValueDictionary> *)values timeout:(NSTimeInterval)timeout queue:(dispatch_queue_t)queue completion:(void (^)(NSError * _Nullable error))completion
{
// Check whether the values coming in make sense.
for (MTRAttributePath * path in values) {
MTRVerifyArgumentOrDie(MTRDataValueDictionaryIsWellFormed(values[path]),
([NSString stringWithFormat:@"waitForAttributeValues handed invalid data-value %@ for path %@", path, values[path]]));
}
// Check whether we have all these values already.
NSMutableArray<MTRAttributeRequestPath *> * requestPaths = [NSMutableArray arrayWithCapacity:values.count];
for (MTRAttributePath * path in values) {
[requestPaths addObject:[MTRAttributeRequestPath requestPathWithEndpointID:path.endpoint clusterID:path.cluster attributeID:path.attribute]];
}
NSArray<MTRDeviceResponseValueDictionary> * currentValues = [self readAttributePaths:requestPaths];
auto * attributeWaiter = [[MTRAttributeValueWaiter alloc] initWithDevice:self values:values queue:queue completion:completion];
for (MTRDeviceResponseValueDictionary currentValue in currentValues) {
// Pretend as if this got reported, for purposes of the attribute
// waiter.
[attributeWaiter _attributeValue:currentValue[MTRDataKey] reportedForPath:currentValue[MTRAttributePathKey] byDevice:self];
}
if (attributeWaiter.allValuesSatisfied) {
MTR_LOG("%@ waitForAttributeValues no need to wait, values already match: %@", self, values);
// We haven't added this waiter to self.attributeValueWaiters yet, so
// no need to remove it before notifying.
[attributeWaiter _notifyWithError:nil];
return attributeWaiter;
}
// Otherwise, wait for one of our termination conditions.
{
std::lock_guard lock(_lock);
if (!self.attributeValueWaiters) {
self.attributeValueWaiters = [NSHashTable weakObjectsHashTable];
}
[self.attributeValueWaiters addObject:attributeWaiter];
}
MTR_LOG("%@ waitForAttributeValues will wait up to %f seconds for %@", self, timeout, values);
[attributeWaiter _startTimerWithTimeout:timeout];
return attributeWaiter;
}
- (void)_attributeValue:(MTRDeviceDataValueDictionary)value reportedForPath:(MTRAttributePath *)path
{
os_unfair_lock_assert_owner(&_lock);
// Check whether anyone was waiting for this attribute.
NSMutableArray * satisfiedWaiters;
for (MTRAttributeValueWaiter * attributeValueWaiter in self.attributeValueWaiters) {
if ([attributeValueWaiter _attributeValue:value reportedForPath:path byDevice:self] && attributeValueWaiter.allValuesSatisfied) {
if (!satisfiedWaiters) {
satisfiedWaiters = [NSMutableArray array];
}
[satisfiedWaiters addObject:attributeValueWaiter];
}
}
for (MTRAttributeValueWaiter * attributeValueWaiter in satisfiedWaiters) {
[self.attributeValueWaiters removeObject:attributeValueWaiter];
[attributeValueWaiter _notifyWithError:nil];
}
}
- (void)_forgetAttributeWaiter:(MTRAttributeValueWaiter *)attributeValueWaiter
{
std::lock_guard lock(_lock);
[self.attributeValueWaiters removeObject:attributeValueWaiter];
}
- (void)_cancelAllAttributeValueWaiters
{
os_unfair_lock_assert_owner(&_lock);
auto * attributeValueWaiters = self.attributeValueWaiters;
self.attributeValueWaiters = nil;
for (MTRAttributeValueWaiter * attributeValueWaiter in attributeValueWaiters) {
[attributeValueWaiter _notifyCancellation];
}
}
@end
/* BEGIN DRAGONS: Note methods here cannot be renamed, and are used by private callers, do not rename, remove or modify behavior here */
@implementation MTRDevice (MatterPrivateForInternalDragonsDoNotFeed)
- (BOOL)_deviceHasActiveSubscription
{
return NO;
}
- (void)_deviceMayBeReachable
{
}
/* END DRAGONS */
@end
@implementation MTRDevice (Deprecated)
+ (MTRDevice *)deviceWithNodeID:(uint64_t)nodeID deviceController:(MTRDeviceController *)deviceController
{
return [self deviceWithNodeID:@(nodeID) controller:deviceController];
}
- (void)invokeCommandWithEndpointID:(NSNumber *)endpointID
clusterID:(NSNumber *)clusterID
commandID:(NSNumber *)commandID
commandFields:(id)commandFields
expectedValues:(NSArray<NSDictionary<NSString *, id> *> * _Nullable)expectedValues
expectedValueInterval:(NSNumber * _Nullable)expectedValueInterval
timedInvokeTimeout:(NSNumber * _Nullable)timeout
clientQueue:(dispatch_queue_t)queue
completion:(MTRDeviceResponseHandler)completion
{
[self invokeCommandWithEndpointID:endpointID
clusterID:clusterID
commandID:commandID
commandFields:commandFields
expectedValues:expectedValues
expectedValueInterval:expectedValueInterval
timedInvokeTimeout:timeout
queue:queue
completion:completion];
}
@end