Implement time sync for darwin (#32185)
* Implement time sync for darwin
* Restyled by whitespace
* Restyled by clang-format
* Fixing format string
* Restyled by clang-format
---------
Co-authored-by: Restyled.io <commits@restyled.io>
diff --git a/src/darwin/Framework/CHIP/MTRConversion.h b/src/darwin/Framework/CHIP/MTRConversion.h
index 543d809..b14f4c2 100644
--- a/src/darwin/Framework/CHIP/MTRConversion.h
+++ b/src/darwin/Framework/CHIP/MTRConversion.h
@@ -47,6 +47,18 @@
bool DateToMatterEpochSeconds(NSDate * date, uint32_t & epoch);
/**
+ * Returns whether the conversion could be performed. Will return false if the
+ * passed-in date is our of the range representable as a Matter epoch-s value.
+ */
+bool DateToMatterEpochMilliseconds(NSDate * date, uint64_t & matterEpochMilliseconds);
+
+/**
+ * Returns whether the conversion could be performed. Will return false if the
+ * passed-in date is our of the range representable as a Matter epoch-s value.
+ */
+bool DateToMatterEpochMicroseconds(NSDate * date, uint64_t & matterEpochMicroseconds);
+
+/**
* Utilities for converting between NSSet<NSNumber *> and chip::CATValues.
*/
CHIP_ERROR SetToCATValues(NSSet<NSNumber *> * catSet, chip::CATValues & values);
diff --git a/src/darwin/Framework/CHIP/MTRConversion.mm b/src/darwin/Framework/CHIP/MTRConversion.mm
index c6ba6ec..5e657b9 100644
--- a/src/darwin/Framework/CHIP/MTRConversion.mm
+++ b/src/darwin/Framework/CHIP/MTRConversion.mm
@@ -63,18 +63,41 @@
bool DateToMatterEpochSeconds(NSDate * date, uint32_t & matterEpochSeconds)
{
- auto timeSinceUnixEpoch = date.timeIntervalSince1970;
- if (timeSinceUnixEpoch < static_cast<NSTimeInterval>(chip::kChipEpochSecondsSinceUnixEpoch)) {
- // This is a pre-Matter-epoch time, and cannot be represented in epoch-s.
+ uint64_t matterEpochMicroseconds = 0;
+ if (!DateToMatterEpochMicroseconds(date, matterEpochMicroseconds)) {
+ // Could not convert time
return false;
}
- auto timeSinceMatterEpoch = timeSinceUnixEpoch - chip::kChipEpochSecondsSinceUnixEpoch;
+ uint64_t timeSinceMatterEpoch = matterEpochMicroseconds / chip::kMicrosecondsPerSecond;
if (timeSinceMatterEpoch > UINT32_MAX) {
// Too far into the future.
return false;
}
-
matterEpochSeconds = static_cast<uint32_t>(timeSinceMatterEpoch);
return true;
}
+
+bool DateToMatterEpochMilliseconds(NSDate * date, uint64_t & matterEpochMilliseconds)
+{
+ uint64_t matterEpochMicroseconds = 0;
+ if (!DateToMatterEpochMicroseconds(date, matterEpochMicroseconds)) {
+ // Could not convert time
+ return false;
+ }
+
+ matterEpochMilliseconds = matterEpochMicroseconds / chip::kMicrosecondsPerMillisecond;
+ return true;
+}
+
+bool DateToMatterEpochMicroseconds(NSDate * date, uint64_t & matterEpochMicroseconds)
+{
+ uint64_t timeSinceUnixEpoch = static_cast<uint64_t>(date.timeIntervalSince1970 * chip::kMicrosecondsPerSecond);
+ if (timeSinceUnixEpoch < chip::kChipEpochUsSinceUnixEpoch) {
+ // This is a pre-Matter-epoch time, and cannot be represented as an epoch time value.
+ return false;
+ }
+
+ matterEpochMicroseconds = timeSinceUnixEpoch - chip::kChipEpochUsSinceUnixEpoch;
+ return true;
+}
diff --git a/src/darwin/Framework/CHIP/MTRDevice.mm b/src/darwin/Framework/CHIP/MTRDevice.mm
index 2307a25..1b42cae 100644
--- a/src/darwin/Framework/CHIP/MTRDevice.mm
+++ b/src/darwin/Framework/CHIP/MTRDevice.mm
@@ -20,11 +20,13 @@
#import "MTRAsyncWorkQueue.h"
#import "MTRAttributeSpecifiedCheck.h"
+#import "MTRBaseClusters.h"
#import "MTRBaseDevice_Internal.h"
#import "MTRBaseSubscriptionCallback.h"
#import "MTRCluster.h"
#import "MTRClusterConstants.h"
#import "MTRCommandTimedCheck.h"
+#import "MTRConversion.h"
#import "MTRDefines_Internal.h"
#import "MTRDeviceController_Internal.h"
#import "MTRDevice_Internal.h"
@@ -143,6 +145,9 @@
@interface MTRDevice ()
@property (nonatomic, readonly) os_unfair_lock lock; // protects the caches and device state
+// protects against concurrent time updates by guarding timeUpdateScheduled flag which manages time updates scheduling,
+// and protects device calls to setUTCTime and setDSTOffset
+@property (nonatomic, readonly) os_unfair_lock timeSyncLock;
@property (nonatomic) chip::FabricIndex fabricIndex;
@property (nonatomic) MTRWeakReference<id<MTRDeviceDelegate>> * weakDelegate;
@property (nonatomic) dispatch_queue_t delegateQueue;
@@ -190,6 +195,8 @@
@property (nonatomic) BOOL expirationCheckScheduled;
+@property (nonatomic) BOOL timeUpdateScheduled;
+
@property (nonatomic) NSDate * estimatedStartTimeFromGeneralDiagnosticsUpTime;
@property (nonatomic) NSMutableDictionary * temporaryMetaDataCache;
@@ -224,6 +231,7 @@
{
if (self = [super init]) {
_lock = OS_UNFAIR_LOCK_INIT;
+ _timeSyncLock = OS_UNFAIR_LOCK_INIT;
_nodeID = [nodeID copy];
_fabricIndex = controller.fabricIndex;
_deviceController = controller;
@@ -249,6 +257,226 @@
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;
+ }
+
+ NSTimeZone * localTimeZone = [NSTimeZone localTimeZone];
+ BOOL setDST = TRUE;
+ if (!localTimeZone) {
+ MTR_LOG_ERROR("%@ Could not retrieve local time zone. Unable to setDSTOffset on endpoints.", self);
+ setDST = FALSE;
+ }
+
+ uint64_t matterEpochTimeMicroseconds = 0;
+ uint64_t nextDSTInMatterEpochTimeMicroseconds = 0;
+ if (!DateToMatterEpochMicroseconds(now, matterEpochTimeMicroseconds)) {
+ MTR_LOG_ERROR("%@ Could not convert NSDate (%@) to Matter Epoch Time. Unable to setUTCTime on endpoints.", self, now);
+ return;
+ }
+
+ int32_t dstOffset = 0;
+ if (setDST) {
+ NSTimeInterval dstOffsetAsInterval = [localTimeZone daylightSavingTimeOffsetForDate:now];
+ dstOffset = int32_t(dstOffsetAsInterval);
+
+ // Calculate time to next DST. This is needed when we set the current DST.
+ NSDate * nextDSTTransitionDate = [localTimeZone nextDaylightSavingTimeTransition];
+ if (!DateToMatterEpochMicroseconds(nextDSTTransitionDate, nextDSTInMatterEpochTimeMicroseconds)) {
+ MTR_LOG_ERROR("%@ Could not convert NSDate (%@) to Matter Epoch Time. Unable to setDSTOffset on endpoints.", self, nextDSTTransitionDate);
+ setDST = FALSE;
+ }
+ }
+
+ // 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];
+ if (setDST) {
+ [self _setDSTOffset:dstOffset validStarting:0 validUntil:nextDSTInMatterEpochTimeMicroseconds forEndpoint:endpoint];
+ }
+ }
+}
+
+- (void)_scheduleNextUpdate:(UInt64)nextUpdateInSeconds
+{
+ MTRWeakReference<MTRDevice *> * weakSelf = [MTRWeakReference weakReferenceWithObject: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);
+ MTRDevice * strongSelf = weakSelf.strongObject;
+ if (strongSelf) {
+ [strongSelf _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
+{
+ os_unfair_lock_lock(&self->_timeSyncLock);
+ // Device needs to still be reachable
+ if (self.state != MTRDeviceStateReachable) {
+ MTR_LOG_DEBUG("%@ Device is not reachable, canceling Device Time Updates.", self);
+ os_unfair_lock_unlock(&self->_timeSyncLock);
+ 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);
+ os_unfair_lock_unlock(&self->_timeSyncLock);
+ return;
+ }
+ self.timeUpdateScheduled = NO;
+ [self _updateDeviceTimeAndScheduleNextUpdate];
+ os_unfair_lock_unlock(&self->_timeSyncLock);
+}
+
+- (NSArray<NSNumber *> *)_endpointsWithTimeSyncClusterServer
+{
+ auto partsList = [self readAttributeWithEndpointID:@(0) clusterID:@(MTRClusterIDTypeDescriptorID) attributeID:@(MTRAttributeIDTypeClusterDescriptorAttributePartsListID) params:nil];
+ NSMutableArray<NSNumber *> * endpointsOnDevice = [self arrayOfNumbersFromAttributeValue:partsList];
+ if (!endpointsOnDevice) {
+ endpointsOnDevice = [[NSMutableArray<NSNumber *> alloc] init];
+ }
+ // Add Root node!
+ [endpointsOnDevice addObject:@(0)];
+
+ 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)_setDSTOffset:(int32_t)dstOffset validStarting:(uint64_t)validStarting validUntil:(uint64_t)validUntil forEndpoint:(NSNumber *)endpoint
+{
+ MTR_LOG_DEBUG("%@ _setDSTOffset with offset: %d, validStarting: %llu, validUntil: %llu, endpoint %@",
+ self,
+ dstOffset, validStarting, validUntil, endpoint);
+
+ MTRTimeSynchronizationClusterSetDSTOffsetParams * params = [[MTRTimeSynchronizationClusterSetDSTOffsetParams
+ alloc] init];
+ MTRTimeSynchronizationClusterDSTOffsetStruct * dstOffsetStruct = [[MTRTimeSynchronizationClusterDSTOffsetStruct alloc] init];
+ dstOffsetStruct.offset = @(dstOffset);
+ dstOffsetStruct.validStarting = @(validStarting);
+ dstOffsetStruct.validUntil = @(validUntil);
+ params.dstOffset = @[ dstOffsetStruct ];
+
+ auto setDSTOffsetResponseHandler = ^(id _Nullable response, NSError * _Nullable error) {
+ if (error) {
+ MTR_LOG_ERROR("%@ _setDSTOffset 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:(NSDictionary *)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 Subscription and delegate handling
// subscription intervals are in seconds
@@ -287,6 +515,10 @@
[_asyncWorkQueue invalidate];
+ os_unfair_lock_lock(&self->_timeSyncLock);
+ _timeUpdateScheduled = NO;
+ os_unfair_lock_unlock(&self->_timeSyncLock);
+
os_unfair_lock_lock(&self->_lock);
_state = MTRDeviceStateUnknown;
@@ -370,6 +602,8 @@
}
}
+// First Time Sync happens 2 minutes after reachability (this can be changed in the future)
+#define MTR_DEVICE_TIME_UPDATE_INITIAL_WAIT_TIME_SEC (60 * 2)
- (void)_handleSubscriptionEstablished
{
os_unfair_lock_lock(&self->_lock);
@@ -380,6 +614,14 @@
[self _changeState:MTRDeviceStateReachable];
os_unfair_lock_unlock(&self->_lock);
+
+ os_unfair_lock_lock(&self->_timeSyncLock);
+
+ if (!self.timeUpdateScheduled) {
+ [self _scheduleNextUpdate:MTR_DEVICE_TIME_UPDATE_INITIAL_WAIT_TIME_SEC];
+ }
+
+ os_unfair_lock_unlock(&self->_timeSyncLock);
}
- (void)_handleSubscriptionError:(NSError *)error
@@ -893,7 +1135,7 @@
return;
}
- MTR_LOG_DEFAULT("%@ Subscribe with data version list size %lu, reduced by %lu", self, dataVersions.count, dataVersionFilterListSizeReduction);
+ MTR_LOG_DEFAULT("%@ Subscribe with data version list size %lu, reduced by %lu", self, (unsigned long) dataVersions.count, (unsigned long) dataVersionFilterListSizeReduction);
// Callback and ClusterStateCache and ReadClient will be deleted
// when OnDone is called.