blob: 134042f974dd5445bb5cd84cf3d564f241f9a3e0 [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 "MTRAsyncWorkQueue.h"
#import "MTRAttributeSpecifiedCheck.h"
#import "MTRBaseClusters.h"
#import "MTRBaseDevice_Internal.h"
#import "MTRCluster.h"
#import "MTRClusterConstants.h"
#import "MTRCommandTimedCheck.h"
#import "MTRConversion.h"
#import "MTRDefines_Internal.h"
#import "MTRDeviceConnectivityMonitor.h"
#import "MTRDeviceControllerOverXPC.h"
#import "MTRDeviceController_Internal.h"
#import "MTRDevice_Internal.h"
#import "MTRError_Internal.h"
#import "MTREventTLVValueDecoder_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"
#import <platform/LockTracker.h>
typedef void (^MTRDeviceAttributeReportHandler)(NSArray * _Nonnull);
NSString * const MTRPreviousDataKey = @"previousData";
NSString * const MTRDataVersionKey = @"dataVersion";
#define kSecondsToWaitBeforeMarkingUnreachableAfterSettingUpSubscription 10
// Disabling pending crashes
#define ENABLE_CONNECTIVITY_MONITORING 0
@implementation MTRDeviceDelegateInfo
- (instancetype)initWithDelegate:(id<MTRDeviceDelegate>)delegate queue:(dispatch_queue_t)queue interestedPathsForAttributes:(NSArray * _Nullable)interestedPathsForAttributes interestedPathsForEvents:(NSArray * _Nullable)interestedPathsForEvents
{
if (self = [super init]) {
_delegate = delegate;
_delegatePointerValue = (__bridge void *) 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, _delegatePointerValue, static_cast<unsigned long>(_interestedPathsForAttributes.count), static_cast<unsigned long>(_interestedPathsForEvents.count)];
}
- (BOOL)callDelegateWithBlock:(void (^)(id<MTRDeviceDelegate>))block
{
id<MTRDeviceDelegate> strongDelegate = _delegate;
VerifyOrReturnValue(strongDelegate, NO);
dispatch_async(_queue, ^{
block(strongDelegate);
});
return YES;
}
#ifdef DEBUG
- (BOOL)callDelegateSynchronouslyWithBlock:(void (^)(id<MTRDeviceDelegate>))block
{
id<MTRDeviceDelegate> strongDelegate = _delegate;
VerifyOrReturnValue(strongDelegate, NO);
block(strongDelegate);
return YES;
}
#endif
@end
/* BEGIN DRAGONS: Note methods here cannot be renamed, and are used by private callers, do not rename, remove or modify behavior here */
@interface NSObject (MatterPrivateForInternalDragonsDoNotFeed)
- (void)_deviceInternalStateChanged:(MTRDevice *)device;
@end
/* END DRAGONS */
using namespace chip;
using namespace chip::app;
using namespace chip::Protocols::InteractionModel;
using namespace chip::Tracing::DarwinFramework;
#pragma mark - MTRDevice
typedef NS_ENUM(NSUInteger, MTRDeviceExpectedValueFieldIndex) {
MTRDeviceExpectedValueFieldExpirationTimeIndex = 0,
MTRDeviceExpectedValueFieldValueIndex = 1,
MTRDeviceExpectedValueFieldIDIndex = 2
};
typedef NS_ENUM(NSUInteger, MTRDeviceReadRequestFieldIndex) {
MTRDeviceReadRequestFieldPathIndex = 0,
MTRDeviceReadRequestFieldParamsIndex = 1
};
typedef NS_ENUM(NSUInteger, MTRDeviceWriteRequestFieldIndex) {
MTRDeviceWriteRequestFieldPathIndex = 0,
MTRDeviceWriteRequestFieldValueIndex = 1,
MTRDeviceWriteRequestFieldTimeoutIndex = 2,
MTRDeviceWriteRequestFieldExpectedValueIDIndex = 3,
};
typedef NS_ENUM(NSUInteger, MTRDeviceWorkItemBatchingID) {
MTRDeviceWorkItemBatchingReadID = 1,
MTRDeviceWorkItemBatchingWriteID = 2,
};
typedef NS_ENUM(NSUInteger, MTRDeviceWorkItemDuplicateTypeID) {
MTRDeviceWorkItemDuplicateReadTypeID = 1,
};
@implementation MTRDeviceClusterData {
NSMutableDictionary<NSNumber *, MTRDeviceDataValueDictionary> * _attributes;
}
- (void)storeValue:(MTRDeviceDataValueDictionary _Nullable)value forAttribute:(NSNumber *)attribute
{
_attributes[attribute] = value;
}
- (void)removeValueForAttribute:(NSNumber *)attribute
{
[_attributes removeObjectForKey:attribute];
}
- (NSDictionary<NSNumber *, MTRDeviceDataValueDictionary> *)attributes
{
return _attributes;
}
+ (BOOL)supportsSecureCoding
{
return YES;
}
- (NSString *)description
{
return [NSString stringWithFormat:@"<MTRDeviceClusterData: dataVersion %@ attributes count %lu>", _dataVersion, static_cast<unsigned long>(_attributes.count)];
}
- (nullable instancetype)init
{
return [self initWithDataVersion:nil attributes:nil];
}
// Attributes dictionary is: attributeID => data-value dictionary
- (nullable instancetype)initWithDataVersion:(NSNumber * _Nullable)dataVersion attributes:(NSDictionary<NSNumber *, MTRDeviceDataValueDictionary> * _Nullable)attributes
{
self = [super init];
if (self == nil) {
return nil;
}
_dataVersion = [dataVersion copy];
_attributes = [NSMutableDictionary dictionaryWithCapacity:attributes.count];
[_attributes addEntriesFromDictionary:attributes];
return self;
}
- (nullable instancetype)initWithCoder:(NSCoder *)decoder
{
self = [super init];
if (self == nil) {
return nil;
}
_dataVersion = [decoder decodeObjectOfClass:[NSNumber class] forKey:sDataVersionKey];
if (_dataVersion != nil && ![_dataVersion isKindOfClass:[NSNumber class]]) {
MTR_LOG_ERROR("MTRDeviceClusterData got %@ for data version, not NSNumber.", _dataVersion);
return nil;
}
static NSSet * const sAttributeValueClasses = [NSSet setWithObjects:[NSDictionary class], [NSArray class], [NSData class], [NSString class], [NSNumber class], nil];
_attributes = [decoder decodeObjectOfClasses:sAttributeValueClasses forKey:sAttributesKey];
if (_attributes != nil && ![_attributes isKindOfClass:[NSDictionary class]]) {
MTR_LOG_ERROR("MTRDeviceClusterData got %@ for attributes, not NSDictionary.", _attributes);
return nil;
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)coder
{
[coder encodeObject:self.dataVersion forKey:sDataVersionKey];
[coder encodeObject:self.attributes forKey:sAttributesKey];
}
- (id)copyWithZone:(NSZone *)zone
{
return [[MTRDeviceClusterData alloc] initWithDataVersion:_dataVersion attributes:_attributes];
}
- (BOOL)isEqualToClusterData:(MTRDeviceClusterData *)otherClusterData
{
return MTREqualObjects(_dataVersion, otherClusterData.dataVersion)
&& MTREqualObjects(_attributes, otherClusterData.attributes);
}
- (BOOL)isEqual:(id)object
{
if ([object class] != [self class]) {
return NO;
}
return [self isEqualToClusterData:object];
}
@end
// Minimal time to wait since our last resubscribe failure before we will allow
// a read attempt to prod our subscription.
//
// TODO: Figure out a better value for this, but for now don't allow this to
// happen more often than once every 10 minutes.
#define MTRDEVICE_MIN_RESUBSCRIBE_DUE_TO_READ_INTERVAL_SECONDS (10 * 60)
// Weight of new data in determining subscription latencies. To avoid random
// outliers causing too much noise in the value, treat an existing value (if
// any) as having 2/3 weight and the new value as having 1/3 weight. These
// weights are subject to change, if it's determined that different ones give
// better behavior.
#define MTRDEVICE_SUBSCRIPTION_LATENCY_NEW_VALUE_WEIGHT (1.0 / 3.0)
@interface MTRDevice ()
// protects against concurrent time updates by guarding timeUpdateScheduled flag which manages time updates scheduling,
// and protects device calls to setUTCTime and setDSTOffset. This can't just be replaced with "lock", because the time
// update code calls public APIs like readAttributeWithEndpointID:.. (which attempt to take "lock") while holding
// whatever lock protects the time sync bits.
@property (nonatomic, readonly) os_unfair_lock timeSyncLock;
@property (nonatomic) chip::FabricIndex fabricIndex;
// TODO: instead of all the BOOL properties that are some facet of the state, move to internal state machine that has (at least):
// Actively receiving report
// Actively receiving priming report
@property (nonatomic) MTRInternalDeviceState internalDeviceState;
#define MTRDEVICE_SUBSCRIPTION_ATTEMPT_MIN_WAIT_SECONDS (1)
#define MTRDEVICE_SUBSCRIPTION_ATTEMPT_MAX_WAIT_SECONDS (3600)
@property (nonatomic) uint32_t lastSubscriptionAttemptWait;
// Expected value cache is attributePath => NSArray of [NSDate of expiration time, NSDictionary of value, expected value ID]
// - See MTRDeviceExpectedValueFieldIndex for the definitions of indices into this array.
// See MTRDeviceResponseHandler definition for value dictionary details.
@property (nonatomic) NSMutableDictionary<MTRAttributePath *, NSArray *> * expectedValueCache;
// This is a monotonically increasing value used when adding entries to expectedValueCache
// Currently used/updated only in _getAttributesToReportWithNewExpectedValues:expirationTime:expectedValueID:
@property (nonatomic) uint64_t expectedValueNextID;
@property (nonatomic) BOOL expirationCheckScheduled;
@property (nonatomic) BOOL timeUpdateScheduled;
@property (nonatomic) NSMutableDictionary * temporaryMetaDataCache;
@end
// 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
@implementation MTRDevice {
// _deviceCachePrimed is true if we have the data that comes from an initial
// subscription priming report (whether it came from storage or from our
// subscription).
BOOL _deviceCachePrimed;
// _persistedClusterData stores data that we have already persisted (when we have
// cluster data persistence enabled). Nil when we have no persistence enabled.
NSCache<MTRClusterPath *, MTRDeviceClusterData *> * _Nullable _persistedClusterData;
// _clusterDataToPersist stores data that needs to be persisted. If we
// don't have persistence enabled, this is our only data store. Nil if we
// currently have nothing that could need persisting.
NSMutableDictionary<MTRClusterPath *, MTRDeviceClusterData *> * _Nullable _clusterDataToPersist;
// _persistedClusters stores the set of "valid" keys into _persistedClusterData.
// These are keys that could have values in _persistedClusterData even if they don't
// right now (because they have been evicted).
NSMutableSet<MTRClusterPath *> * _persistedClusters;
// Storage behavior configuration and variables to keep track of the logic
// _clusterDataPersistenceFirstScheduledTime is used to track the start time of the delay between
// report and persistence.
// _mostRecentReportTimes is a list of the most recent report timestamps used for calculating
// the running average time between reports.
// _deviceReportingExcessivelyStartTime tracks when a device starts reporting excessively.
// _reportToPersistenceDelayCurrentMultiplier is the current multiplier that is calculated when a
// report comes in.
MTRDeviceStorageBehaviorConfiguration * _storageBehaviorConfiguration;
NSDate * _Nullable _clusterDataPersistenceFirstScheduledTime;
NSMutableArray<NSDate *> * _mostRecentReportTimes;
NSDate * _Nullable _deviceReportingExcessivelyStartTime;
double _reportToPersistenceDelayCurrentMultiplier;
// System time change observer reference
id _systemTimeChangeObserverToken;
// Protects mutable state used by our description getter. This is a separate lock from "lock"
// so that we don't need to worry about getting our description while holding "lock" (e.g due to
// logging self). This lock _must_ be held narrowly, with no other lock acquisitions allowed
// while it's held, to avoid deadlock.
os_unfair_lock _descriptionLock;
// State used by our description getter: access to these must be protected by descriptionLock.
NSNumber * _Nullable _vid; // nil if unknown
NSNumber * _Nullable _pid; // nil if unknown
// _allNetworkFeatures is a bitwise or of the feature maps of all network commissioning clusters
// present on the device, or nil if there aren't any.
NSNumber * _Nullable _allNetworkFeatures;
// Most recent entry in _mostRecentReportTimes, if any.
NSDate * _Nullable _mostRecentReportTimeForDescription;
}
- (instancetype)initForSubclassesWithNodeID:(NSNumber *)nodeID controller:(MTRDeviceController *)controller
{
if (self = [super init]) {
_lock = OS_UNFAIR_LOCK_INIT;
_delegates = [NSMutableSet set];
_deviceController = controller;
_nodeID = nodeID;
_state = MTRDeviceStateUnknown;
}
return self;
}
- (instancetype)initWithNodeID:(NSNumber *)nodeID controller:(MTRDeviceController *)controller
{
if (self = [super init]) {
_lock = OS_UNFAIR_LOCK_INIT;
_timeSyncLock = OS_UNFAIR_LOCK_INIT;
_descriptionLock = OS_UNFAIR_LOCK_INIT;
_nodeID = [nodeID copy];
_fabricIndex = controller.fabricIndex;
_deviceController = controller;
_queue
= dispatch_queue_create("org.csa-iot.matter.framework.device.workqueue", DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
_expectedValueCache = [NSMutableDictionary dictionary];
_asyncWorkQueue = [[MTRAsyncWorkQueue alloc] initWithContext:self];
_state = MTRDeviceStateUnknown;
_internalDeviceState = MTRInternalDeviceStateUnsubscribed;
if (controller.controllerDataStore) {
_persistedClusterData = [[NSCache alloc] init];
} else {
_persistedClusterData = nil;
}
_clusterDataToPersist = nil;
_persistedClusters = [NSMutableSet set];
// If there is a data store, make sure we have an observer to monitor system clock changes, so
// NSDate-based write coalescing could be reset and not get into a bad state.
if (_persistedClusterData) {
mtr_weakify(self);
_systemTimeChangeObserverToken = [[NSNotificationCenter defaultCenter] addObserverForName:NSSystemClockDidChangeNotification object:nil queue:nil usingBlock:^(NSNotification * _Nonnull notification) {
mtr_strongify(self);
std::lock_guard lock(self->_lock);
[self _resetStorageBehaviorState];
}];
}
_delegates = [NSMutableSet set];
MTR_LOG_DEBUG("%@ init with hex nodeID 0x%016llX", self, _nodeID.unsignedLongLongValue);
}
return self;
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:_systemTimeChangeObserverToken];
// TODO: retain cycle and clean up https://github.com/project-chip/connectedhomeip/issues/34267
MTR_LOG("MTRDevice dealloc: %p", self);
}
+ (MTRDevice *)deviceWithNodeID:(NSNumber *)nodeID controller:(MTRDeviceController *)controller
{
return [controller deviceForNodeID:nodeID];
}
#pragma mark - Time Synchronization
- (void)_setTimeOnDevice
{
NSDate * now = [NSDate date];
// If no date available, error
if (!now) {
MTR_LOG_ERROR("%@ Could not retrieve current date. Unable to setUTCTime on endpoints.", self);
return;
}
uint64_t matterEpochTimeMicroseconds = 0;
if (!DateToMatterEpochMicroseconds(now, matterEpochTimeMicroseconds)) {
MTR_LOG_ERROR("%@ Could not convert NSDate (%@) to Matter Epoch Time. Unable to setUTCTime on endpoints.", self, now);
return;
}
// Set Time on each Endpoint with a Time Synchronization Cluster Server
NSArray<NSNumber *> * endpointsToSync = [self _endpointsWithTimeSyncClusterServer];
for (NSNumber * endpoint in endpointsToSync) {
MTR_LOG_DEBUG("%@ Setting Time on Endpoint %@", self, endpoint);
[self _setUTCTime:matterEpochTimeMicroseconds withGranularity:MTRTimeSynchronizationGranularityMicrosecondsGranularity forEndpoint:endpoint];
// Check how many DST offsets this endpoint supports.
auto dstOffsetsMaxSizePath = [MTRAttributePath attributePathWithEndpointID:endpoint clusterID:@(MTRClusterIDTypeTimeSynchronizationID) attributeID:@(MTRAttributeIDTypeClusterTimeSynchronizationAttributeDSTOffsetListMaxSizeID)];
auto dstOffsetsMaxSize = [self readAttributeWithEndpointID:dstOffsetsMaxSizePath.endpoint clusterID:dstOffsetsMaxSizePath.cluster attributeID:dstOffsetsMaxSizePath.attribute params:nil];
if (dstOffsetsMaxSize == nil) {
// This endpoint does not support TZ, so won't support SetDSTOffset.
MTR_LOG("%@ Unable to SetDSTOffset on endpoint %@, since it does not support the TZ feature", self, endpoint);
continue;
}
auto attrReport = [[MTRAttributeReport alloc] initWithResponseValue:@{
MTRAttributePathKey : dstOffsetsMaxSizePath,
MTRDataKey : dstOffsetsMaxSize,
}
error:nil];
uint8_t maxOffsetCount;
if (attrReport == nil) {
MTR_LOG_ERROR("%@ DSTOffsetListMaxSize value on endpoint %@ is invalid. Defaulting to 1.", self, endpoint);
maxOffsetCount = 1;
} else {
NSNumber * maxOffsetCountAsNumber = attrReport.value;
maxOffsetCount = maxOffsetCountAsNumber.unsignedCharValue;
if (maxOffsetCount == 0) {
MTR_LOG_ERROR("%@ DSTOffsetListMaxSize value on endpoint %@ is 0, which is not allowed. Defaulting to 1.", self, endpoint);
maxOffsetCount = 1;
}
}
auto * dstOffsets = MTRComputeDSTOffsets(maxOffsetCount);
if (dstOffsets == nil) {
MTR_LOG_ERROR("%@ Could not retrieve DST offset information. Unable to setDSTOffset on endpoint %@.", self, endpoint);
continue;
}
[self _setDSTOffsets:dstOffsets forEndpoint:endpoint];
}
}
- (void)_scheduleNextUpdate:(UInt64)nextUpdateInSeconds
{
mtr_weakify(self);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t) (nextUpdateInSeconds * NSEC_PER_SEC)), self.queue, ^{
MTR_LOG_DEBUG("%@ Timer expired, start Device Time Update", self);
mtr_strongify(self);
if (self) {
[self _performScheduledTimeUpdate];
} else {
MTR_LOG_DEBUG("%@ MTRDevice no longer valid. No Timer Scheduled will be scheduled for a Device Time Update.", self);
return;
}
});
self.timeUpdateScheduled = YES;
MTR_LOG_DEBUG("%@ Timer Scheduled for next Device Time Update, in %llu seconds", self, nextUpdateInSeconds);
}
// Time Updates are a day apart (this can be changed in the future)
#define MTR_DEVICE_TIME_UPDATE_DEFAULT_WAIT_TIME_SEC (24 * 60 * 60)
// assume lock is held
- (void)_updateDeviceTimeAndScheduleNextUpdate
{
os_unfair_lock_assert_owner(&self->_timeSyncLock);
if (self.timeUpdateScheduled) {
MTR_LOG_DEBUG("%@ Device Time Update already scheduled", self);
return;
}
[self _setTimeOnDevice];
[self _scheduleNextUpdate:MTR_DEVICE_TIME_UPDATE_DEFAULT_WAIT_TIME_SEC];
}
- (void)_performScheduledTimeUpdate
{
std::lock_guard lock(_timeSyncLock);
// Device needs to still be reachable
if (self.state != MTRDeviceStateReachable) {
MTR_LOG_DEBUG("%@ Device is not reachable, canceling Device Time Updates.", self);
return;
}
// Device must not be invalidated
if (!self.timeUpdateScheduled) {
MTR_LOG_DEBUG("%@ Device Time Update is no longer scheduled, MTRDevice may have been invalidated.", self);
return;
}
self.timeUpdateScheduled = NO;
[self _updateDeviceTimeAndScheduleNextUpdate];
}
- (NSArray<NSNumber *> *)_endpointsWithTimeSyncClusterServer
{
NSArray<NSNumber *> * endpointsOnDevice;
{
std::lock_guard lock(_lock);
endpointsOnDevice = [self _endpointList];
}
NSMutableArray<NSNumber *> * endpointsWithTimeSyncCluster = [[NSMutableArray<NSNumber *> alloc] init];
for (NSNumber * endpoint in endpointsOnDevice) {
// Get list of server clusters on endpoint
auto clusterList = [self readAttributeWithEndpointID:endpoint clusterID:@(MTRClusterIDTypeDescriptorID) attributeID:@(MTRAttributeIDTypeClusterDescriptorAttributeServerListID) params:nil];
NSArray<NSNumber *> * clusterArray = [self arrayOfNumbersFromAttributeValue:clusterList];
if (clusterArray && [clusterArray containsObject:@(MTRClusterIDTypeTimeSynchronizationID)]) {
[endpointsWithTimeSyncCluster addObject:endpoint];
}
}
MTR_LOG_DEBUG("%@ Device has following endpoints with Time Sync Cluster Server: %@", self, endpointsWithTimeSyncCluster);
return endpointsWithTimeSyncCluster;
}
- (void)_setUTCTime:(UInt64)matterEpochTime withGranularity:(uint8_t)granularity forEndpoint:(NSNumber *)endpoint
{
MTR_LOG_DEBUG(" %@ _setUTCTime with matterEpochTime: %llu, endpoint %@", self, matterEpochTime, endpoint);
MTRTimeSynchronizationClusterSetUTCTimeParams * params = [[MTRTimeSynchronizationClusterSetUTCTimeParams
alloc] init];
params.utcTime = @(matterEpochTime);
params.granularity = @(granularity);
auto setUTCTimeResponseHandler = ^(id _Nullable response, NSError * _Nullable error) {
if (error) {
MTR_LOG_ERROR("%@ _setUTCTime failed on endpoint %@, with parameters %@, error: %@", self, endpoint, params, error);
}
};
[self _invokeKnownCommandWithEndpointID:endpoint
clusterID:@(MTRClusterIDTypeTimeSynchronizationID)
commandID:@(MTRCommandIDTypeClusterTimeSynchronizationCommandSetUTCTimeID)
commandPayload:params
expectedValues:nil
expectedValueInterval:nil
timedInvokeTimeout:nil
serverSideProcessingTimeout:params.serverSideProcessingTimeout
responseClass:nil
queue:self.queue
completion:setUTCTimeResponseHandler];
}
- (void)_setDSTOffsets:(NSArray<MTRTimeSynchronizationClusterDSTOffsetStruct *> *)dstOffsets forEndpoint:(NSNumber *)endpoint
{
MTR_LOG_DEBUG("%@ _setDSTOffsets with offsets: %@, endpoint %@",
self, dstOffsets, endpoint);
MTRTimeSynchronizationClusterSetDSTOffsetParams * params = [[MTRTimeSynchronizationClusterSetDSTOffsetParams
alloc] init];
params.dstOffset = dstOffsets;
auto setDSTOffsetResponseHandler = ^(id _Nullable response, NSError * _Nullable error) {
if (error) {
MTR_LOG_ERROR("%@ _setDSTOffsets failed on endpoint %@, with parameters %@, error: %@", self, endpoint, params, error);
}
};
[self _invokeKnownCommandWithEndpointID:endpoint
clusterID:@(MTRClusterIDTypeTimeSynchronizationID)
commandID:@(MTRCommandIDTypeClusterTimeSynchronizationCommandSetDSTOffsetID)
commandPayload:params
expectedValues:nil
expectedValueInterval:nil
timedInvokeTimeout:nil
serverSideProcessingTimeout:params.serverSideProcessingTimeout
responseClass:nil
queue:self.queue
completion:setDSTOffsetResponseHandler];
}
- (NSMutableArray<NSNumber *> *)arrayOfNumbersFromAttributeValue:(MTRDeviceDataValueDictionary)dataDictionary
{
if (![MTRArrayValueType isEqual:dataDictionary[MTRTypeKey]]) {
return nil;
}
id value = dataDictionary[MTRValueKey];
if (![value isKindOfClass:NSArray.class]) {
return nil;
}
NSArray * valueArray = value;
__auto_type outputArray = [NSMutableArray<NSNumber *> arrayWithCapacity:valueArray.count];
for (id item in valueArray) {
if (![item isKindOfClass:NSDictionary.class]) {
return nil;
}
NSDictionary * itemDictionary = item;
id data = itemDictionary[MTRDataKey];
if (![data isKindOfClass:NSDictionary.class]) {
return nil;
}
NSDictionary * dataDictionary = data;
id dataType = dataDictionary[MTRTypeKey];
id dataValue = dataDictionary[MTRValueKey];
if (![dataType isKindOfClass:NSString.class] || ![dataValue isKindOfClass:NSNumber.class]) {
return nil;
}
[outputArray addObject:dataValue];
}
return outputArray;
}
#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);
// Replace delegate info with the same delegate object, and opportunistically remove defunct delegate references
NSMutableSet<MTRDeviceDelegateInfo *> * delegatesToRemove = [NSMutableSet set];
for (MTRDeviceDelegateInfo * delegateInfo in _delegates) {
id<MTRDeviceDelegate> strongDelegate = delegateInfo.delegate;
if (!strongDelegate) {
[delegatesToRemove addObject:delegateInfo];
MTR_LOG("%@ removing delegate info for nil delegate %p", self, delegateInfo.delegatePointerValue);
} else if (strongDelegate == delegate) {
[delegatesToRemove addObject:delegateInfo];
MTR_LOG("%@ replacing delegate info for %p", self, delegate);
}
}
if (delegatesToRemove.count) {
NSUInteger oldDelegatesCount = _delegates.count;
[_delegates minusSet:delegatesToRemove];
MTR_LOG("%@ addDelegate: removed %lu", self, static_cast<unsigned long>(_delegates.count - oldDelegatesCount));
}
MTRDeviceDelegateInfo * newDelegateInfo = [[MTRDeviceDelegateInfo alloc] initWithDelegate:delegate queue:queue interestedPathsForAttributes:interestedPathsForAttributes interestedPathsForEvents:interestedPathsForEvents];
[_delegates addObject:newDelegateInfo];
MTR_LOG("%@ added delegate info %@", self, newDelegateInfo);
// Call hook to allow subclasses to act on delegate addition.
[self _delegateAdded];
}
- (void)_delegateAdded
{
// Nothing to do; this is a hook for subclasses. If that ever changes for
// some reason, subclasses need to start calling this hook on their super.
}
- (void)removeDelegate:(id<MTRDeviceDelegate>)delegate
{
MTR_LOG("%@ removeDelegate %@", self, delegate);
std::lock_guard lock(_lock);
NSMutableSet<MTRDeviceDelegateInfo *> * delegatesToRemove = [NSMutableSet set];
[self _iterateDelegatesWithBlock:^(MTRDeviceDelegateInfo * delegateInfo) {
id<MTRDeviceDelegate> strongDelegate = delegateInfo.delegate;
if (!strongDelegate) {
[delegatesToRemove addObject:delegateInfo];
MTR_LOG("%@ removing delegate info for nil delegate %p", self, delegateInfo.delegatePointerValue);
} else if (strongDelegate == delegate) {
[delegatesToRemove addObject:delegateInfo];
MTR_LOG("%@ removing delegate info %@ for %p", self, delegateInfo, delegate);
}
}];
if (delegatesToRemove.count) {
NSUInteger oldDelegatesCount = _delegates.count;
[_delegates minusSet:delegatesToRemove];
MTR_LOG("%@ removeDelegate: removed %lu", self, static_cast<unsigned long>(_delegates.count - oldDelegatesCount));
}
}
- (void)invalidate
{
std::lock_guard lock(_lock);
[_delegates removeAllObjects];
}
- (void)nodeMayBeAdvertisingOperational
{
assertChipStackLockedByCurrentThread();
MTR_LOG("%@ saw new operational advertisement", self);
}
- (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);
if (!_delegates.count) {
MTR_LOG_DEBUG("%@ no delegates to iterate", self);
return NO;
}
// Opportunistically remove defunct delegate references on every iteration
NSMutableSet * delegatesToRemove = nil;
for (MTRDeviceDelegateInfo * delegateInfo in _delegates) {
id<MTRDeviceDelegate> strongDelegate = delegateInfo.delegate;
if (strongDelegate) {
if (block) {
@autoreleasepool {
block(delegateInfo);
}
}
(void) strongDelegate; // ensure it stays alive
} else {
if (!delegatesToRemove) {
delegatesToRemove = [NSMutableSet set];
}
[delegatesToRemove addObject:delegateInfo];
}
}
if (delegatesToRemove.count) {
[_delegates minusSet:delegatesToRemove];
MTR_LOG("%@ _iterateDelegatesWithBlock: removed %lu remaining %lu", self, static_cast<unsigned long>(delegatesToRemove.count), (unsigned long) static_cast<unsigned long>(_delegates.count));
}
return (_delegates.count > 0);
}
- (BOOL)_callDelegatesWithBlock:(void (^)(id<MTRDeviceDelegate> delegate))block
{
os_unfair_lock_assert_owner(&self->_lock);
__block NSUInteger delegatesCalled = 0;
[self _iterateDelegatesWithBlock:^(MTRDeviceDelegateInfo * delegateInfo) {
if ([delegateInfo callDelegateWithBlock:block]) {
delegatesCalled++;
}
}];
return (delegatesCalled > 0);
}
- (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);
for (MTRDeviceDelegateInfo * delegateInfo in _delegates) {
if ([delegateInfo callDelegateSynchronouslyWithBlock:block]) {
MTR_LOG("%@ _callFirstDelegateSynchronouslyWithBlock: successfully called %@", self, delegateInfo);
return;
}
}
}
#endif
#ifdef DEBUG
- (MTRInternalDeviceState)_getInternalState
{
std::lock_guard lock(self->_lock);
return _internalDeviceState;
}
#endif
- (BOOL)deviceUsesThread
{
std::lock_guard lock(_lock);
return [self _deviceUsesThread];
}
// This method is used for signaling whether to use the subscription pool. This functions as
// a heuristic for whether to throttle subscriptions to the device via a pool of subscriptions.
// If products appear that have both Thread and Wifi enabled but are primarily on wifi, this
// method will need to be updated to reflect that.
- (BOOL)_deviceUsesThread
{
os_unfair_lock_assert_owner(&self->_lock);
#ifdef DEBUG
// Note: This is a hack to allow our unit tests to test the subscription pooling behavior we have implemented for thread, so we mock devices to be a thread device
__block BOOL pretendThreadEnabled = NO;
[self _callFirstDelegateSynchronouslyWithBlock:^(id testDelegate) {
if ([testDelegate respondsToSelector:@selector(unitTestPretendThreadEnabled:)]) {
pretendThreadEnabled = [testDelegate unitTestPretendThreadEnabled:self];
}
}];
if (pretendThreadEnabled) {
return YES;
}
#endif
MTRClusterPath * networkCommissioningClusterPath = [MTRClusterPath clusterPathWithEndpointID:@(kRootEndpointId) clusterID:@(MTRClusterIDTypeNetworkCommissioningID)];
MTRDeviceClusterData * networkCommissioningClusterData = [self _clusterDataForPath:networkCommissioningClusterPath];
NSNumber * networkCommissioningClusterFeatureMapValueNumber = networkCommissioningClusterData.attributes[@(MTRClusterGlobalAttributeFeatureMapID)][MTRValueKey];
if (networkCommissioningClusterFeatureMapValueNumber == nil)
return NO;
if (![networkCommissioningClusterFeatureMapValueNumber isKindOfClass:[NSNumber class]]) {
MTR_LOG_ERROR("%@ Unexpected NetworkCommissioning FeatureMap value %@", self, networkCommissioningClusterFeatureMapValueNumber);
return NO;
}
uint32_t networkCommissioningClusterFeatureMapValue = static_cast<uint32_t>(networkCommissioningClusterFeatureMapValueNumber.unsignedLongValue);
return (networkCommissioningClusterFeatureMapValue & MTRNetworkCommissioningFeatureThreadNetworkInterface) != 0 ? YES : NO;
}
- (NSDictionary<MTRClusterPath *, MTRDeviceClusterData *> *)_clusterDataToPersistSnapshot
{
os_unfair_lock_assert_owner(&self->_lock);
NSMutableDictionary * clusterDataToReturn = [NSMutableDictionary dictionary];
for (MTRClusterPath * clusterPath in _clusterDataToPersist) {
clusterDataToReturn[clusterPath] = [_clusterDataToPersist[clusterPath] copy];
}
return clusterDataToReturn;
}
- (NSTimeInterval)_reportToPersistenceDelayTimeAfterMutiplier
{
return _storageBehaviorConfiguration.reportToPersistenceDelayTime * _reportToPersistenceDelayCurrentMultiplier;
}
- (NSTimeInterval)_reportToPersistenceDelayTimeMaxAfterMutiplier
{
return _storageBehaviorConfiguration.reportToPersistenceDelayTimeMax * _reportToPersistenceDelayCurrentMultiplier;
}
- (BOOL)_dataStoreExists
{
os_unfair_lock_assert_owner(&self->_lock);
return _persistedClusterData != nil;
}
- (void)_persistClusterData
{
os_unfair_lock_assert_owner(&self->_lock);
// Sanity check
if (![self _dataStoreExists]) {
MTR_LOG_ERROR("%@ storage behavior: no data store in _persistClusterData!", self);
return;
}
// Nothing to persist
if (!_clusterDataToPersist.count) {
return;
}
MTR_LOG("%@ Storing cluster information (data version and attributes) count: %lu", self, static_cast<unsigned long>(_clusterDataToPersist.count));
// We're going to hand out these MTRDeviceClusterData objects to our
// storage implementation, which will try to read them later. Make sure
// we snapshot the state here instead of handing out live copies.
NSDictionary<MTRClusterPath *, MTRDeviceClusterData *> * clusterData = [self _clusterDataToPersistSnapshot];
[_deviceController.controllerDataStore storeClusterData:clusterData forNodeID:_nodeID];
for (MTRClusterPath * clusterPath in _clusterDataToPersist) {
[_persistedClusterData setObject:_clusterDataToPersist[clusterPath] forKey:clusterPath];
[_persistedClusters addObject:clusterPath];
}
// TODO: There is one edge case not handled well here: if the
// storeClusterData call above fails somehow, and then the data gets
// evicted from _persistedClusterData, we could end up in a situation
// where when we page things in from storage we have stale values and
// hence effectively lose the delta that we failed to persist.
//
// The only way to handle this would be to detect it when it happens,
// then re-subscribe at that point, which would cause the relevant data
// to be sent to us via the priming read.
_clusterDataToPersist = nil;
#ifdef DEBUG
[self _callDelegatesWithBlock:^(id testDelegate) {
if ([testDelegate respondsToSelector:@selector(unitTestClusterDataPersisted:)]) {
[testDelegate unitTestClusterDataPersisted:self];
}
}];
#endif
}
- (BOOL)_deviceIsReportingExcessively
{
os_unfair_lock_assert_owner(&self->_lock);
if (!_deviceReportingExcessivelyStartTime) {
return NO;
}
NSTimeInterval intervalSinceDeviceReportingExcessively = -[_deviceReportingExcessivelyStartTime timeIntervalSinceNow];
BOOL deviceIsReportingExcessively = intervalSinceDeviceReportingExcessively > _storageBehaviorConfiguration.deviceReportingExcessivelyIntervalThreshold;
if (deviceIsReportingExcessively) {
MTR_LOG("%@ storage behavior: device has been reporting excessively for %.3lf seconds", self, intervalSinceDeviceReportingExcessively);
}
return deviceIsReportingExcessively;
}
- (void)_persistClusterDataAsNeeded
{
std::lock_guard lock(_lock);
// Nothing to persist
if (!_clusterDataToPersist.count) {
return;
}
// This is run with a dispatch_after, and need to check again if this device is reporting excessively
if ([self _deviceIsReportingExcessively]) {
return;
}
NSDate * lastReportTime = [_mostRecentReportTimes lastObject];
NSTimeInterval intervalSinceLastReport = -[lastReportTime timeIntervalSinceNow];
if (intervalSinceLastReport < [self _reportToPersistenceDelayTimeAfterMutiplier]) {
// A report came in after this call was scheduled
if (!_clusterDataPersistenceFirstScheduledTime) {
MTR_LOG_ERROR("%@ storage behavior: expects _clusterDataPersistenceFirstScheduledTime if _clusterDataToPersist exists", self);
return;
}
NSTimeInterval intervalSinceFirstScheduledPersistence = -[_clusterDataPersistenceFirstScheduledTime timeIntervalSinceNow];
if (intervalSinceFirstScheduledPersistence < [self _reportToPersistenceDelayTimeMaxAfterMutiplier]) {
MTR_LOG("%@ storage behavior: not persisting: intervalSinceLastReport %lf intervalSinceFirstScheduledPersistence %lf", self, intervalSinceLastReport, intervalSinceFirstScheduledPersistence);
// The max delay is also not reached - do not persist yet
return;
}
}
// At this point, there is data to persist, and either _reportToPersistenceDelayTime was
// reached, or _reportToPersistenceDelayTimeMax was reached. Time to persist:
[self _persistClusterData];
_clusterDataPersistenceFirstScheduledTime = nil;
}
#ifdef DEBUG
- (void)unitTestSetMostRecentReportTimes:(NSMutableArray<NSDate *> *)mostRecentReportTimes
{
_mostRecentReportTimes = mostRecentReportTimes;
std::lock_guard lock(_descriptionLock);
_mostRecentReportTimeForDescription = [mostRecentReportTimes lastObject];
}
#endif
- (void)_scheduleClusterDataPersistence
{
os_unfair_lock_assert_owner(&self->_lock);
// No persisted data / lack of controller data store
if (![self _dataStoreExists]) {
MTR_LOG_DEBUG("%@ storage behavior: no data store", self);
return;
}
// Nothing to persist
if (!_clusterDataToPersist.count) {
MTR_LOG_DEBUG("%@ storage behavior: nothing to persist", self);
return;
}
// If there is no storage behavior configuration, make a default one
if (!_storageBehaviorConfiguration) {
_storageBehaviorConfiguration = [[MTRDeviceStorageBehaviorConfiguration alloc] init];
[_storageBehaviorConfiguration checkValuesAndResetToDefaultIfNecessary];
}
// Directly store if the storage behavior optimization is disabled
if (_storageBehaviorConfiguration.disableStorageBehaviorOptimization) {
[self _persistClusterData];
return;
}
// If we have nothing stored at all yet, store directly, so we move into a
// primed state.
if (!_deviceCachePrimed) {
[self _persistClusterData];
return;
}
// Ensure there is an array to keep the most recent report times
if (!_mostRecentReportTimes) {
_mostRecentReportTimes = [NSMutableArray array];
}
// Mark when first report comes in to know when _reportToPersistenceDelayTimeMax is hit
if (!_clusterDataPersistenceFirstScheduledTime) {
_clusterDataPersistenceFirstScheduledTime = [NSDate now];
}
// Make sure there is space in the array, and note report time
while (_mostRecentReportTimes.count >= _storageBehaviorConfiguration.recentReportTimesMaxCount) {
[_mostRecentReportTimes removeObjectAtIndex:0];
}
[_mostRecentReportTimes addObject:[NSDate now]];
{
std::lock_guard lock(_descriptionLock);
_mostRecentReportTimeForDescription = [_mostRecentReportTimes lastObject];
}
// Calculate running average and update multiplier - need at least 2 items to calculate intervals
if (_mostRecentReportTimes.count > 2) {
NSTimeInterval cumulativeIntervals = 0;
for (int i = 1; i < _mostRecentReportTimes.count; i++) {
NSDate * lastDate = [_mostRecentReportTimes objectAtIndex:i - 1];
NSDate * currentDate = [_mostRecentReportTimes objectAtIndex:i];
NSTimeInterval intervalSinceLastReport = [currentDate timeIntervalSinceDate:lastDate];
// Check to guard against clock change
if (intervalSinceLastReport > 0) {
cumulativeIntervals += intervalSinceLastReport;
}
}
NSTimeInterval averageTimeBetweenReports = cumulativeIntervals / (_mostRecentReportTimes.count - 1);
if (averageTimeBetweenReports < _storageBehaviorConfiguration.timeBetweenReportsTooShortThreshold) {
// Multiplier goes from 1 to _reportToPersistenceDelayMaxMultiplier uniformly, as
// averageTimeBetweenReports go from timeBetweenReportsTooShortThreshold to
// timeBetweenReportsTooShortMinThreshold
double intervalAmountBelowThreshold = _storageBehaviorConfiguration.timeBetweenReportsTooShortThreshold - averageTimeBetweenReports;
double intervalAmountBetweenThresholdAndMinThreshold = _storageBehaviorConfiguration.timeBetweenReportsTooShortThreshold - _storageBehaviorConfiguration.timeBetweenReportsTooShortMinThreshold;
double proportionTowardMinThreshold = intervalAmountBelowThreshold / intervalAmountBetweenThresholdAndMinThreshold;
if (proportionTowardMinThreshold > 1) {
// Clamp to 100%
proportionTowardMinThreshold = 1;
}
// Set current multiplier to [1, MaxMultiplier]
_reportToPersistenceDelayCurrentMultiplier = 1 + (proportionTowardMinThreshold * (_storageBehaviorConfiguration.reportToPersistenceDelayMaxMultiplier - 1));
MTR_LOG("%@ storage behavior: device reporting frequently - setting delay multiplier to %lf", self, _reportToPersistenceDelayCurrentMultiplier);
} else {
_reportToPersistenceDelayCurrentMultiplier = 1;
}
// Also note when the running average first dips below the min threshold
if (averageTimeBetweenReports < _storageBehaviorConfiguration.timeBetweenReportsTooShortMinThreshold) {
if (!_deviceReportingExcessivelyStartTime) {
_deviceReportingExcessivelyStartTime = [NSDate now];
MTR_LOG_DEBUG("%@ storage behavior: device is reporting excessively @%@", self, _deviceReportingExcessivelyStartTime);
}
} else {
_deviceReportingExcessivelyStartTime = nil;
}
}
// Do not schedule persistence if device is reporting excessively
if ([self _deviceIsReportingExcessively]) {
return;
}
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t) ([self _reportToPersistenceDelayTimeAfterMutiplier] * NSEC_PER_SEC)), self.queue, ^{
[self _persistClusterDataAsNeeded];
});
}
// Used to clear the storage behavior state when needed (system time change, or when new
// configuration is set.
//
// Also flushes unwritten cluster data to storage, if data store exists.
- (void)_resetStorageBehaviorState
{
os_unfair_lock_assert_owner(&self->_lock);
_clusterDataPersistenceFirstScheduledTime = nil;
_mostRecentReportTimes = nil;
{
std::lock_guard lock(_descriptionLock);
_mostRecentReportTimeForDescription = nil;
}
_deviceReportingExcessivelyStartTime = nil;
_reportToPersistenceDelayCurrentMultiplier = 1;
// Sanity check that there is a data
if ([self _dataStoreExists]) {
[self _persistClusterData];
}
}
- (void)setStorageBehaviorConfiguration:(MTRDeviceStorageBehaviorConfiguration *)storageBehaviorConfiguration
{
MTR_LOG("%@ storage behavior: setStorageBehaviorConfiguration %@", self, storageBehaviorConfiguration);
std::lock_guard lock(_lock);
_storageBehaviorConfiguration = storageBehaviorConfiguration;
// Make sure the values are sane
[_storageBehaviorConfiguration checkValuesAndResetToDefaultIfNecessary];
[self _resetStorageBehaviorState];
}
- (BOOL)_interestedPaths:(NSArray * _Nullable)interestedPaths includesAttributePath:(MTRAttributePath *)attributePath
{
for (id interestedPath in interestedPaths) {
if ([interestedPath isKindOfClass:[NSNumber class]]) {
NSNumber * interestedEndpointIDNumber = interestedPath;
if ([interestedEndpointIDNumber isEqualToNumber:attributePath.endpoint]) {
return YES;
}
} else if ([interestedPath isKindOfClass:[MTRClusterPath class]]) {
MTRClusterPath * interestedClusterPath = interestedPath;
if ([interestedClusterPath.cluster isEqualToNumber:attributePath.cluster]) {
return YES;
}
} else if ([interestedPath isKindOfClass:[MTRAttributePath class]]) {
MTRAttributePath * interestedAttributePath = interestedPath;
if (([interestedAttributePath.cluster isEqualToNumber:attributePath.cluster]) && ([interestedAttributePath.attribute isEqualToNumber:attributePath.attribute])) {
return YES;
}
}
}
return NO;
}
// Returns filtered set of attributes using an interestedPaths array.
// Returns nil if no attribute report has a path that matches the paths in the interestedPaths array.
- (NSArray<NSDictionary<NSString *, id> *> *)_filteredAttributes:(NSArray<NSDictionary<NSString *, id> *> *)attributes forInterestedPaths:(NSArray * _Nullable)interestedPaths
{
if (!interestedPaths) {
return attributes;
}
if (!interestedPaths.count) {
return nil;
}
NSMutableArray * filteredAttributes = nil;
for (NSDictionary<NSString *, id> * responseValue in attributes) {
MTRAttributePath * attributePath = responseValue[MTRAttributePathKey];
if ([self _interestedPaths:interestedPaths includesAttributePath:attributePath]) {
if (!filteredAttributes) {
filteredAttributes = [NSMutableArray array];
}
[filteredAttributes addObject:responseValue];
}
}
if (filteredAttributes.count && (filteredAttributes.count != attributes.count)) {
MTR_LOG("%@ filtered attribute report %lu => %lu", self, static_cast<unsigned long>(attributes.count), static_cast<unsigned long>(filteredAttributes.count));
}
return filteredAttributes;
}
// assume lock is held
- (void)_reportAttributes:(NSArray<NSDictionary<NSString *, id> *> *)attributes
{
os_unfair_lock_assert_owner(&self->_lock);
if (attributes.count) {
[self _iterateDelegatesWithBlock:^(MTRDeviceDelegateInfo * delegateInfo) {
// _iterateDelegatesWithBlock calls this with an autorelease pool, and so temporary filtered attributes reports don't bloat memory
NSArray<NSDictionary<NSString *, id> *> * filteredAttributes = [self _filteredAttributes:attributes forInterestedPaths:delegateInfo.interestedPathsForAttributes];
if (filteredAttributes.count) {
[delegateInfo callDelegateWithBlock:^(id<MTRDeviceDelegate> delegate) {
[delegate device:self receivedAttributeReport:filteredAttributes];
}];
}
}];
}
}
#ifdef DEBUG
- (void)unitTestInjectEventReport:(NSArray<NSDictionary<NSString *, id> *> *)eventReport
{
NSAssert(NO, @"Unit test injection of reports needs to be handled by subclasses");
}
- (void)unitTestInjectAttributeReport:(NSArray<NSDictionary<NSString *, id> *> *)attributeReport fromSubscription:(BOOL)isFromSubscription
{
NSAssert(NO, @"Unit test injection of reports needs to be handled by subclasses");
}
#endif
- (BOOL)_interestedPaths:(NSArray * _Nullable)interestedPaths includesEventPath:(MTREventPath *)eventPath
{
for (id interestedPath in interestedPaths) {
if ([interestedPath isKindOfClass:[NSNumber class]]) {
NSNumber * interestedEndpointIDNumber = interestedPath;
if ([interestedEndpointIDNumber isEqualToNumber:eventPath.endpoint]) {
return YES;
}
} else if ([interestedPath isKindOfClass:[MTRClusterPath class]]) {
MTRClusterPath * interestedClusterPath = interestedPath;
if ([interestedClusterPath.cluster isEqualToNumber:eventPath.cluster]) {
return YES;
}
} else if ([interestedPath isKindOfClass:[MTREventPath class]]) {
MTREventPath * interestedEventPath = interestedPath;
if (([interestedEventPath.cluster isEqualToNumber:eventPath.cluster]) && ([interestedEventPath.event isEqualToNumber:eventPath.event])) {
return YES;
}
}
}
return NO;
}
// Returns filtered set of events using an interestedPaths array.
// Returns nil if no event report has a path that matches the paths in the interestedPaths array.
- (NSArray<NSDictionary<NSString *, id> *> *)_filteredEvents:(NSArray<NSDictionary<NSString *, id> *> *)events forInterestedPaths:(NSArray * _Nullable)interestedPaths
{
if (!interestedPaths) {
return events;
}
if (!interestedPaths.count) {
return nil;
}
NSMutableArray * filteredEvents = nil;
for (NSDictionary<NSString *, id> * responseValue in events) {
MTREventPath * eventPath = responseValue[MTREventPathKey];
if ([self _interestedPaths:interestedPaths includesEventPath:eventPath]) {
if (!filteredEvents) {
filteredEvents = [NSMutableArray array];
}
[filteredEvents addObject:responseValue];
}
}
if (filteredEvents.count && (filteredEvents.count != events.count)) {
MTR_LOG("%@ filtered event report %lu => %lu", self, static_cast<unsigned long>(events.count), static_cast<unsigned long>(filteredEvents.count));
}
return filteredEvents;
}
#ifdef DEBUG
- (void)unitTestClearClusterData
{
std::lock_guard lock(_lock);
NSAssert([self _dataStoreExists], @"Test is not going to test what it thinks is testing!");
[_persistedClusterData removeAllObjects];
}
#endif
- (void)_reconcilePersistedClustersWithStorage
{
os_unfair_lock_assert_owner(&self->_lock);
NSMutableSet * clusterPathsToRemove = [NSMutableSet set];
for (MTRClusterPath * clusterPath in _persistedClusters) {
MTRDeviceClusterData * data = [_deviceController.controllerDataStore getStoredClusterDataForNodeID:_nodeID endpointID:clusterPath.endpoint clusterID:clusterPath.cluster];
if (!data) {
[clusterPathsToRemove addObject:clusterPath];
}
}
MTR_LOG_ERROR("%@ Storage missing %lu / %lu clusters - reconciling in-memory records", self, static_cast<unsigned long>(clusterPathsToRemove.count), static_cast<unsigned long>(_persistedClusters.count));
[_persistedClusters minusSet:clusterPathsToRemove];
}
- (nullable MTRDeviceClusterData *)_clusterDataForPath:(MTRClusterPath *)clusterPath
{
os_unfair_lock_assert_owner(&self->_lock);
if (_clusterDataToPersist != nil) {
// Use the "dirty" values, if we have them.
MTRDeviceClusterData * data = _clusterDataToPersist[clusterPath];
if (data != nil) {
return data;
}
}
if ([self _dataStoreExists]) {
MTRDeviceClusterData * data = [_persistedClusterData objectForKey:clusterPath];
if (data != nil) {
return data;
}
}
if (![_persistedClusters containsObject:clusterPath]) {
// We are not expected to have this cluster, so no point in paging it in
// loading it from storage.
return nil;
}
NSAssert(_deviceController.controllerDataStore != nil,
@"How can _persistedClusters have an entry if we have no persistence?");
NSAssert(_persistedClusterData != nil,
@"How can _persistedClusterData not exist if we have persisted clusters?");
// Page in the stored value for the data.
MTRDeviceClusterData * data = [_deviceController.controllerDataStore getStoredClusterDataForNodeID:_nodeID endpointID:clusterPath.endpoint clusterID:clusterPath.cluster];
MTR_LOG("%@ cluster path %@ cache miss - load from storage success %@", self, clusterPath, YES_NO(data));
if (data != nil) {
[_persistedClusterData setObject:data forKey:clusterPath];
} else {
// If clusterPath is in _persistedClusters and the data store returns nil for it, then the in-memory cache is now not dependable, and subscription should be reset and reestablished to reload cache from device
// First make sure _persistedClusters is consistent with storage, so repeated calls don't immediately re-trigger this
[self _reconcilePersistedClustersWithStorage];
}
return data;
}
- (NSSet<MTRClusterPath *> *)_knownClusters
{
os_unfair_lock_assert_owner(&self->_lock);
// We might have some clusters that have not been persisted at all yet, and
// some that have been persisted but are still present in
// _clusterDataToPersist because they have been modified since then.
NSMutableSet<MTRClusterPath *> * clusterPaths = [_persistedClusters mutableCopy];
if (_clusterDataToPersist != nil) {
[clusterPaths unionSet:[NSSet setWithArray:[_clusterDataToPersist allKeys]]];
}
return clusterPaths;
}
- (NSDictionary<MTRClusterPath *, NSNumber *> *)_getCachedDataVersions
{
NSMutableDictionary<MTRClusterPath *, NSNumber *> * dataVersions = [NSMutableDictionary dictionary];
std::lock_guard lock(_lock);
for (MTRClusterPath * path in [self _knownClusters]) {
dataVersions[path] = [self _clusterDataForPath:path].dataVersion;
}
MTR_LOG_DEBUG("%@ _getCachedDataVersions dataVersions count: %lu", self, static_cast<unsigned long>(dataVersions.count));
return dataVersions;
}
- (MTRDeviceDataValueDictionary _Nullable)_cachedAttributeValueForPath:(MTRAttributePath *)path
{
os_unfair_lock_assert_owner(&self->_lock);
// We need an actual MTRClusterPath, not a subsclass, to do _clusterDataForPath.
auto * clusterPath = [MTRClusterPath clusterPathWithEndpointID:path.endpoint clusterID:path.cluster];
MTRDeviceClusterData * clusterData = [self _clusterDataForPath:clusterPath];
if (clusterData == nil) {
return nil;
}
return clusterData.attributes[path.attribute];
}
#ifdef DEBUG
- (void)unitTestResetSubscription
{
}
#endif
#ifdef DEBUG
- (NSUInteger)unitTestAttributesReportedSinceLastCheck
{
return 0;
}
- (NSUInteger)unitTestNonnullDelegateCount
{
std::lock_guard lock(self->_lock);
NSUInteger nonnullDelegateCount = 0;
for (MTRDeviceDelegateInfo * delegateInfo in _delegates) {
if (delegateInfo.delegate) {
nonnullDelegateCount++;
}
}
return nonnullDelegateCount;
}
#endif
#pragma mark Device Interactions
- (NSDictionary<NSString *, id> * _Nullable)readAttributeWithEndpointID:(NSNumber *)endpointID
clusterID:(NSNumber *)clusterID
attributeID:(NSNumber *)attributeID
params:(MTRReadParams * _Nullable)params
{
#define MTRDeviceErrorStr "MTRDevice readAttributeWithEndpointID:clusterID:attributeID:params: must be handled by subclasses"
MTR_LOG_ERROR(MTRDeviceErrorStr);
#ifdef DEBUG
NSAssert(NO, @MTRDeviceErrorStr);
#endif // DEBUG
#undef MTRDeviceErrorStr
return nil;
}
- (void)writeAttributeWithEndpointID:(NSNumber *)endpointID
clusterID:(NSNumber *)clusterID
attributeID:(NSNumber *)attributeID
value:(id)value
expectedValueInterval:(NSNumber *)expectedValueInterval
timedWriteTimeout:(NSNumber * _Nullable)timeout
{
#define MTRDeviceErrorStr "MTRDevice writeAttributeWithEndpointID:clusterID:attributeID:value:expectedValueInterval:timedWriteTimeout: must be handled by subclasses"
MTR_LOG_ERROR(MTRDeviceErrorStr);
#ifdef DEBUG
NSAssert(NO, @MTRDeviceErrorStr);
#endif // DEBUG
#undef MTRDeviceErrorStr
}
- (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
[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:(id)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
{
if (!expectedValueInterval || ([expectedValueInterval compare:@(0)] == NSOrderedAscending)) {
expectedValues = nil;
} else {
expectedValueInterval = MTRClampedNumber(expectedValueInterval, @(1), @(UINT32_MAX));
}
serverSideProcessingTimeout = [serverSideProcessingTimeout copy];
timeout = [timeout copy];
if (timeout == nil && MTRCommandNeedsTimedInvoke(clusterID, commandID)) {
timeout = @(MTR_DEFAULT_TIMED_INTERACTION_TIMEOUT_MS);
}
NSDate * cutoffTime;
if (timeout) {
cutoffTime = [NSDate dateWithTimeIntervalSinceNow:(timeout.doubleValue / 1000)];
}
uint64_t expectedValueID = 0;
NSMutableArray<MTRAttributePath *> * attributePaths = nil;
if (expectedValues) {
[self setExpectedValues:expectedValues expectedValueInterval:expectedValueInterval expectedValueID:&expectedValueID];
attributePaths = [NSMutableArray array];
for (NSDictionary<NSString *, id> * expectedValue in expectedValues) {
[attributePaths addObject:expectedValue[MTRAttributePathKey]];
}
}
MTRAsyncWorkItem * workItem = [[MTRAsyncWorkItem alloc] initWithQueue:self.queue];
uint64_t workItemID = workItem.uniqueID; // capture only the ID, not the work item
// The command operation will install a duplicate check handler, to return NO for "isDuplicate". Since a command operation may
// change values, only read requests after this should be considered for duplicate requests.
[workItem setDuplicateTypeID:MTRDeviceWorkItemDuplicateReadTypeID handler:^(id opaqueItemData, BOOL * isDuplicate, BOOL * stop) {
*isDuplicate = NO;
*stop = YES;
}];
[workItem setReadyHandler:^(MTRDevice * self, NSInteger retryCount, MTRAsyncWorkCompletionBlock workCompletion) {
auto workDone = ^(NSArray<NSDictionary<NSString *, id> *> * _Nullable values, NSError * _Nullable error) {
dispatch_async(queue, ^{
completion(values, error);
});
if (error && expectedValues) {
[self removeExpectedValuesForAttributePaths:attributePaths expectedValueID:expectedValueID];
}
workCompletion(MTRAsyncWorkComplete);
};
NSNumber * timedInvokeTimeout = nil;
if (timeout) {
auto * now = [NSDate now];
if ([now compare:cutoffTime] == NSOrderedDescending) {
// Our timed invoke timeout has expired already. Command
// was queued for too long. Do not send it out.
workDone(nil, [MTRError errorForIMStatusCode:Status::Timeout]);
return;
}
// Recompute the actual timeout left, accounting for time spent
// in our queuing and retries.
timedInvokeTimeout = @([cutoffTime timeIntervalSinceDate:now] * 1000);
}
MTRBaseDevice * baseDevice = [self newBaseDevice];
[baseDevice
_invokeCommandWithEndpointID:endpointID
clusterID:clusterID
commandID:commandID
commandFields:commandFields
timedInvokeTimeout:timedInvokeTimeout
serverSideProcessingTimeout:serverSideProcessingTimeout
queue:self.queue
completion:^(NSArray<NSDictionary<NSString *, id> *> * _Nullable values, NSError * _Nullable error) {
// Log the data at the INFO level (not usually persisted permanently),
// but make sure we log the work completion at the DEFAULT level.
MTR_LOG("Invoke work item [%llu] received command response: %@ error: %@", workItemID, values, error);
// TODO: This 5-retry cap is very arbitrary.
// TODO: Should there be some sort of backoff here?
if (error != nil && error.domain == MTRInteractionErrorDomain && error.code == MTRInteractionErrorCodeBusy && retryCount < 5) {
workCompletion(MTRAsyncWorkNeedsRetry);
return;
}
workDone(values, error);
}];
}];
[_asyncWorkQueue enqueueWorkItem:workItem descriptionWithFormat:@"invoke %@ 0x%llx 0x%llx", endpointID, clusterID.unsignedLongLongValue, commandID.unsignedLongLongValue];
}
- (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)openCommissioningWindowWithSetupPasscode:(NSNumber *)setupPasscode
discriminator:(NSNumber *)discriminator
duration:(NSNumber *)duration
queue:(dispatch_queue_t)queue
completion:(MTRDeviceOpenCommissioningWindowHandler)completion
{
auto * baseDevice = [self newBaseDevice];
[baseDevice openCommissioningWindowWithSetupPasscode:setupPasscode
discriminator:discriminator
duration:duration
queue:queue
completion:completion];
}
- (void)openCommissioningWindowWithDiscriminator:(NSNumber *)discriminator
duration:(NSNumber *)duration
queue:(dispatch_queue_t)queue
completion:(MTRDeviceOpenCommissioningWindowHandler)completion
{
auto * baseDevice = [self newBaseDevice];
[baseDevice openCommissioningWindowWithDiscriminator:discriminator duration:duration queue:queue completion:completion];
}
- (void)downloadLogOfType:(MTRDiagnosticLogType)type
timeout:(NSTimeInterval)timeout
queue:(dispatch_queue_t)queue
completion:(void (^)(NSURL * _Nullable url, NSError * _Nullable error))completion
{
auto * baseDevice = [self newBaseDevice];
[baseDevice downloadLogOfType:type
timeout:timeout
queue:queue
completion:completion];
}
#pragma mark - Cache management
// assume lock is held
- (void)_checkExpiredExpectedValues
{
os_unfair_lock_assert_owner(&self->_lock);
// find expired attributes, and calculate next timer fire date
NSDate * now = [NSDate date];
NSDate * nextExpirationDate = nil;
// Set of NSArray with 2 elements [path, value] - this is used in this method only
NSMutableSet<NSArray *> * attributeInfoToRemove = [NSMutableSet set];
for (MTRAttributePath * attributePath in _expectedValueCache) {
NSArray * expectedValue = _expectedValueCache[attributePath];
NSDate * attributeExpirationDate = expectedValue[MTRDeviceExpectedValueFieldExpirationTimeIndex];
if (expectedValue) {
if ([now compare:attributeExpirationDate] == NSOrderedDescending) {
// expired - save [path, values] pair to attributeToRemove
[attributeInfoToRemove addObject:@[ attributePath, expectedValue[MTRDeviceExpectedValueFieldValueIndex] ]];
} else {
// get the next expiration date
if (!nextExpirationDate || [nextExpirationDate compare:attributeExpirationDate] == NSOrderedDescending) {
nextExpirationDate = attributeExpirationDate;
}
}
}
}
// remove from expected value cache and report attributes as needed
NSMutableArray * attributesToReport = [NSMutableArray array];
NSMutableArray * attributePathsToReport = [NSMutableArray array];
for (NSArray * attributeInfo in attributeInfoToRemove) {
// compare with known value and mark for report if different
MTRAttributePath * attributePath = attributeInfo[0];
NSDictionary * attributeDataValue = attributeInfo[1];
NSDictionary * cachedAttributeDataValue = [self _cachedAttributeValueForPath:attributePath];
if (cachedAttributeDataValue
&& ![self _attributeDataValue:attributeDataValue isEqualToDataValue:cachedAttributeDataValue]) {
[attributesToReport addObject:@{ MTRAttributePathKey : attributePath, MTRDataKey : cachedAttributeDataValue, MTRPreviousDataKey : attributeDataValue }];
[attributePathsToReport addObject:attributePath];
}
_expectedValueCache[attributePath] = nil;
}
// log attribute paths
MTR_LOG("%@ report from expired expected values %@", self, attributePathsToReport);
[self _reportAttributes:attributesToReport];
// Have a reasonable minimum wait time for expiration timers
#define MTR_DEVICE_EXPIRATION_CHECK_TIMER_MINIMUM_WAIT_TIME (0.1)
if (nextExpirationDate && _expectedValueCache.count && !self.expirationCheckScheduled) {
NSTimeInterval waitTime = [nextExpirationDate timeIntervalSinceDate:now];
if (waitTime < MTR_DEVICE_EXPIRATION_CHECK_TIMER_MINIMUM_WAIT_TIME) {
waitTime = MTR_DEVICE_EXPIRATION_CHECK_TIMER_MINIMUM_WAIT_TIME;
}
mtr_weakify(self);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t) (waitTime * NSEC_PER_SEC)), self.queue, ^{
mtr_strongify(self);
[self _performScheduledExpirationCheck];
});
}
}
- (void)_performScheduledExpirationCheck
{
std::lock_guard lock(_lock);
self.expirationCheckScheduled = NO;
[self _checkExpiredExpectedValues];
}
- (BOOL)_attributeDataValue:(NSDictionary *)one isEqualToDataValue:(NSDictionary *)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]]);
}
// Utility to return data value dictionary without data version
- (NSDictionary *)_dataValueWithoutDataVersion:(NSDictionary *)attributeValue
{
// Sanity check for nil - return the same input to fail gracefully
if (!attributeValue || !attributeValue[MTRTypeKey]) {
return attributeValue;
}
if (attributeValue[MTRValueKey]) {
return @{ MTRTypeKey : attributeValue[MTRTypeKey], MTRValueKey : attributeValue[MTRValueKey] };
} else {
return @{ MTRTypeKey : attributeValue[MTRTypeKey] };
}
}
#ifdef DEBUG
- (NSUInteger)unitTestAttributeCount
{
std::lock_guard lock(_lock);
NSUInteger count = 0;
for (MTRClusterPath * path in [self _knownClusters]) {
count += [self _clusterDataForPath:path].attributes.count;
}
return count;
}
#endif
- (void)setPersistedClusterData:(NSDictionary<MTRClusterPath *, MTRDeviceClusterData *> *)clusterData
{
MTR_LOG("%@ setPersistedClusterData count: %lu", self, static_cast<unsigned long>(clusterData.count));
if (!clusterData.count) {
return;
}
std::lock_guard lock(_lock);
NSAssert([self _dataStoreExists], @"Why is controller setting persisted data when we shouldn't have it?");
for (MTRClusterPath * clusterPath in clusterData) {
// The caller has mutable references to MTRDeviceClusterData and
// MTRClusterPath, but that should be OK, since we control all the
// callers. If that stops being OK, we'll need to copy the key and
// value here.
[_persistedClusters addObject:clusterPath];
[_persistedClusterData setObject:clusterData[clusterPath] forKey:clusterPath];
}
[self _updateAttributeDependentDescriptionData];
// We have some stored data. Since we don't store data until the end of the
// initial priming report, our device cache must be primed.
_deviceCachePrimed = YES;
}
- (void)_setLastInitialSubscribeLatency:(id)latency
{
os_unfair_lock_assert_owner(&self->_lock);
if (![latency isKindOfClass:NSNumber.class]) {
// Unexpected value of some sort; just ignore it.
return;
}
_estimatedSubscriptionLatency = latency;
}
- (void)setPersistedDeviceData:(NSDictionary<NSString *, id> *)data
{
MTR_LOG_DEBUG("%@ setPersistedDeviceData: %@", self, data);
std::lock_guard lock(_lock);
// For now the only data we care about is our initial subscribe latency.
id initialSubscribeLatency = data[sLastInitialSubscribeLatencyKey];
if (initialSubscribeLatency != nil) {
[self _setLastInitialSubscribeLatency:initialSubscribeLatency];
}
}
#ifdef DEBUG
- (MTRDeviceClusterData *)unitTestGetClusterDataForPath:(MTRClusterPath *)path
{
std::lock_guard lock(_lock);
return [[self _clusterDataForPath:path] copy];
}
- (NSSet<MTRClusterPath *> *)unitTestGetPersistedClusters
{
std::lock_guard lock(_lock);
return [_persistedClusters copy];
}
- (BOOL)unitTestClusterHasBeenPersisted:(MTRClusterPath *)path
{
std::lock_guard lock(_lock);
return [_persistedClusters containsObject:path];
}
#endif
- (BOOL)deviceCachePrimed
{
std::lock_guard lock(_lock);
return _deviceCachePrimed;
}
// If value is non-nil, associate with expectedValueID
// If value is nil, remove only if expectedValueID matches
// previousValue is an out parameter
- (void)_setExpectedValue:(NSDictionary<NSString *, id> *)expectedAttributeValue
attributePath:(MTRAttributePath *)attributePath
expirationTime:(NSDate *)expirationTime
shouldReportValue:(BOOL *)shouldReportValue
attributeValueToReport:(NSDictionary<NSString *, id> **)attributeValueToReport
expectedValueID:(uint64_t)expectedValueID
previousValue:(NSDictionary **)previousValue
{
os_unfair_lock_assert_owner(&self->_lock);
*shouldReportValue = NO;
NSArray * previousExpectedValue = _expectedValueCache[attributePath];
if (previousExpectedValue) {
if (expectedAttributeValue
&& ![self _attributeDataValue:expectedAttributeValue
isEqualToDataValue:previousExpectedValue[MTRDeviceExpectedValueFieldValueIndex]]) {
// Case where new expected value overrides previous expected value - report new expected value
*shouldReportValue = YES;
*attributeValueToReport = expectedAttributeValue;
*previousValue = previousExpectedValue[MTRDeviceExpectedValueFieldValueIndex];
} else if (!expectedAttributeValue) {
// Remove previous expected value only if it's from the same setExpectedValues operation
NSNumber * previousExpectedValueID = previousExpectedValue[MTRDeviceExpectedValueFieldIDIndex];
if (previousExpectedValueID.unsignedLongLongValue == expectedValueID) {
MTRDeviceDataValueDictionary cachedValue = [self _cachedAttributeValueForPath:attributePath];
if (![self _attributeDataValue:previousExpectedValue[MTRDeviceExpectedValueFieldValueIndex]
isEqualToDataValue:cachedValue]) {
// Case of removing expected value that is different than read cache - report read cache value
*shouldReportValue = YES;
*attributeValueToReport = cachedValue;
*previousValue = previousExpectedValue[MTRDeviceExpectedValueFieldValueIndex];
_expectedValueCache[attributePath] = nil;
}
}
}
} else {
MTRDeviceDataValueDictionary cachedValue = [self _cachedAttributeValueForPath:attributePath];
if (expectedAttributeValue
&& ![self _attributeDataValue:expectedAttributeValue isEqualToDataValue:cachedValue]) {
// Case where new expected value is different than read cache - report new expected value
*shouldReportValue = YES;
*attributeValueToReport = expectedAttributeValue;
*previousValue = cachedValue;
} else {
*previousValue = nil;
}
// No need to report if new and previous expected value are both nil
}
if (expectedAttributeValue) {
_expectedValueCache[attributePath] = @[ expirationTime, expectedAttributeValue, @(expectedValueID) ];
}
}
// assume lock is held
- (NSArray *)_getAttributesToReportWithNewExpectedValues:(NSArray<NSDictionary<NSString *, id> *> *)expectedAttributeValues
expirationTime:(NSDate *)expirationTime
expectedValueID:(uint64_t *)expectedValueID
{
os_unfair_lock_assert_owner(&self->_lock);
uint64_t expectedValueIDToReturn = _expectedValueNextID++;
NSMutableArray * attributesToReport = [NSMutableArray array];
NSMutableArray * attributePathsToReport = [NSMutableArray array];
for (NSDictionary<NSString *, id> * attributeResponseValue in expectedAttributeValues) {
MTRAttributePath * attributePath = attributeResponseValue[MTRAttributePathKey];
NSDictionary * attributeDataValue = attributeResponseValue[MTRDataKey];
BOOL shouldReportValue = NO;
NSDictionary<NSString *, id> * attributeValueToReport;
NSDictionary<NSString *, id> * previousValue;
[self _setExpectedValue:attributeDataValue
attributePath:attributePath
expirationTime:expirationTime
shouldReportValue:&shouldReportValue
attributeValueToReport:&attributeValueToReport
expectedValueID:expectedValueIDToReturn
previousValue:&previousValue];
if (shouldReportValue) {
if (previousValue) {
[attributesToReport addObject:@{ MTRAttributePathKey : attributePath, MTRDataKey : attributeValueToReport, MTRPreviousDataKey : previousValue }];
} else {
[attributesToReport addObject:@{ MTRAttributePathKey : attributePath, MTRDataKey : attributeValueToReport }];
}
[attributePathsToReport addObject:attributePath];
}
}
if (expectedValueID) {
*expectedValueID = expectedValueIDToReturn;
}
MTR_LOG("%@ report from new expected values %@", self, attributePathsToReport);
return attributesToReport;
}
// expectedValueID is an out-argument that returns an identifier to be used when removing expected values
- (void)setExpectedValues:(NSArray<NSDictionary<NSString *, id> *> *)values
expectedValueInterval:(NSNumber *)expectedValueInterval
expectedValueID:(uint64_t *)expectedValueID
{
// since NSTimeInterval is in seconds, convert ms into seconds in double
NSDate * expirationTime = [NSDate dateWithTimeIntervalSinceNow:expectedValueInterval.doubleValue / 1000];
MTR_LOG(
"%@ Setting expected values %@ with expiration time %f seconds from now", self, values, [expirationTime timeIntervalSinceNow]);
std::lock_guard lock(_lock);
// _getAttributesToReportWithNewExpectedValues will log attribute paths reported
NSArray * attributesToReport = [self _getAttributesToReportWithNewExpectedValues:values
expirationTime:expirationTime
expectedValueID:expectedValueID];
[self _reportAttributes:attributesToReport];
[self _checkExpiredExpectedValues];
}
- (void)removeExpectedValuesForAttributePaths:(NSArray<MTRAttributePath *> *)attributePaths
expectedValueID:(uint64_t)expectedValueID
{
std::lock_guard lock(_lock);
for (MTRAttributePath * attributePath in attributePaths) {
[self _removeExpectedValueForAttributePath:attributePath expectedValueID:expectedValueID];
}
}
- (void)_removeExpectedValueForAttributePath:(MTRAttributePath *)attributePath expectedValueID:(uint64_t)expectedValueID
{
os_unfair_lock_assert_owner(&self->_lock);
BOOL shouldReportValue;
NSDictionary<NSString *, id> * attributeValueToReport;
NSDictionary<NSString *, id> * previousValue;
[self _setExpectedValue:nil
attributePath:attributePath
expirationTime:nil
shouldReportValue:&shouldReportValue
attributeValueToReport:&attributeValueToReport
expectedValueID:expectedValueID
previousValue:&previousValue];
MTR_LOG("%@ remove expected value for path %@ should report %@", self, attributePath, shouldReportValue ? @"YES" : @"NO");
if (shouldReportValue) {
NSMutableDictionary * attribute = [NSMutableDictionary dictionaryWithObject:attributePath forKey:MTRAttributePathKey];
if (attributeValueToReport) {
attribute[MTRDataKey] = attributeValueToReport;
}
if (previousValue) {
attribute[MTRPreviousDataKey] = previousValue;
}
[self _reportAttributes:@[ attribute ]];
}
}
- (MTRBaseDevice *)newBaseDevice
{
return [MTRBaseDevice deviceWithNodeID:self.nodeID controller:self.deviceController];
}
// Client Metadata Storage
- (NSArray *)supportedClientDataClasses
{
return @[ [NSData class], [NSString class], [NSNumber class], [NSDictionary class], [NSArray class] ];
}
- (NSArray * _Nullable)clientDataKeys
{
return [self.temporaryMetaDataCache allKeys];
}
- (id<NSSecureCoding> _Nullable)clientDataForKey:(NSString *)key
{
if (key == nil)
return nil;
return [self.temporaryMetaDataCache objectForKey:[NSString stringWithFormat:@"%@:-1", key]];
}
- (void)setClientDataForKey:(NSString *)key value:(id<NSSecureCoding>)value
{
// TODO: Check supported data types, and also if they conform to NSSecureCoding, when we store these
// TODO: Need to add a delegate method, so when this value changes we call back to the client
if (key == nil || value == nil)
return;
if (self.temporaryMetaDataCache == nil) {
self.temporaryMetaDataCache = [NSMutableDictionary dictionary];
}
[self.temporaryMetaDataCache setObject:value forKey:[NSString stringWithFormat:@"%@:-1", key]];
}
- (void)removeClientDataForKey:(NSString *)key
{
if (key == nil)
return;
[self.temporaryMetaDataCache removeObjectForKey:[NSString stringWithFormat:@"%@:-1", key]];
}
- (NSArray * _Nullable)clientDataKeysForEndpointID:(NSNumber *)endpointID
{
if (endpointID == nil)
return nil;
// TODO: When hooked up to storage, enumerate this better
return [self.temporaryMetaDataCache allKeys];
}
- (id<NSSecureCoding> _Nullable)clientDataForKey:(NSString *)key endpointID:(NSNumber *)endpointID
{
if (key == nil || endpointID == nil)
return nil;
return [self.temporaryMetaDataCache objectForKey:[NSString stringWithFormat:@"%@:%@", key, endpointID]];
}
- (void)setClientDataForKey:(NSString *)key endpointID:(NSNumber *)endpointID value:(id<NSSecureCoding>)value
{
if (key == nil || value == nil || endpointID == nil)
return;
if (self.temporaryMetaDataCache == nil) {
self.temporaryMetaDataCache = [NSMutableDictionary dictionary];
}
[self.temporaryMetaDataCache setObject:value forKey:[NSString stringWithFormat:@"%@:%@", key, endpointID]];
}
- (void)removeClientDataForKey:(NSString *)key endpointID:(NSNumber *)endpointID
{
if (key == nil || endpointID == nil)
return;
[self.temporaryMetaDataCache removeObjectForKey:[NSString stringWithFormat:@"%@:%@", key, endpointID]];
}
#pragma mark Log Help
- (nullable NSNumber *)_informationalNumberAtAttributePath:(MTRAttributePath *)attributePath
{
auto * cachedData = [self _cachedAttributeValueForPath:attributePath];
if (cachedData == nil) {
return nil;
}
auto * attrReport = [[MTRAttributeReport alloc] initWithResponseValue:@{
MTRAttributePathKey : attributePath,
MTRDataKey : cachedData,
}
error:nil];
return attrReport.value;
}
- (nullable NSNumber *)_informationalVendorID
{
auto * vendorIDPath = [MTRAttributePath attributePathWithEndpointID:@(kRootEndpointId)
clusterID:@(MTRClusterIDTypeBasicInformationID)
attributeID:@(MTRClusterBasicAttributeVendorIDID)];
return [self _informationalNumberAtAttributePath:vendorIDPath];
}
- (nullable NSNumber *)_informationalProductID
{
auto * productIDPath = [MTRAttributePath attributePathWithEndpointID:@(kRootEndpointId)
clusterID:@(MTRClusterIDTypeBasicInformationID)
attributeID:@(MTRClusterBasicAttributeProductIDID)];
return [self _informationalNumberAtAttributePath:productIDPath];
}
#pragma mark - Description handling
- (void)_updateAttributeDependentDescriptionData
{
os_unfair_lock_assert_owner(&_lock);
NSNumber * _Nullable vid = [self _informationalVendorID];
NSNumber * _Nullable pid = [self _informationalProductID];
NSNumber * _Nullable networkFeatures = [self _networkFeatures];
std::lock_guard lock(_descriptionLock);
_vid = vid;
_pid = pid;
_allNetworkFeatures = networkFeatures;
}
- (NSArray<NSNumber *> *)_endpointList
{
os_unfair_lock_assert_owner(&_lock);
auto * partsListPath = [MTRAttributePath attributePathWithEndpointID:@(kRootEndpointId)
clusterID:@(MTRClusterIDTypeDescriptorID)
attributeID:@(MTRAttributeIDTypeClusterDescriptorAttributePartsListID)];
auto * partsList = [self _cachedAttributeValueForPath:partsListPath];
NSMutableArray<NSNumber *> * endpointsOnDevice = [self arrayOfNumbersFromAttributeValue:partsList];
if (!endpointsOnDevice) {
endpointsOnDevice = [[NSMutableArray<NSNumber *> alloc] init];
}
// Add Root node!
[endpointsOnDevice addObject:@(0)];
return endpointsOnDevice;
}
- (NSNumber * _Nullable)_networkFeatures
{
NSNumber * _Nullable result = nil;
auto * endpoints = [self _endpointList];
for (NSNumber * endpoint in endpoints) {
auto * featureMapPath = [MTRAttributePath attributePathWithEndpointID:endpoint
clusterID:@(MTRClusterIDTypeNetworkCommissioningID)
attributeID:@(MTRAttributeIDTypeGlobalAttributeFeatureMapID)];
auto * featureMap = [self _informationalNumberAtAttributePath:featureMapPath];
if (featureMap == nil) {
// No network commissioning cluster on this endpoint, or no known
// FeatureMap attribute value for it yet.
continue;
}
if (result == nil) {
result = featureMap;
} else {
result = @(featureMap.unsignedLongLongValue | result.unsignedLongLongValue);
}
}
return result;
}
@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