rs_matter/dm/clusters/time_sync.rs
1/*
2 *
3 * Copyright (c) 2026 Project CHIP Authors
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18//! Implementation of the Time Synchronization cluster.
19//!
20//! # Cluster-shape selection — endpoint-side, via [`Options`]
21//!
22//! Each Matter feature (`TIME_ZONE`, `NTP_CLIENT`, `NTP_SERVER`,
23//! `TIME_SYNC_CLIENT`) is mirrored 1:1 by a bit in the [`Options`]
24//! bitflags type, consumed by the [`cluster`] const-generic fn which
25//! returns the matching `Cluster<'static>` metadata.
26//!
27//! The shape is picked **endpoint-side**, on the `clusters!` /
28//! `root_endpoint!` macros — e.g. `clusters!(eth, time_sync(time_zone,
29//! ntp_client); …)` — not on the handler. [`TimeSyncHandler`] itself
30//! is non-generic and its [`Self::CLUSTER`](ClusterHandler::CLUSTER)
31//! is pinned to the empty-options shape; only `CLUSTER.id` is
32//! actually consulted by the dispatcher, and the per-attribute /
33//! per-command dispatch is driven by what the endpoint advertises.
34//!
35//! Spec invariant carried over independently of features: `TimeSource`
36//! is opted in even in the empty-options shape so the Matter test
37//! harness's `has_attribute(TimeSource)` gate on `TC_TIMESYNC_2_1`
38//! matches and the test runs rather than skipping.
39//!
40//! # Pluggable data source — [`TimeSync`]
41//!
42//! The cluster's mandatory members (`UTCTime`, `Granularity`,
43//! `TimeSource`, and the `SetUTCTime` command) are handled by
44//! [`TimeSyncHandler`] directly against the Matter-wide
45//! [Last-Known-Good UTC Time](crate::Matter::last_known_utc_time)
46//! state — they require no implementor input.
47//!
48//! [`TimeSync`] only carries the feature-gated members
49//! (`TIME_ZONE` / `NTP_CLIENT` / `NTP_SERVER` / `TIME_SYNC_CLIENT`).
50//! Every method has a "no value" default so `impl TimeSync for ()`
51//! is a fully usable no-op provider; implementors only override the
52//! methods matching the options they advertised.
53
54use core::num::NonZeroU8;
55
56use bitflags::bitflags;
57
58use crate::dm::endpoints::ROOT_ENDPOINT_ID;
59use crate::dm::{
60 ArrayAttributeRead, AttrChangeNotifier, Attribute, Cluster, Command, Dataver, EndptId,
61 EventEmitter, InvokeContext, NodeId, Quality, ReadContext,
62};
63use crate::error::{Error, ErrorCode};
64use crate::persist::{
65 KvBlobStore, KvBlobStoreAccess, Persist, LKG_UTC_KEY, TRUSTED_TIME_SOURCE_KEY,
66};
67use crate::tlv::{
68 FromTLV, Nullable, NullableBuilder, TLVBuilderParent, TLVElement, ToTLV, Utf8StrBuilder,
69};
70use crate::utils::epoch::FIRMWARE_BUILD_MATTER_US;
71use crate::utils::init::{init, Init};
72
73pub use crate::dm::clusters::decl::time_synchronization::*;
74
75pub mod client;
76
77/// An enum describing the current UTC timestamp the real-time clock is aware of.
78///
79/// The timestamp is expressed as Matter-epoch microseconds.
80#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
81#[cfg_attr(feature = "defmt", derive(defmt::Format))]
82pub enum UtcTime {
83 /// The RTC is actively tracking the current time, anchored at the given Matter-epoch microseconds value.
84 Reliable(u64),
85 /// The RTC is not currently tracking the current time, but the given Matter-epoch microseconds value is
86 /// the last known good UTC time persisted on the device.
87 ///
88 /// Do note that a "last known" time is always available, as the firmware build timestamp is used as the
89 /// initial value on a freshly-flashed device.
90 LastKnown(u64),
91}
92
93impl UtcTime {
94 /// Return the current UTC time if available, or `None` if no reliable time is currently tracked.
95 pub const fn reliable(&self) -> Option<u64> {
96 match self {
97 UtcTime::Reliable(utc) => Some(*utc),
98 UtcTime::LastKnown(_) => None,
99 }
100 }
101
102 /// Return the current UTC time if available, or the persisted LKG UTC otherwise.
103 pub const fn any(&self) -> u64 {
104 match self {
105 UtcTime::Reliable(utc) | UtcTime::LastKnown(utc) => *utc,
106 }
107 }
108
109 /// Return the current UTC time in seconds if available, or `None` if no reliable time is currently tracked.
110 pub const fn reliable_secs(&self) -> Option<u64> {
111 match self {
112 UtcTime::Reliable(utc) => Some(*utc / 1_000_000),
113 UtcTime::LastKnown(_) => None,
114 }
115 }
116
117 /// Return the current UTC time in seconds if available, or the persisted LKG UTC otherwise.
118 pub const fn any_secs(&self) -> u64 {
119 match self {
120 UtcTime::Reliable(utc) | UtcTime::LastKnown(utc) => *utc / 1_000_000,
121 }
122 }
123}
124
125/// Last-Known-Good UTC Time tracking for the device (Matter Core spec).
126///
127/// The persisted `utc_us` field is the spec-mandated stored
128/// fallback used by cert path validation when no live time
129/// synchronization is available; it is seeded from
130/// [`crate::utils::epoch::FIRMWARE_BUILD_MATTER_US`] on a
131/// freshly-flashed device.
132///
133/// `anchor`, `granularity`, and `source` are **volatile** — they
134/// describe the current monotonic-clock anchoring around the most
135/// recent [`Matter::set_utc_time`] call. After reboot, `anchor` is
136/// `None` (no live current-time tracking is active), so the TimeSync
137/// cluster reports `UTCTime = Null`, `Granularity = NoTimeGranularity`
138/// and `TimeSource = None` (per spec) — while
139/// `utc_us` still carries the persisted LKG value for cert validity.
140pub struct Rtc {
141 /// Last-Known-Good UTC time, Matter-epoch microseconds.
142 utc_us: u64,
143 /// Same as `utc_us` except always equal to the last persisted value.
144 utc_us_persisted: u64,
145 /// Granularity at the time of the last `set_utc_time` call, with
146 /// the "one level lower than supplied" step-down already
147 /// applied and floored at `MinutesGranularity`.
148 /// **Not persisted** — resets to `NoTimeGranularity` at boot,
149 /// matching the `anchor = None` post-reboot state.
150 granularity: GranularityEnum,
151 /// Authority that last called `set_utc_time`. **Not persisted** —
152 /// resets to `None` at boot.
153 source: TimeSourceEnum,
154 /// `Instant::now()` captured at the last `set_utc_time` call.
155 /// Volatile — `None` after reboot until next set.
156 anchor: Option<embassy_time::Instant>,
157 /// Configured Trusted Time Source for the device (Matter Core spec).
158 /// At most one entry; the fabric that
159 /// installed it owns it and is cleared on fabric removal.
160 /// Persisted under [`crate::persist::TRUSTED_TIME_SOURCE_KEY`].
161 trusted_time_source: Option<TrustedTimeSource>,
162}
163
164impl Rtc {
165 #[inline(always)]
166 pub(crate) const fn new() -> Self {
167 Self {
168 utc_us: FIRMWARE_BUILD_MATTER_US,
169 utc_us_persisted: FIRMWARE_BUILD_MATTER_US,
170 granularity: GranularityEnum::NoTimeGranularity,
171 source: TimeSourceEnum::None,
172 anchor: None,
173 trusted_time_source: None,
174 }
175 }
176
177 /// Return an in-place initializer for `LkgUtc`.
178 pub(crate) fn init() -> impl Init<Self> {
179 init!(Self {
180 utc_us: FIRMWARE_BUILD_MATTER_US,
181 utc_us_persisted: FIRMWARE_BUILD_MATTER_US,
182 granularity: GranularityEnum::NoTimeGranularity,
183 source: TimeSourceEnum::None,
184 anchor: None,
185 trusted_time_source: None,
186 })
187 }
188
189 fn reset(&mut self) {
190 self.utc_us = FIRMWARE_BUILD_MATTER_US;
191 self.utc_us_persisted = FIRMWARE_BUILD_MATTER_US;
192 self.granularity = GranularityEnum::NoTimeGranularity;
193 self.source = TimeSourceEnum::None;
194 self.anchor = None;
195 self.trusted_time_source = None;
196 }
197
198 pub fn reset_persist<S: KvBlobStore>(
199 &mut self,
200 mut store: S,
201 buf: &mut [u8],
202 ) -> Result<(), Error> {
203 self.reset();
204
205 store.remove(LKG_UTC_KEY, buf)?;
206 store.remove(TRUSTED_TIME_SOURCE_KEY, buf)?;
207 Ok(())
208 }
209
210 pub fn load_persist<S: KvBlobStore>(&mut self, mut kv: S, buf: &mut [u8]) -> Result<(), Error> {
211 self.reset();
212
213 // Load the persisted Last-Known-Good UTC Time, if any.
214 // Floor at `FIRMWARE_BUILD_MATTER_US` per Matter Core spec -
215 // the on-disk value must never regress us
216 // below the build timestamp (the documented lower bound
217 // we never adjust backwards past).
218 if let Some(data) = kv.load(LKG_UTC_KEY, buf)? {
219 let stored = u64::from_tlv(&TLVElement::new(data))?;
220 let floor = FIRMWARE_BUILD_MATTER_US;
221
222 self.utc_us_persisted = stored;
223 self.utc_us = stored.max(floor);
224 }
225
226 // Load the persisted Trusted Time Source, if any.
227 if let Some(data) = kv.load(TRUSTED_TIME_SOURCE_KEY, buf)? {
228 self.trusted_time_source = Some(TrustedTimeSource::from_tlv(&TLVElement::new(data))?);
229 }
230
231 Ok(())
232 }
233
234 /// Return the configured Trusted Time Source, or `None` if unset
235 /// (Matter Core spec).
236 pub fn trusted_time_source(&self) -> Option<TrustedTimeSource> {
237 self.trusted_time_source
238 }
239
240 /// Install or clear the Trusted Time Source (Matter Core spec).
241 /// `fab_idx` is the fabric performing the change —
242 /// recorded so that fabric removal can clear an entry it owns.
243 pub fn set_trusted_time_source<E: EventEmitter>(
244 &mut self,
245 source: Option<TrustedTimeSource>,
246 change_notifier: &dyn AttrChangeNotifier,
247 event_emitter: E,
248 ) -> Result<(), Error> {
249 if self.trusted_time_source != source {
250 let previous = self.trusted_time_source;
251
252 self.trusted_time_source = source;
253
254 change_notifier.notify_attr_changed(
255 ROOT_ENDPOINT_ID,
256 TimeSyncHandler::CLUSTER.id,
257 AttributeId::TrustedTimeSource as _,
258 );
259
260 // Matter Core spec: emit `MissingTrustedTimeSource`
261 // when SetTrustedTimeSource clears a previously-set entry (null
262 // request payload, transitioning from `Some(..)` → `None`).
263 if self.trusted_time_source.is_none() && previous.is_some() {
264 MissingTrustedTimeSource::emit_for(event_emitter, ROOT_ENDPOINT_ID, |b| b.end())?;
265 }
266 }
267
268 Ok(())
269 }
270
271 /// Install or clear the Trusted Time Source (Matter Core spec).
272 /// `fab_idx` is the fabric performing the change —
273 /// recorded so that fabric removal can clear an entry it owns.
274 /// `source = None` clears any existing entry.
275 ///
276 /// Updates in-memory state, persists under
277 /// [`crate::persist::TRUSTED_TIME_SOURCE_KEY`], and notifies
278 /// subscribers of the `TrustedTimeSource` attribute change.
279 pub fn set_trusted_time_source_persist<S: KvBlobStoreAccess, E: EventEmitter>(
280 &mut self,
281 source: Option<TrustedTimeSource>,
282 persist: &mut Persist<S>,
283 change_notifier: &dyn AttrChangeNotifier,
284 event_emitter: E,
285 ) -> Result<(), Error> {
286 if self.trusted_time_source != source {
287 self.set_trusted_time_source(source, change_notifier, event_emitter)?;
288
289 match source {
290 Some(source) => {
291 persist.store_tlv(TRUSTED_TIME_SOURCE_KEY, source)?;
292 }
293 None => {
294 persist.remove(TRUSTED_TIME_SOURCE_KEY)?;
295 }
296 }
297 }
298
299 Ok(())
300 }
301
302 /// Return the current UTC time if available, or the persisted Last-Known-Good UTC Time otherwise.
303 pub fn utc_time(&self) -> UtcTime {
304 if let Some(anchor) = self.anchor {
305 let elapsed_us = embassy_time::Instant::now()
306 .checked_duration_since(anchor)
307 .map(|d| d.as_micros())
308 .unwrap_or(0);
309
310 UtcTime::Reliable(self.utc_us.saturating_add(elapsed_us))
311 } else {
312 UtcTime::LastKnown(self.utc_us)
313 }
314 }
315
316 /// Return the Granularity reported on the wire for the TimeSync
317 /// cluster's `Granularity` attribute, derived from the most
318 /// recent [`Self::set_utc_time`] (with the spec-required
319 /// step-down and floor already applied) — or `NoTimeGranularity`
320 /// if no `set_utc_time` has been called since boot (per
321 /// the spec, which forbids `NoTimeGranularity` only while
322 /// `UTCTime ≠ Null`).
323 pub fn utc_time_granularity(&self) -> GranularityEnum {
324 if self.anchor.is_some() {
325 self.granularity
326 } else {
327 GranularityEnum::NoTimeGranularity
328 }
329 }
330
331 /// Return the TimeSource reported on the wire for the TimeSync
332 /// cluster's `TimeSource` attribute — `None` until the first
333 /// [`Self::set_utc_time`] (per spec).
334 pub fn utc_time_source(&self) -> TimeSourceEnum {
335 if self.anchor.is_some() {
336 self.source
337 } else {
338 TimeSourceEnum::None
339 }
340 }
341
342 /// Update the Last-Known-Good UTC Time (Matter Core spec),
343 /// capturing a fresh monotonic anchor so subsequent
344 /// [`Self::utc_time`] reads advance from the supplied value.
345 ///
346 /// Per the spec: the supplied `granularity` is recorded
347 /// stepped-down by one level (with a floor of
348 /// `MinutesGranularity` per spec); the supplied `source`
349 /// is recorded verbatim.
350 ///
351 /// The new value is written to the in-memory state immediately.
352 /// Persistence to `LKG_UTC_KEY` happens separately — the
353 /// TimeSync cluster handler invokes this from inside a
354 /// `kv.access(...)` closure and writes through the same handle.
355 /// Direct callers that need on-disk durability should call
356 /// [`Self::persist_lkg_utc`] explicitly.
357 pub fn set_utc_time(
358 &mut self,
359 utc_us: u64,
360 granularity: GranularityEnum,
361 source: TimeSourceEnum,
362 change_notifier: &dyn AttrChangeNotifier,
363 ) -> bool {
364 let stepped = match granularity {
365 GranularityEnum::MicrosecondsGranularity => GranularityEnum::MillisecondsGranularity,
366 GranularityEnum::MillisecondsGranularity => GranularityEnum::SecondsGranularity,
367 GranularityEnum::SecondsGranularity => GranularityEnum::MinutesGranularity,
368 // Minutes / NoTime → floor at Minutes
369 // (spec forbids NoTime while UTCTime is non-null).
370 _ => GranularityEnum::MinutesGranularity,
371 };
372
373 let changed = self.utc_us != utc_us || self.granularity != stepped || self.source != source;
374
375 if changed || self.anchor.is_none() {
376 self.utc_us = utc_us;
377 self.granularity = stepped;
378 self.source = source;
379 self.anchor = Some(embassy_time::Instant::now());
380
381 change_notifier.notify_attr_changed(
382 ROOT_ENDPOINT_ID,
383 TimeSyncHandler::CLUSTER.id,
384 AttributeId::UTCTime as _,
385 );
386 change_notifier.notify_attr_changed(
387 ROOT_ENDPOINT_ID,
388 TimeSyncHandler::CLUSTER.id,
389 AttributeId::Granularity as _,
390 );
391 change_notifier.notify_attr_changed(
392 ROOT_ENDPOINT_ID,
393 TimeSyncHandler::CLUSTER.id,
394 AttributeId::TimeSource as _,
395 );
396 }
397
398 changed
399 }
400
401 pub fn set_utc_time_persist<S: KvBlobStoreAccess>(
402 &mut self,
403 utc_us: u64,
404 granularity: GranularityEnum,
405 source: TimeSourceEnum,
406 persist: &mut Persist<S>,
407 change_notifier: &dyn AttrChangeNotifier,
408 ) -> Result<(), Error> {
409 const DELTA: u64 = 24 * 60 * 60 * 1_000_000; // 1 day in microseconds
410
411 let delta = self.utc_us_persisted.abs_diff(utc_us);
412
413 self.set_utc_time(utc_us, granularity, source, change_notifier);
414
415 if delta >= DELTA {
416 // As per the Matter Core spec, we have to persist the new LKG UTC at least once per month
417 // Since this would be an involved math, we instead persist if the new LKG UTC is different
418 // by more than a day than the previous one, which should be good enough to cover the requirement
419 // without needing a separate timer for periodic persistence.
420
421 info!("TimeSync: UTC time changed by more than a day, persisting");
422
423 persist.store_tlv(LKG_UTC_KEY, utc_us.to_le_bytes())?;
424 self.utc_us_persisted = utc_us;
425 }
426
427 Ok(())
428 }
429}
430
431/// Persisted Trusted Time Source descriptor (Matter Core spec).
432/// Records which fabric configured the source so that fabric removal can
433/// clear it and emit `MissingTrustedTimeSource`.
434#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, FromTLV, ToTLV)]
435#[cfg_attr(feature = "defmt", derive(defmt::Format))]
436pub struct TrustedTimeSource {
437 /// Fabric that installed the source (the `FabricIndex` injected by
438 /// the IM dispatcher into the `SetTrustedTimeSource` invoke).
439 pub fab_idx: NonZeroU8,
440 /// Node ID of the trusted source on that fabric.
441 pub node_id: NodeId,
442 /// Endpoint on the trusted source's node that hosts the TimeSync
443 /// cluster server.
444 pub endpoint: EndptId,
445}
446
447bitflags! {
448 /// Cluster-shape selectors for the [`TimeSyncHandler`]. Each bit
449 /// turns on exactly one Matter `Feature` — there are no
450 /// independent-optional toggles on this cluster, so the mapping
451 /// is 1:1.
452 ///
453 /// Used as the const-generic argument to [`cluster`] (via its
454 /// `bits()` value) to compute the matching `Cluster<'static>`
455 /// metadata, which is then installed onto the endpoint via the
456 /// `clusters!` / `root_endpoint!` macros (e.g.
457 /// `clusters!(eth, time_sync(time_zone, ntp_client); …)`).
458 #[derive(Default, Debug, Copy, Clone, Eq, PartialEq, Hash)]
459 pub struct Options: u8 {
460 /// Claim the Matter `TIME_ZONE` feature. Advertises `TimeZone`,
461 /// `DSTOffset`, `LocalTime`, `TimeZoneDatabase`,
462 /// `TimeZoneListMaxSize`, `DSTOffsetListMaxSize` attributes
463 /// and the `SetTimeZone` + `SetDSTOffset` commands.
464 const TIME_ZONE = 0x1;
465 /// Claim the Matter `NTP_CLIENT` feature. Advertises
466 /// `DefaultNTP` + `SupportsDNSResolve` attributes and the
467 /// `SetDefaultNTP` command.
468 const NTP_CLIENT = 0x2;
469 /// Claim the Matter `NTP_SERVER` feature. Advertises the
470 /// `NTPServerAvailable` attribute.
471 const NTP_SERVER = 0x4;
472 /// Claim the Matter `TIME_SYNC_CLIENT` feature. Advertises the
473 /// `TrustedTimeSource` attribute and the `SetTrustedTimeSource`
474 /// command.
475 const TIME_SYNC_CLIENT = 0x8;
476 }
477}
478
479/// One time-zone entry yielded by [`TimeSync::time_zone`] via the
480/// visitor callback. The lifetime `'a` is the borrow of the
481/// implementor's internal storage for the duration of the visit, so
482/// `name` can point straight into the implementor's table without
483/// copying.
484#[derive(Debug, Clone, Eq, PartialEq, Hash)]
485pub struct TimeZoneEntry<'a> {
486 /// Offset from UTC, in seconds.
487 pub offset: i32,
488 /// Matter-epoch microseconds after which this offset takes effect.
489 pub valid_at: u64,
490 /// Human-readable IANA time-zone name (`Europe/Sofia` …); `None`
491 /// if the implementation doesn't track names.
492 pub name: Option<&'a str>,
493}
494
495/// One DST-offset entry yielded by [`TimeSync::dst_offset`] via the
496/// visitor callback.
497#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
498pub struct DSTOffsetEntry {
499 /// Offset from local standard time, in seconds, while DST is in
500 /// effect.
501 pub offset: i32,
502 /// Matter-epoch microseconds at which the offset becomes valid.
503 pub valid_starting: u64,
504 /// Matter-epoch microseconds at which the offset stops being
505 /// valid. `None` means "indefinitely" (`Null` on the wire).
506 pub valid_until: Option<u64>,
507}
508
509/// Snapshot of the device's currently-configured trusted time source.
510/// Returned by [`TimeSync::trusted_time_source`] wrapped in a
511/// [`Nullable`].
512#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
513pub struct TrustedTimeSourceData {
514 /// Fabric index that configured this trusted time source.
515 pub fabric_index: u8,
516 /// Node ID of the trusted source.
517 pub node_id: u64,
518 /// Endpoint on the trusted source's node.
519 pub endpoint: u16,
520}
521
522/// Pluggable data source for the feature-gated members of the Time
523/// Synchronization cluster (`TIME_ZONE` / `NTP_CLIENT` / `NTP_SERVER`
524/// / `TIME_SYNC_CLIENT`).
525///
526/// The mandatory members — `UTCTime`, `Granularity`, `TimeSource`,
527/// and the `SetUTCTime` command — are handled by [`TimeSyncHandler`] directly
528/// against the built-in Matter RTC state and do **not** appear on this trait.
529///
530/// The `TIME_SYNC_CLIENT` feature (if enabled) is also handled by the handler
531/// directly against the `TrustedTimeSource` entry in the built-in Matter RTC state,
532/// so it also doesn't appear here.
533pub trait TimeSync {
534 // ---- NTP_CLIENT feature
535
536 /// Hostname or IP address of the default NTP server, or `Null` if
537 /// none is configured.
538 fn default_ntp(&self) -> Result<Nullable<&str>, Error>;
539
540 /// Whether the device's NTP-client resolver supports DNS names
541 /// (vs. only literal IP addresses).
542 fn supports_dns_resolve(&self) -> Result<bool, Error>;
543
544 // ---- NTP_SERVER feature
545
546 /// Whether the device is currently serving NTP queries.
547 fn ntp_server_available(&self) -> Result<bool, Error>;
548
549 // ---- TIME_ZONE feature
550
551 /// Stream the active time-zone entries into `visit`.
552 fn time_zone(
553 &self,
554 _visit: &mut dyn FnMut(&TimeZoneEntry<'_>) -> Result<(), Error>,
555 ) -> Result<(), Error>;
556
557 /// Stream the active DST-offset entries into `visit`.
558 fn dst_offset(
559 &self,
560 _visit: &mut dyn FnMut(&DSTOffsetEntry) -> Result<(), Error>,
561 ) -> Result<(), Error>;
562
563 /// Current local time in Matter-epoch microseconds, or `Null`.
564 fn local_time(&self) -> Result<Nullable<u64>, Error>;
565
566 /// How complete the device's IANA time-zone database is.
567 fn time_zone_database(&self) -> Result<TimeZoneDatabaseEnum, Error>;
568
569 /// Maximum length of the `TimeZone` list this device accepts.
570 fn time_zone_list_max_size(&self) -> Result<u8, Error>;
571
572 /// Maximum length of the `DSTOffset` list this device accepts.
573 fn dst_offset_list_max_size(&self) -> Result<u8, Error>;
574
575 // ---- Commands (feature-gated; default to `CommandNotFound`)
576
577 /// Handle `SetTimeZone` — gated by `TIME_ZONE`. Returns the
578 /// `DSTOffsetRequired` field for the response.
579 fn set_time_zone(&self, _request: &SetTimeZoneRequest<'_>) -> Result<bool, Error>;
580
581 /// Handle `SetDSTOffset` — gated by `TIME_ZONE`.
582 fn set_dst_offset(&self, _request: &SetDSTOffsetRequest<'_>) -> Result<(), Error>;
583
584 /// Handle `SetDefaultNTP` — gated by `NTP_CLIENT`.
585 fn set_default_ntp(&self, _request: &SetDefaultNTPRequest<'_>) -> Result<(), Error>;
586}
587
588impl<T> TimeSync for &T
589where
590 T: TimeSync,
591{
592 fn default_ntp(&self) -> Result<Nullable<&str>, Error> {
593 (*self).default_ntp()
594 }
595
596 fn supports_dns_resolve(&self) -> Result<bool, Error> {
597 (*self).supports_dns_resolve()
598 }
599
600 fn ntp_server_available(&self) -> Result<bool, Error> {
601 (*self).ntp_server_available()
602 }
603
604 fn time_zone(
605 &self,
606 visit: &mut dyn FnMut(&TimeZoneEntry<'_>) -> Result<(), Error>,
607 ) -> Result<(), Error> {
608 (*self).time_zone(visit)
609 }
610
611 fn dst_offset(
612 &self,
613 visit: &mut dyn FnMut(&DSTOffsetEntry) -> Result<(), Error>,
614 ) -> Result<(), Error> {
615 (*self).dst_offset(visit)
616 }
617
618 fn local_time(&self) -> Result<Nullable<u64>, Error> {
619 (*self).local_time()
620 }
621
622 fn time_zone_database(&self) -> Result<TimeZoneDatabaseEnum, Error> {
623 (*self).time_zone_database()
624 }
625
626 fn time_zone_list_max_size(&self) -> Result<u8, Error> {
627 (*self).time_zone_list_max_size()
628 }
629
630 fn dst_offset_list_max_size(&self) -> Result<u8, Error> {
631 (*self).dst_offset_list_max_size()
632 }
633
634 fn set_time_zone(&self, request: &SetTimeZoneRequest<'_>) -> Result<bool, Error> {
635 (*self).set_time_zone(request)
636 }
637
638 fn set_dst_offset(&self, request: &SetDSTOffsetRequest<'_>) -> Result<(), Error> {
639 (*self).set_dst_offset(request)
640 }
641
642 fn set_default_ntp(&self, request: &SetDefaultNTPRequest<'_>) -> Result<(), Error> {
643 (*self).set_default_ntp(request)
644 }
645}
646
647/// Default [`TimeSync`] implementation.
648///
649/// Suitable for devices that don't advertise the features
650/// `TIME_ZONE` / `NTP_CLIENT` / `NTP_SERVER`.
651impl TimeSync for () {
652 // ---- NTP_CLIENT feature
653
654 /// Hostname or IP address of the default NTP server, or `Null` if
655 /// none is configured.
656 fn default_ntp(&self) -> Result<Nullable<&str>, Error> {
657 Ok(Nullable::none())
658 }
659
660 /// Whether the device's NTP-client resolver supports DNS names
661 /// (vs. only literal IP addresses).
662 fn supports_dns_resolve(&self) -> Result<bool, Error> {
663 Ok(false)
664 }
665
666 // ---- NTP_SERVER feature
667
668 /// Whether the device is currently serving NTP queries.
669 fn ntp_server_available(&self) -> Result<bool, Error> {
670 Ok(false)
671 }
672
673 // ---- TIME_ZONE feature
674
675 /// Stream the active time-zone entries into `visit`. Default:
676 /// emit nothing (empty list on the wire).
677 fn time_zone(
678 &self,
679 _visit: &mut dyn FnMut(&TimeZoneEntry<'_>) -> Result<(), Error>,
680 ) -> Result<(), Error> {
681 Ok(())
682 }
683
684 /// Stream the active DST-offset entries into `visit`. Default:
685 /// emit nothing.
686 fn dst_offset(
687 &self,
688 _visit: &mut dyn FnMut(&DSTOffsetEntry) -> Result<(), Error>,
689 ) -> Result<(), Error> {
690 Ok(())
691 }
692
693 /// Current local time in Matter-epoch microseconds, or `Null`.
694 fn local_time(&self) -> Result<Nullable<u64>, Error> {
695 Ok(Nullable::none())
696 }
697
698 /// How complete the device's IANA time-zone database is.
699 fn time_zone_database(&self) -> Result<TimeZoneDatabaseEnum, Error> {
700 Ok(TimeZoneDatabaseEnum::None)
701 }
702
703 /// Maximum length of the `TimeZone` list this device accepts.
704 fn time_zone_list_max_size(&self) -> Result<u8, Error> {
705 Ok(0)
706 }
707
708 /// Maximum length of the `DSTOffset` list this device accepts.
709 fn dst_offset_list_max_size(&self) -> Result<u8, Error> {
710 Ok(0)
711 }
712
713 // ---- Commands (feature-gated; default to `CommandNotFound`)
714
715 /// Handle `SetTimeZone` — gated by `TIME_ZONE`. Returns the
716 /// `DSTOffsetRequired` field for the response.
717 fn set_time_zone(&self, _request: &SetTimeZoneRequest<'_>) -> Result<bool, Error> {
718 Err(ErrorCode::CommandNotFound.into())
719 }
720
721 /// Handle `SetDSTOffset` — gated by `TIME_ZONE`.
722 fn set_dst_offset(&self, _request: &SetDSTOffsetRequest<'_>) -> Result<(), Error> {
723 Err(ErrorCode::CommandNotFound.into())
724 }
725
726 /// Handle `SetDefaultNTP` — gated by `NTP_CLIENT`.
727 fn set_default_ntp(&self, _request: &SetDefaultNTPRequest<'_>) -> Result<(), Error> {
728 Err(ErrorCode::CommandNotFound.into())
729 }
730}
731
732// ---- Cluster-shape selection -------------------------------------------------
733
734const fn time_sync_attrs<const OPTS: u8>(attr: &Attribute, _: u16, _: u32) -> bool {
735 use AttributeId as A;
736
737 // Mandatory always (UTCTime, Granularity)
738 if !attr.quality.contains(Quality::OPTIONAL) {
739 return true;
740 }
741
742 // TimeSource: always exposed independently of features so the
743 // Matter test harness's TC_TIMESYNC_2_1 gate matches.
744 if attr.id == A::TimeSource as u32 {
745 return true;
746 }
747
748 let opts = Options::from_bits_truncate(OPTS);
749 if opts.contains(Options::TIME_ZONE)
750 && (attr.id == A::TimeZone as u32
751 || attr.id == A::DSTOffset as u32
752 || attr.id == A::LocalTime as u32
753 || attr.id == A::TimeZoneDatabase as u32
754 || attr.id == A::TimeZoneListMaxSize as u32
755 || attr.id == A::DSTOffsetListMaxSize as u32)
756 {
757 return true;
758 }
759
760 if opts.contains(Options::NTP_CLIENT)
761 && (attr.id == A::DefaultNTP as u32 || attr.id == A::SupportsDNSResolve as u32)
762 {
763 return true;
764 }
765
766 if opts.contains(Options::NTP_SERVER) && attr.id == A::NTPServerAvailable as u32 {
767 return true;
768 }
769
770 if opts.contains(Options::TIME_SYNC_CLIENT) && attr.id == A::TrustedTimeSource as u32 {
771 return true;
772 }
773
774 false
775}
776
777const fn time_sync_cmds<const OPTS: u8>(cmd: &Command, _: u16, _: u32) -> bool {
778 use CommandId as C;
779
780 // `SetUTCTime` is mandatory whenever the cluster is present
781 // (Matter Core spec, conformance `M`), independent of
782 // features. Devices reporting `Granularity = NoTimeGranularity`
783 // are additionally required to accept it.
784 if cmd.id == C::SetUTCTime as u32 {
785 return true;
786 }
787
788 let opts = Options::from_bits_truncate(OPTS);
789
790 if opts.contains(Options::TIME_ZONE)
791 && (cmd.id == C::SetTimeZone as u32 || cmd.id == C::SetDSTOffset as u32)
792 {
793 return true;
794 }
795
796 if opts.contains(Options::NTP_CLIENT) && cmd.id == C::SetDefaultNTP as u32 {
797 return true;
798 }
799
800 if opts.contains(Options::TIME_SYNC_CLIENT) && cmd.id == C::SetTrustedTimeSource as u32 {
801 return true;
802 }
803
804 false
805}
806
807/// Compute the `Cluster<'static>` metadata for a TimeSync handler
808/// advertising the features encoded in `OPTS` (the [`Options::bits`]
809/// value). See the [`Options`] flags for the per-bit detail.
810///
811/// Pair the returned shape with a [`TimeSync`] implementation whose
812/// methods supply real values for the corresponding option bits.
813pub const fn cluster<const OPTS: u8>() -> Cluster<'static> {
814 let opts = Options::from_bits_truncate(OPTS);
815
816 let mut features = 0u32;
817
818 if opts.contains(Options::TIME_ZONE) {
819 features |= Feature::TIME_ZONE.bits();
820 }
821
822 if opts.contains(Options::NTP_CLIENT) {
823 features |= Feature::NTP_CLIENT.bits();
824 }
825
826 if opts.contains(Options::NTP_SERVER) {
827 features |= Feature::NTP_SERVER.bits();
828 }
829
830 if opts.contains(Options::TIME_SYNC_CLIENT) {
831 features |= Feature::TIME_SYNC_CLIENT.bits();
832 }
833
834 Cluster {
835 feature_map: features,
836 with_attrs: time_sync_attrs::<OPTS>,
837 with_cmds: time_sync_cmds::<OPTS>,
838 ..FULL_CLUSTER
839 }
840}
841
842// ---- Handler -----------------------------------------------------------------
843
844/// Handler for the Time Synchronization Matter cluster.
845///
846/// Borrows a `&dyn TimeSync` data provider for the lifetime `'a` and
847/// forwards every non-builtin attribute read / command invoke to it.
848///
849/// The handler is **not** parameterized by cluster shape:
850/// [`Self::CLUSTER`](ClusterHandler::CLUSTER) is pinned to the
851/// empty-options form and only its `id` is consulted by the
852/// dispatcher. The on-wire shape — which optional attributes /
853/// commands / features are advertised — is decided by the cluster
854/// metadata supplied on the endpoint side (e.g. `clusters!(eth,
855/// time_sync(time_zone, ntp_client); …)`); per-attribute dispatch
856/// follows the endpoint's metadata, so the handler answers exactly
857/// what the endpoint exposes.
858#[derive(Clone)]
859pub struct TimeSyncHandler<'a> {
860 dataver: Dataver,
861 time_sync: &'a dyn TimeSync,
862}
863
864impl<'a> TimeSyncHandler<'a> {
865 /// Create a new handler bound to `time_sync` for its lifetime.
866 /// Pass `&()` (the no-op [`TimeSync`] impl) when no real time
867 /// source is available.
868 pub const fn new(dataver: Dataver, time_sync: &'a dyn TimeSync) -> Self {
869 Self { dataver, time_sync }
870 }
871
872 /// Adapt the handler instance to the generic `rs-matter` `Handler` trait
873 pub const fn adapt(self) -> HandlerAdaptor<Self> {
874 HandlerAdaptor(self)
875 }
876}
877
878impl ClusterHandler for TimeSyncHandler<'_> {
879 const CLUSTER: Cluster<'static> = cluster::<0>();
880
881 fn dataver(&self) -> u32 {
882 self.dataver.get()
883 }
884
885 fn dataver_changed(&self) {
886 self.dataver.changed();
887 }
888
889 // ---- Always-on reads (served from Matter-wide LKG state, not
890 // from the user-supplied `TimeSync` provider).
891
892 fn utc_time(&self, ctx: impl ReadContext) -> Result<Nullable<u64>, Error> {
893 Ok(Nullable::new(
894 ctx.matter()
895 .with_state(|state| state.rtc.utc_time())
896 .reliable(),
897 ))
898 }
899
900 fn granularity(&self, ctx: impl ReadContext) -> Result<GranularityEnum, Error> {
901 Ok(ctx
902 .matter()
903 .with_state(|state| state.rtc.utc_time_granularity()))
904 }
905
906 fn time_source(&self, ctx: impl ReadContext) -> Result<TimeSourceEnum, Error> {
907 Ok(ctx.matter().with_state(|state| state.rtc.utc_time_source()))
908 }
909
910 // ---- Feature-gated reads
911
912 // Served directly from the Matter-wide TrustedTimeSource state
913 // (Matter Core spec) — fabric-scoped storage lives on
914 // `MatterState::rtc`, not on the user-supplied `TimeSync` provider.
915 fn trusted_time_source<P: TLVBuilderParent>(
916 &self,
917 ctx: impl ReadContext,
918 builder: NullableBuilder<P, TrustedTimeSourceStructBuilder<P>>,
919 ) -> Result<P, Error> {
920 match ctx
921 .matter()
922 .with_state(|state| state.rtc.trusted_time_source())
923 {
924 Some(tts) => builder
925 .non_null()?
926 .fabric_index(tts.fab_idx.get())?
927 .node_id(tts.node_id)?
928 .endpoint(tts.endpoint)?
929 .end(),
930 None => builder.null(),
931 }
932 }
933
934 fn default_ntp<P: TLVBuilderParent>(
935 &self,
936 _ctx: impl ReadContext,
937 builder: NullableBuilder<P, Utf8StrBuilder<P>>,
938 ) -> Result<P, Error> {
939 match self.time_sync.default_ntp()?.into_option() {
940 Some(s) => builder.non_null()?.set(s),
941 None => builder.null(),
942 }
943 }
944
945 fn supports_dns_resolve(&self, _ctx: impl ReadContext) -> Result<bool, Error> {
946 self.time_sync.supports_dns_resolve()
947 }
948
949 fn ntp_server_available(&self, _ctx: impl ReadContext) -> Result<bool, Error> {
950 self.time_sync.ntp_server_available()
951 }
952
953 fn time_zone<P: TLVBuilderParent>(
954 &self,
955 _ctx: impl ReadContext,
956 builder: ArrayAttributeRead<TimeZoneStructArrayBuilder<P>, TimeZoneStructBuilder<P>>,
957 ) -> Result<P, Error> {
958 match builder {
959 ArrayAttributeRead::ReadAll(array) => {
960 let mut array_opt = Some(array);
961 self.time_sync.time_zone(&mut |entry| {
962 let array = unwrap!(array_opt.take());
963 let next = array
964 .push()?
965 .offset(entry.offset)?
966 .valid_at(entry.valid_at)?
967 .name(entry.name)?
968 .end()?;
969 array_opt = Some(next);
970 Ok(())
971 })?;
972 unwrap!(array_opt.take()).end()
973 }
974 ArrayAttributeRead::ReadOne(index, item_builder) => {
975 let mut item_opt = Some(item_builder);
976 let mut returned: Option<P> = None;
977 let mut current = 0u16;
978 self.time_sync.time_zone(&mut |entry| {
979 if returned.is_none() && current == index {
980 let b = unwrap!(item_opt.take());
981 returned = Some(
982 b.offset(entry.offset)?
983 .valid_at(entry.valid_at)?
984 .name(entry.name)?
985 .end()?,
986 );
987 }
988 current = current.saturating_add(1);
989 Ok(())
990 })?;
991 returned.ok_or_else(|| ErrorCode::ConstraintError.into())
992 }
993 ArrayAttributeRead::ReadNone(array) => array.end(),
994 }
995 }
996
997 fn dst_offset<P: TLVBuilderParent>(
998 &self,
999 _ctx: impl ReadContext,
1000 builder: ArrayAttributeRead<DSTOffsetStructArrayBuilder<P>, DSTOffsetStructBuilder<P>>,
1001 ) -> Result<P, Error> {
1002 match builder {
1003 ArrayAttributeRead::ReadAll(array) => {
1004 let mut array_opt = Some(array);
1005 self.time_sync.dst_offset(&mut |entry| {
1006 let array = unwrap!(array_opt.take());
1007 let next = array
1008 .push()?
1009 .offset(entry.offset)?
1010 .valid_starting(entry.valid_starting)?
1011 .valid_until(Nullable::new(entry.valid_until))?
1012 .end()?;
1013 array_opt = Some(next);
1014 Ok(())
1015 })?;
1016 unwrap!(array_opt.take()).end()
1017 }
1018 ArrayAttributeRead::ReadOne(index, item_builder) => {
1019 let mut item_opt = Some(item_builder);
1020 let mut returned: Option<P> = None;
1021 let mut current = 0u16;
1022 self.time_sync.dst_offset(&mut |entry| {
1023 if returned.is_none() && current == index {
1024 let b = unwrap!(item_opt.take());
1025 returned = Some(
1026 b.offset(entry.offset)?
1027 .valid_starting(entry.valid_starting)?
1028 .valid_until(Nullable::new(entry.valid_until))?
1029 .end()?,
1030 );
1031 }
1032 current = current.saturating_add(1);
1033 Ok(())
1034 })?;
1035 returned.ok_or_else(|| ErrorCode::ConstraintError.into())
1036 }
1037 ArrayAttributeRead::ReadNone(array) => array.end(),
1038 }
1039 }
1040
1041 fn local_time(&self, _ctx: impl ReadContext) -> Result<Nullable<u64>, Error> {
1042 self.time_sync.local_time()
1043 }
1044
1045 fn time_zone_database(&self, _ctx: impl ReadContext) -> Result<TimeZoneDatabaseEnum, Error> {
1046 self.time_sync.time_zone_database()
1047 }
1048
1049 fn time_zone_list_max_size(&self, _ctx: impl ReadContext) -> Result<u8, Error> {
1050 self.time_sync.time_zone_list_max_size()
1051 }
1052
1053 fn dst_offset_list_max_size(&self, _ctx: impl ReadContext) -> Result<u8, Error> {
1054 self.time_sync.dst_offset_list_max_size()
1055 }
1056
1057 // ---- Commands
1058
1059 fn handle_set_utc_time(
1060 &self,
1061 ctx: impl InvokeContext,
1062 request: SetUTCTimeRequest<'_>,
1063 ) -> Result<(), Error> {
1064 // Matter Core spec: regardless of the optional
1065 // `TimeSource` field in the request, the device SHALL set
1066 // `TimeSource` to `Admin` when `SetUTCTime` populates UTCTime.
1067 let utc_us = request.utc_time()?;
1068 let granularity = request.granularity()?;
1069 ctx.matter().with_state(|state| {
1070 state
1071 .rtc
1072 .set_utc_time(utc_us, granularity, TimeSourceEnum::Admin, &ctx)
1073 });
1074
1075 Ok(())
1076 }
1077
1078 // Matter Core spec — installs or clears the per-device
1079 // Trusted Time Source. The fabric performing the change is
1080 // recorded so that fabric removal can clear an entry it owns
1081 // and emit `MissingTrustedTimeSource`.
1082 fn handle_set_trusted_time_source(
1083 &self,
1084 ctx: impl InvokeContext,
1085 request: SetTrustedTimeSourceRequest<'_>,
1086 ) -> Result<(), Error> {
1087 let fab_idx = NonZeroU8::new(ctx.cmd().fab_idx).ok_or(ErrorCode::InvalidCommand)?;
1088
1089 let source = request
1090 .trusted_time_source()?
1091 .into_option()
1092 .map(|tts| {
1093 Ok::<_, Error>(TrustedTimeSource {
1094 fab_idx,
1095 node_id: tts.node_id()?,
1096 endpoint: tts.endpoint()?,
1097 })
1098 })
1099 .transpose()?;
1100
1101 let mut persist = Persist::new(ctx.kv());
1102
1103 ctx.matter().with_state(|state| {
1104 state
1105 .rtc
1106 .set_trusted_time_source_persist(source, &mut persist, &ctx, &ctx)
1107 })?;
1108
1109 persist.run()?;
1110
1111 Ok(())
1112 }
1113
1114 fn handle_set_time_zone<P: TLVBuilderParent>(
1115 &self,
1116 _ctx: impl InvokeContext,
1117 request: SetTimeZoneRequest<'_>,
1118 response: SetTimeZoneResponseBuilder<P>,
1119 ) -> Result<P, Error> {
1120 let dst_offset_required = self.time_sync.set_time_zone(&request)?;
1121 response.dst_offset_required(dst_offset_required)?.end()
1122 }
1123
1124 fn handle_set_dst_offset(
1125 &self,
1126 _ctx: impl InvokeContext,
1127 request: SetDSTOffsetRequest<'_>,
1128 ) -> Result<(), Error> {
1129 self.time_sync.set_dst_offset(&request)
1130 }
1131
1132 fn handle_set_default_ntp(
1133 &self,
1134 _ctx: impl InvokeContext,
1135 request: SetDefaultNTPRequest<'_>,
1136 ) -> Result<(), Error> {
1137 self.time_sync.set_default_ntp(&request)
1138 }
1139}
1140
1141impl core::fmt::Debug for TimeSyncHandler<'_> {
1142 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1143 f.debug_struct("TimeSyncHandler")
1144 .field("dataver", &self.dataver)
1145 .finish()
1146 }
1147}
1148
1149#[cfg(feature = "defmt")]
1150impl defmt::Format for TimeSyncHandler<'_> {
1151 fn format(&self, f: defmt::Formatter) {
1152 defmt::write!(f, "TimeSyncHandler {{ dataver: {} }}", self.dataver.get());
1153 }
1154}