/* * Copyright (c) 2023 Project CHIP Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "time-synchronization-server.h" #include "DefaultTimeSyncDelegate.h" #include "time-synchronization-delegate.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if TIME_SYNC_ENABLE_TSC_FEATURE #include #endif using namespace chip; using namespace chip::app; using namespace chip::DeviceLayer; using namespace chip::app::Clusters; using namespace chip::app::Clusters::TimeSynchronization; using namespace chip::app::Clusters::TimeSynchronization::Attributes; using chip::TimeSyncDataProvider; using chip::Protocols::InteractionModel::Status; // ----------------------------------------------------------------------------- // Delegate Implementation namespace { Delegate * gDelegate = nullptr; Delegate * GetDelegate() { if (gDelegate == nullptr) { static DefaultTimeSyncDelegate dg; gDelegate = &dg; } return gDelegate; } #if TIME_SYNC_ENABLE_TSC_FEATURE void OnDeviceConnectedWrapper(void * context, Messaging::ExchangeManager & exchangeMgr, const SessionHandle & sessionHandle) { TimeSynchronizationServer * server = reinterpret_cast(context); server->OnDeviceConnectedFn(exchangeMgr, sessionHandle); } void OnDeviceConnectionFailureWrapper(void * context, const ScopedNodeId & peerId, CHIP_ERROR error) { TimeSynchronizationServer * server = reinterpret_cast(context); server->OnDeviceConnectionFailureFn(); } #endif void OnPlatformEventWrapper(const DeviceLayer::ChipDeviceEvent * event, intptr_t ptr) { TimeSynchronizationServer * server = reinterpret_cast(ptr); server->OnPlatformEventFn(*event); } void OnTimeSyncCompletionWrapper(void * context, TimeSourceEnum timeSource, GranularityEnum granularity) { TimeSynchronizationServer * server = reinterpret_cast(context); server->OnTimeSyncCompletionFn(timeSource, granularity); } void OnFallbackNTPCompletionWrapper(void * context, bool timeSyncSuccessful) { TimeSynchronizationServer * server = reinterpret_cast(context); server->OnFallbackNTPCompletionFn(timeSyncSuccessful); } } // namespace namespace chip { namespace app { namespace Clusters { namespace TimeSynchronization { void SetDefaultDelegate(Delegate * delegate) { gDelegate = delegate; } Delegate * GetDefaultDelegate() { return GetDelegate(); } } // namespace TimeSynchronization } // namespace Clusters } // namespace app } // namespace chip static CHIP_ERROR UpdateUTCTime(uint64_t UTCTimeInChipEpochUs) { uint64_t UTCTimeInUnixEpochUs; VerifyOrReturnError(ChipEpochToUnixEpochMicros(UTCTimeInChipEpochUs, UTCTimeInUnixEpochUs), CHIP_ERROR_INVALID_TIME); uint64_t secs = UTCTimeInChipEpochUs / chip::kMicrosecondsPerSecond; // https://github.com/project-chip/connectedhomeip/issues/27501 VerifyOrReturnError(secs <= UINT32_MAX, CHIP_IM_GLOBAL_STATUS(ResourceExhausted)); ReturnErrorOnFailure(Server::GetInstance().GetFabricTable().SetLastKnownGoodChipEpochTime( System::Clock::Seconds32(static_cast(secs)))); ReturnErrorOnFailure(System::SystemClock().SetClock_RealTime(System::Clock::Microseconds64(UTCTimeInUnixEpochUs))); return CHIP_NO_ERROR; } static bool emitDSTTableEmptyEvent(EndpointId ep) { Events::DSTTableEmpty::Type event; EventNumber eventNumber; CHIP_ERROR error = LogEvent(event, ep, eventNumber); if (CHIP_NO_ERROR != error) { ChipLogError(Zcl, "Unable to emit DSTTableEmpty event [ep=%d]", ep); return false; } ChipLogProgress(Zcl, "Emit DSTTableEmpty event [ep=%d]", ep); // TODO: re-schedule event for after min 1hr https://github.com/project-chip/connectedhomeip/issues/27200 // delegate->scheduleDSTTableEmptyEvent() return true; } static bool emitDSTStatusEvent(EndpointId ep, bool dstOffsetActive) { Events::DSTStatus::Type event; event.DSTOffsetActive = dstOffsetActive; EventNumber eventNumber; CHIP_ERROR error = LogEvent(event, ep, eventNumber); if (CHIP_NO_ERROR != error) { ChipLogError(Zcl, "Unable to emit DSTStatus event [ep=%d]", ep); return false; } ChipLogProgress(Zcl, "Emit DSTStatus event [ep=%d]", ep); return true; } static bool emitTimeZoneStatusEvent(EndpointId ep) { const auto & tzList = TimeSynchronizationServer::Instance().GetTimeZone(); VerifyOrReturnValue(tzList.size() != 0, false); const auto & tz = tzList[0].timeZone; Events::TimeZoneStatus::Type event; event.offset = tz.offset; if (tz.name.HasValue()) { event.name.SetValue(tz.name.Value()); } EventNumber eventNumber; CHIP_ERROR error = LogEvent(event, ep, eventNumber); if (CHIP_NO_ERROR != error) { ChipLogError(Zcl, "Unable to emit TimeZoneStatus event [ep=%d]", ep); return false; } ChipLogProgress(Zcl, "Emit TimeZoneStatus event [ep=%d]", ep); return true; } static bool emitTimeFailureEvent(EndpointId ep) { Events::TimeFailure::Type event; EventNumber eventNumber; CHIP_ERROR error = LogEvent(event, ep, eventNumber); if (CHIP_NO_ERROR != error) { ChipLogError(Zcl, "Unable to emit TimeFailure event [ep=%d]", ep); return false; } // TODO: re-schedule event for after min 1hr if no time is still available // https://github.com/project-chip/connectedhomeip/issues/27200 ChipLogProgress(Zcl, "Emit TimeFailure event [ep=%d]", ep); GetDelegate()->NotifyTimeFailure(); return true; } static bool emitMissingTrustedTimeSourceEvent(EndpointId ep) { Events::MissingTrustedTimeSource::Type event; EventNumber eventNumber; CHIP_ERROR error = LogEvent(event, ep, eventNumber); if (CHIP_NO_ERROR != error) { ChipLogError(Zcl, "Unable to emit MissingTrustedTimeSource event [ep=%d]", ep); return false; } // TODO: re-schedule event for after min 1hr if TTS is null or cannot be reached // https://github.com/project-chip/connectedhomeip/issues/27200 ChipLogProgress(Zcl, "Emit MissingTrustedTimeSource event [ep=%d]", ep); return true; } TimeSynchronizationServer TimeSynchronizationServer::sTimeSyncInstance; TimeSynchronizationServer & TimeSynchronizationServer::Instance() { return sTimeSyncInstance; } TimeSynchronizationServer::TimeSynchronizationServer() : #if TIME_SYNC_ENABLE_TSC_FEATURE mOnDeviceConnectedCallback(OnDeviceConnectedWrapper, this), mOnDeviceConnectionFailureCallback(OnDeviceConnectionFailureWrapper, this), #endif mOnTimeSyncCompletion(OnTimeSyncCompletionWrapper, this), mOnFallbackNTPCompletion(OnFallbackNTPCompletionWrapper, this) {} void TimeSynchronizationServer::AttemptToGetFallbackNTPTimeFromDelegate() { // Sent as a char-string to the delegate so they can read it easily char defaultNTP[kMaxDefaultNTPSize]; MutableCharSpan span(defaultNTP); if (GetDefaultNtp(span) != CHIP_NO_ERROR) { emitTimeFailureEvent(kRootEndpointId); return; } if (span.size() > kMaxDefaultNTPSize) { emitTimeFailureEvent(kRootEndpointId); return; } if (GetDelegate()->UpdateTimeUsingNTPFallback(span, &mOnFallbackNTPCompletion) != CHIP_NO_ERROR) { emitTimeFailureEvent(kRootEndpointId); } } #if TIME_SYNC_ENABLE_TSC_FEATURE void TimeSynchronizationServer::OnDeviceConnectedFn(Messaging::ExchangeManager & exchangeMgr, const SessionHandle & sessionHandle) { // Connected to our trusted time source, let's read the time. AttributePathParams readPaths[2]; readPaths[0] = AttributePathParams(kRootEndpointId, Id, Attributes::UTCTime::Id); readPaths[1] = AttributePathParams(kRootEndpointId, Id, Attributes::Granularity::Id); InteractionModelEngine * engine = InteractionModelEngine::GetInstance(); ReadPrepareParams readParams(sessionHandle); readParams.mpAttributePathParamsList = readPaths; readParams.mAttributePathParamsListSize = 2; auto readInfo = Platform::MakeUnique(engine, &exchangeMgr, *this, ReadClient::InteractionType::Read); if (readInfo == nullptr) { // This is unlikely to work if we don't have memory, but let's try OnDeviceConnectionFailureFn(); return; } CHIP_ERROR err = readInfo->readClient.SendRequest(readParams); if (err != CHIP_NO_ERROR) { ChipLogError(Zcl, "Failed to read UTC time from trusted source"); OnDeviceConnectionFailureFn(); return; } mTimeReadInfo = std::move(readInfo); } void TimeSynchronizationServer::OnDeviceConnectionFailureFn() { // No way to read from the TrustedTimeSource, fall back to default NTP AttemptToGetFallbackNTPTimeFromDelegate(); } void TimeSynchronizationServer::OnAttributeData(const ConcreteDataAttributePath & aPath, TLV::TLVReader * apData, const StatusIB & aStatus) { if (aPath.mClusterId != Id || aStatus.IsFailure()) { return; } switch (aPath.mAttributeId) { case Attributes::UTCTime::Id: if (DataModel::Decode(*apData, mTimeReadInfo->utcTime) != CHIP_NO_ERROR) { mTimeReadInfo->utcTime.SetNull(); } break; case Attributes::Granularity::Id: if (DataModel::Decode(*apData, mTimeReadInfo->granularity) != CHIP_NO_ERROR) { mTimeReadInfo->granularity = GranularityEnum::kNoTimeGranularity; } break; default: break; } } void TimeSynchronizationServer::OnDone(ReadClient * apReadClient) { if (!mTimeReadInfo->utcTime.IsNull() && mTimeReadInfo->granularity != GranularityEnum::kNoTimeGranularity) { GranularityEnum ourGranularity; // Being conservative with granularity - nothing smaller than seconds because of network delay switch (mTimeReadInfo->granularity) { case GranularityEnum::kMinutesGranularity: case GranularityEnum::kSecondsGranularity: ourGranularity = GranularityEnum::kMinutesGranularity; break; default: ourGranularity = GranularityEnum::kSecondsGranularity; break; } CHIP_ERROR err = SetUTCTime(kRootEndpointId, mTimeReadInfo->utcTime.Value(), ourGranularity, TimeSourceEnum::kNodeTimeCluster); if (err == CHIP_NO_ERROR) { mTimeReadInfo = nullptr; return; } } // We get here if we didn't get a time, or failed to set the time source // If we failed to set the UTC time, it doesn't hurt to try the backup - NTP system might have different permissions on the // system clock AttemptToGetFallbackNTPTimeFromDelegate(); mTimeReadInfo = nullptr; } #endif void TimeSynchronizationServer::OnTimeSyncCompletionFn(TimeSourceEnum timeSource, GranularityEnum granularity) { if (timeSource != TimeSourceEnum::kNone && granularity == GranularityEnum::kNoTimeGranularity) { // Unable to get time from the delegate. Try remaining sources. CHIP_ERROR err = AttemptToGetTimeFromTrustedNode(); if (err != CHIP_NO_ERROR) { AttemptToGetFallbackNTPTimeFromDelegate(); } return; } mGranularity = granularity; Status status = TimeSource::Set(kRootEndpointId, timeSource); if (!(status == Status::Success || status == Status::UnsupportedAttribute)) { ChipLogError(Zcl, "Writing TimeSource failed."); } } void TimeSynchronizationServer::OnFallbackNTPCompletionFn(bool timeSyncSuccessful) { if (timeSyncSuccessful) { mGranularity = GranularityEnum::kMillisecondsGranularity; // Non-matter SNTP because we know it's external and there's only one source Status status = TimeSource::Set(kRootEndpointId, TimeSourceEnum::kNonMatterSNTP); if (!(status == Status::Success || status == Status::UnsupportedAttribute)) { ChipLogError(Zcl, "Writing TimeSource failed."); } } else { emitTimeFailureEvent(kRootEndpointId); } } CHIP_ERROR TimeSynchronizationServer::AttemptToGetTimeFromTrustedNode() { #if TIME_SYNC_ENABLE_TSC_FEATURE if (!mTrustedTimeSource.IsNull()) { CASESessionManager * caseSessionManager = Server::GetInstance().GetCASESessionManager(); ScopedNodeId nodeId(mTrustedTimeSource.Value().nodeID, mTrustedTimeSource.Value().fabricIndex); caseSessionManager->FindOrEstablishSession(nodeId, &mOnDeviceConnectedCallback, &mOnDeviceConnectionFailureCallback); return CHIP_NO_ERROR; } return CHIP_ERROR_NOT_FOUND; #else return CHIP_ERROR_NOT_IMPLEMENTED; #endif } void TimeSynchronizationServer::AttemptToGetTime() { // Let's check the delegate and see if can get us a time. Even if the time is already set, we want to ask the delegate so we can // set the time source as appropriate. CHIP_ERROR err = GetDelegate()->UpdateTimeFromPlatformSource(&mOnTimeSyncCompletion); if (err != CHIP_NO_ERROR) { err = AttemptToGetTimeFromTrustedNode(); } if (err != CHIP_NO_ERROR) { AttemptToGetFallbackNTPTimeFromDelegate(); } } void TimeSynchronizationServer::Init() { mTimeSyncDataProvider.Init(Server::GetInstance().GetPersistentStorage()); Structs::TrustedTimeSourceStruct::Type tts; if (mTimeSyncDataProvider.LoadTrustedTimeSource(tts) == CHIP_NO_ERROR) { mTrustedTimeSource.SetNonNull(tts); } if (LoadTimeZone() != CHIP_NO_ERROR) { ClearTimeZone(); } if (LoadDSTOffset() != CHIP_NO_ERROR) { ClearDSTOffset(); } // Set the granularity to none for now - this will force us to go to the delegate so it can // properly report the time source mGranularity = GranularityEnum::kNoTimeGranularity; // This can error, but it's not clear what should happen in this case. For now, just ignore it because we still // want time sync even if we can't register the deletgate here. CHIP_ERROR err = chip::Server::GetInstance().GetFabricTable().AddFabricDelegate(this); if (err != CHIP_NO_ERROR) { ChipLogError(Zcl, "Unable to register Fabric table delegate for time sync"); } PlatformMgr().AddEventHandler(OnPlatformEventWrapper, reinterpret_cast(this)); } void TimeSynchronizationServer::Shutdown() { PlatformMgr().RemoveEventHandler(OnPlatformEventWrapper, 0); } void TimeSynchronizationServer::OnPlatformEventFn(const DeviceLayer::ChipDeviceEvent & event) { switch (event.Type) { case DeviceEventType::kServerReady: if (mGranularity == GranularityEnum::kNoTimeGranularity) { AttemptToGetTime(); } break; default: break; } } CHIP_ERROR TimeSynchronizationServer::SetTrustedTimeSource(const DataModel::Nullable & tts) { CHIP_ERROR err = CHIP_NO_ERROR; mTrustedTimeSource = tts; if (!mTrustedTimeSource.IsNull()) { err = mTimeSyncDataProvider.StoreTrustedTimeSource(mTrustedTimeSource.Value()); } else { err = mTimeSyncDataProvider.ClearTrustedTimeSource(); } if (mGranularity == GranularityEnum::kNoTimeGranularity) { AttemptToGetTime(); } GetDelegate()->TrustedTimeSourceAvailabilityChanged(!mTrustedTimeSource.IsNull(), mGranularity); return err; } CHIP_ERROR TimeSynchronizationServer::SetDefaultNTP(const DataModel::Nullable & dntp) { CHIP_ERROR err = CHIP_NO_ERROR; if (!dntp.IsNull()) { err = mTimeSyncDataProvider.StoreDefaultNtp(dntp.Value()); } else { err = mTimeSyncDataProvider.ClearDefaultNtp(); } return err; } void TimeSynchronizationServer::InitTimeZone() { mTimeZoneObj.validSize = 1; // one default time zone item is needed mTimeZoneObj.timeZoneList = Span(mTz); for (auto & tzStore : mTimeZoneObj.timeZoneList) { memset(tzStore.name, 0, sizeof(tzStore.name)); tzStore.timeZone = { .offset = 0, .validAt = 0, .name = chip::NullOptional }; } } CHIP_ERROR TimeSynchronizationServer::SetTimeZone(const DataModel::DecodableList & tzL) { size_t items; VerifyOrReturnError(CHIP_NO_ERROR == tzL.ComputeSize(&items), CHIP_IM_GLOBAL_STATUS(InvalidCommand)); if (items > CHIP_CONFIG_TIME_ZONE_LIST_MAX_SIZE) { return CHIP_ERROR_BUFFER_TOO_SMALL; } if (items == 0) { return ClearTimeZone(); } char name[TimeSyncDataProvider::kTimeZoneNameLength]; Structs::TimeZoneStruct::Type lastTz; TimeState lastTzState = UpdateTimeZoneState(); if (lastTzState != TimeState::kInvalid) { const auto & tzStore = GetTimeZone()[0]; lastTz.offset = tzStore.timeZone.offset; if (tzStore.timeZone.name.HasValue()) { lastTz.name.SetValue(CharSpan(name)); memcpy(name, tzStore.name, sizeof(tzStore.name)); } } auto newTzL = tzL.begin(); uint8_t i = 0; InitTimeZone(); while (newTzL.Next()) { auto & tzStore = mTimeZoneObj.timeZoneList[i]; const auto & newTz = newTzL.GetValue(); if (newTz.offset < -43200 || newTz.offset > 50400) { ReturnErrorOnFailure(LoadTimeZone()); return CHIP_ERROR_IM_MALFORMED_COMMAND_DATA_IB; } // first element shall have validAt entry of 0 if (i == 0 && newTz.validAt != 0) { ReturnErrorOnFailure(LoadTimeZone()); return CHIP_ERROR_IM_MALFORMED_COMMAND_DATA_IB; } // if second element, it shall have validAt entry of non-0 if (i != 0 && newTz.validAt == 0) { ReturnErrorOnFailure(LoadTimeZone()); return CHIP_ERROR_IM_MALFORMED_COMMAND_DATA_IB; } tzStore.timeZone.offset = newTz.offset; tzStore.timeZone.validAt = newTz.validAt; if (newTz.name.HasValue() && newTz.name.Value().size() > 0) { size_t len = newTz.name.Value().size(); if (len > sizeof(tzStore.name)) { ReturnErrorOnFailure(LoadTimeZone()); return CHIP_ERROR_IM_MALFORMED_COMMAND_DATA_IB; } memset(tzStore.name, 0, sizeof(tzStore.name)); chip::MutableCharSpan tempSpan(tzStore.name, len); if (CHIP_NO_ERROR != CopyCharSpanToMutableCharSpan(newTz.name.Value(), tempSpan)) { ReturnErrorOnFailure(LoadTimeZone()); return CHIP_IM_GLOBAL_STATUS(InvalidCommand); } tzStore.timeZone.name.SetValue(CharSpan(tzStore.name, len)); } else { tzStore.timeZone.name.ClearValue(); } i++; } if (CHIP_NO_ERROR != newTzL.GetStatus()) { ReturnErrorOnFailure(LoadTimeZone()); return CHIP_IM_GLOBAL_STATUS(InvalidCommand); } mTimeZoneObj.validSize = i; if (lastTzState != TimeState::kInvalid && TimeState::kInvalid != UpdateTimeZoneState()) { bool emit = false; const auto & tz = GetTimeZone()[0].timeZone; if (tz.offset != lastTz.offset) { emit = true; } if ((tz.name.HasValue() && lastTz.name.HasValue()) && !(tz.name.Value().data_equal(lastTz.name.Value()))) { emit = true; } if (emit) { mEventFlag = TimeSyncEventFlag::kTimeZoneStatus; } } return mTimeSyncDataProvider.StoreTimeZone(GetTimeZone()); } CHIP_ERROR TimeSynchronizationServer::LoadTimeZone() { InitTimeZone(); return mTimeSyncDataProvider.LoadTimeZone(mTimeZoneObj); } CHIP_ERROR TimeSynchronizationServer::ClearTimeZone() { InitTimeZone(); return mTimeSyncDataProvider.StoreTimeZone(GetTimeZone()); } void TimeSynchronizationServer::InitDSTOffset() { mDstOffsetObj.validSize = 0; mDstOffsetObj.dstOffsetList = DataModel::List(mDst); } CHIP_ERROR TimeSynchronizationServer::SetDSTOffset(const DataModel::DecodableList & dstL) { size_t items; VerifyOrReturnError(CHIP_NO_ERROR == dstL.ComputeSize(&items), CHIP_IM_GLOBAL_STATUS(InvalidCommand)); if (items > CHIP_CONFIG_DST_OFFSET_LIST_MAX_SIZE) { return CHIP_ERROR_BUFFER_TOO_SMALL; } if (items == 0) { return ClearDSTOffset(); } auto newDstL = dstL.begin(); size_t i = 0; InitDSTOffset(); while (newDstL.Next()) { auto & dst = mDstOffsetObj.dstOffsetList[i]; dst = newDstL.GetValue(); i++; } if (CHIP_NO_ERROR != newDstL.GetStatus()) { ReturnErrorOnFailure(LoadDSTOffset()); return CHIP_IM_GLOBAL_STATUS(InvalidCommand); } mDstOffsetObj.validSize = i; // only 1 validuntil null value and shall be last in the list uint64_t lastValidUntil = 0; for (i = 0; i < mDstOffsetObj.validSize; i++) { const auto & dstItem = GetDSTOffset()[i]; // list should be sorted by validStarting // validUntil shall be larger than validStarting if (!dstItem.validUntil.IsNull() && dstItem.validStarting >= dstItem.validUntil.Value()) { ReturnErrorOnFailure(LoadDSTOffset()); return CHIP_ERROR_IM_MALFORMED_COMMAND_DATA_IB; } // validStarting shall not be smaller than validUntil of previous entry if (dstItem.validStarting < lastValidUntil) { ReturnErrorOnFailure(LoadDSTOffset()); return CHIP_ERROR_IM_MALFORMED_COMMAND_DATA_IB; } lastValidUntil = !dstItem.validUntil.IsNull() ? dstItem.validUntil.Value() : lastValidUntil; // only 1 validUntil null value and shall be last in the list if (dstItem.validUntil.IsNull() && (i != mDstOffsetObj.validSize - 1)) { ReturnErrorOnFailure(LoadDSTOffset()); return CHIP_ERROR_IM_MALFORMED_COMMAND_DATA_IB; } } return mTimeSyncDataProvider.StoreDSTOffset(GetDSTOffset()); } CHIP_ERROR TimeSynchronizationServer::LoadDSTOffset() { InitDSTOffset(); return mTimeSyncDataProvider.LoadDSTOffset(mDstOffsetObj); } CHIP_ERROR TimeSynchronizationServer::ClearDSTOffset() { InitDSTOffset(); ReturnErrorOnFailure(mTimeSyncDataProvider.ClearDSTOffset()); emitDSTTableEmptyEvent(GetDelegate()->GetEndpoint()); return CHIP_NO_ERROR; } DataModel::Nullable & TimeSynchronizationServer::GetTrustedTimeSource() { return mTrustedTimeSource; } CHIP_ERROR TimeSynchronizationServer::GetDefaultNtp(MutableCharSpan & dntp) { return mTimeSyncDataProvider.LoadDefaultNtp(dntp); } Span & TimeSynchronizationServer::GetTimeZone() { mTimeZoneObj.timeZoneList = mTimeZoneObj.timeZoneList.SubSpan(0, mTimeZoneObj.validSize); return mTimeZoneObj.timeZoneList; } DataModel::List & TimeSynchronizationServer::GetDSTOffset() { mDstOffsetObj.dstOffsetList = mDstOffsetObj.dstOffsetList.SubSpan(0, mDstOffsetObj.validSize); return mDstOffsetObj.dstOffsetList; } void TimeSynchronizationServer::ScheduleDelayedAction(System::Clock::Seconds32 delay, System::TimerCompleteCallback action, void * aAppState) { if (CHIP_NO_ERROR != SystemLayer().StartTimer(std::chrono::duration_cast(delay), action, aAppState)) { ChipLogError(Zcl, "Time Synchronization failed to schedule timer."); } } CHIP_ERROR TimeSynchronizationServer::SetUTCTime(EndpointId ep, uint64_t utcTime, GranularityEnum granularity, TimeSourceEnum source) { CHIP_ERROR err = UpdateUTCTime(utcTime); if (err != CHIP_NO_ERROR && !RuntimeOptionsProvider::Instance().GetSimulateNoInternalTime()) { ChipLogError(Zcl, "Error setting UTC time on the device: %" CHIP_ERROR_FORMAT, err.Format()); return err; } GetDelegate()->UTCTimeAvailabilityChanged(utcTime); mGranularity = granularity; Status status = TimeSource::Set(ep, source); if (!(status == Status::Success || status == Status::UnsupportedAttribute)) { ChipLogError(Zcl, "Writing TimeSource failed."); return CHIP_IM_GLOBAL_STATUS(Failure); } return CHIP_NO_ERROR; } CHIP_ERROR TimeSynchronizationServer::GetLocalTime(EndpointId ep, DataModel::Nullable & localTime) { int64_t timeZoneOffset = 0, dstOffset = 0; System::Clock::Microseconds64 utcTime; uint64_t chipEpochTime; if (mGranularity == GranularityEnum::kNoTimeGranularity) { return CHIP_ERROR_INVALID_TIME; } TimeState newState = UpdateDSTOffsetState(); VerifyOrReturnError(TimeState::kInvalid != newState, CHIP_ERROR_INVALID_TIME); ReturnErrorOnFailure(System::SystemClock().GetClock_RealTime(utcTime)); VerifyOrReturnError(UnixEpochToChipEpochMicros(utcTime.count(), chipEpochTime), CHIP_ERROR_INVALID_TIME); if (TimeState::kChanged == UpdateTimeZoneState()) { emitTimeZoneStatusEvent(ep); } VerifyOrReturnError(GetTimeZone().size() != 0, CHIP_ERROR_INVALID_TIME); const auto & tzStore = GetTimeZone()[0]; timeZoneOffset = static_cast(tzStore.timeZone.offset); VerifyOrReturnError(GetDSTOffset().size() != 0, CHIP_ERROR_INVALID_TIME); const auto & dst = GetDSTOffset()[0]; if (dst.validStarting <= chipEpochTime) { dstOffset = static_cast(dst.offset); } uint64_t usRemainder = chipEpochTime % chip::kMicrosecondsPerSecond; // microseconds part of chipEpochTime chipEpochTime = (chipEpochTime / chip::kMicrosecondsPerSecond); // make it safe to cast to int64 by converting to seconds uint64_t localTimeSec = static_cast(static_cast(chipEpochTime) + timeZoneOffset + dstOffset); localTime.SetNonNull((localTimeSec * chip::kMicrosecondsPerSecond) + usRemainder); if (newState == TimeState::kChanged) { emitDSTStatusEvent(0, dstOffset != 0); } return CHIP_NO_ERROR; } TimeState TimeSynchronizationServer::UpdateTimeZoneState() { System::Clock::Microseconds64 utcTime; auto & tzList = GetTimeZone(); size_t activeTzIndex = 0; uint64_t chipEpochTime; // This return allows us to simulate no internal time for testing purposes // This will be set once we receive a good time either from the delegate or via a command if (mGranularity == GranularityEnum::kNoTimeGranularity) { return TimeState::kInvalid; } VerifyOrReturnValue(System::SystemClock().GetClock_RealTime(utcTime) == CHIP_NO_ERROR, TimeState::kInvalid); VerifyOrReturnValue(tzList.size() != 0, TimeState::kInvalid); VerifyOrReturnValue(UnixEpochToChipEpochMicros(utcTime.count(), chipEpochTime), TimeState::kInvalid); for (size_t i = 0; i < tzList.size(); i++) { auto & tz = tzList[i].timeZone; if (tz.validAt != 0 && tz.validAt <= chipEpochTime) { tz.validAt = 0; activeTzIndex = i; } } if (activeTzIndex != 0) { mTimeZoneObj.validSize = tzList.size() - activeTzIndex; auto newTimeZoneList = tzList.SubSpan(activeTzIndex); VerifyOrReturnValue(mTimeSyncDataProvider.StoreTimeZone(newTimeZoneList) == CHIP_NO_ERROR, TimeState::kInvalid); VerifyOrReturnValue(LoadTimeZone() == CHIP_NO_ERROR, TimeState::kInvalid); return TimeState::kChanged; } return TimeState::kActive; } TimeState TimeSynchronizationServer::UpdateDSTOffsetState() { System::Clock::Microseconds64 utcTime; auto & dstList = GetDSTOffset(); size_t activeDstIndex = 0; uint64_t chipEpochTime; bool dstStopped = true; // This return allows us to simulate no internal time for testing purposes // This will be set once we receive a good time either from the delegate or via a command if (mGranularity == GranularityEnum::kNoTimeGranularity) { return TimeState::kInvalid; } VerifyOrReturnValue(System::SystemClock().GetClock_RealTime(utcTime) == CHIP_NO_ERROR, TimeState::kInvalid); VerifyOrReturnValue(dstList.size() != 0, TimeState::kInvalid); VerifyOrReturnValue(UnixEpochToChipEpochMicros(utcTime.count(), chipEpochTime), TimeState::kInvalid); for (size_t i = 0; i < dstList.size(); i++) { if (dstList[i].validStarting <= chipEpochTime) { activeDstIndex = i; dstStopped = false; } } VerifyOrReturnValue(!dstStopped, TimeState::kStopped); // if offset is zero and validUntil is null then no DST is used if (dstList[activeDstIndex].offset == 0 && dstList[activeDstIndex].validUntil.IsNull()) { return TimeState::kStopped; } if (!dstList[activeDstIndex].validUntil.IsNull() && dstList[activeDstIndex].validUntil.Value() <= chipEpochTime) { if (activeDstIndex + 1 >= mDstOffsetObj.validSize) // last item in the list { VerifyOrReturnValue(ClearDSTOffset() == CHIP_NO_ERROR, TimeState::kInvalid); return TimeState::kInvalid; } int32_t previousOffset = dstList[activeDstIndex].offset; dstList[activeDstIndex].offset = 0; // not using dst and last DST item in the list is not active yet // TODO: This enum mixes state and transitions in a way that's very confusing. This should return either an active, an // inactive or an invalid and the caller should make the judgement about whether that has changed OR this function should // just return a bool indicating whether a change happened return previousOffset == 0 ? TimeState::kStopped : TimeState::kChanged; } if (activeDstIndex > 0) { mDstOffsetObj.validSize = dstList.size() - activeDstIndex; auto newDstOffsetList = dstList.SubSpan(activeDstIndex); VerifyOrReturnValue(mTimeSyncDataProvider.StoreDSTOffset(newDstOffsetList) == CHIP_NO_ERROR, TimeState::kInvalid); VerifyOrReturnValue(LoadDSTOffset() == CHIP_NO_ERROR, TimeState::kInvalid); return TimeState::kChanged; } return TimeState::kActive; } TimeSyncEventFlag TimeSynchronizationServer::GetEventFlag() { return mEventFlag; } void TimeSynchronizationServer::ClearEventFlag(TimeSyncEventFlag flag) { uint8_t eventFlag = to_underlying(mEventFlag) ^ to_underlying(flag); mEventFlag = static_cast(eventFlag); } void TimeSynchronizationServer::OnFabricRemoved(const FabricTable & fabricTable, FabricIndex fabricIndex) { if (!mTrustedTimeSource.IsNull() && mTrustedTimeSource.Value().fabricIndex == fabricIndex) { DataModel::Nullable tts; TimeSynchronizationServer::Instance().SetTrustedTimeSource(tts); emitMissingTrustedTimeSourceEvent(0); } } namespace { class TimeSynchronizationAttrAccess : public AttributeAccessInterface { public: // register for the TimeSync cluster on all endpoints TimeSynchronizationAttrAccess() : AttributeAccessInterface(Optional::Missing(), Id) {} CHIP_ERROR Read(const ConcreteReadAttributePath & aPath, AttributeValueEncoder & aEncoder) override; private: CHIP_ERROR ReadTrustedTimeSource(EndpointId endpoint, AttributeValueEncoder & aEncoder); CHIP_ERROR ReadDefaultNtp(EndpointId endpoint, AttributeValueEncoder & aEncoder); CHIP_ERROR ReadTimeZone(EndpointId endpoint, AttributeValueEncoder & aEncoder); CHIP_ERROR ReadDSTOffset(EndpointId endpoint, AttributeValueEncoder & aEncoder); CHIP_ERROR ReadLocalTime(EndpointId endpoint, AttributeValueEncoder & aEncoder); }; TimeSynchronizationAttrAccess gAttrAccess; CHIP_ERROR TimeSynchronizationAttrAccess::ReadTrustedTimeSource(EndpointId endpoint, AttributeValueEncoder & aEncoder) { const auto & tts = TimeSynchronizationServer::Instance().GetTrustedTimeSource(); return aEncoder.Encode(tts); } CHIP_ERROR TimeSynchronizationAttrAccess::ReadDefaultNtp(EndpointId endpoint, AttributeValueEncoder & aEncoder) { CHIP_ERROR err = CHIP_NO_ERROR; char buffer[DefaultNTP::TypeInfo::MaxLength()]; MutableCharSpan dntp(buffer); err = TimeSynchronizationServer::Instance().GetDefaultNtp(dntp); if (err == CHIP_NO_ERROR) { err = aEncoder.Encode(CharSpan(buffer, dntp.size())); } else if (err == CHIP_ERROR_PERSISTED_STORAGE_VALUE_NOT_FOUND) { err = aEncoder.EncodeNull(); } return err; } CHIP_ERROR TimeSynchronizationAttrAccess::ReadTimeZone(EndpointId endpoint, AttributeValueEncoder & aEncoder) { CHIP_ERROR err = aEncoder.EncodeList([](const auto & encoder) -> CHIP_ERROR { const auto & tzList = TimeSynchronizationServer::Instance().GetTimeZone(); for (const auto & tzStore : tzList) { ReturnErrorOnFailure(encoder.Encode(tzStore.timeZone)); } return CHIP_NO_ERROR; }); return err; } CHIP_ERROR TimeSynchronizationAttrAccess::ReadDSTOffset(EndpointId endpoint, AttributeValueEncoder & aEncoder) { CHIP_ERROR err = aEncoder.EncodeList([](const auto & encoder) -> CHIP_ERROR { const auto & dstList = TimeSynchronizationServer::Instance().GetDSTOffset(); for (const auto & dstOffset : dstList) { ReturnErrorOnFailure(encoder.Encode(dstOffset)); } return CHIP_NO_ERROR; }); return err; } CHIP_ERROR TimeSynchronizationAttrAccess::ReadLocalTime(EndpointId endpoint, AttributeValueEncoder & aEncoder) { DataModel::Nullable localTime; CHIP_ERROR err = TimeSynchronizationServer::Instance().GetLocalTime(endpoint, localTime); err = aEncoder.Encode(localTime); return err; } CHIP_ERROR TimeSynchronizationAttrAccess::Read(const ConcreteReadAttributePath & aPath, AttributeValueEncoder & aEncoder) { CHIP_ERROR err = CHIP_NO_ERROR; if (aPath.mClusterId != Id) { return CHIP_ERROR_INVALID_PATH_LIST; } switch (aPath.mAttributeId) { case UTCTime::Id: { System::Clock::Microseconds64 utcTimeUnix; uint64_t chipEpochTime; // This return allows us to simulate no internal time for testing purposes // This will be set once we receive a good time either from the delegate or via a command if (TimeSynchronizationServer::Instance().GetGranularity() == GranularityEnum::kNoTimeGranularity) { return aEncoder.EncodeNull(); } VerifyOrReturnError(System::SystemClock().GetClock_RealTime(utcTimeUnix) == CHIP_NO_ERROR, aEncoder.EncodeNull()); VerifyOrReturnError(UnixEpochToChipEpochMicros(utcTimeUnix.count(), chipEpochTime), aEncoder.EncodeNull()); return aEncoder.Encode(chipEpochTime); } case Granularity::Id: { return aEncoder.Encode(TimeSynchronizationServer::Instance().GetGranularity()); } case TrustedTimeSource::Id: { return ReadTrustedTimeSource(aPath.mEndpointId, aEncoder); } case DefaultNTP::Id: { return ReadDefaultNtp(aPath.mEndpointId, aEncoder); } case TimeZone::Id: { return ReadTimeZone(aPath.mEndpointId, aEncoder); } case DSTOffset::Id: { return ReadDSTOffset(aPath.mEndpointId, aEncoder); } case TimeZoneListMaxSize::Id: { uint8_t max = CHIP_CONFIG_TIME_ZONE_LIST_MAX_SIZE; return aEncoder.Encode(max); } case DSTOffsetListMaxSize::Id: { uint8_t max = CHIP_CONFIG_DST_OFFSET_LIST_MAX_SIZE; return aEncoder.Encode(max); } case LocalTime::Id: { return ReadLocalTime(aPath.mEndpointId, aEncoder); } default: { break; } } return err; } } // anonymous namespace bool emberAfTimeSynchronizationClusterSetUTCTimeCallback( chip::app::CommandHandler * commandObj, const chip::app::ConcreteCommandPath & commandPath, const chip::app::Clusters::TimeSynchronization::Commands::SetUTCTime::DecodableType & commandData) { const auto & utcTime = commandData.UTCTime; const auto & granularity = commandData.granularity; const auto & timeSource = commandData.timeSource; auto currentGranularity = TimeSynchronizationServer::Instance().GetGranularity(); if (granularity < GranularityEnum::kNoTimeGranularity || granularity > GranularityEnum::kMicrosecondsGranularity) { commandObj->AddStatus(commandPath, Status::InvalidCommand); return true; } if (timeSource.HasValue() && (timeSource.Value() < TimeSourceEnum::kNone || timeSource.Value() > TimeSourceEnum::kGnss)) { commandObj->AddStatus(commandPath, Status::InvalidCommand); return true; } if (granularity != GranularityEnum::kNoTimeGranularity && (currentGranularity == GranularityEnum::kNoTimeGranularity || granularity >= currentGranularity) && CHIP_NO_ERROR == TimeSynchronizationServer::Instance().SetUTCTime(commandPath.mEndpointId, utcTime, granularity, TimeSourceEnum::kAdmin)) { commandObj->AddStatus(commandPath, Status::Success); } else { commandObj->AddClusterSpecificFailure(commandPath, to_underlying(StatusCode::kTimeNotAccepted)); } return true; } bool emberAfTimeSynchronizationClusterSetTrustedTimeSourceCallback( chip::app::CommandHandler * commandObj, const chip::app::ConcreteCommandPath & commandPath, const chip::app::Clusters::TimeSynchronization::Commands::SetTrustedTimeSource::DecodableType & commandData) { const auto & timeSource = commandData.trustedTimeSource; DataModel::Nullable tts; if (!timeSource.IsNull()) { Structs::TrustedTimeSourceStruct::Type ts = { commandObj->GetAccessingFabricIndex(), timeSource.Value().nodeID, timeSource.Value().endpoint }; tts.SetNonNull(ts); // TODO: schedule a utctime read from this time source and emit event only on failure to get time emitTimeFailureEvent(commandPath.mEndpointId); } else { tts.SetNull(); emitMissingTrustedTimeSourceEvent(commandPath.mEndpointId); } TimeSynchronizationServer::Instance().SetTrustedTimeSource(tts); commandObj->AddStatus(commandPath, Status::Success); return true; } bool emberAfTimeSynchronizationClusterSetTimeZoneCallback( chip::app::CommandHandler * commandObj, const chip::app::ConcreteCommandPath & commandPath, const chip::app::Clusters::TimeSynchronization::Commands::SetTimeZone::DecodableType & commandData) { const auto & timeZone = commandData.timeZone; CHIP_ERROR err = TimeSynchronizationServer::Instance().SetTimeZone(timeZone); if (err != CHIP_NO_ERROR) { if (err == CHIP_ERROR_BUFFER_TOO_SMALL) { commandObj->AddStatus(commandPath, Status::ResourceExhausted); } else if (err == CHIP_IM_GLOBAL_STATUS(InvalidCommand)) { commandObj->AddStatus(commandPath, Status::InvalidCommand); } else { commandObj->AddStatus(commandPath, Status::ConstraintError); } return true; } if (to_underlying(TimeSynchronizationServer::Instance().GetEventFlag()) & to_underlying(TimeSyncEventFlag::kTimeZoneStatus)) { TimeSynchronizationServer::Instance().ClearEventFlag(TimeSyncEventFlag::kTimeZoneStatus); emitTimeZoneStatusEvent(commandPath.mEndpointId); } GetDelegate()->TimeZoneListChanged(TimeSynchronizationServer::Instance().GetTimeZone()); TimeZoneDatabaseEnum tzDb; TimeZoneDatabase::Get(commandPath.mEndpointId, &tzDb); Commands::SetTimeZoneResponse::Type response; TimeSynchronizationServer::Instance().UpdateTimeZoneState(); const auto & tzList = TimeSynchronizationServer::Instance().GetTimeZone(); if (GetDelegate()->HasFeature(Feature::kTimeZone) && tzDb != TimeZoneDatabaseEnum::kNone && tzList.size() != 0) { auto & tz = tzList[0].timeZone; if (tz.name.HasValue() && GetDelegate()->HandleUpdateDSTOffset(tz.name.Value())) { response.DSTOffsetRequired = false; emitDSTStatusEvent(commandPath.mEndpointId, true); } else { response.DSTOffsetRequired = true; } } else { response.DSTOffsetRequired = true; } if (response.DSTOffsetRequired) { TimeState dstState = TimeSynchronizationServer::Instance().UpdateDSTOffsetState(); TimeSynchronizationServer::Instance().ClearDSTOffset(); if (dstState == TimeState::kActive || dstState == TimeState::kChanged) { emitDSTStatusEvent(commandPath.mEndpointId, false); } } commandObj->AddResponse(commandPath, response); return true; } bool emberAfTimeSynchronizationClusterSetDSTOffsetCallback( chip::app::CommandHandler * commandObj, const chip::app::ConcreteCommandPath & commandPath, const chip::app::Clusters::TimeSynchronization::Commands::SetDSTOffset::DecodableType & commandData) { const auto & dstOffset = commandData.DSTOffset; TimeState dstState = TimeSynchronizationServer::Instance().UpdateDSTOffsetState(); CHIP_ERROR err = TimeSynchronizationServer::Instance().SetDSTOffset(dstOffset); if (err != CHIP_NO_ERROR) { if (err == CHIP_ERROR_BUFFER_TOO_SMALL) { commandObj->AddStatus(commandPath, Status::ResourceExhausted); } else if (err == CHIP_IM_GLOBAL_STATUS(InvalidCommand)) { commandObj->AddStatus(commandPath, Status::InvalidCommand); } else { commandObj->AddStatus(commandPath, Status::ConstraintError); } return true; } // if DST state changes, generate DSTStatus event if (dstState != TimeSynchronizationServer::Instance().UpdateDSTOffsetState()) { emitDSTStatusEvent(commandPath.mEndpointId, TimeState::kActive == TimeSynchronizationServer::Instance().UpdateDSTOffsetState()); } commandObj->AddStatus(commandPath, Status::Success); return true; } bool emberAfTimeSynchronizationClusterSetDefaultNTPCallback( chip::app::CommandHandler * commandObj, const chip::app::ConcreteCommandPath & commandPath, const chip::app::Clusters::TimeSynchronization::Commands::SetDefaultNTP::DecodableType & commandData) { Status status = Status::Success; const auto & dNtpChar = commandData.defaultNTP; if (!dNtpChar.IsNull() && dNtpChar.Value().size() > 0) { size_t len = dNtpChar.Value().size(); if (len > DefaultNTP::TypeInfo::MaxLength()) { commandObj->AddStatus(commandPath, Status::ConstraintError); return true; } bool dnsResolve; if (Status::Success != SupportsDNSResolve::Get(commandPath.mEndpointId, &dnsResolve)) { commandObj->AddStatus(commandPath, Status::Failure); return true; } bool isDomain = GetDelegate()->IsNTPAddressDomain(dNtpChar.Value()); bool isIPv6 = GetDelegate()->IsNTPAddressValid(dNtpChar.Value()); bool useable = isIPv6 || (isDomain && dnsResolve); if (!useable) { commandObj->AddStatus(commandPath, Status::InvalidCommand); return true; } } status = (CHIP_NO_ERROR == TimeSynchronizationServer::Instance().SetDefaultNTP(dNtpChar)) ? Status::Success : Status::Failure; commandObj->AddStatus(commandPath, status); return true; } void MatterTimeSynchronizationPluginServerInitCallback() { TimeSynchronizationServer::Instance().Init(); AttributeAccessInterfaceRegistry::Instance().Register(&gAttrAccess); }