diff --git a/src/darwin/Framework/CHIP/MTRDevice.mm b/src/darwin/Framework/CHIP/MTRDevice.mm index 134042f974dd54..65c36c16e2760d 100644 --- a/src/darwin/Framework/CHIP/MTRDevice.mm +++ b/src/darwin/Framework/CHIP/MTRDevice.mm @@ -111,32 +111,6 @@ - (void)_deviceInternalStateChanged:(MTRDevice *)device; #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 * _attributes; @@ -238,20 +212,6 @@ - (BOOL)isEqual:(id)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 @@ -267,21 +227,6 @@ @interface MTRDevice () @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 * 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; @@ -379,7 +324,6 @@ - (instancetype)initWithNodeID:(NSNumber *)nodeID controller:(MTRDeviceControlle _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; @@ -1145,77 +1089,6 @@ - (void)setStorageBehaviorConfiguration:(MTRDeviceStorageBehaviorConfiguration * [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 *> *)_filteredAttributes:(NSArray *> *)attributes forInterestedPaths:(NSArray * _Nullable)interestedPaths -{ - if (!interestedPaths) { - return attributes; - } - - if (!interestedPaths.count) { - return nil; - } - - NSMutableArray * filteredAttributes = nil; - for (NSDictionary * 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(attributes.count), static_cast(filteredAttributes.count)); - } - - return filteredAttributes; -} - -// assume lock is held -- (void)_reportAttributes:(NSArray *> *)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 *> * filteredAttributes = [self _filteredAttributes:attributes forInterestedPaths:delegateInfo.interestedPathsForAttributes]; - if (filteredAttributes.count) { - [delegateInfo callDelegateWithBlock:^(id delegate) { - [delegate device:self receivedAttributeReport:filteredAttributes]; - }]; - } - }]; - } -} - #ifdef DEBUG - (void)unitTestInjectEventReport:(NSArray *> *)eventReport { @@ -1228,60 +1101,6 @@ - (void)unitTestInjectAttributeReport:(NSArray *> * } #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 *> *)_filteredEvents:(NSArray *> *)events forInterestedPaths:(NSArray * _Nullable)interestedPaths -{ - if (!interestedPaths) { - return events; - } - - if (!interestedPaths.count) { - return nil; - } - - NSMutableArray * filteredEvents = nil; - for (NSDictionary * 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(events.count), static_cast(filteredEvents.count)); - } - - return filteredEvents; -} - #ifdef DEBUG - (void)unitTestClearClusterData { @@ -1517,90 +1336,12 @@ - (void)_invokeCommandWithEndpointID:(NSNumber *)endpointID 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 * attributePaths = nil; - if (expectedValues) { - [self setExpectedValues:expectedValues expectedValueInterval:expectedValueInterval expectedValueID:&expectedValueID]; - attributePaths = [NSMutableArray array]; - for (NSDictionary * 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 *> * _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 *> * _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]; +#define MTRDeviceErrorStr "MTRDevice _invokeCommandWithEndpointID: must be handled by subclasses" + MTR_LOG_ERROR(MTRDeviceErrorStr); +#ifdef DEBUG + NSAssert(NO, @MTRDeviceErrorStr); +#endif // DEBUG +#undef MTRDeviceErrorStr } - (void)_invokeKnownCommandWithEndpointID:(NSNumber *)endpointID @@ -1692,93 +1433,6 @@ - (void)downloadLogOfType:(MTRDiagnosticLogType)type #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 * 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 { @@ -1887,169 +1541,6 @@ - (BOOL)deviceCachePrimed 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 *)expectedAttributeValue - attributePath:(MTRAttributePath *)attributePath - expirationTime:(NSDate *)expirationTime - shouldReportValue:(BOOL *)shouldReportValue - attributeValueToReport:(NSDictionary **)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 *> *)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 * attributeResponseValue in expectedAttributeValues) { - MTRAttributePath * attributePath = attributeResponseValue[MTRAttributePathKey]; - NSDictionary * attributeDataValue = attributeResponseValue[MTRDataKey]; - - BOOL shouldReportValue = NO; - NSDictionary * attributeValueToReport; - NSDictionary * 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 *> *)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 *)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 * attributeValueToReport; - NSDictionary * 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];