Skip to content

Commit 1eac9bf

Browse files
bzbarsky-appleshripad621git
authored andcommitted
Set a timezone and DST offsets during commissioning on Darwin. (project-chip#29933)
This will set things up right if the commissionee implements the Time Synchronization cluster. Fixes project-chip#29768
1 parent 51dd3af commit 1eac9bf

8 files changed

+158
-20
lines changed

.github/workflows/darwin.yaml

+3-1
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,9 @@ jobs:
112112
# but to instrument the code in the underlying libCHIP we need to pass CHIP_IS_UBSAN=YES
113113
TEST_RUNNER_ASAN_OPTIONS=__CURRENT_VALUE__:detect_stack_use_after_return=1 xcodebuild test -target "Matter" -scheme "Matter Framework Tests" -sdk macosx -enableAddressSanitizer YES -enableUndefinedBehaviorSanitizer YES OTHER_CFLAGS='${inherited} -Werror -Wconversion' CHIP_IS_UBSAN=YES CHIP_IS_BLE=NO GCC_PREPROCESSOR_DEFINITIONS='${inherited} MTR_NO_AVAILABILITY=1'> >(tee /tmp/darwin/framework-tests/darwin-tests-asan.log) 2> >(tee /tmp/darwin/framework-tests/darwin-tests-asan-err.log >&2)
114114
# And the same thing, but with MTR_PER_CONTROLLER_STORAGE_ENABLED turned on.
115-
TEST_RUNNER_ASAN_OPTIONS=__CURRENT_VALUE__:detect_stack_use_after_return=1 xcodebuild test -target "Matter" -scheme "Matter Framework Tests" -sdk macosx -enableAddressSanitizer YES -enableUndefinedBehaviorSanitizer YES OTHER_CFLAGS='${inherited} -Werror -Wconversion' CHIP_IS_UBSAN=YES CHIP_IS_BLE=NO GCC_PREPROCESSOR_DEFINITIONS='${inherited} MTR_NO_AVAILABILITY=1 MTR_PER_CONTROLLER_STORAGE_ENABLED=1' > >(tee /tmp/darwin/framework-tests/darwin-tests-asan-provisional.log) 2> >(tee /tmp/darwin/framework-tests/darwin-tests-asan-provisional-err.log >&2)
115+
TEST_RUNNER_ASAN_OPTIONS=__CURRENT_VALUE__:detect_stack_use_after_return=1 xcodebuild test -target "Matter" -scheme "Matter Framework Tests" -sdk macosx -enableAddressSanitizer YES -enableUndefinedBehaviorSanitizer YES OTHER_CFLAGS='${inherited} -Werror -Wconversion' CHIP_IS_UBSAN=YES CHIP_IS_BLE=NO GCC_PREPROCESSOR_DEFINITIONS='${inherited} MTR_NO_AVAILABILITY=1 MTR_PER_CONTROLLER_STORAGE_ENABLED=1' > >(tee /tmp/darwin/framework-tests/darwin-tests-asan-controller-storage.log) 2> >(tee /tmp/darwin/framework-tests/darwin-tests-asan-controller-storage-err.log >&2)
116+
# And the same thing, but with MTR_ENABLE_PROVISIONAL also turned on.
117+
TEST_RUNNER_ASAN_OPTIONS=__CURRENT_VALUE__:detect_stack_use_after_return=1 xcodebuild test -target "Matter" -scheme "Matter Framework Tests" -sdk macosx -enableAddressSanitizer YES -enableUndefinedBehaviorSanitizer YES OTHER_CFLAGS='${inherited} -Werror -Wconversion' CHIP_IS_UBSAN=YES CHIP_IS_BLE=NO GCC_PREPROCESSOR_DEFINITIONS='${inherited} MTR_NO_AVAILABILITY=1 MTR_PER_CONTROLLER_STORAGE_ENABLED=1 MTR_ENABLE_PROVISIONAL=1' > >(tee /tmp/darwin/framework-tests/darwin-tests-asan-provisional.log) 2> >(tee /tmp/darwin/framework-tests/darwin-tests-asan-provisional-err.log >&2)
116118
# And the same thing, but with MTR_NO_AVAILABILITY not turned on. This requires -Wno-unguarded-availability-new to avoid availability errors.
117119
TEST_RUNNER_ASAN_OPTIONS=__CURRENT_VALUE__:detect_stack_use_after_return=1 xcodebuild test -target "Matter" -scheme "Matter Framework Tests" -sdk macosx -enableAddressSanitizer YES -enableUndefinedBehaviorSanitizer YES OTHER_CFLAGS='${inherited} -Werror -Wconversion -Wno-unguarded-availability-new' CHIP_IS_UBSAN=YES CHIP_IS_BLE=NO GCC_PREPROCESSOR_DEFINITIONS='${inherited}' > >(tee /tmp/darwin/framework-tests/darwin-tests-asan-with-availability-annotations.log) 2> >(tee /tmp/darwin/framework-tests/darwin-tests-asan-with-availability-annotations-err.log >&2)
118120
# -enableThreadSanitizer instruments the code in Matter.framework,

src/controller/AutoCommissioner.cpp

+2
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,8 @@ CHIP_ERROR AutoCommissioner::SetCommissioningParameters(const CommissioningParam
201201
mTimeZoneBuf[i].name.SetValue(span);
202202
}
203203
}
204+
auto list = app::DataModel::List<app::Clusters::TimeSynchronization::Structs::TimeZoneStruct::Type>(mTimeZoneBuf, size);
205+
mParams.SetTimeZone(list);
204206
}
205207

