Skip to content

Commit

Permalink
Set a timezone and DST offsets during commissioning on Darwin. (#29933)
Browse files Browse the repository at this point in the history
This will set things up right if the commissionee implements the Time
Synchronization cluster.

Fixes #29768
  • Loading branch information
bzbarsky-apple committed Oct 24, 2023
1 parent f74e95b commit a010ae4
Show file tree
Hide file tree
Showing 8 changed files with 158 additions and 20 deletions.
4 changes: 3 additions & 1 deletion .github/workflows/darwin.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,9 @@ jobs:
# but to instrument the code in the underlying libCHIP we need to pass CHIP_IS_UBSAN=YES
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)
# And the same thing, but with MTR_PER_CONTROLLER_STORAGE_ENABLED turned on.
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)
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)
# And the same thing, but with MTR_ENABLE_PROVISIONAL also turned on.
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)
# And the same thing, but with MTR_NO_AVAILABILITY not turned on. This requires -Wno-unguarded-availability-new to avoid availability errors.
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)
# -enableThreadSanitizer instruments the code in Matter.framework,
Expand Down
2 changes: 2 additions & 0 deletions src/controller/AutoCommissioner.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,8 @@ CHIP_ERROR AutoCommissioner::SetCommissioningParameters(const CommissioningParam
mTimeZoneBuf[i].name.SetValue(span);
}
}
auto list = app::DataModel::List<app::Clusters::TimeSynchronization::Structs::TimeZoneStruct::Type>(mTimeZoneBuf, size);
mParams.SetTimeZone(list);
}

return CHIP_NO_ERROR;
Expand Down
4 changes: 2 additions & 2 deletions src/darwin/Framework/CHIP/MTRCertificateInfo.mm
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,13 @@ - (MTRDistinguishedNameInfo *)subject

- (NSDate *)notBefore
{
return ChipEpochSecondsAsDate(_data.mNotBeforeTime);
return MatterEpochSecondsAsDate(_data.mNotBeforeTime);
}

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

- (id)copyWithZone:(nullable NSZone *)zone
Expand Down
10 changes: 8 additions & 2 deletions src/darwin/Framework/CHIP/MTRConversion.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,17 @@ AsNumber(chip::Optional<T> optional)
return (optional.HasValue()) ? @(optional.Value()) : nil;
}

inline NSDate * ChipEpochSecondsAsDate(uint32_t chipEpochSeconds)
inline NSDate * MatterEpochSecondsAsDate(uint32_t matterEpochSeconds)
{
return [NSDate dateWithTimeIntervalSince1970:(chip::kChipEpochSecondsSinceUnixEpoch + (NSTimeInterval) chipEpochSeconds)];
return [NSDate dateWithTimeIntervalSince1970:(chip::kChipEpochSecondsSinceUnixEpoch + (NSTimeInterval) matterEpochSeconds)];
}

/**
* 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 DateToMatterEpochSeconds(NSDate * date, uint32_t & epoch);

/**
* Utilities for converting between NSSet<NSNumber *> and chip::CATValues.
*/
Expand Down
19 changes: 19 additions & 0 deletions src/darwin/Framework/CHIP/MTRConversion.mm
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#import "MTRLogging_Internal.h"

#include <lib/support/SafeInt.h>
#include <lib/support/TimeUtils.h>

CHIP_ERROR SetToCATValues(NSSet<NSNumber *> * catSet, chip::CATValues & values)
{
Expand Down Expand Up @@ -59,3 +60,21 @@ CHIP_ERROR SetToCATValues(NSSet<NSNumber *> * catSet, chip::CATValues & values)
}
return [NSSet setWithSet:catSet];
}

bool DateToMatterEpochSeconds(NSDate * date, uint32_t & matterEpochSeconds)
{
NSCalendar * calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian];
NSDateComponents * components = [calendar componentsInTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0] fromDate:date];

if (!chip::CanCastTo<uint16_t>(components.year)) {
return false;
}

uint16_t year = static_cast<uint16_t>([components year]);
uint8_t month = static_cast<uint8_t>([components month]);
uint8_t day = static_cast<uint8_t>([components day]);
uint8_t hour = static_cast<uint8_t>([components hour]);
uint8_t minute = static_cast<uint8_t>([components minute]);
uint8_t second = static_cast<uint8_t>([components second]);
return chip::CalendarToChipEpochTime(year, month, day, hour, minute, second, matterEpochSeconds);
}
53 changes: 53 additions & 0 deletions src/darwin/Framework/CHIP/MTRDeviceController.mm
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@

