blob: 7fbff236a2d9cb04211445900e2fd18c3e42d086 [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/MTRDefines.h>
#import <os/lock.h>
#import "MTRAsyncCallbackWorkQueue_Internal.h"
#import "MTRAttributeSpecifiedCheck.h"
#import "MTRBaseDevice_Internal.h"
#import "MTRBaseSubscriptionCallback.h"
#import "MTRCluster.h"
#import "MTRClusterConstants.h"
#import "MTRDeviceController_Internal.h"
#import "MTRDevice_Internal.h"
#import "MTRError_Internal.h"
#import "MTREventTLVValueDecoder_Internal.h"
#import "MTRLogging_Internal.h"
#include "lib/core/CHIPError.h"
#include "lib/core/DataModelTypes.h"
#include <app/ConcreteAttributePath.h>
#include <app/AttributePathParams.h>
#include <app/BufferedReadCallback.h>
#include <app/ClusterStateCache.h>
#include <app/InteractionModelEngine.h>
#include <platform/PlatformManager.h>
typedef void (^MTRDeviceAttributeReportHandler)(NSArray * _Nonnull);
// Consider moving utility classes to their own file
#pragma mark - Utility Classes
// This class is for storing weak references in a container
MTR_HIDDEN
@interface MTRWeakReference<ObjectType> : NSObject
+ (instancetype)weakReferenceWithObject:(ObjectType)object;
- (instancetype)initWithObject:(ObjectType)object;
- (ObjectType)strongObject; // returns strong object or NULL
@end
@interface MTRWeakReference () {
@private
__weak id _object;
}
@end
@implementation MTRWeakReference
- (instancetype)initWithObject:(id)object
{
if (self = [super init]) {
_object = object;
}
return self;
}
+ (instancetype)weakReferenceWithObject:(id)object
{
return [[self alloc] initWithObject:object];
}
- (id)strongObject
{
return _object;
}
@end
NSNumber * MTRClampedNumber(NSNumber * aNumber, NSNumber * min, NSNumber * max)
{
if ([aNumber compare:min] == NSOrderedAscending) {
return min;
} else if ([aNumber compare:max] == NSOrderedDescending) {
return max;
}
return aNumber;
}
#pragma mark - SubscriptionCallback class declaration
using namespace chip;
using namespace chip::app;
using namespace chip::Protocols::InteractionModel;
typedef void (^FirstReportHandler)(void);
namespace {
class SubscriptionCallback final : public MTRBaseSubscriptionCallback {
public:
SubscriptionCallback(DataReportCallback attributeReportCallback, DataReportCallback eventReportCallback,
ErrorCallback errorCallback, MTRDeviceResubscriptionScheduledHandler resubscriptionCallback,
SubscriptionEstablishedHandler subscriptionEstablishedHandler, OnDoneHandler onDoneHandler,
UnsolicitedMessageFromPublisherHandler unsolicitedMessageFromPublisherHandler, ReportBeginHandler reportBeginHandler,
ReportEndHandler reportEndHandler)
: MTRBaseSubscriptionCallback(attributeReportCallback, eventReportCallback, errorCallback, resubscriptionCallback,
subscriptionEstablishedHandler, onDoneHandler, unsolicitedMessageFromPublisherHandler, reportBeginHandler,
reportEndHandler)
{
}
private:
void OnEventData(const EventHeader & aEventHeader, TLV::TLVReader * apData, const StatusIB * apStatus) override;
void OnAttributeData(const ConcreteDataAttributePath & aPath, TLV::TLVReader * apData, const StatusIB & aStatus) override;
};
} // anonymous namespace
#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, MTRDeviceWorkItemBatchingID) {
MTRDeviceWorkItemBatchingReadID = 1,
};
typedef NS_ENUM(NSUInteger, MTRDeviceWorkItemDuplicateTypeID) {
MTRDeviceWorkItemDuplicateReadTypeID = 1,
};
@interface MTRDevice ()
@property (nonatomic, readonly) os_unfair_lock lock; // protects the caches and device state
@property (nonatomic) chip::FabricIndex fabricIndex;
@property (nonatomic) MTRWeakReference<id<MTRDeviceDelegate>> * weakDelegate;
@property (nonatomic) dispatch_queue_t delegateQueue;
@property (nonatomic) NSArray<NSDictionary<NSString *, id> *> * unreportedEvents;
/**
* If subscriptionActive is true that means that either we are in the middle of
* trying to get a CASE session for the publisher or we have a live ReadClient
* right now (possibly with a lost subscription and trying to re-subscribe).
*/
@property (nonatomic) BOOL subscriptionActive;
#define MTRDEVICE_SUBSCRIPTION_ATTEMPT_MIN_WAIT_SECONDS (1)
#define MTRDEVICE_SUBSCRIPTION_ATTEMPT_MAX_WAIT_SECONDS (3600)
@property (nonatomic) uint32_t lastSubscriptionAttemptWait;
/**
* If reattemptingSubscription is true, that means that we have failed to get a
* CASE session for the publisher and are now waiting to try again. In this
* state we never have subscriptionActive true or a non-null currentReadClient.
*/
@property (nonatomic) BOOL reattemptingSubscription;
// Read cache is attributePath => NSDictionary of value.
// See MTRDeviceResponseHandler definition for value dictionary details.
@property (nonatomic) NSMutableDictionary<MTRAttributePath *, NSDictionary *> * readCache;
// 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) NSDate * estimatedStartTimeFromGeneralDiagnosticsUpTime;
/**
* If currentReadClient is non-null, that means that we successfully
* called SendAutoResubscribeRequest on the ReadClient and have not yet gotten
* an OnDone for that ReadClient.
*/
@property (nonatomic) ReadClient * currentReadClient;
@end
@implementation MTRDevice
- (instancetype)initWithNodeID:(NSNumber *)nodeID controller:(MTRDeviceController *)controller
{
if (self = [super init]) {
_lock = 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);
_readCache = [NSMutableDictionary dictionary];
_expectedValueCache = [NSMutableDictionary dictionary];
_asyncCallbackWorkQueue = [[MTRAsyncCallbackWorkQueue alloc] initWithContext:self queue:_queue];
_state = MTRDeviceStateUnknown;
MTR_LOG_INFO("%@ init with hex nodeID 0x%016llX", self, _nodeID.unsignedLongLongValue);
}
return self;
}
- (NSString *)description
{
return [NSString
stringWithFormat:@"<MTRDevice: %p>[fabric: %u, nodeID: 0x%016llX]", self, _fabricIndex, _nodeID.unsignedLongLongValue];
}
+ (MTRDevice *)deviceWithNodeID:(NSNumber *)nodeID controller:(MTRDeviceController *)controller
{
return [controller deviceForNodeID:nodeID];
}
#pragma mark Subscription and delegate handling
// subscription intervals are in seconds
#define MTR_DEVICE_SUBSCRIPTION_MAX_INTERVAL_MIN (2)
#define MTR_DEVICE_SUBSCRIPTION_MAX_INTERVAL_MAX (60)
- (void)setDelegate:(id<MTRDeviceDelegate>)delegate queue:(dispatch_queue_t)queue
{
MTR_LOG_INFO("%@ setDelegate %@", self, delegate);
os_unfair_lock_lock(&self->_lock);
_weakDelegate = [MTRWeakReference weakReferenceWithObject:delegate];
_delegateQueue = queue;
[self _setupSubscription];
os_unfair_lock_unlock(&self->_lock);
}
- (void)invalidate
{
MTR_LOG_INFO("%@ invalidate", self);
os_unfair_lock_lock(&self->_lock);
_weakDelegate = nil;
// Make sure we don't try to resubscribe if we have a pending resubscribe
// attempt, since we now have no delegate.
_reattemptingSubscription = NO;
os_unfair_lock_unlock(&self->_lock);
}
- (void)nodeMayBeAdvertisingOperational
{
MTR_LOG_DEFAULT("%@ saw new operational advertisement", self);
// We might want to trigger a resubscribe on our existing ReadClient. Do
// that outside the scope of our lock, so we're not calling arbitrary code
// we don't control with the lock held. This is safe, because when
// nodeMayBeAdvertisingOperational is called we are running on the Matter
// queue, and the ReadClient can't get destroyed while we are on that queue.
ReadClient * readClientToResubscribe = nullptr;
os_unfair_lock_lock(&self->_lock);
// Don't change state to MTRDeviceStateReachable, since the device might not
// in fact be reachable yet; we won't know until we have managed to
// establish a CASE session. And at that point, our subscription will
// trigger the state change as needed.
if (self.reattemptingSubscription) {
[self _reattemptSubscriptionNowIfNeeded];
} else {
readClientToResubscribe = self->_currentReadClient;
}
os_unfair_lock_unlock(&self->_lock);
if (readClientToResubscribe) {
readClientToResubscribe->TriggerResubscribeIfScheduled("operational advertisement seen");
}
}
// Return YES if there's a valid delegate AND subscription is expected to report value
- (BOOL)_subscriptionAbleToReport
{
return (_weakDelegate.strongObject) && (_state == MTRDeviceStateReachable);
}
// assume lock is held
- (void)_changeState:(MTRDeviceState)state
{
os_unfair_lock_assert_owner(&self->_lock);
MTRDeviceState lastState = _state;
_state = state;
if (lastState != state) {
if (state != MTRDeviceStateReachable) {
MTR_LOG_INFO("%@ State change %lu => %lu, set estimated start time to nil", self, lastState, state);
_estimatedStartTime = nil;
_estimatedStartTimeFromGeneralDiagnosticsUpTime = nil;
} else {
MTR_LOG_INFO("%@ State change %lu => %lu", self, lastState, state);
}
id<MTRDeviceDelegate> delegate = _weakDelegate.strongObject;
if (delegate) {
dispatch_async(_delegateQueue, ^{
[delegate device:self stateChanged:state];
});
}
}
}
- (void)_handleSubscriptionEstablished
{
os_unfair_lock_lock(&self->_lock);
// reset subscription attempt wait time when subscription succeeds
_lastSubscriptionAttemptWait = 0;
[self _changeState:MTRDeviceStateReachable];
os_unfair_lock_unlock(&self->_lock);
}
- (void)_handleSubscriptionError:(NSError *)error
{
os_unfair_lock_lock(&self->_lock);
_subscriptionActive = NO;
_unreportedEvents = nil;
[self _changeState:MTRDeviceStateUnreachable];
os_unfair_lock_unlock(&self->_lock);
}
- (void)_handleResubscriptionNeeded
{
os_unfair_lock_lock(&self->_lock);
[self _changeState:MTRDeviceStateUnknown];
os_unfair_lock_unlock(&self->_lock);
}
- (void)_handleSubscriptionReset
{
os_unfair_lock_lock(&self->_lock);
// if there is no delegate then also do not retry
id<MTRDeviceDelegate> delegate = _weakDelegate.strongObject;
if (!delegate) {
// NOTE: Do not log anythig here: we have been invalidated, and the
// Matter stack might already be torn down.
os_unfair_lock_unlock(&self->_lock);
return;
}
// don't schedule multiple retries
if (self.reattemptingSubscription) {
MTR_LOG_DEFAULT("%@ already reattempting subscription", self);
os_unfair_lock_unlock(&self->_lock);
return;
}
self.reattemptingSubscription = YES;
if (_lastSubscriptionAttemptWait < MTRDEVICE_SUBSCRIPTION_ATTEMPT_MIN_WAIT_SECONDS) {
_lastSubscriptionAttemptWait = MTRDEVICE_SUBSCRIPTION_ATTEMPT_MIN_WAIT_SECONDS;
} else {
_lastSubscriptionAttemptWait *= 2;
if (_lastSubscriptionAttemptWait > MTRDEVICE_SUBSCRIPTION_ATTEMPT_MAX_WAIT_SECONDS) {
_lastSubscriptionAttemptWait = MTRDEVICE_SUBSCRIPTION_ATTEMPT_MAX_WAIT_SECONDS;
}
}
MTR_LOG_DEFAULT("%@ scheduling to reattempt subscription in %u seconds", self, _lastSubscriptionAttemptWait);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_lastSubscriptionAttemptWait * NSEC_PER_SEC)), self.queue, ^{
os_unfair_lock_lock(&self->_lock);
[self _reattemptSubscriptionNowIfNeeded];
os_unfair_lock_unlock(&self->_lock);
});
os_unfair_lock_unlock(&self->_lock);
}
- (void)_reattemptSubscriptionNowIfNeeded
{
os_unfair_lock_assert_owner(&self->_lock);
if (!self.reattemptingSubscription) {
return;
}
MTR_LOG_DEFAULT("%@ reattempting subscription", self);
self.reattemptingSubscription = NO;
[self _setupSubscription];
}
- (void)_handleUnsolicitedMessageFromPublisher
{
os_unfair_lock_lock(&self->_lock);
[self _changeState:MTRDeviceStateReachable];
id<MTRDeviceDelegate> delegate = _weakDelegate.strongObject;
if (delegate && [delegate respondsToSelector:@selector(deviceBecameActive:)]) {
dispatch_async(_delegateQueue, ^{
[delegate deviceBecameActive:self];
});
}
// in case this is called during exponential back off of subscription
// reestablishment, this starts the attempt right away
// TODO: This doesn't really make sense. If we _don't_ have a live
// ReadClient how did we get this notification and if we _do_ have an active
// ReadClient, this call or _setupSubscription would be no-ops.
[self _reattemptSubscriptionNowIfNeeded];
os_unfair_lock_unlock(&self->_lock);
}
- (void)_handleReportBegin
{
os_unfair_lock_lock(&self->_lock);
[self _changeState:MTRDeviceStateReachable];
os_unfair_lock_unlock(&self->_lock);
}
- (void)_handleReportEnd
{
os_unfair_lock_lock(&self->_lock);
_estimatedStartTimeFromGeneralDiagnosticsUpTime = nil;
os_unfair_lock_unlock(&self->_lock);
}
// assume lock is held
- (void)_reportAttributes:(NSArray<NSDictionary<NSString *, id> *> *)attributes
{
os_unfair_lock_assert_owner(&self->_lock);
if (attributes.count) {
id<MTRDeviceDelegate> delegate = _weakDelegate.strongObject;
if (delegate) {
dispatch_async(_delegateQueue, ^{
[delegate device:self receivedAttributeReport:attributes];
});
}
}
}
- (void)_handleAttributeReport:(NSArray<NSDictionary<NSString *, id> *> *)attributeReport
{
os_unfair_lock_lock(&self->_lock);
// _getAttributesToReportWithReportedValues will log attribute paths reported
[self _reportAttributes:[self _getAttributesToReportWithReportedValues:attributeReport]];
os_unfair_lock_unlock(&self->_lock);
}
- (void)_handleEventReport:(NSArray<NSDictionary<NSString *, id> *> *)eventReport
{
os_unfair_lock_lock(&self->_lock);
NSDate * oldEstimatedStartTime = _estimatedStartTime;
for (NSDictionary<NSString *, id> * eventDict in eventReport) {
// Whenever a StartUp event is received, reset the estimated start time
// New subscription case
// - Starts Unreachable
// - Start CASE and send subscription request
// - Receive priming report ReportBegin
// - Optionally receive UpTime attribute - update time and save start time estimate
// - Optionally receive StartUp event
// - Set estimated system time from event receipt time, or saved UpTime estimate if exists
// - ReportEnd handler clears the saved start time estimate based on UpTime
// Subscription dropped from client point of view case
// - Starts Unreachable
// - Resubscribe happens after some time, and then same as the above
// Server resuming subscription after reboot case
// - Starts Reachable
// - Receive priming report ReportBegin
// - Optionally receive UpTime attribute - update time and save value
// - Optionally receive StartUp event
// - Set estimated system time from event receipt time, or saved UpTime estimate if exists
// - ReportEnd handler clears the saved start time estimate based on UpTime
// Server resuming subscription after timeout case
// - Starts Reachable
// - Receive priming report ReportBegin
// - Optionally receive UpTime attribute - update time and save value
// - ReportEnd handler clears the saved start time estimate based on UpTime
MTREventPath * eventPath = eventDict[MTREventPathKey];
BOOL isStartUpEvent = (eventPath.cluster.unsignedLongValue == MTRClusterIDTypeBasicInformationID)
&& (eventPath.event.unsignedLongValue == MTREventIDTypeClusterBasicInformationEventStartUpID);
if (isStartUpEvent) {
if (_estimatedStartTimeFromGeneralDiagnosticsUpTime) {
// If UpTime was received, make use of it as mark of system start time
MTR_LOG_INFO("%@ StartUp event: set estimated start time forward to %@", self,
_estimatedStartTimeFromGeneralDiagnosticsUpTime);
_estimatedStartTime = _estimatedStartTimeFromGeneralDiagnosticsUpTime;
} else {
// If UpTime was not received, reset estimated start time in case of reboot
MTR_LOG_INFO("%@ StartUp event: set estimated start time to nil", self);
_estimatedStartTime = nil;
}
}
// If event time is of MTREventTimeTypeSystemUpTime type, then update estimated start time as needed
NSNumber * eventTimeTypeNumber = eventDict[MTREventTimeTypeKey];
if (!eventTimeTypeNumber) {
MTR_LOG_ERROR("Event %@ missing event time type", eventDict);
continue;
}
MTREventTimeType eventTimeType = (MTREventTimeType) eventTimeTypeNumber.unsignedIntegerValue;
if (eventTimeType == MTREventTimeTypeSystemUpTime) {
NSNumber * eventTimeValueNumber = eventDict[MTREventSystemUpTimeKey];
if (!eventTimeValueNumber) {
MTR_LOG_ERROR("Event %@ missing event time value", eventDict);
continue;
}
NSTimeInterval eventTimeValue = eventTimeValueNumber.doubleValue;
NSDate * potentialSystemStartTime = [NSDate dateWithTimeIntervalSinceNow:-eventTimeValue];
if (!_estimatedStartTime || ([potentialSystemStartTime compare:_estimatedStartTime] == NSOrderedAscending)) {
_estimatedStartTime = potentialSystemStartTime;
}
}
}
if (oldEstimatedStartTime != _estimatedStartTime) {
MTR_LOG_DEFAULT("%@ updated estimated start time to %@", self, _estimatedStartTime);
}
// Combine with previous unreported events, if they exist
if (_unreportedEvents) {
eventReport = [_unreportedEvents arrayByAddingObjectsFromArray:eventReport];
_unreportedEvents = nil;
}
id<MTRDeviceDelegate> delegate = _weakDelegate.strongObject;
if (delegate) {
dispatch_async(_delegateQueue, ^{
[delegate device:self receivedEventReport:eventReport];
});
} else {
// save unreported events
_unreportedEvents = eventReport;
}
os_unfair_lock_unlock(&self->_lock);
}
// assume lock is held
- (void)_setupSubscription
{
os_unfair_lock_assert_owner(&self->_lock);
// for now just subscribe once
if (_subscriptionActive) {
return;
}
_subscriptionActive = YES;
[_deviceController
getSessionForNode:_nodeID.unsignedLongLongValue
completion:^(chip::Messaging::ExchangeManager * _Nullable exchangeManager,
const chip::Optional<chip::SessionHandle> & session, NSError * _Nullable error) {
if (error != nil) {
MTR_LOG_ERROR("%@ getSessionForNode error %@", self, error);
dispatch_async(self.queue, ^{
[self _handleSubscriptionError:error];
[self _handleSubscriptionReset];
});
return;
}
// Wildcard endpoint, cluster, attribute, event.
auto attributePath = std::make_unique<AttributePathParams>();
auto eventPath = std::make_unique<EventPathParams>();
// We want to get event reports at the minInterval, not the maxInterval.
eventPath->mIsUrgentEvent = true;
ReadPrepareParams readParams(session.Value());
readParams.mMinIntervalFloorSeconds = 0;
// Select a max interval based on the device's claimed idle sleep interval.
auto idleSleepInterval = std::chrono::duration_cast<System::Clock::Seconds32>(
session.Value()->GetRemoteMRPConfig().mIdleRetransTimeout);
if (idleSleepInterval.count() < MTR_DEVICE_SUBSCRIPTION_MAX_INTERVAL_MIN) {
idleSleepInterval = System::Clock::Seconds32(MTR_DEVICE_SUBSCRIPTION_MAX_INTERVAL_MIN);
}
if (idleSleepInterval.count() > MTR_DEVICE_SUBSCRIPTION_MAX_INTERVAL_MAX) {
idleSleepInterval = System::Clock::Seconds32(MTR_DEVICE_SUBSCRIPTION_MAX_INTERVAL_MAX);
}
readParams.mMaxIntervalCeilingSeconds = static_cast<uint16_t>(idleSleepInterval.count());
readParams.mpAttributePathParamsList = attributePath.get();
readParams.mAttributePathParamsListSize = 1;
readParams.mpEventPathParamsList = eventPath.get();
readParams.mEventPathParamsListSize = 1;
readParams.mKeepSubscriptions = true;
readParams.mIsFabricFiltered = false;
attributePath.release();
eventPath.release();
auto callback = std::make_unique<SubscriptionCallback>(
^(NSArray * value) {
MTR_LOG_INFO("%@ got attribute report %@", self, value);
dispatch_async(self.queue, ^{
// OnAttributeData (after OnReportEnd)
[self _handleAttributeReport:value];
});
},
^(NSArray * value) {
MTR_LOG_INFO("%@ got event report %@", self, value);
dispatch_async(self.queue, ^{
// OnEventReport (after OnReportEnd)
[self _handleEventReport:value];
});
},
^(NSError * error) {
MTR_LOG_ERROR("%@ got subscription error %@", self, error);
dispatch_async(self.queue, ^{
// OnError
[self _handleSubscriptionError:error];
});
},
^(NSError * error, NSNumber * resubscriptionDelay) {
MTR_LOG_DEFAULT("%@ got resubscription error %@ delay %@", self, error, resubscriptionDelay);
dispatch_async(self.queue, ^{
// OnResubscriptionNeeded
[self _handleResubscriptionNeeded];
});
},
^(void) {
MTR_LOG_DEFAULT("%@ got subscription established", self);
dispatch_async(self.queue, ^{
// OnSubscriptionEstablished
[self _handleSubscriptionEstablished];
});
},
^(void) {
MTR_LOG_DEFAULT("%@ got subscription done", self);
// Drop our pointer to the ReadClient immediately, since
// it's about to be destroyed and we don't want to be
// holding a dangling pointer.
os_unfair_lock_lock(&self->_lock);
self->_currentReadClient = nullptr;
os_unfair_lock_unlock(&self->_lock);
dispatch_async(self.queue, ^{
// OnDone
[self _handleSubscriptionReset];
});
},
^(void) {
MTR_LOG_DEFAULT("%@ got unsolicited message from publisher", self);
dispatch_async(self.queue, ^{
// OnUnsolicitedMessageFromPublisher
[self _handleUnsolicitedMessageFromPublisher];
});
},
^(void) {
MTR_LOG_DEFAULT("%@ got report begin", self);
dispatch_async(self.queue, ^{
[self _handleReportBegin];
});
},
^(void) {
MTR_LOG_DEFAULT("%@ got report end", self);
dispatch_async(self.queue, ^{
[self _handleReportEnd];
});
});
// Set up a cluster state cache. We just want this for the logic it has for
// tracking data versions and event numbers so we minimize the amount of data we
// request on resubscribes, so tell it not to store data.
auto clusterStateCache = std::make_unique<ClusterStateCache>(*callback.get(),
/* highestReceivedEventNumber = */ NullOptional,
/* cacheData = */ false);
auto readClient = std::make_unique<ReadClient>(InteractionModelEngine::GetInstance(), exchangeManager,
clusterStateCache->GetBufferedCallback(), ReadClient::InteractionType::Subscribe);
// SendAutoResubscribeRequest cleans up the params, even on failure.
CHIP_ERROR err = readClient->SendAutoResubscribeRequest(std::move(readParams));
if (err != CHIP_NO_ERROR) {
NSError * error = [MTRError errorForCHIPErrorCode:err];
MTR_LOG_ERROR("%@ SendAutoResubscribeRequest error %@", self, error);
dispatch_async(self.queue, ^{
[self _handleSubscriptionError:error];
});
return;
}
// Callback and ClusterStateCache and ReadClient will be deleted
// when OnDone is called.
os_unfair_lock_lock(&self->_lock);
self->_currentReadClient = readClient.get();
os_unfair_lock_unlock(&self->_lock);
callback->AdoptReadClient(std::move(readClient));
callback->AdoptClusterStateCache(std::move(clusterStateCache));
callback.release();
}];
}
#pragma mark Device Interactions
// Helper function to determine whether an attribute has "Changes Omitted" quality, which indicates that past the priming report in
// a subscription, this attribute is not expected to be reported when its value changes
// * TODO: xml+codegen version to replace this hardcoded list.
static BOOL AttributeHasChangesOmittedQuality(MTRAttributePath * attributePath)
{
switch (attributePath.cluster.unsignedLongValue) {
case MTRClusterEthernetNetworkDiagnosticsID:
switch (attributePath.attribute.unsignedLongValue) {
case MTRClusterEthernetNetworkDiagnosticsAttributePacketRxCountID:
case MTRClusterEthernetNetworkDiagnosticsAttributePacketTxCountID:
case MTRClusterEthernetNetworkDiagnosticsAttributeTxErrCountID:
case MTRClusterEthernetNetworkDiagnosticsAttributeCollisionCountID:
case MTRClusterEthernetNetworkDiagnosticsAttributeOverrunCountID:
case MTRClusterEthernetNetworkDiagnosticsAttributeCarrierDetectID:
case MTRClusterEthernetNetworkDiagnosticsAttributeTimeSinceResetID:
return YES;
default:
return NO;
}
case MTRClusterGeneralDiagnosticsID:
switch (attributePath.attribute.unsignedLongValue) {
case MTRClusterGeneralDiagnosticsAttributeUpTimeID:
case MTRClusterGeneralDiagnosticsAttributeTotalOperationalHoursID:
return YES;
default:
return NO;
}
case MTRClusterThreadNetworkDiagnosticsID:
switch (attributePath.attribute.unsignedLongValue) {
case MTRClusterThreadNetworkDiagnosticsAttributeOverrunCountID:
case MTRClusterThreadNetworkDiagnosticsAttributeDetachedRoleCountID:
case MTRClusterThreadNetworkDiagnosticsAttributeChildRoleCountID:
case MTRClusterThreadNetworkDiagnosticsAttributeRouterRoleCountID:
case MTRClusterThreadNetworkDiagnosticsAttributeLeaderRoleCountID:
case MTRClusterThreadNetworkDiagnosticsAttributeAttachAttemptCountID:
case MTRClusterThreadNetworkDiagnosticsAttributePartitionIdChangeCountID:
case MTRClusterThreadNetworkDiagnosticsAttributeBetterPartitionAttachAttemptCountID:
case MTRClusterThreadNetworkDiagnosticsAttributeParentChangeCountID:
case MTRClusterThreadNetworkDiagnosticsAttributeTxTotalCountID:
case MTRClusterThreadNetworkDiagnosticsAttributeTxUnicastCountID:
case MTRClusterThreadNetworkDiagnosticsAttributeTxBroadcastCountID:
case MTRClusterThreadNetworkDiagnosticsAttributeTxAckRequestedCountID:
case MTRClusterThreadNetworkDiagnosticsAttributeTxAckedCountID:
case MTRClusterThreadNetworkDiagnosticsAttributeTxNoAckRequestedCountID:
case MTRClusterThreadNetworkDiagnosticsAttributeTxDataCountID:
case MTRClusterThreadNetworkDiagnosticsAttributeTxDataPollCountID:
case MTRClusterThreadNetworkDiagnosticsAttributeTxBeaconCountID:
case MTRClusterThreadNetworkDiagnosticsAttributeTxBeaconRequestCountID:
case MTRClusterThreadNetworkDiagnosticsAttributeTxOtherCountID:
case MTRClusterThreadNetworkDiagnosticsAttributeTxRetryCountID:
case MTRClusterThreadNetworkDiagnosticsAttributeTxDirectMaxRetryExpiryCountID:
case MTRClusterThreadNetworkDiagnosticsAttributeTxIndirectMaxRetryExpiryCountID:
case MTRClusterThreadNetworkDiagnosticsAttributeTxErrCcaCountID:
case MTRClusterThreadNetworkDiagnosticsAttributeTxErrAbortCountID:
case MTRClusterThreadNetworkDiagnosticsAttributeTxErrBusyChannelCountID:
case MTRClusterThreadNetworkDiagnosticsAttributeRxTotalCountID:
case MTRClusterThreadNetworkDiagnosticsAttributeRxUnicastCountID:
case MTRClusterThreadNetworkDiagnosticsAttributeRxBroadcastCountID:
case MTRClusterThreadNetworkDiagnosticsAttributeRxDataCountID:
case MTRClusterThreadNetworkDiagnosticsAttributeRxDataPollCountID:
case MTRClusterThreadNetworkDiagnosticsAttributeRxBeaconCountID:
case MTRClusterThreadNetworkDiagnosticsAttributeRxBeaconRequestCountID:
case MTRClusterThreadNetworkDiagnosticsAttributeRxOtherCountID:
case MTRClusterThreadNetworkDiagnosticsAttributeRxAddressFilteredCountID:
case MTRClusterThreadNetworkDiagnosticsAttributeRxDestAddrFilteredCountID:
case MTRClusterThreadNetworkDiagnosticsAttributeRxDuplicatedCountID:
case MTRClusterThreadNetworkDiagnosticsAttributeRxErrNoFrameCountID:
case MTRClusterThreadNetworkDiagnosticsAttributeRxErrUnknownNeighborCountID:
case MTRClusterThreadNetworkDiagnosticsAttributeRxErrInvalidSrcAddrCountID:
case MTRClusterThreadNetworkDiagnosticsAttributeRxErrSecCountID:
case MTRClusterThreadNetworkDiagnosticsAttributeRxErrFcsCountID:
case MTRClusterThreadNetworkDiagnosticsAttributeRxErrOtherCountID:
return YES;
default:
return NO;
}
case MTRClusterWiFiNetworkDiagnosticsID:
switch (attributePath.attribute.unsignedLongValue) {
case MTRClusterWiFiNetworkDiagnosticsAttributeRssiID:
case MTRClusterWiFiNetworkDiagnosticsAttributeBeaconLostCountID:
case MTRClusterWiFiNetworkDiagnosticsAttributeBeaconRxCountID:
case MTRClusterWiFiNetworkDiagnosticsAttributePacketMulticastRxCountID:
case MTRClusterWiFiNetworkDiagnosticsAttributePacketMulticastTxCountID:
case MTRClusterWiFiNetworkDiagnosticsAttributePacketUnicastRxCountID:
case MTRClusterWiFiNetworkDiagnosticsAttributePacketUnicastTxCountID:
case MTRClusterWiFiNetworkDiagnosticsAttributeCurrentMaxRateID:
case MTRClusterWiFiNetworkDiagnosticsAttributeOverrunCountID:
return YES;
default:
return NO;
}
case MTRClusterOperationalCredentialsID:
switch (attributePath.attribute.unsignedLongValue) {
case MTRClusterOperationalCredentialsAttributeNOCsID:
case MTRClusterOperationalCredentialsAttributeTrustedRootCertificatesID:
return YES;
default:
return NO;
}
case MTRClusterPowerSourceID:
switch (attributePath.attribute.unsignedLongValue) {
case MTRClusterPowerSourceAttributeWiredAssessedInputVoltageID:
case MTRClusterPowerSourceAttributeWiredAssessedInputFrequencyID:
case MTRClusterPowerSourceAttributeWiredAssessedCurrentID:
case MTRClusterPowerSourceAttributeBatVoltageID:
case MTRClusterPowerSourceAttributeBatPercentRemainingID:
case MTRClusterPowerSourceAttributeBatTimeRemainingID:
case MTRClusterPowerSourceAttributeBatTimeToFullChargeID:
case MTRClusterPowerSourceAttributeBatChargingCurrentID:
return YES;
default:
return NO;
}
case MTRClusterTimeSynchronizationID:
switch (attributePath.attribute.unsignedLongValue) {
case MTRClusterTimeSynchronizationAttributeUTCTimeID:
case MTRClusterTimeSynchronizationAttributeLocalTimeID:
return YES;
default:
return NO;
}
default:
return NO;
}
}
- (NSDictionary<NSString *, id> *)readAttributeWithEndpointID:(NSNumber *)endpointID
clusterID:(NSNumber *)clusterID
attributeID:(NSNumber *)attributeID
params:(MTRReadParams *)params
{
NSString * logPrefix = [NSString stringWithFormat:@"%@ read %@ %@ %@", self, endpointID, clusterID, attributeID];
MTRAttributePath * attributePath = [MTRAttributePath attributePathWithEndpointID:endpointID
clusterID:clusterID
attributeID:attributeID];
BOOL attributeIsSpecified = MTRAttributeIsSpecified(clusterID.unsignedIntValue, attributeID.unsignedIntValue);
BOOL hasChangesOmittedQuality = AttributeHasChangesOmittedQuality(attributePath);
// Return current known / expected value right away
NSDictionary<NSString *, id> * attributeValueToReturn = [self _attributeValueDictionaryForAttributePath:attributePath];
// Send read request to device if any of the following are true:
// 1. The attribute is not in the specification (so we don't know whether hasChangesOmittedQuality can be trusted).
// 2. Subscription not in a state we can expect reports
// 3. There is subscription but attribute has Changes Omitted quality
// 4. Cache has no entry
// TODO: add option for BaseSubscriptionCallback to report during priming, to reduce when case 4 is hit
if (!attributeIsSpecified || ![self _subscriptionAbleToReport] || hasChangesOmittedQuality || !attributeValueToReturn) {
// Read requests container will be a mutable array of items, each being an array containing:
// [attribute request path, params]
// Batching handler should only coalesce when params are equal.
// For this single read API there's only 1 array item. Use NSNull to stand in for nil params for easy comparison.
MTRAttributeRequestPath * readRequestPath = [MTRAttributeRequestPath requestPathWithEndpointID:endpointID
clusterID:clusterID
attributeID:attributeID];
NSArray * readRequestData = @[ readRequestPath, params ?: [NSNull null] ];
// But first, check if a duplicate read request is already queued and return
if ([_asyncCallbackWorkQueue isDuplicateForTypeID:MTRDeviceWorkItemDuplicateReadTypeID workItemData:readRequestData]) {
return attributeValueToReturn;
}
NSMutableArray<NSArray *> * readRequests = [NSMutableArray arrayWithObject:readRequestData];
// Create work item, set ready handler to perform task, then enqueue the work
MTRAsyncCallbackQueueWorkItem * workItem = [[MTRAsyncCallbackQueueWorkItem alloc] initWithQueue:self.queue];
MTRAsyncCallbackBatchingHandler batchingHandler = ^(id opaqueDataCurrent, id opaqueDataNext, BOOL * fullyMerged) {
NSMutableArray<NSArray *> * readRequestsCurrent = opaqueDataCurrent;
NSMutableArray<NSArray *> * readRequestsNext = opaqueDataNext;
*fullyMerged = NO;
// Can only read up to 9 paths at a time, per spec
if (readRequestsCurrent.count >= 9) {
MTR_LOG_DEFAULT("%@ batching cannot add more", logPrefix);
return;
}
while (readRequestsNext.count) {
// if params don't match then they cannot be merged
if (![readRequestsNext[0][MTRDeviceReadRequestFieldParamsIndex]
isEqual:readRequestsCurrent[0][MTRDeviceReadRequestFieldParamsIndex]]) {
MTR_LOG_DEFAULT("%@ batching merged all possible items", logPrefix);
return;
}
// merge the next item's first request into the current item's list
[readRequestsCurrent addObject:readRequestsNext[0]];
MTR_LOG_INFO("%@ batching merging %@ => %lu total", logPrefix, readRequestsNext[0],
(unsigned long) readRequestsCurrent.count);
[readRequestsNext removeObjectAtIndex:0];
// Can only read up to 9 paths at a time, per spec
if (readRequestsCurrent.count == 9) {
MTR_LOG_DEFAULT("%@ batching to max paths allowed", logPrefix);
break;
}
}
if (readRequestsNext.count == 0) {
MTR_LOG_DEFAULT("%@ batching - fully merged next item", logPrefix);
*fullyMerged = YES;
}
};
MTRAsyncCallbackDuplicateCheckHandler duplicateCheckHandler = ^(id opaqueItemData, BOOL * isDuplicate, BOOL * stop) {
for (NSArray * readItem in readRequests) {
if ([readItem isEqual:opaqueItemData]) {
MTR_LOG_DEFAULT("%@ duplicate check found %@ - report duplicate", logPrefix, readItem);
*isDuplicate = YES;
*stop = YES;
return;
}
}
*stop = NO;
};
MTRAsyncCallbackReadyHandler readyHandler = ^(MTRDevice * device, NSUInteger retryCount) {
MTR_LOG_DEFAULT("%@ dequeueWorkItem %@", logPrefix, self->_asyncCallbackWorkQueue);
// Sanity check
if (readRequests.count == 0) {
MTR_LOG_ERROR("%@ dequeueWorkItem no read requests", logPrefix);
[workItem endWork];
return;
}
// Build the attribute paths from the read requests
NSMutableArray<MTRAttributeRequestPath *> * attributePaths = [NSMutableArray array];
for (NSArray * readItem in readRequests) {
// Sanity check
if (readItem.count < 2) {
MTR_LOG_ERROR("%@ dequeueWorkItem read item missing info %@", logPrefix, readItem);
[workItem endWork];
return;
}
[attributePaths addObject:readItem[MTRDeviceReadRequestFieldPathIndex]];
}
// If param is the NSNull stand-in, then just use nil
id readParamObject = readRequests[0][MTRDeviceReadRequestFieldParamsIndex];
MTRReadParams * readParams = (![readParamObject isEqual:[NSNull null]]) ? readParamObject : nil;
MTRBaseDevice * baseDevice = [self newBaseDevice];
[baseDevice
readAttributePaths:attributePaths
eventPaths:nil
params:readParams
queue:self.queue
completion:^(NSArray<NSDictionary<NSString *, id> *> * _Nullable values, NSError * _Nullable error) {
if (values) {
// Since the format is the same data-value dictionary, this looks like an
// attribute report
MTR_LOG_INFO("%@ completion values %@", logPrefix, values);
[self _handleAttributeReport:values];
}
// TODO: better retry logic
if (error && (retryCount < 2)) {
MTR_LOG_ERROR("%@ completion error %@ retryWork %lu", logPrefix, error, (unsigned long) retryCount);
[workItem retryWork];
} else {
MTR_LOG_DEFAULT("%@ completion error %@ endWork", logPrefix, error);
[workItem endWork];
}
}];
};
workItem.readyHandler = readyHandler;
[workItem setBatchingID:MTRDeviceWorkItemBatchingReadID data:readRequests handler:batchingHandler];
[workItem setDuplicateTypeID:MTRDeviceWorkItemDuplicateReadTypeID handler:duplicateCheckHandler];
MTR_LOG_DEFAULT("%@ enqueueWorkItem %@", logPrefix, _asyncCallbackWorkQueue);
[_asyncCallbackWorkQueue enqueueWorkItem:workItem];
}
return attributeValueToReturn;
}
- (void)writeAttributeWithEndpointID:(NSNumber *)endpointID
clusterID:(NSNumber *)clusterID
attributeID:(NSNumber *)attributeID
value:(id)value
expectedValueInterval:(NSNumber *)expectedValueInterval
timedWriteTimeout:(NSNumber * _Nullable)timeout
{
NSString * logPrefix = [NSString stringWithFormat:@"%@ write %@ %@ %@", self, endpointID, clusterID, attributeID];
if (timeout) {
timeout = MTRClampedNumber(timeout, @(1), @(UINT16_MAX));
}
expectedValueInterval = MTRClampedNumber(expectedValueInterval, @(1), @(UINT32_MAX));
MTRAttributePath * attributePath = [MTRAttributePath attributePathWithEndpointID:endpointID
clusterID:clusterID
attributeID:attributeID];
// Commit change into expected value cache
NSDictionary * newExpectedValueDictionary = @{ MTRAttributePathKey : attributePath, MTRDataKey : value };
uint64_t expectedValueID;
[self setExpectedValues:@[ newExpectedValueDictionary ]
expectedValueInterval:expectedValueInterval
expectedValueID:&expectedValueID];
MTRAsyncCallbackQueueWorkItem * workItem = [[MTRAsyncCallbackQueueWorkItem alloc] initWithQueue:self.queue];
// The write operation will install a duplicate check handler, to return NO for "isDuplicate". Since a write operation may
// change values, only read requests after this should be considered for duplicate requests.
MTRAsyncCallbackDuplicateCheckHandler duplicateCheckHandler = ^(id opaqueItemData, BOOL * isDuplicate, BOOL * stop) {
*isDuplicate = NO;
*stop = YES;
};
MTRAsyncCallbackReadyHandler readyHandler = ^(MTRDevice * device, NSUInteger retryCount) {
MTR_LOG_DEFAULT("%@ dequeueWorkItem %@", logPrefix, self->_asyncCallbackWorkQueue);
MTRBaseDevice * baseDevice = [self newBaseDevice];
[baseDevice
writeAttributeWithEndpointID:endpointID
clusterID:clusterID
attributeID:attributeID
value:value
timedWriteTimeout:timeout
queue:self.queue
completion:^(NSArray<NSDictionary<NSString *, id> *> * _Nullable values, NSError * _Nullable error) {
MTR_LOG_DEFAULT("%@ completion error %@ endWork", logPrefix, error);
[workItem endWork];
if (error) {
[self removeExpectedValueForAttributePath:attributePath expectedValueID:expectedValueID];
}
}];
};
workItem.readyHandler = readyHandler;
[workItem setDuplicateTypeID:MTRDeviceWorkItemDuplicateReadTypeID handler:duplicateCheckHandler];
MTR_LOG_DEFAULT("%@ enqueueWorkItem %@", logPrefix, _asyncCallbackWorkQueue);
[_asyncCallbackWorkQueue enqueueWorkItem:workItem];
}
- (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
{
NSString * logPrefix = [NSString stringWithFormat:@"%@ command %@ %@ %@", self, endpointID, clusterID, commandID];
if (timeout) {
timeout = MTRClampedNumber(timeout, @(1), @(UINT16_MAX));
}
if (!expectedValueInterval || ([expectedValueInterval compare:@(0)] == NSOrderedAscending)) {
expectedValues = nil;
} else {
expectedValueInterval = MTRClampedNumber(expectedValueInterval, @(1), @(UINT32_MAX));
}
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]];
}
}
MTRAsyncCallbackQueueWorkItem * workItem = [[MTRAsyncCallbackQueueWorkItem alloc] initWithQueue:self.queue];
// 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.
MTRAsyncCallbackDuplicateCheckHandler duplicateCheckHandler = ^(id opaqueItemData, BOOL * isDuplicate, BOOL * stop) {
*isDuplicate = NO;
*stop = YES;
};
MTRAsyncCallbackReadyHandler readyHandler = ^(MTRDevice * device, NSUInteger retryCount) {
MTR_LOG_DEFAULT("%@ dequeueWorkItem %@", logPrefix, self->_asyncCallbackWorkQueue);
MTRBaseDevice * baseDevice = [self newBaseDevice];
[baseDevice
invokeCommandWithEndpointID:endpointID
clusterID:clusterID
commandID:commandID
commandFields:commandFields
timedInvokeTimeout:timeout
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_INFO("%@ received response: %@ error: %@", logPrefix, values, error);
MTR_LOG_DEFAULT("%@ endWork", logPrefix);
dispatch_async(queue, ^{
completion(values, error);
});
[workItem endWork];
if (error && expectedValues) {
[self removeExpectedValuesForAttributePaths:attributePaths expectedValueID:expectedValueID];
}
}];
};
workItem.readyHandler = readyHandler;
[workItem setDuplicateTypeID:MTRDeviceWorkItemDuplicateReadTypeID handler:duplicateCheckHandler];
MTR_LOG_DEFAULT("%@ enqueueWorkItem %@", logPrefix, _asyncCallbackWorkQueue);
[_asyncCallbackWorkQueue enqueueWorkItem:workItem];
}
- (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];
}
#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 = _readCache[attributePath];
if (cachedAttributeDataValue
&& ![self _attributeDataValue:attributeDataValue isEqualToDataValue:cachedAttributeDataValue]) {
[attributesToReport addObject:@{ MTRAttributePathKey : attributePath, MTRDataKey : cachedAttributeDataValue }];
[attributePathsToReport addObject:attributePath];
}
_expectedValueCache[attributePath] = nil;
}
// log attribute paths
MTR_LOG_INFO("%@ 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;
}
MTRWeakReference<MTRDevice *> * weakSelf = [MTRWeakReference weakReferenceWithObject:self];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(waitTime * NSEC_PER_SEC)), self.queue, ^{
MTRDevice * strongSelf = weakSelf.strongObject;
[strongSelf _performScheduledExpirationCheck];
});
}
}
- (void)_performScheduledExpirationCheck
{
os_unfair_lock_lock(&self->_lock);
self.expirationCheckScheduled = NO;
[self _checkExpiredExpectedValues];
os_unfair_lock_unlock(&self->_lock);
}
// Get attribute value dictionary for an attribute path from the right cache
- (NSDictionary<NSString *, id> *)_attributeValueDictionaryForAttributePath:(MTRAttributePath *)attributePath
{
os_unfair_lock_lock(&self->_lock);
// First check expected value cache
NSArray * expectedValue = _expectedValueCache[attributePath];
if (expectedValue) {
NSDate * now = [NSDate date];
if ([now compare:expectedValue[MTRDeviceExpectedValueFieldExpirationTimeIndex]] == NSOrderedDescending) {
// expired - purge and fall through
_expectedValueCache[attributePath] = nil;
} else {
os_unfair_lock_unlock(&self->_lock);
// not yet expired - return result
return expectedValue[MTRDeviceExpectedValueFieldValueIndex];
}
}
// Then check read cache
NSDictionary<NSString *, id> * cachedAttributeValue = _readCache[attributePath];
if (cachedAttributeValue) {
os_unfair_lock_unlock(&self->_lock);
return cachedAttributeValue;
} else {
// TODO: when not found in cache, generated default values should be used
MTR_LOG_INFO("%@ _attributeValueDictionaryForAttributePath: could not find cached attribute values for attribute %@", self,
attributePath);
}
os_unfair_lock_unlock(&self->_lock);
return nil;
}
- (BOOL)_attributeDataValue:(NSDictionary *)one isEqualToDataValue:(NSDictionary *)theOther
{
// Attribute data-value dictionary should be all standard containers which
// means isEqual: comparisons all the way down, making a deep comparison.
return [one isEqualToDictionary:theOther];
}
// assume lock is held
- (NSArray *)_getAttributesToReportWithReportedValues:(NSArray<NSDictionary<NSString *, id> *> *)reportedAttributeValues
{
os_unfair_lock_assert_owner(&self->_lock);
NSMutableArray * attributesToReport = [NSMutableArray array];
NSMutableArray * attributePathsToReport = [NSMutableArray array];
for (NSDictionary<NSString *, id> * attributeReponseValue in reportedAttributeValues) {
MTRAttributePath * attributePath = attributeReponseValue[MTRAttributePathKey];
NSDictionary * attributeDataValue = attributeReponseValue[MTRDataKey];
NSError * attributeError = attributeReponseValue[MTRErrorKey];
// sanity check either data value or error must exist
if (!attributeDataValue && !attributeError) {
MTR_LOG_INFO("%@ report %@ no data value or error: %@", self, attributePath, attributeReponseValue);
continue;
}
// check if value is different than cache, and report if needed
BOOL shouldReportAttribute = NO;
// if this is an error, report and purge cache
if (attributeError) {
shouldReportAttribute = YES;
MTR_LOG_INFO("%@ report %@ error %@ purge expected value %@ read cache %@", self, attributePath, attributeError,
_expectedValueCache[attributePath], _readCache[attributePath]);
_expectedValueCache[attributePath] = nil;
_readCache[attributePath] = nil;
} else {
// if expected values exists, purge and update read cache
NSArray * expectedValue = _expectedValueCache[attributePath];
if (expectedValue) {
if (![self _attributeDataValue:attributeDataValue
isEqualToDataValue:expectedValue[MTRDeviceExpectedValueFieldValueIndex]]) {
shouldReportAttribute = YES;
}
_expectedValueCache[attributePath] = nil;
_readCache[attributePath] = attributeDataValue;
} else if (![self _attributeDataValue:attributeDataValue isEqualToDataValue:_readCache[attributePath]]) {
// otherwise compare and update read cache
_readCache[attributePath] = attributeDataValue;
shouldReportAttribute = YES;
}
if (!shouldReportAttribute) {
if (expectedValue) {
MTR_LOG_INFO("%@ report %@ value filtered - same as expected values", self, attributePath);
} else {
MTR_LOG_INFO("%@ report %@ value filtered - same values as cache", self, attributePath);
}
}
// If General Diagnostics UpTime attribute, update the estimated start time as needed.
if ((attributePath.cluster.unsignedLongValue == MTRClusterGeneralDiagnosticsID)
&& (attributePath.attribute.unsignedLongValue == MTRClusterGeneralDiagnosticsAttributeUpTimeID)) {
// verify that the uptime is indeed the data type we want
if ([attributeDataValue[MTRTypeKey] isEqual:MTRUnsignedIntegerValueType]) {
NSNumber * upTimeNumber = attributeDataValue[MTRValueKey];
NSTimeInterval upTime = upTimeNumber.unsignedLongLongValue; // UpTime unit is defined as seconds in the spec
NSDate * potentialSystemStartTime = [NSDate dateWithTimeIntervalSinceNow:-upTime];
NSDate * oldSystemStartTime = _estimatedStartTime;
if (!_estimatedStartTime || ([potentialSystemStartTime compare:_estimatedStartTime] == NSOrderedAscending)) {
MTR_LOG_INFO("%@ General Diagnostics UpTime %.3lf: estimated start time %@ => %@", self, upTime,
oldSystemStartTime, potentialSystemStartTime);
_estimatedStartTime = potentialSystemStartTime;
}
// Save estimate in the subscription resumption case, for when StartUp event uses it
_estimatedStartTimeFromGeneralDiagnosticsUpTime = potentialSystemStartTime;
}
}
}
if (shouldReportAttribute) {
[attributesToReport addObject:attributeReponseValue];
[attributePathsToReport addObject:attributePath];
}
}
MTR_LOG_INFO("%@ report from reported values %@", self, attributePathsToReport);
return attributesToReport;
}
// If value is non-nil, associate with expectedValueID
// If value is nil, remove only if expectedValueID matches
- (void)_setExpectedValue:(NSDictionary<NSString *, id> *)expectedAttributeValue
attributePath:(MTRAttributePath *)attributePath
expirationTime:(NSDate *)expirationTime
shouldReportValue:(BOOL *)shouldReportValue
attributeValueToReport:(NSDictionary<NSString *, id> **)attributeValueToReport
expectedValueID:(uint64_t)expectedValueID
{
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;
} else if (!expectedAttributeValue) {
// Remove previous expected value only if it's from the same setExpectedValues operation
NSNumber * previousExpectedValueID = previousExpectedValue[MTRDeviceExpectedValueFieldIDIndex];
if (previousExpectedValueID.unsignedLongLongValue == expectedValueID) {
if (![self _attributeDataValue:previousExpectedValue[MTRDeviceExpectedValueFieldValueIndex]
isEqualToDataValue:_readCache[attributePath]]) {
// Case of removing expected value that is different than read cache - report read cache value
*shouldReportValue = YES;
*attributeValueToReport = _readCache[attributePath];
_expectedValueCache[attributePath] = nil;
}
}
}
} else {
if (expectedAttributeValue
&& ![self _attributeDataValue:expectedAttributeValue isEqualToDataValue:_readCache[attributePath]]) {
// Case where new expected value is different than read cache - report new expected value
*shouldReportValue = YES;
*attributeValueToReport = expectedAttributeValue;
}
// 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> * attributeReponseValue in expectedAttributeValues) {
MTRAttributePath * attributePath = attributeReponseValue[MTRAttributePathKey];
NSDictionary * attributeDataValue = attributeReponseValue[MTRDataKey];
BOOL shouldReportValue = NO;
NSDictionary<NSString *, id> * attributeValueToReport;
[self _setExpectedValue:attributeDataValue
attributePath:attributePath
expirationTime:expirationTime
shouldReportValue:&shouldReportValue
attributeValueToReport:&attributeValueToReport
expectedValueID:expectedValueIDToReturn];
if (shouldReportValue) {
[attributesToReport addObject:@{ MTRAttributePathKey : attributePath, MTRDataKey : attributeValueToReport }];
[attributePathsToReport addObject:attributePath];
}
}
if (expectedValueID) {
*expectedValueID = expectedValueIDToReturn;
}
MTR_LOG_INFO("%@ report from new expected values %@", self, attributePathsToReport);
return attributesToReport;
}
- (void)setExpectedValues:(NSArray<NSDictionary<NSString *, id> *> *)values expectedValueInterval:(NSNumber *)expectedValueInterval
{
[self setExpectedValues:values expectedValueInterval:expectedValueInterval expectedValueID:nil];
}
// 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_INFO(
"Setting expected values %@ with expiration time %f seconds from now", values, [expirationTime timeIntervalSinceNow]);
os_unfair_lock_lock(&self->_lock);
// _getAttributesToReportWithNewExpectedValues will log attribute paths reported
NSArray * attributesToReport = [self _getAttributesToReportWithNewExpectedValues:values
expirationTime:expirationTime
expectedValueID:expectedValueID];
[self _reportAttributes:attributesToReport];
[self _checkExpiredExpectedValues];
os_unfair_lock_unlock(&self->_lock);
}
- (void)removeExpectedValuesForAttributePaths:(NSArray<MTRAttributePath *> *)attributePaths
expectedValueID:(uint64_t)expectedValueID
{
os_unfair_lock_lock(&self->_lock);
for (MTRAttributePath * attributePath in attributePaths) {
[self _removeExpectedValueForAttributePath:attributePath expectedValueID:expectedValueID];
}
os_unfair_lock_unlock(&self->_lock);
}
- (void)removeExpectedValueForAttributePath:(MTRAttributePath *)attributePath expectedValueID:(uint64_t)expectedValueID
{
os_unfair_lock_lock(&self->_lock);
[self _removeExpectedValueForAttributePath:attributePath expectedValueID:expectedValueID];
os_unfair_lock_unlock(&self->_lock);
}
- (void)_removeExpectedValueForAttributePath:(MTRAttributePath *)attributePath expectedValueID:(uint64_t)expectedValueID
{
os_unfair_lock_assert_owner(&self->_lock);
BOOL shouldReportValue;
NSDictionary<NSString *, id> * attributeValueToReport;
[self _setExpectedValue:nil
attributePath:attributePath
expirationTime:nil
shouldReportValue:&shouldReportValue
attributeValueToReport:&attributeValueToReport
expectedValueID:expectedValueID];
MTR_LOG_INFO("%@ remove expected value for path %@ should report %@", self, attributePath, shouldReportValue ? @"YES" : @"NO");
if (shouldReportValue) {
if (attributeValueToReport) {
[self _reportAttributes:@[ @{ MTRAttributePathKey : attributePath, MTRDataKey : attributeValueToReport } ]];
} else {
[self _reportAttributes:@[ @{ MTRAttributePathKey : attributePath } ]];
}
}
}
- (MTRBaseDevice *)newBaseDevice
{
return [[MTRBaseDevice alloc] initWithNodeID:self.nodeID controller:self.deviceController];
}
@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
#pragma mark - SubscriptionCallback
namespace {
void SubscriptionCallback::OnEventData(const EventHeader & aEventHeader, TLV::TLVReader * apData, const StatusIB * apStatus)
{
if (mEventReports == nil) {
// Never got a OnReportBegin? Not much to do other than tear things down.
ReportError(CHIP_ERROR_INCORRECT_STATE);
return;
}
MTREventPath * eventPath = [[MTREventPath alloc] initWithPath:aEventHeader.mPath];
if (apStatus != nullptr) {
[mEventReports addObject:@ { MTREventPathKey : eventPath, MTRErrorKey : [MTRError errorForIMStatus:*apStatus] }];
} else if (apData == nullptr) {
[mEventReports addObject:@ {
MTREventPathKey : eventPath,
MTRErrorKey : [MTRError errorForCHIPErrorCode:CHIP_ERROR_INVALID_ARGUMENT]
}];
} else {
id value = MTRDecodeDataValueDictionaryFromCHIPTLV(apData);
if (value == nil) {
MTR_LOG_ERROR("Failed to decode event data for path %@", eventPath);
[mEventReports addObject:@ {
MTREventPathKey : eventPath,
MTRErrorKey : [MTRError errorForCHIPErrorCode:CHIP_ERROR_DECODE_FAILED],
}];
} else {
[mEventReports addObject:[MTRBaseDevice eventReportForHeader:aEventHeader andData:value]];
}
}
}
void SubscriptionCallback::OnAttributeData(
const ConcreteDataAttributePath & aPath, TLV::TLVReader * apData, const StatusIB & aStatus)
{
if (aPath.IsListItemOperation()) {
ReportError(CHIP_ERROR_INCORRECT_STATE);
return;
}
if (mAttributeReports == nil) {
// Never got a OnReportBegin? Not much to do other than tear things down.
ReportError(CHIP_ERROR_INCORRECT_STATE);
return;
}
MTRAttributePath * attributePath = [[MTRAttributePath alloc] initWithPath:aPath];
if (aStatus.mStatus != Status::Success) {
[mAttributeReports addObject:@ { MTRAttributePathKey : attributePath, MTRErrorKey : [MTRError errorForIMStatus:aStatus] }];
} else if (apData == nullptr) {
[mAttributeReports addObject:@ {
MTRAttributePathKey : attributePath,
MTRErrorKey : [MTRError errorForCHIPErrorCode:CHIP_ERROR_INVALID_ARGUMENT]
}];
} else {
id value = MTRDecodeDataValueDictionaryFromCHIPTLV(apData);
if (value == nil) {
MTR_LOG_ERROR("Failed to decode attribute data for path %@", attributePath);
[mAttributeReports addObject:@ {
MTRAttributePathKey : attributePath,
MTRErrorKey : [MTRError errorForCHIPErrorCode:CHIP_ERROR_DECODE_FAILED],
}];
} else {
[mAttributeReports addObject:@ { MTRAttributePathKey : attributePath, MTRDataKey : value }];
}
}
}
} // anonymous namespace