206208
return CHIP_NO_ERROR;

src/darwin/Framework/CHIP/MTRCertificateInfo.mm

+2-2
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,13 @@ - (MTRDistinguishedNameInfo *)subject
6262

6363
- (NSDate *)notBefore
6464
{
65-
return ChipEpochSecondsAsDate(_data.mNotBeforeTime);
65+
return MatterEpochSecondsAsDate(_data.mNotBeforeTime);
6666
}
6767

6868
- (NSDate *)notAfter
6969
{
7070
// "no expiry" is encoded as kNullCertTime (see ChipEpochToASN1Time)
71-
return (_data.mNotAfterTime != kNullCertTime) ? ChipEpochSecondsAsDate(_data.mNotAfterTime) : NSDate.distantFuture;
71+
return (_data.mNotAfterTime != kNullCertTime) ? MatterEpochSecondsAsDate(_data.mNotAfterTime) : NSDate.distantFuture;
7272
}
7373

7474
- (id)copyWithZone:(nullable NSZone *)zone

src/darwin/Framework/CHIP/MTRConversion.h

+8-2
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,17 @@ AsNumber(chip::Optional<T> optional)
3535
return (optional.HasValue()) ? @(optional.Value()) : nil;
3636
}
3737

38-
inline NSDate * ChipEpochSecondsAsDate(uint32_t chipEpochSeconds)
38+
inline NSDate * MatterEpochSecondsAsDate(uint32_t matterEpochSeconds)
3939
{
40-
return [NSDate dateWithTimeIntervalSince1970:(chip::kChipEpochSecondsSinceUnixEpoch + (NSTimeInterval) chipEpochSeconds)];
40+
return [NSDate dateWithTimeIntervalSince1970:(chip::kChipEpochSecondsSinceUnixEpoch + (NSTimeInterval) matterEpochSeconds)];
4141
}
4242

43+
/**
44+
* Returns whether the conversion could be performed. Will return false if the
45+
* passed-in date is our of the range representable as a Matter epoch-s value.
46+
*/
47+
bool DateToMatterEpochSeconds(NSDate * date, uint32_t & epoch);
48+
4349
/**
4450
* Utilities for converting between NSSet<NSNumber *> and chip::CATValues.
4551
*/

