From a010ae4ac4d7a1cc6bd96054c7c3ba42a48a8c5b Mon Sep 17 00:00:00 2001 From: Boris Zbarsky Date: Tue, 24 Oct 2023 10:53:24 -0400 Subject: [PATCH] Set a timezone and DST offsets during commissioning on Darwin. (#29933) This will set things up right if the commissionee implements the Time Synchronization cluster. Fixes https://github.com/project-chip/connectedhomeip/issues/29768 --- .github/workflows/darwin.yaml | 4 +- src/controller/AutoCommissioner.cpp | 2 + .../Framework/CHIP/MTRCertificateInfo.mm | 4 +- src/darwin/Framework/CHIP/MTRConversion.h | 10 ++- src/darwin/Framework/CHIP/MTRConversion.mm | 19 ++++++ .../Framework/CHIP/MTRDeviceController.mm | 53 +++++++++++++++ .../CHIP/MTROperationalCredentialsDelegate.mm | 19 ++---- .../Framework/CHIPTests/MTRDeviceTests.m | 67 +++++++++++++++++++ 8 files changed, 158 insertions(+), 20 deletions(-) diff --git a/.github/workflows/darwin.yaml b/.github/workflows/darwin.yaml index 09ff0b2fce1925..8b7e44ef82ee00 100644 --- a/.github/workflows/darwin.yaml +++ b/.github/workflows/darwin.yaml @@ -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, diff --git a/src/controller/AutoCommissioner.cpp b/src/controller/AutoCommissioner.cpp index 85367d4532ccc7..72f47fe1e9ad4a 100644 --- a/src/controller/AutoCommissioner.cpp +++ b/src/controller/AutoCommissioner.cpp @@ -201,6 +201,8 @@ CHIP_ERROR AutoCommissioner::SetCommissioningParameters(const CommissioningParam mTimeZoneBuf[i].name.SetValue(span); } } + auto list = app::DataModel::List(mTimeZoneBuf, size); + mParams.SetTimeZone(list); } return CHIP_NO_ERROR; diff --git a/src/darwin/Framework/CHIP/MTRCertificateInfo.mm b/src/darwin/Framework/CHIP/MTRCertificateInfo.mm index 09a1fe4e55a48d..f3aacb9dd51f24 100644 --- a/src/darwin/Framework/CHIP/MTRCertificateInfo.mm +++ b/src/darwin/Framework/CHIP/MTRCertificateInfo.mm @@ -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 diff --git a/src/darwin/Framework/CHIP/MTRConversion.h b/src/darwin/Framework/CHIP/MTRConversion.h index 5932ff88bf597b..543d80968566c6 100644 --- a/src/darwin/Framework/CHIP/MTRConversion.h +++ b/src/darwin/Framework/CHIP/MTRConversion.h @@ -35,11 +35,17 @@ AsNumber(chip::Optional 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 and chip::CATValues. */ diff --git a/src/darwin/Framework/CHIP/MTRConversion.mm b/src/darwin/Framework/CHIP/MTRConversion.mm index 77882742dea3e7..491a585c00585d 100644 --- a/src/darwin/Framework/CHIP/MTRConversion.mm +++ b/src/darwin/Framework/CHIP/MTRConversion.mm @@ -18,6 +18,7 @@ #import "MTRLogging_Internal.h" #include +#include CHIP_ERROR SetToCATValues(NSSet * catSet, chip::CATValues & values) { @@ -59,3 +60,21 @@ CHIP_ERROR SetToCATValues(NSSet * 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(components.year)) { + return false; + } + + uint16_t year = static_cast([components year]); + uint8_t month = static_cast([components month]); + uint8_t day = static_cast([components day]); + uint8_t hour = static_cast([components hour]); + uint8_t minute = static_cast([components minute]); + uint8_t second = static_cast([components second]); + return chip::CalendarToChipEpochTime(year, month, day, hour, minute, second, matterEpochSeconds); +} diff --git a/src/darwin/Framework/CHIP/MTRDeviceController.mm b/src/darwin/Framework/CHIP/MTRDeviceController.mm index 9b132a2ddfefca..d4c5903c93077d 100644 --- a/src/darwin/Framework/CHIP/MTRDeviceController.mm +++ b/src/darwin/Framework/CHIP/MTRDeviceController.mm @@ -53,6 +53,8 @@ #include +#include +#include #include #include #include @@ -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(tz.secondsFromGMT - tz.daylightSavingTimeOffset); + timeZone.name.Emplace(AsCharSpan(tz.name)); + + params.SetTimeZone(chip::app::DataModel::List(&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(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(dstOffsets, dstOffsetCount)); + chip::NodeId deviceId = [nodeID unsignedLongLongValue]; self->_operationalCredentialsDelegate->SetDeviceID(deviceId); auto errorCode = self.cppCommissioner->Commission(deviceId, params); diff --git a/src/darwin/Framework/CHIP/MTROperationalCredentialsDelegate.mm b/src/darwin/Framework/CHIP/MTROperationalCredentialsDelegate.mm index 159e772838258c..71ee2e6c198f50 100644 --- a/src/darwin/Framework/CHIP/MTROperationalCredentialsDelegate.mm +++ b/src/darwin/Framework/CHIP/MTROperationalCredentialsDelegate.mm @@ -35,8 +35,6 @@ #include #include #include -#include -#include #include using namespace chip; @@ -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(components.year)) { - uint16_t year = static_cast([components year]); - uint8_t month = static_cast([components month]); - uint8_t day = static_cast([components day]); - uint8_t hour = static_cast([components hour]); - uint8_t minute = static_cast([components minute]); - uint8_t second = static_cast([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(components.year)); diff --git a/src/darwin/Framework/CHIPTests/MTRDeviceTests.m b/src/darwin/Framework/CHIPTests/MTRDeviceTests.m index 462cc5959bf556..2028d1f03ad9f8 100644 --- a/src/darwin/Framework/CHIPTests/MTRDeviceTests.m +++ b/src/darwin/Framework/CHIPTests/MTRDeviceTests.m @@ -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 * timeZone; + [cluster readAttributeTimeZoneWithCompletion:^(NSArray * _Nullable value, NSError * _Nullable error) { + XCTAssertNil(error); + timeZone = value; + [readTimeZoneExpectation fulfill]; + }]; + + [self waitForExpectations:@[ readTimeZoneExpectation ] timeout:kTimeoutInSeconds]; + + __block NSArray * 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();