#include <platform/CHIPDeviceBuildConfig.h>

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

// Set up the right timezone and DST information. For timezone, just
// use our current timezone and don't schedule any sort of timezone
// change.
auto * tz = [NSTimeZone localTimeZone];
using TimeZoneType = chip::app::Clusters::TimeSynchronization::Structs::TimeZoneStruct::Type;
TimeZoneType timeZone;
timeZone.validAt = 0;
timeZone.offset = static_cast<int32_t>(tz.secondsFromGMT - tz.daylightSavingTimeOffset);
timeZone.name.Emplace(AsCharSpan(tz.name));

params.SetTimeZone(chip::app::DataModel::List<TimeZoneType>(&timeZone, 1));

// For DST, there is no limit to the number of transitions we could try
// to add, but in practice devices likely support only 2 and
// AutoCommissioner caps the list at 10. Let's do up to 4 transitions
// for now.
using DSTOffsetType = chip::app::Clusters::TimeSynchronization::Structs::DSTOffsetStruct::Type;

DSTOffsetType dstOffsets[4];
size_t dstOffsetCount = 0;
auto nextOffset = tz.daylightSavingTimeOffset;
uint64_t nextValidStarting = 0;
auto * nextTransition = tz.nextDaylightSavingTimeTransition;
for (auto & dstOffset : dstOffsets) {
++dstOffsetCount;
dstOffset.offset = static_cast<int32_t>(nextOffset);
dstOffset.validStarting = nextValidStarting;
if (nextTransition != nil) {
uint32_t transitionEpochS;
if (DateToMatterEpochSeconds(nextTransition, transitionEpochS)) {
using Microseconds64 = chip::System::Clock::Microseconds64;
using Seconds32 = chip::System::Clock::Seconds32;
dstOffset.validUntil.SetNonNull(Microseconds64(Seconds32(transitionEpochS)).count());
} else {
// Out of range; treat as "forever".
dstOffset.validUntil.SetNull();
}
} else {
dstOffset.validUntil.SetNull();
}

if (dstOffset.validUntil.IsNull()) {
break;
}

nextOffset = [tz daylightSavingTimeOffsetForDate:nextTransition];
nextValidStarting = dstOffset.validUntil.Value();
nextTransition = [tz nextDaylightSavingTimeTransitionAfterDate:nextTransition];
}
params.SetDSTOffsets(chip::app::DataModel::List<DSTOffsetType>(dstOffsets, dstOffsetCount));

chip::NodeId deviceId = [nodeID unsignedLongLongValue];
self->_operationalCredentialsDelegate->SetDeviceID(deviceId);
auto errorCode = self.cppCommissioner->Commission(deviceId, params);
Expand Down
19 changes: 4 additions & 15 deletions src/darwin/Framework/CHIP/MTROperationalCredentialsDelegate.mm
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,6 @@
#include <lib/core/Optional.h>
#include <lib/core/TLV.h>
#include <lib/support/PersistentStorageMacros.h>
#include <lib/support/SafeInt.h>
#include <lib/support/TimeUtils.h>
#include <platform/LockTracker.h>

using namespace chip;
Expand Down Expand Up @@ -322,21 +320,12 @@

bool MTROperationalCredentialsDelegate::ToChipEpochTime(NSDate * date, uint32_t & epoch)
{
NSCalendar * calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian];
NSDateComponents * components = [calendar componentsInTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0] fromDate:date];

if (CanCastTo<uint16_t>(components.year)) {
uint16_t year = static_cast<uint16_t>([components year]);
uint8_t month = static_cast<uint8_t>([components month]);
uint8_t day = static_cast<uint8_t>([components day]);
uint8_t hour = static_cast<uint8_t>([components hour]);
uint8_t minute = static_cast<uint8_t>([components minute]);
uint8_t second = static_cast<uint8_t>([components second]);
if (chip::CalendarToChipEpochTime(year, month, day, hour, minute, second, epoch)) {
return true;
}
if (DateToMatterEpochSeconds(date, epoch)) {
return true;
}

NSCalendar * calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian];
NSDateComponents * components = [calendar componentsInTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0] fromDate:date];
MTR_LOG_ERROR(
"Year %lu is out of range for Matter epoch time. Please use [NSDate distantFuture] to represent \"never expires\".",
static_cast<unsigned long>(components.year));
Expand Down
67 changes: 67 additions & 0 deletions src/darwin/Framework/CHIPTests/MTRDeviceTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -2555,6 +2555,73 @@ - (void)test027_AttestationChallenge
[self waitForExpectations:@[ attestationRequestedViaDevice ] timeout:kTimeoutInSeconds];
}