src/darwin/Framework/CHIP/MTRConversion.mm

+19
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
#import "MTRLogging_Internal.h"
1919

2020
#include <lib/support/SafeInt.h>
21+
#include <lib/support/TimeUtils.h>
2122

2223
CHIP_ERROR SetToCATValues(NSSet<NSNumber *> * catSet, chip::CATValues & values)
2324
{
@@ -59,3 +60,21 @@ CHIP_ERROR SetToCATValues(NSSet<NSNumber *> * catSet, chip::CATValues & values)
5960
}
6061
return [NSSet setWithSet:catSet];
6162
}
63+
64+
bool DateToMatterEpochSeconds(NSDate * date, uint32_t & matterEpochSeconds)
65+
{
66+
NSCalendar * calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian];
67+
NSDateComponents * components = [calendar componentsInTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0] fromDate:date];
68+
69+
if (!chip::CanCastTo<uint16_t>(components.year)) {
70+
return false;
71+
}
72+
73+
uint16_t year = static_cast<uint16_t>([components year]);
74+
uint8_t month = static_cast<uint8_t>([components month]);
75+
uint8_t day = static_cast<uint8_t>([components day]);
76+
uint8_t hour = static_cast<uint8_t>([components hour]);
77+
uint8_t minute = static_cast<uint8_t>([components minute]);
78+
uint8_t second = static_cast<uint8_t>([components second]);
79+
return chip::CalendarToChipEpochTime(year, month, day, hour, minute, second, matterEpochSeconds);
80+
}

src/darwin/Framework/CHIP/MTRDeviceController.mm

+53
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@
5353

5454
#include <platform/CHIPDeviceBuildConfig.h>
5555

56+
#include <app-common/zap-generated/cluster-objects.h>
57+
#include <app/data-model/List.h>
5658
#include <controller/CHIPDeviceController.h>
5759
#include <controller/CHIPDeviceControllerFactory.h>
5860
#include <controller/CommissioningWindowOpener.h>
@@ -679,6 +681,57 @@ - (BOOL)commissionNodeWithID:(NSNumber *)nodeID
679681
params.SetCountryCode(AsCharSpan(commissioningParams.countryCode));
680682
}
681683

684+
// Set up the right timezone and DST information. For timezone, just
685+
// use our current timezone and don't schedule any sort of timezone
686+
// change.
687+
auto * tz = [NSTimeZone localTimeZone];
688+
using TimeZoneType = chip::app::Clusters::TimeSynchronization::Structs::TimeZoneStruct::Type;
689+
TimeZoneType timeZone;
690+
timeZone.validAt = 0;
691+
timeZone.offset = static_cast<int32_t>(tz.secondsFromGMT - tz.daylightSavingTimeOffset);
692+
timeZone.name.Emplace(AsCharSpan(tz.name));
693+
694+
params.SetTimeZone(chip::app::DataModel::List<TimeZoneType>(&timeZone, 1));
695+
696+
// For DST, there is no limit to the number of transitions we could try
697+
// to add, but in practice devices likely support only 2 and
698+
// AutoCommissioner caps the list at 10. Let's do up to 4 transitions
699+
// for now.
700+
using DSTOffsetType = chip::app::Clusters::TimeSynchronization::Structs::DSTOffsetStruct::Type;
701+
702+
DSTOffsetType dstOffsets[4];
703+
size_t dstOffsetCount = 0;
704+
auto nextOffset = tz.daylightSavingTimeOffset;
705+
uint64_t nextValidStarting = 0;
706+
auto * nextTransition = tz.nextDaylightSavingTimeTransition;
707+
for (auto & dstOffset : dstOffsets) {
708+
++dstOffsetCount;
709+
dstOffset.offset = static_cast<int32_t>(nextOffset);
710+
dstOffset.validStarting = nextValidStarting;
711+
if (nextTransition != nil) {
712+
uint32_t transitionEpochS;
713+
if (DateToMatterEpochSeconds(nextTransition, transitionEpochS)) {
714+
using Microseconds64 = chip::System::Clock::Microseconds64;
715+
using Seconds32 = chip::System::Clock::Seconds32;
716+
dstOffset.validUntil.SetNonNull(Microseconds64(Seconds32(transitionEpochS)).count());
717+
} else {
718+
// Out of range; treat as "forever".
719+
dstOffset.validUntil.SetNull();
720+
}
721+
} else {
722+
dstOffset.validUntil.SetNull();
723+
}
724+
725+
if (dstOffset.validUntil.IsNull()) {
726+
break;
727+
}
728+
729+
nextOffset = [tz daylightSavingTimeOffsetForDate:nextTransition];
730+
nextValidStarting = dstOffset.validUntil.Value();
731+
nextTransition = [tz nextDaylightSavingTimeTransitionAfterDate:nextTransition];
732+
}
733+
params.SetDSTOffsets(chip::app::DataModel::List<DSTOffsetType>(dstOffsets, dstOffsetCount));
734+
682735
chip::NodeId deviceId = [nodeID unsignedLongLongValue];
683736
self->_operationalCredentialsDelegate->SetDeviceID(deviceId);
684737
auto errorCode = self.cppCommissioner->Commission(deviceId, params);

