Skip to content

Commit 262d1f7

Browse files
Set more than one DST offset in MTRDevice if the server supports that.
This way when the next DST transition happens, the device won't be confused about the local time.
1 parent c90481f commit 262d1f7

File tree

7 files changed

+237
-61
lines changed

7 files changed

+237
-61
lines changed

src/darwin/Framework/CHIP/MTRDefines_Internal.h

+6
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@
3434
#define MTR_TESTABLE
3535
#endif
3636

37+
#ifdef DEBUG
38+
#define MTR_TESTABLE_FUNC MTR_EXTERN
39+
#else
40+
#define MTR_TESTABLE_FUNC
41+
#endif
42+
3743
// clang-format off
3844
/// Creates a weak shadow copy of the variable `local`
3945
#define mtr_weakify(local) \

src/darwin/Framework/CHIP/MTRDevice.mm

+38-33
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
#import "MTRError_Internal.h"
3434
#import "MTREventTLVValueDecoder_Internal.h"
3535
#import "MTRLogging_Internal.h"
36+
#import "MTRTimeUtils.h"
3637
#import "MTRUnfairLock.h"
3738
#import "zap-generated/MTRCommandPayloads_Internal.h"
3839

@@ -270,41 +271,50 @@ - (void)_setTimeOnDevice
270271
return;
271272
}
272273

273-
NSTimeZone * localTimeZone = [NSTimeZone localTimeZone];
274-
BOOL setDST = TRUE;
275-
if (!localTimeZone) {
276-
MTR_LOG_ERROR("%@ Could not retrieve local time zone. Unable to setDSTOffset on endpoints.", self);
277-
setDST = FALSE;
278-
}
279-
280274
uint64_t matterEpochTimeMicroseconds = 0;
281-
uint64_t nextDSTInMatterEpochTimeMicroseconds = 0;
282275
if (!DateToMatterEpochMicroseconds(now, matterEpochTimeMicroseconds)) {
283276
MTR_LOG_ERROR("%@ Could not convert NSDate (%@) to Matter Epoch Time. Unable to setUTCTime on endpoints.", self, now);
284277
return;
285278
}
286279

287-
int32_t dstOffset = 0;
288-
if (setDST) {
289-
NSTimeInterval dstOffsetAsInterval = [localTimeZone daylightSavingTimeOffsetForDate:now];
290-
dstOffset = int32_t(dstOffsetAsInterval);
291-
292-
// Calculate time to next DST. This is needed when we set the current DST.
293-
NSDate * nextDSTTransitionDate = [localTimeZone nextDaylightSavingTimeTransition];
294-
if (!DateToMatterEpochMicroseconds(nextDSTTransitionDate, nextDSTInMatterEpochTimeMicroseconds)) {
295-
MTR_LOG_ERROR("%@ Could not convert NSDate (%@) to Matter Epoch Time. Unable to setDSTOffset on endpoints.", self, nextDSTTransitionDate);
296-
setDST = FALSE;
297-
}
298-
}
299-
300280
// Set Time on each Endpoint with a Time Synchronization Cluster Server
301281
NSArray<NSNumber *> * endpointsToSync = [self _endpointsWithTimeSyncClusterServer];
302282
for (NSNumber * endpoint in endpointsToSync) {
303283
MTR_LOG_DEBUG("%@ Setting Time on Endpoint %@", self, endpoint);
304284
[self _setUTCTime:matterEpochTimeMicroseconds withGranularity:MTRTimeSynchronizationGranularityMicrosecondsGranularity forEndpoint:endpoint];
305-
if (setDST) {
306-
[self _setDSTOffset:dstOffset validStarting:0 validUntil:nextDSTInMatterEpochTimeMicroseconds forEndpoint:endpoint];
285+
286+
// Check how many DST offsets this endpoint supports.
287+
auto dstOffsetsMaxSizePath = [MTRAttributePath attributePathWithEndpointID:endpoint clusterID:@(MTRClusterIDTypeTimeSynchronizationID) attributeID:@(MTRAttributeIDTypeClusterTimeSynchronizationAttributeDSTOffsetListMaxSizeID)];
288+
auto dstOffsetsMaxSize = [self readAttributeWithEndpointID:dstOffsetsMaxSizePath.endpoint clusterID:dstOffsetsMaxSizePath.cluster attributeID:dstOffsetsMaxSizePath.attribute params:nil];
289+
if (dstOffsetsMaxSize == nil) {
290+
// This endpoint does not support TZ, so won't support SetDSTOffset.
291+
MTR_LOG_DEFAULT("%@ Unable to SetDSTOffset on endpoint %@, since it does not support the TZ feature", self, endpoint);
292+
continue;
307293
}
294+
auto attrReport = [[MTRAttributeReport alloc] initWithResponseValue:@{
295+
MTRAttributePathKey : dstOffsetsMaxSizePath,
296+
MTRDataKey : dstOffsetsMaxSize,
297+
}
298+
error:nil];
299+
uint8_t maxOffsetCount;
300+
if (attrReport == nil) {
301+
MTR_LOG_ERROR("%@ DSTOffsetListMaxSize value on endpoint %@ is invalid. Defaulting to 1.", self, endpoint);
302+
maxOffsetCount = 1;
303+
} else {
304+
NSNumber * maxOffsetCountAsNumber = attrReport.value;
305+
maxOffsetCount = maxOffsetCountAsNumber.unsignedCharValue;
306+
if (maxOffsetCount == 0) {
307+
MTR_LOG_ERROR("%@ DSTOffsetListMaxSize value on endpoint %@ is 0, which is not allowed. Defaulting to 1.", self, endpoint);
308+
maxOffsetCount = 1;
309+
}
310+
}
311+
auto * dstOffsets = MTRComputeDSTOffsets(maxOffsetCount);
312+
if (dstOffsets == nil) {
313+
MTR_LOG_ERROR("%@ Could not retrieve DST offset information. Unable to setDSTOffset on endpoint %@.", self, endpoint);
314+
continue;
315+
}
316+
317+
[self _setDSTOffsets:dstOffsets forEndpoint:endpoint];
308318
}
309319
}
310320