- (void)test028_TimeZoneAndDST
{
// Time synchronization is marked provisional so far, so we can only test it
// when MTR_ENABLE_PROVISIONAL is set.
#if MTR_ENABLE_PROVISIONAL
dispatch_queue_t queue = dispatch_get_main_queue();

__auto_type * device = GetConnectedDevice();
__auto_type * cluster = [[MTRBaseClusterTimeSynchronization alloc] initWithDevice:device endpointID:@(0) queue:queue];

XCTestExpectation * readTimeZoneExpectation = [self expectationWithDescription:@"Read TimeZone attribute"];
__block NSArray<MTRTimeSynchronizationClusterTimeZoneStruct *> * timeZone;
[cluster readAttributeTimeZoneWithCompletion:^(NSArray * _Nullable value, NSError * _Nullable error) {
XCTAssertNil(error);
timeZone = value;
[readTimeZoneExpectation fulfill];
}];

[self waitForExpectations:@[ readTimeZoneExpectation ] timeout:kTimeoutInSeconds];

__block NSArray<MTRTimeSynchronizationClusterDSTOffsetStruct *> * dstOffset;
XCTestExpectation * readDSTOffsetExpectation = [self expectationWithDescription:@"Read DSTOffset attribute"];
[cluster readAttributeDSTOffsetWithCompletion:^(NSArray * _Nullable value, NSError * _Nullable error) {
XCTAssertNil(error);
dstOffset = value;
[readDSTOffsetExpectation fulfill];
}];

[self waitForExpectations:@[ readDSTOffsetExpectation ] timeout:kTimeoutInSeconds];

// Check that the first DST offset entry matches what we expect. If we
// happened to cross a DST boundary during execution of this function, some
// of these checks will fail, but that seems pretty low-probability.

XCTAssertTrue(dstOffset.count > 0);
MTRTimeSynchronizationClusterDSTOffsetStruct * currentDSTOffset = dstOffset[0];

__auto_type * utcTz = [NSTimeZone timeZoneForSecondsFromGMT:0];
__auto_type * dateComponents = [[NSDateComponents alloc] init];
dateComponents.timeZone = utcTz;
dateComponents.year = 2000;
dateComponents.month = 1;
dateComponents.day = 1;
NSCalendar * gregorianCalendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian];
NSDate * matterEpoch = [gregorianCalendar dateFromComponents:dateComponents];

NSDate * nextReportedDSTTransition;
if (currentDSTOffset.validUntil == nil) {
nextReportedDSTTransition = nil;
} else {
double validUntilMicroSeconds = currentDSTOffset.validUntil.doubleValue;
nextReportedDSTTransition = [NSDate dateWithTimeInterval:validUntilMicroSeconds / 1e6 sinceDate:matterEpoch];
}

__auto_type * tz = [NSTimeZone localTimeZone];
NSDate * nextDSTTransition = tz.nextDaylightSavingTimeTransition;
XCTAssertEqualObjects(nextReportedDSTTransition, nextDSTTransition);

XCTAssertEqual(currentDSTOffset.offset.intValue, tz.daylightSavingTimeOffset);

// Now check the timezone info we got. We always set exactly one timezone.
XCTAssertEqual(timeZone.count, 1);
MTRTimeSynchronizationClusterTimeZoneStruct * currentTimeZone = timeZone[0];
XCTAssertEqual(tz.secondsFromGMT, currentTimeZone.offset.intValue + currentDSTOffset.offset.intValue);
#endif // MTR_ENABLE_PROVISIONAL
}

- (void)test900_SubscribeAllAttributes
{
MTRBaseDevice * device = GetConnectedDevice();
Expand Down

0 comments on commit a010ae4

Please sign in to comment.