src/darwin/Framework/CHIP/MTROperationalCredentialsDelegate.mm

+4-15
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,6 @@
3535
#include <lib/core/Optional.h>
3636
#include <lib/core/TLV.h>
3737
#include <lib/support/PersistentStorageMacros.h>
38-
#include <lib/support/SafeInt.h>
39-
#include <lib/support/TimeUtils.h>
4038
#include <platform/LockTracker.h>
4139

4240
using namespace chip;
@@ -322,21 +320,12 @@
322320

323321
bool MTROperationalCredentialsDelegate::ToChipEpochTime(NSDate * date, uint32_t & epoch)
324322
{
325-
NSCalendar * calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian];
326-
NSDateComponents * components = [calendar componentsInTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0] fromDate:date];
327-
328-
if (CanCastTo<uint16_t>(components.year)) {
329-
uint16_t year = static_cast<uint16_t>([components year]);
330-
uint8_t month = static_cast<uint8_t>([components month]);
331-
uint8_t day = static_cast<uint8_t>([components day]);
332-
uint8_t hour = static_cast<uint8_t>([components hour]);
333-
uint8_t minute = static_cast<uint8_t>([components minute]);
334-
uint8_t second = static_cast<uint8_t>([components second]);
335-
if (chip::CalendarToChipEpochTime(year, month, day, hour, minute, second, epoch)) {
336-
return true;
337-
}
323+
if (DateToMatterEpochSeconds(date, epoch)) {
324+
return true;
338325
}
339326

327+
NSCalendar * calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian];
328+
NSDateComponents * components = [calendar componentsInTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0] fromDate:date];
340329
MTR_LOG_ERROR(
341330
"Year %lu is out of range for Matter epoch time. Please use [NSDate distantFuture] to represent \"never expires\".",
342331
static_cast<unsigned long>(components.year));

src/darwin/Framework/CHIPTests/MTRDeviceTests.m

+67
Original file line numberDiff line numberDiff line change
@@ -2555,6 +2555,73 @@ - (void)test027_AttestationChallenge
25552555
[self waitForExpectations:@[ attestationRequestedViaDevice ] timeout:kTimeoutInSeconds];
25562556
}
25572557

