Skip to main content

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}