@@ -410,23 +420,18 @@ - (void)_setUTCTime:(UInt64)matterEpochTime withGranularity:(uint8_t)granularity
410420
completion:setUTCTimeResponseHandler];
411421
}
412422

413-
- (void)_setDSTOffset:(int32_t)dstOffset validStarting:(uint64_t)validStarting validUntil:(uint64_t)validUntil forEndpoint:(NSNumber *)endpoint
423+
- (void)_setDSTOffsets:(NSArray<MTRTimeSynchronizationClusterDSTOffsetStruct *> *)dstOffsets forEndpoint:(NSNumber *)endpoint
414424
{
415-
MTR_LOG_DEBUG("%@ _setDSTOffset with offset: %d, validStarting: %llu, validUntil: %llu, endpoint %@",
416-
self,
417-
dstOffset, validStarting, validUntil, endpoint);
425+
MTR_LOG_DEBUG("%@ _setDSTOffsets with offsets: %@, endpoint %@",
426+
self, dstOffsets, endpoint);
418427

419428
MTRTimeSynchronizationClusterSetDSTOffsetParams * params = [[MTRTimeSynchronizationClusterSetDSTOffsetParams
420429
alloc] init];
421-
MTRTimeSynchronizationClusterDSTOffsetStruct * dstOffsetStruct = [[MTRTimeSynchronizationClusterDSTOffsetStruct alloc] init];
422-
dstOffsetStruct.offset = @(dstOffset);
423-
dstOffsetStruct.validStarting = @(validStarting);
424-
dstOffsetStruct.validUntil = @(validUntil);
425-
params.dstOffset = @[ dstOffsetStruct ];
430+
params.dstOffset = dstOffsets;
426431

427432
auto setDSTOffsetResponseHandler = ^(id _Nullable response, NSError * _Nullable error) {
428433
if (error) {
429-
MTR_LOG_ERROR("%@ _setDSTOffset failed on endpoint %@, with parameters %@, error: %@", self, endpoint, params, error);
434+
MTR_LOG_ERROR("%@ _setDSTOffsets failed on endpoint %@, with parameters %@, error: %@", self, endpoint, params, error);
430435
}
431436
};
432437

src/darwin/Framework/CHIP/MTRDeviceController.mm

+23-28
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
#import "MTRPersistentStorageDelegateBridge.h"
4545
#import "MTRServerEndpoint_Internal.h"
4646
#import "MTRSetupPayload.h"
47+
#import "MTRTimeUtils.h"
4748
#import "NSDataSpanConversion.h"
4849
#import "NSStringSpanConversion.h"
4950
#import <setup_payload/ManualSetupPayloadGenerator.h>
@@ -737,40 +738,34 @@ - (BOOL)commissionNodeWithID:(NSNumber *)nodeID
737738
// to add, but in practice devices likely support only 2 and
738739
// AutoCommissioner caps the list at 10. Let's do up to 4 transitions
739740
// for now.
741+
constexpr size_t dstOffsetMaxCount = 4;
740742
using DSTOffsetType = chip::app::Clusters::TimeSynchronization::Structs::DSTOffsetStruct::Type;
741-
742-
DSTOffsetType dstOffsets[4];
743-
size_t dstOffsetCount = 0;
744-
auto nextOffset = tz.daylightSavingTimeOffset;
745-
uint64_t nextValidStarting = 0;
746-
auto * nextTransition = tz.nextDaylightSavingTimeTransition;
747-
for (auto & dstOffset : dstOffsets) {
748-
++dstOffsetCount;
749-
dstOffset.offset = static_cast<int32_t>(nextOffset);
750-
dstOffset.validStarting = nextValidStarting;
751-
if (nextTransition != nil) {
752-
uint32_t transitionEpochS;
753-
if (DateToMatterEpochSeconds(nextTransition, transitionEpochS)) {
754-
using Microseconds64 = chip::System::Clock::Microseconds64;
755-
using Seconds32 = chip::System::Clock::Seconds32;
756-
dstOffset.validUntil.SetNonNull(Microseconds64(Seconds32(transitionEpochS)).count());
743+
// dstOffsets needs to live long enough, so its existence is not
744+
// conditional on having offsets.
745+
DSTOffsetType dstOffsets[dstOffsetMaxCount];
746+
747+
auto * offsets = MTRComputeDSTOffsets(dstOffsetMaxCount);
748+
if (offsets != nil) {
749+
size_t dstOffsetCount = 0;
750+
for (MTRTimeSynchronizationClusterDSTOffsetStruct * offset in offsets) {
751+
if (dstOffsetCount >= dstOffsetMaxCount) {
752+
// Really shouldn't happen, but let's be extra careful about
753+
// buffer overruns.
754+
break;
755+
}
756+
auto & targetOffset = dstOffsets[dstOffsetCount];
757+
targetOffset.offset = offset.offset.intValue;
758+
targetOffset.validStarting = offset.validStarting.unsignedLongLongValue;
759+
if (offset.validUntil == nil) {
760+
targetOffset.validUntil.SetNull();
757761
} else {
758-
// Out of range; treat as "forever".
759-
dstOffset.validUntil.SetNull();
762+
targetOffset.validUntil.SetNonNull(offset.validUntil.unsignedLongLongValue);
760763
}
761-
} else {
762-
dstOffset.validUntil.SetNull();
763-
}
764-
765-
if (dstOffset.validUntil.IsNull()) {
766-
break;
764+
++dstOffsetCount;
767765
}
768766

769-
nextOffset = [tz daylightSavingTimeOffsetForDate:nextTransition];
770-
nextValidStarting = dstOffset.validUntil.Value();
771-
nextTransition = [tz nextDaylightSavingTimeTransitionAfterDate:nextTransition];
767+
params.SetDSTOffsets(chip::app::DataModel::List<DSTOffsetType>(dstOffsets, dstOffsetCount));
772768
}
773-
params.SetDSTOffsets(chip::app::DataModel::List<DSTOffsetType>(dstOffsets, dstOffsetCount));
774769

775770
chip::NodeId deviceId = [nodeID unsignedLongLongValue];
776771
self->_operationalCredentialsDelegate->SetDeviceID(deviceId);
+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/**
2+
* Copyright (c) 2024 Project CHIP Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#import <Foundation/Foundation.h>
18+
#import <Matter/MTRDefines.h>
19+
#import <Matter/MTRStructsObjc.h>
20+
21+
#import "MTRDefines_Internal.h"
22+
23+
NS_ASSUME_NONNULL_BEGIN
24+
25+
/**
26+
* Utility method to compute up to the given count of instances of
27+
* MTRTimeSynchronizationClusterDSTOffsetStruct and return them.
28+
*
29+
* Returns nil on errors.
30+
*/
31+
MTR_TESTABLE_FUNC
32+
NSArray<MTRTimeSynchronizationClusterDSTOffsetStruct *> * _Nullable MTRComputeDSTOffsets(size_t maxCount);
33+
34+
NS_ASSUME_NONNULL_END
+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/**
2+
* Copyright (c) 2024 Project CHIP Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#import "MTRTimeUtils.h"
18+
#import "MTRConversion.h"
19+
#import "MTRLogging_Internal.h"
20+
21+
NSArray<MTRTimeSynchronizationClusterDSTOffsetStruct *> * _Nullable MTRComputeDSTOffsets(size_t maxCount)
22+
{
23+
auto * tz = [NSTimeZone localTimeZone];
24+
if (!tz) {
25+
MTR_LOG_ERROR("Could not retrieve local time zone. Unable to determine DST offsets.");
26+
return nil;
27+
}
28+
29+
NSMutableArray<MTRTimeSynchronizationClusterDSTOffsetStruct *> * retval = [NSMutableArray arrayWithCapacity:maxCount];
30+
31+
auto nextOffset = tz.daylightSavingTimeOffset;
32+
NSNumber * nextValidStarting = @(0);
33+
auto * nextTransition = tz.nextDaylightSavingTimeTransition;
34+
for (size_t offsetsAdded = 0; offsetsAdded < maxCount; ++offsetsAdded) {
35+
auto offset = [[MTRTimeSynchronizationClusterDSTOffsetStruct alloc] init];
36+
offset.offset = @(nextOffset);
37+
offset.validStarting = nextValidStarting;
38+
if (nextTransition == nil) {
39+
// This one is valid forever.
40+
offset.validUntil = nil;
41+
} else {
42+
uint64_t nextTransitionEpochUs;
43+
if (!DateToMatterEpochMicroseconds(nextTransition, nextTransitionEpochUs)) {
44+
// Transition is somehow before Matter epoch start. This really
45+
// should not happen, but if it does just don't pretend like we
46+
// know what's going on with timezones here.
47+
MTR_LOG_ERROR("Future daylight savings transition is before Matter epoch start?");
48+
return nil;
49+
}
50+
51+
offset.validUntil = @(nextTransitionEpochUs);
52+
}
53+
54+
[retval addObject:offset];
55+
56+
if (offset.validUntil == nil) {
57+
// Valid forever, so no need for more offsets.
58+
break;
59+
}
60+
61+
nextOffset = [tz daylightSavingTimeOffsetForDate:nextTransition];
62+
nextValidStarting = offset.validUntil;
63+
nextTransition = [tz nextDaylightSavingTimeTransitionAfterDate:nextTransition];
64+
}
65+
66+
return [retval copy];
67+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright (c) 2024 Project CHIP Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
// module headers
18+
#import "MTRTimeUtils.h"
19+
#import <Matter/Matter.h>
20+
21+
// system dependencies
22+
#import <XCTest/XCTest.h>
23+
24+
@interface MTRDSTOffsetTests : XCTestCase
25+
@end
26+
27+
@implementation MTRDSTOffsetTests
28+
29+
- (void)test001_SingleOffset
30+
{
31+
__auto_type * offsets = MTRComputeDSTOffsets(1);
32+
// We should be able to get offsets.
33+
XCTAssertNotNil(offsets);
34+
35+
// And there is always at least one, even if all it says is "no offset, forever".
36+
XCTAssertEqual(offsets.count, 1);
37+
XCTAssertEqualObjects(offsets[0].validStarting, @(0));
38+
}
39+
40+
- (void)test002_TryGetting2Offsets
41+
{
42+
__auto_type * offsets = MTRComputeDSTOffsets(2);
43+
// We should be able to get offsets.
44+
XCTAssertNotNil(offsets);
45+
46+
// And there is always at least one, even if all it says is "no offset,
47+
// forever". And we should not get too many offsets.
48+
XCTAssertTrue(offsets.count >= 1);
49+
XCTAssertTrue(offsets.count <= 2);
50+
NSNumber * previousValidUntil = @(0);
51+
for (MTRTimeSynchronizationClusterDSTOffsetStruct * offset in offsets) {
52+
XCTAssertEqualObjects(previousValidUntil, offset.validStarting);
53+
previousValidUntil = offset.validUntil;
54+
}
55+
}
56+
57+
@end

0 commit comments

Comments
 (0)