2558+
- (void)test028_TimeZoneAndDST
2559+
{
2560+
// Time synchronization is marked provisional so far, so we can only test it
2561+
// when MTR_ENABLE_PROVISIONAL is set.
2562+
#if MTR_ENABLE_PROVISIONAL
2563+
dispatch_queue_t queue = dispatch_get_main_queue();
2564+
2565+
__auto_type * device = GetConnectedDevice();
2566+
__auto_type * cluster = [[MTRBaseClusterTimeSynchronization alloc] initWithDevice:device endpointID:@(0) queue:queue];
2567+
2568+
XCTestExpectation * readTimeZoneExpectation = [self expectationWithDescription:@"Read TimeZone attribute"];
2569+
__block NSArray<MTRTimeSynchronizationClusterTimeZoneStruct *> * timeZone;
2570+
[cluster readAttributeTimeZoneWithCompletion:^(NSArray * _Nullable value, NSError * _Nullable error) {
2571+
XCTAssertNil(error);
2572+
timeZone = value;
2573+
[readTimeZoneExpectation fulfill];
2574+
}];
2575+
2576+
[self waitForExpectations:@[ readTimeZoneExpectation ] timeout:kTimeoutInSeconds];
2577+
2578+
__block NSArray<MTRTimeSynchronizationClusterDSTOffsetStruct *> * dstOffset;
2579+
XCTestExpectation * readDSTOffsetExpectation = [self expectationWithDescription:@"Read DSTOffset attribute"];
2580+
[cluster readAttributeDSTOffsetWithCompletion:^(NSArray * _Nullable value, NSError * _Nullable error) {
2581+
XCTAssertNil(error);
2582+
dstOffset = value;
2583+
[readDSTOffsetExpectation fulfill];
2584+
}];
2585+
2586+
[self waitForExpectations:@[ readDSTOffsetExpectation ] timeout:kTimeoutInSeconds];
2587+
2588+
// Check that the first DST offset entry matches what we expect. If we
2589+
// happened to cross a DST boundary during execution of this function, some
2590+
// of these checks will fail, but that seems pretty low-probability.
2591+
2592+
XCTAssertTrue(dstOffset.count > 0);
2593+
MTRTimeSynchronizationClusterDSTOffsetStruct * currentDSTOffset = dstOffset[0];
2594+
2595+
__auto_type * utcTz = [NSTimeZone timeZoneForSecondsFromGMT:0];
2596+
__auto_type * dateComponents = [[NSDateComponents alloc] init];
2597+
dateComponents.timeZone = utcTz;
2598+
dateComponents.year = 2000;
2599+
dateComponents.month = 1;
2600+
dateComponents.day = 1;
2601+
NSCalendar * gregorianCalendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian];
2602+
NSDate * matterEpoch = [gregorianCalendar dateFromComponents:dateComponents];
2603+
2604+
NSDate * nextReportedDSTTransition;
2605+
if (currentDSTOffset.validUntil == nil) {
2606+
nextReportedDSTTransition = nil;
2607+
} else {
2608+
double validUntilMicroSeconds = currentDSTOffset.validUntil.doubleValue;
2609+
nextReportedDSTTransition = [NSDate dateWithTimeInterval:validUntilMicroSeconds / 1e6 sinceDate:matterEpoch];
2610+
}
2611+
2612+
__auto_type * tz = [NSTimeZone localTimeZone];
2613+
NSDate * nextDSTTransition = tz.nextDaylightSavingTimeTransition;
2614+
XCTAssertEqualObjects(nextReportedDSTTransition, nextDSTTransition);
2615+
2616+
XCTAssertEqual(currentDSTOffset.offset.intValue, tz.daylightSavingTimeOffset);
2617+
2618+
// Now check the timezone info we got. We always set exactly one timezone.
2619+
XCTAssertEqual(timeZone.count, 1);
2620+
MTRTimeSynchronizationClusterTimeZoneStruct * currentTimeZone = timeZone[0];
2621+
XCTAssertEqual(tz.secondsFromGMT, currentTimeZone.offset.intValue + currentDSTOffset.offset.intValue);
2622+
#endif // MTR_ENABLE_PROVISIONAL
2623+
}
2624+
25582625
- (void)test900_SubscribeAllAttributes
25592626
{
25602627
MTRBaseDevice * device = GetConnectedDevice();

0 commit comments

Comments
 (0)