use core::num::NonZeroU8;
use bitflags::bitflags;
use heapless::String;
use embassy_futures::select::select;
use crate::dm::endpoints::ROOT_ENDPOINT_ID;
use crate::dm::{
ArrayAttributeRead, AttrChangeNotifier, Attribute, Cluster, Command, Dataver, EndptId,
EventEmitter, HandlerContext, InvokeContext, NodeId, Quality, ReadContext,
};
use crate::error::{Error, ErrorCode};
use crate::persist::{
KvBlobStore, KvBlobStoreAccess, Persist, LKG_UTC_KEY, TIME_ZONE_KEY, TRUSTED_TIME_SOURCE_KEY,
};
use crate::tlv::{
FromTLV, Nullable, NullableBuilder, TLVBuilderParent, TLVElement, TLVTag, TLVWrite, ToTLV,
Utf8StrBuilder, TLV,
};
use crate::utils::cell::RefCell;
use crate::utils::epoch::FIRMWARE_BUILD_MATTER_US;
use crate::utils::init::{init, into_init, try_init, Init};
use crate::utils::storage::Vec;
use crate::utils::sync::blocking::Mutex;
use crate::utils::sync::Notification;
pub use crate::dm::clusters::decl::time_synchronization::*;
pub mod client;
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum UtcTime {
Reliable(u64),
LastKnown(u64),
}
impl UtcTime {
pub const fn reliable(&self) -> Option<u64> {
match self {
UtcTime::Reliable(utc) => Some(*utc),
UtcTime::LastKnown(_) => None,
}
}
pub const fn any(&self) -> u64 {
match self {
UtcTime::Reliable(utc) | UtcTime::LastKnown(utc) => *utc,
}
}
pub const fn reliable_secs(&self) -> Option<u64> {
match self {
UtcTime::Reliable(utc) => Some(*utc / 1_000_000),
UtcTime::LastKnown(_) => None,
}
}
pub const fn any_secs(&self) -> u64 {
match self {
UtcTime::Reliable(utc) | UtcTime::LastKnown(utc) => *utc / 1_000_000,
}
}
}
pub struct Rtc {
utc_us: u64,
utc_us_persisted: u64,
granularity: GranularityEnum,
source: TimeSourceEnum,
anchor: Option<embassy_time::Instant>,
trusted_time_source: Option<TrustedTimeSource>,
}
impl Rtc {
#[inline(always)]
pub(crate) const fn new() -> Self {
Self {
utc_us: FIRMWARE_BUILD_MATTER_US,
utc_us_persisted: FIRMWARE_BUILD_MATTER_US,
granularity: GranularityEnum::NoTimeGranularity,
source: TimeSourceEnum::None,
anchor: None,
trusted_time_source: None,
}
}
pub(crate) fn init() -> impl Init<Self> {
init!(Self {
utc_us: FIRMWARE_BUILD_MATTER_US,
utc_us_persisted: FIRMWARE_BUILD_MATTER_US,
granularity: GranularityEnum::NoTimeGranularity,
source: TimeSourceEnum::None,
anchor: None,
trusted_time_source: None,
})
}
fn reset(&mut self) {
self.utc_us = FIRMWARE_BUILD_MATTER_US;
self.utc_us_persisted = FIRMWARE_BUILD_MATTER_US;
self.granularity = GranularityEnum::NoTimeGranularity;
self.source = TimeSourceEnum::None;
self.anchor = None;
self.trusted_time_source = None;
}
pub fn reset_persist<S: KvBlobStore>(
&mut self,
mut store: S,
buf: &mut [u8],
) -> Result<(), Error> {
self.reset();
store.remove(LKG_UTC_KEY, buf)?;
store.remove(TRUSTED_TIME_SOURCE_KEY, buf)?;
Ok(())
}
pub fn load_persist<S: KvBlobStore>(&mut self, mut kv: S, buf: &mut [u8]) -> Result<(), Error> {
self.reset();
if let Some(data) = kv.load(LKG_UTC_KEY, buf)? {
let stored = u64::from_tlv(&TLVElement::new(data))?;
let floor = FIRMWARE_BUILD_MATTER_US;
self.utc_us_persisted = stored;
self.utc_us = stored.max(floor);
}
if let Some(data) = kv.load(TRUSTED_TIME_SOURCE_KEY, buf)? {
self.trusted_time_source = Some(TrustedTimeSource::from_tlv(&TLVElement::new(data))?);
}
Ok(())
}
pub fn trusted_time_source(&self) -> Option<TrustedTimeSource> {
self.trusted_time_source
}
pub fn set_trusted_time_source<E: EventEmitter>(
&mut self,
source: Option<TrustedTimeSource>,
change_notifier: &dyn AttrChangeNotifier,
event_emitter: E,
) -> Result<(), Error> {
if self.trusted_time_source != source {
let previous = self.trusted_time_source;
self.trusted_time_source = source;
change_notifier.notify_attr_changed(
ROOT_ENDPOINT_ID,
TimeSyncHandler::CLUSTER.id,
AttributeId::TrustedTimeSource as _,
);
if self.trusted_time_source.is_none() && previous.is_some() {
MissingTrustedTimeSource::emit_for(event_emitter, ROOT_ENDPOINT_ID, |b| b.end())?;
}
}
Ok(())
}
pub fn set_trusted_time_source_persist<S: KvBlobStoreAccess, E: EventEmitter>(
&mut self,
source: Option<TrustedTimeSource>,
persist: &mut Persist<S>,
change_notifier: &dyn AttrChangeNotifier,
event_emitter: E,
) -> Result<(), Error> {
if self.trusted_time_source != source {
self.set_trusted_time_source(source, change_notifier, event_emitter)?;
match source {
Some(source) => {
persist.store_tlv(TRUSTED_TIME_SOURCE_KEY, source)?;
}
None => {
persist.remove(TRUSTED_TIME_SOURCE_KEY)?;
}
}
}
Ok(())
}
pub fn utc_time(&self) -> UtcTime {
if let Some(anchor) = self.anchor {
let elapsed_us = embassy_time::Instant::now()
.checked_duration_since(anchor)
.map(|d| d.as_micros())
.unwrap_or(0);
UtcTime::Reliable(self.utc_us.saturating_add(elapsed_us))
} else {
UtcTime::LastKnown(self.utc_us)
}
}
pub fn utc_time_granularity(&self) -> GranularityEnum {
if self.anchor.is_some() {
self.granularity
} else {
GranularityEnum::NoTimeGranularity
}
}
pub fn utc_time_source(&self) -> TimeSourceEnum {
if self.anchor.is_some() {
self.source
} else {
TimeSourceEnum::None
}
}
pub fn set_utc_time(
&mut self,
utc_us: u64,
granularity: GranularityEnum,
source: TimeSourceEnum,
change_notifier: &dyn AttrChangeNotifier,
) -> bool {
let stepped = match granularity {
GranularityEnum::MicrosecondsGranularity => GranularityEnum::MillisecondsGranularity,
GranularityEnum::MillisecondsGranularity => GranularityEnum::SecondsGranularity,
GranularityEnum::SecondsGranularity => GranularityEnum::MinutesGranularity,
_ => GranularityEnum::MinutesGranularity,
};
let changed = self.utc_us != utc_us || self.granularity != stepped || self.source != source;
if changed || self.anchor.is_none() {
self.utc_us = utc_us;
self.granularity = stepped;
self.source = source;
self.anchor = Some(embassy_time::Instant::now());
change_notifier.notify_attr_changed(
ROOT_ENDPOINT_ID,
TimeSyncHandler::CLUSTER.id,
AttributeId::UTCTime as _,
);
change_notifier.notify_attr_changed(
ROOT_ENDPOINT_ID,
TimeSyncHandler::CLUSTER.id,
AttributeId::Granularity as _,
);
change_notifier.notify_attr_changed(
ROOT_ENDPOINT_ID,
TimeSyncHandler::CLUSTER.id,
AttributeId::TimeSource as _,
);
}
changed
}
pub fn set_utc_time_persist<S: KvBlobStoreAccess>(
&mut self,
utc_us: u64,
granularity: GranularityEnum,
source: TimeSourceEnum,
persist: &mut Persist<S>,
change_notifier: &dyn AttrChangeNotifier,
) -> Result<(), Error> {
const DELTA: u64 = 24 * 60 * 60 * 1_000_000;
let delta = self.utc_us_persisted.abs_diff(utc_us);
self.set_utc_time(utc_us, granularity, source, change_notifier);
if delta >= DELTA {
info!("TimeSync: UTC time changed by more than a day, persisting");
persist.store_tlv(LKG_UTC_KEY, utc_us.to_le_bytes())?;
self.utc_us_persisted = utc_us;
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, FromTLV, ToTLV)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct TrustedTimeSource {
pub fab_idx: NonZeroU8,
pub node_id: NodeId,
pub endpoint: EndptId,
}
bitflags! {
#[derive(Default, Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct Options: u8 {
const TIME_ZONE = 0x1;
const NTP_CLIENT = 0x2;
const NTP_SERVER = 0x4;
const TIME_SYNC_CLIENT = 0x8;
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct TimeZoneEntry<'a> {
pub offset: i32,
pub valid_at: u64,
pub name: Option<&'a str>,
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, FromTLV, ToTLV)]
pub struct DSTOffsetEntry {
pub offset: i32,
pub valid_starting: u64,
pub valid_until: Option<u64>,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub struct TrustedTimeSourceData {
pub fabric_index: u8,
pub node_id: u64,
pub endpoint: u16,
}
pub trait TimeZones {
fn time_zone(
&self,
visit: &mut dyn FnMut(&TimeZoneEntry<'_>) -> Result<(), Error>,
) -> Result<(), Error>;
fn dst_offset(
&self,
visit: &mut dyn FnMut(&DSTOffsetEntry) -> Result<(), Error>,
) -> Result<(), Error>;
fn time_zone_database(&self) -> Result<TimeZoneDatabaseEnum, Error>;
fn time_zone_list_max_size(&self) -> Result<u8, Error>;
fn dst_offset_list_max_size(&self) -> Result<u8, Error>;
fn set_time_zone(&self, request: &SetTimeZoneRequest<'_>) -> Result<bool, Error>;
fn set_dst_offset(&self, request: &SetDSTOffsetRequest<'_>) -> Result<(), Error>;
}
impl<T> TimeZones for &T
where
T: TimeZones,
{
fn time_zone(
&self,
visit: &mut dyn FnMut(&TimeZoneEntry<'_>) -> Result<(), Error>,
) -> Result<(), Error> {
(*self).time_zone(visit)
}
fn dst_offset(
&self,
visit: &mut dyn FnMut(&DSTOffsetEntry) -> Result<(), Error>,
) -> Result<(), Error> {
(*self).dst_offset(visit)
}
fn time_zone_database(&self) -> Result<TimeZoneDatabaseEnum, Error> {
(*self).time_zone_database()
}
fn time_zone_list_max_size(&self) -> Result<u8, Error> {
(*self).time_zone_list_max_size()
}
fn dst_offset_list_max_size(&self) -> Result<u8, Error> {
(*self).dst_offset_list_max_size()
}
fn set_time_zone(&self, request: &SetTimeZoneRequest<'_>) -> Result<bool, Error> {
(*self).set_time_zone(request)
}
fn set_dst_offset(&self, request: &SetDSTOffsetRequest<'_>) -> Result<(), Error> {
(*self).set_dst_offset(request)
}
}
pub trait NtpClient {
fn default_ntp(&self) -> Result<Nullable<&str>, Error>;
fn supports_dns_resolve(&self) -> Result<bool, Error>;
fn set_default_ntp(&self, request: &SetDefaultNTPRequest<'_>) -> Result<(), Error>;
}
impl<T> NtpClient for &T
where
T: NtpClient,
{
fn default_ntp(&self) -> Result<Nullable<&str>, Error> {
(*self).default_ntp()
}
fn supports_dns_resolve(&self) -> Result<bool, Error> {
(*self).supports_dns_resolve()
}
fn set_default_ntp(&self, request: &SetDefaultNTPRequest<'_>) -> Result<(), Error> {
(*self).set_default_ntp(request)
}
}
pub trait NtpServer {
fn ntp_server_available(&self) -> Result<bool, Error>;
}
impl<T> NtpServer for &T
where
T: NtpServer,
{
fn ntp_server_available(&self) -> Result<bool, Error> {
(*self).ntp_server_available()
}
}
pub const TIME_ZONE_NAME_MAX: usize = 64;
#[derive(FromTLV, ToTLV)]
struct TimeZoneOwned {
offset: i32,
valid_at: u64,
name: Option<String<TIME_ZONE_NAME_MAX>>,
}
struct TimeZoneStoreData<const TIME_ZONE_MAX: usize, const DST_OFFSET_MAX: usize> {
time_zone: Vec<TimeZoneOwned, TIME_ZONE_MAX>,
dst_offset: Vec<DSTOffsetEntry, DST_OFFSET_MAX>,
}
impl<const TIME_ZONE_MAX: usize, const DST_OFFSET_MAX: usize>
TimeZoneStoreData<TIME_ZONE_MAX, DST_OFFSET_MAX>
{
const fn new() -> Self {
Self {
time_zone: Vec::new(),
dst_offset: Vec::new(),
}
}
fn init() -> impl Init<Self> {
init!(Self {
time_zone <- Vec::init(),
dst_offset <- Vec::init(),
})
}
}
impl<'a, const TIME_ZONE_MAX: usize, const DST_OFFSET_MAX: usize> FromTLV<'a>
for TimeZoneStoreData<TIME_ZONE_MAX, DST_OFFSET_MAX>
{
fn from_tlv(tlv: &TLVElement<'a>) -> Result<Self, Error> {
let tlv = tlv.structure()?;
Ok(Self {
time_zone: FromTLV::from_tlv(&tlv.ctx(0)?)?,
dst_offset: FromTLV::from_tlv(&tlv.ctx(1)?)?,
})
}
fn init_from_tlv(tlv: TLVElement<'a>) -> impl Init<Self, Error> {
into_init(move || {
let seq = tlv.structure()?;
let init = try_init!(Self {
time_zone <- Vec::<TimeZoneOwned, TIME_ZONE_MAX>::init_from_tlv(seq.ctx(0)?),
dst_offset <- Vec::<DSTOffsetEntry, DST_OFFSET_MAX>::init_from_tlv(seq.ctx(1)?),
}? Error);
Ok(init)
})
}
}
impl<const TIME_ZONE_MAX: usize, const DST_OFFSET_MAX: usize> ToTLV
for TimeZoneStoreData<TIME_ZONE_MAX, DST_OFFSET_MAX>
{
fn to_tlv<W: TLVWrite>(&self, tag: &TLVTag, mut tw: W) -> Result<(), Error> {
tw.start_struct(tag)?;
self.time_zone.to_tlv(&TLVTag::Context(0), &mut tw)?;
self.dst_offset.to_tlv(&TLVTag::Context(1), &mut tw)?;
tw.end_container()
}
fn tlv_iter(&self, tag: TLVTag) -> impl Iterator<Item = Result<TLV<'_>, Error>> {
use crate::tlv::TLVIter;
core::iter::empty()
.start_struct(tag)
.chain_iter(self.time_zone.tlv_iter(TLVTag::Context(0)))
.chain_iter(self.dst_offset.tlv_iter(TLVTag::Context(1)))
.end_container()
}
}
struct TimeZoneStoreState<const TIME_ZONE_MAX: usize, const DST_OFFSET_MAX: usize> {
data: TimeZoneStoreData<TIME_ZONE_MAX, DST_OFFSET_MAX>,
generation: u32,
}
impl<const TIME_ZONE_MAX: usize, const DST_OFFSET_MAX: usize>
TimeZoneStoreState<TIME_ZONE_MAX, DST_OFFSET_MAX>
{
const fn new() -> Self {
Self {
data: TimeZoneStoreData::new(),
generation: 0,
}
}
fn init() -> impl Init<Self> {
init!(Self {
data <- TimeZoneStoreData::init(),
generation: 0,
})
}
}
pub struct TimeZoneStore<const TIME_ZONE_MAX: usize = 2, const DST_OFFSET_MAX: usize = 2> {
state: Mutex<RefCell<TimeZoneStoreState<TIME_ZONE_MAX, DST_OFFSET_MAX>>>,
changed: Notification,
}
impl<const TIME_ZONE_MAX: usize, const DST_OFFSET_MAX: usize>
TimeZoneStore<TIME_ZONE_MAX, DST_OFFSET_MAX>
{
pub const fn new() -> Self {
Self {
state: Mutex::new(RefCell::new(TimeZoneStoreState::new())),
changed: Notification::new(),
}
}
pub fn init() -> impl Init<Self> {
init!(Self {
state <- Mutex::init(RefCell::init(TimeZoneStoreState::init())),
changed <- Notification::init(),
})
}
pub async fn wait_changed(&self) {
self.changed.wait().await
}
pub fn note_changed(&self) {
self.changed.notify();
}
pub fn load_persist<S: KvBlobStore>(&self, mut store: S, buf: &mut [u8]) -> Result<(), Error> {
let Some(data) = store.load(TIME_ZONE_KEY, buf)? else {
return Ok(());
};
let loaded = TimeZoneStoreData::from_tlv(&TLVElement::new(data))?;
self.state.lock(|state| {
let mut state = state.borrow_mut();
state.data = loaded;
});
info!("Loaded TimeZone / DSTOffset lists from storage");
Ok(())
}
fn store_persist<S: KvBlobStoreAccess>(&self, kv: S) -> Result<(), Error> {
let mut persist = Persist::new(kv);
self.state.lock(|state| {
let state = state.borrow();
persist.store_tlv(TIME_ZONE_KEY, &state.data)
})?;
persist.run()
}
pub fn generation(&self) -> u32 {
self.state.lock(|state| state.borrow().generation)
}
pub fn active_time_zone(&self, now: u64) -> (i32, Option<String<TIME_ZONE_NAME_MAX>>) {
self.state.lock(|state| {
let state = state.borrow();
state
.data
.time_zone
.iter()
.rfind(|entry| entry.valid_at <= now)
.map(|entry| (entry.offset, entry.name.clone()))
.unwrap_or((0, None))
})
}
pub fn active_dst_offset(&self, now: u64) -> Option<i32> {
self.state.lock(|state| {
let state = state.borrow();
state
.data
.dst_offset
.iter()
.find(|entry| {
entry.valid_starting <= now
&& entry.valid_until.map(|until| now < until).unwrap_or(true)
})
.map(|entry| entry.offset)
})
}
pub fn dst_table_empty(&self) -> bool {
self.state
.lock(|state| state.borrow().data.dst_offset.is_empty())
}
pub fn dst_usable(&self, now: u64) -> bool {
self.state.lock(|state| {
let state = state.borrow();
state
.data
.dst_offset
.iter()
.any(|entry| entry.valid_until.map(|until| now < until).unwrap_or(true))
})
}
pub fn next_transition(&self, now: u64) -> Option<u64> {
self.state.lock(|state| {
let state = state.borrow();
let tz = state
.data
.time_zone
.iter()
.map(|entry| entry.valid_at)
.filter(|at| *at > now)
.min();
let dst = state
.data
.dst_offset
.iter()
.flat_map(|entry| {
[Some(entry.valid_starting), entry.valid_until]
.into_iter()
.flatten()
})
.filter(|at| *at > now)
.min();
match (tz, dst) {
(Some(a), Some(b)) => Some(a.min(b)),
(a, b) => a.or(b),
}
})
}
fn validate_dst_offset(&self, request: &SetDSTOffsetRequest<'_>) -> Result<(), Error> {
let mut prev_starting: Option<u64> = None;
let mut prev_until: Option<u64> = None;
let mut seen_null_until = false;
for (index, entry) in request.dst_offset()?.iter().enumerate() {
let entry = entry?;
if index == DST_OFFSET_MAX {
return Err(ErrorCode::ResourceExhausted.into());
}
if seen_null_until {
Err(ErrorCode::ConstraintError)?;
}
let starting = entry.valid_starting()?;
if let Some(prev) = prev_starting {
if starting <= prev {
Err(ErrorCode::ConstraintError)?;
}
}
if let Some(until) = prev_until {
if starting < until {
Err(ErrorCode::ConstraintError)?;
}
}
match entry.valid_until()?.into_option() {
Some(until) => {
if starting >= until {
Err(ErrorCode::ConstraintError)?;
}
prev_until = Some(until);
}
None => seen_null_until = true,
}
prev_starting = Some(starting);
}
Ok(())
}
}
impl<const TIME_ZONE_MAX: usize, const DST_OFFSET_MAX: usize> Default
for TimeZoneStore<TIME_ZONE_MAX, DST_OFFSET_MAX>
{
fn default() -> Self {
Self::new()
}
}
impl<const TIME_ZONE_MAX: usize, const DST_OFFSET_MAX: usize> TimeZones
for TimeZoneStore<TIME_ZONE_MAX, DST_OFFSET_MAX>
{
fn time_zone(
&self,
visit: &mut dyn FnMut(&TimeZoneEntry<'_>) -> Result<(), Error>,
) -> Result<(), Error> {
self.state.lock(|state| {
let state = state.borrow();
if state.data.time_zone.is_empty() {
return visit(&TimeZoneEntry {
offset: 0,
valid_at: 0,
name: None,
});
}
for entry in state.data.time_zone.iter() {
visit(&TimeZoneEntry {
offset: entry.offset,
valid_at: entry.valid_at,
name: entry.name.as_deref(),
})?;
}
Ok(())
})
}
fn dst_offset(
&self,
visit: &mut dyn FnMut(&DSTOffsetEntry) -> Result<(), Error>,
) -> Result<(), Error> {
self.state.lock(|state| {
let state = state.borrow();
for entry in state.data.dst_offset.iter() {
visit(entry)?;
}
Ok(())
})
}
fn time_zone_database(&self) -> Result<TimeZoneDatabaseEnum, Error> {
Ok(TimeZoneDatabaseEnum::None)
}
fn time_zone_list_max_size(&self) -> Result<u8, Error> {
Ok(TIME_ZONE_MAX as u8)
}
fn dst_offset_list_max_size(&self) -> Result<u8, Error> {
Ok(DST_OFFSET_MAX as u8)
}
fn set_time_zone(&self, request: &SetTimeZoneRequest<'_>) -> Result<bool, Error> {
let mut prev_valid_at: Option<u64> = None;
for (index, entry) in request.time_zone()?.iter().enumerate() {
let entry = entry?;
if index == TIME_ZONE_MAX {
return Err(ErrorCode::ResourceExhausted.into());
}
let valid_at = entry.valid_at()?;
if (index == 0) != (valid_at == 0) {
Err(ErrorCode::ConstraintError)?;
}
if let Some(prev) = prev_valid_at {
if valid_at <= prev {
Err(ErrorCode::ConstraintError)?;
}
}
prev_valid_at = Some(valid_at);
let offset = entry.offset()?;
if !(-12 * 3600..=14 * 3600).contains(&offset) {
Err(ErrorCode::ConstraintError)?;
}
if let Some(name) = entry.name()? {
if name.len() > TIME_ZONE_NAME_MAX {
Err(ErrorCode::ConstraintError)?;
}
}
}
self.state.lock(|state| {
let mut state = state.borrow_mut();
state.data.time_zone.clear();
for entry in request.time_zone()?.iter() {
let entry = entry?;
let name = match entry.name()? {
Some(name) => {
Some(String::try_from(name).map_err(|_| ErrorCode::ConstraintError)?)
}
None => None,
};
unwrap!(state
.data
.time_zone
.push(TimeZoneOwned {
offset: entry.offset()?,
valid_at: entry.valid_at()?,
name,
})
.ok());
}
state.data.dst_offset.clear();
state.generation = state.generation.wrapping_add(1);
Ok::<_, Error>(())
})?;
self.note_changed();
Ok(true)
}
fn set_dst_offset(&self, request: &SetDSTOffsetRequest<'_>) -> Result<(), Error> {
let validated = self.validate_dst_offset(request);
if let Err(e) = validated {
self.state.lock(|state| {
let mut state = state.borrow_mut();
state.data.dst_offset.clear();
state.generation = state.generation.wrapping_add(1);
});
self.note_changed();
return Err(e);
}
self.state.lock(|state| {
let mut state = state.borrow_mut();
state.data.dst_offset.clear();
for entry in request.dst_offset()?.iter() {
let entry = entry?;
unwrap!(state
.data
.dst_offset
.push(DSTOffsetEntry {
offset: entry.offset()?,
valid_starting: entry.valid_starting()?,
valid_until: entry.valid_until()?.into_option(),
})
.ok());
}
state.generation = state.generation.wrapping_add(1);
Ok::<_, Error>(())
})?;
self.note_changed();
Ok(())
}
}
const fn time_sync_attrs<const OPTS: u8>(attr: &Attribute, _: u16, _: u32) -> bool {
use AttributeId as A;
if !attr.quality.contains(Quality::OPTIONAL) {
return true;
}
if attr.id == A::TimeSource as u32 {
return true;
}
let opts = Options::from_bits_truncate(OPTS);
if opts.contains(Options::TIME_ZONE)
&& (attr.id == A::TimeZone as u32
|| attr.id == A::DSTOffset as u32
|| attr.id == A::LocalTime as u32
|| attr.id == A::TimeZoneDatabase as u32
|| attr.id == A::TimeZoneListMaxSize as u32
|| attr.id == A::DSTOffsetListMaxSize as u32)
{
return true;
}
if opts.contains(Options::NTP_CLIENT)
&& (attr.id == A::DefaultNTP as u32 || attr.id == A::SupportsDNSResolve as u32)
{
return true;
}
if opts.contains(Options::NTP_SERVER) && attr.id == A::NTPServerAvailable as u32 {
return true;
}
if opts.contains(Options::TIME_SYNC_CLIENT) && attr.id == A::TrustedTimeSource as u32 {
return true;
}
false
}
const fn time_sync_cmds<const OPTS: u8>(cmd: &Command, _: u16, _: u32) -> bool {
use CommandId as C;
if cmd.id == C::SetUTCTime as u32 {
return true;
}
let opts = Options::from_bits_truncate(OPTS);
if opts.contains(Options::TIME_ZONE)
&& (cmd.id == C::SetTimeZone as u32 || cmd.id == C::SetDSTOffset as u32)
{
return true;
}
if opts.contains(Options::NTP_CLIENT) && cmd.id == C::SetDefaultNTP as u32 {
return true;
}
if opts.contains(Options::TIME_SYNC_CLIENT) && cmd.id == C::SetTrustedTimeSource as u32 {
return true;
}
false
}
pub const fn cluster<const OPTS: u8>() -> Cluster<'static> {
let opts = Options::from_bits_truncate(OPTS);
let mut features = 0u32;
if opts.contains(Options::TIME_ZONE) {
features |= Feature::TIME_ZONE.bits();
}
if opts.contains(Options::NTP_CLIENT) {
features |= Feature::NTP_CLIENT.bits();
}
if opts.contains(Options::NTP_SERVER) {
features |= Feature::NTP_SERVER.bits();
}
if opts.contains(Options::TIME_SYNC_CLIENT) {
features |= Feature::TIME_SYNC_CLIENT.bits();
}
Cluster {
feature_map: features,
with_attrs: time_sync_attrs::<OPTS>,
with_cmds: time_sync_cmds::<OPTS>,
..FULL_CLUSTER
}
}
#[derive(Clone)]
pub struct TimeSyncHandler<'a> {
dataver: Dataver,
time_zones: Option<&'a dyn TimeZones>,
ntp_client: Option<&'a dyn NtpClient>,
ntp_server: Option<&'a dyn NtpServer>,
tz_store: Option<&'a TimeZoneStore>,
}
impl<'a> TimeSyncHandler<'a> {
pub const fn new(dataver: Dataver) -> Self {
Self {
dataver,
time_zones: None,
ntp_client: None,
ntp_server: None,
tz_store: None,
}
}
pub const fn new_with_time_zone(dataver: Dataver, store: &'a TimeZoneStore) -> Self {
Self {
dataver,
time_zones: Some(store),
ntp_client: None,
ntp_server: None,
tz_store: Some(store),
}
}
pub const fn with_time_zones(mut self, time_zones: &'a dyn TimeZones) -> Self {
self.time_zones = Some(time_zones);
self
}
pub const fn with_ntp_client(mut self, ntp_client: &'a dyn NtpClient) -> Self {
self.ntp_client = Some(ntp_client);
self
}
pub const fn with_ntp_server(mut self, ntp_server: &'a dyn NtpServer) -> Self {
self.ntp_server = Some(ntp_server);
self
}
pub const fn adapt(self) -> HandlerAdaptor<Self> {
HandlerAdaptor(self)
}
}
impl ClusterHandler for TimeSyncHandler<'_> {
const CLUSTER: Cluster<'static> = cluster::<0>();
fn dataver(&self) -> u32 {
self.dataver.get()
}
fn dataver_changed(&self) {
self.dataver.changed();
}
async fn run(&self, ctx: impl HandlerContext) -> Result<(), Error> {
let Some(store) = self.tz_store else {
return core::future::pending().await;
};
let mut last_tz: Option<i32> = None;
let mut last_dst_active: Option<bool> = None;
let mut last_dst_usable: Option<bool> = None;
loop {
let now = ctx
.matter()
.with_state(|state| state.rtc.utc_time())
.reliable();
let next = if let Some(now) = now {
let (tz_offset, tz_name) = store.active_time_zone(now);
if last_tz != Some(tz_offset) {
if last_tz.is_some() {
let emitted = TimeZoneStatus::emit_for(&ctx, ROOT_ENDPOINT_ID, |event| {
event.offset(tz_offset)?.name(tz_name.as_deref())?.end()
});
if let Err(e) = emitted {
warn!("Failed to emit TimeZoneStatus: {:?}", e);
}
}
last_tz = Some(tz_offset);
}
let dst_active = store.active_dst_offset(now).is_some();
if last_dst_active != Some(dst_active) {
if last_dst_active.is_some() || dst_active {
let emitted = DSTStatus::emit_for(&ctx, ROOT_ENDPOINT_ID, |event| {
event.dst_offset_active(dst_active)?.end()
});
if let Err(e) = emitted {
warn!("Failed to emit DSTStatus: {:?}", e);
}
}
last_dst_active = Some(dst_active);
}
let dst_usable = store.dst_usable(now);
if last_dst_usable != Some(dst_usable) {
if !dst_usable && last_dst_usable == Some(true) {
let emitted =
DSTTableEmpty::emit_for(&ctx, ROOT_ENDPOINT_ID, |event| event.end());
if let Err(e) = emitted {
warn!("Failed to emit DSTTableEmpty: {:?}", e);
}
}
last_dst_usable = Some(dst_usable);
}
store.next_transition(now)
} else {
None
};
let boundary = async {
match (next, now) {
(Some(at), Some(now)) => {
let delta_us = at.saturating_sub(now).saturating_add(100_000);
embassy_time::Timer::after(embassy_time::Duration::from_micros(delta_us))
.await
}
_ => core::future::pending().await,
}
};
select(boundary, store.wait_changed()).await;
}
}
fn utc_time(&self, ctx: impl ReadContext) -> Result<Nullable<u64>, Error> {
Ok(Nullable::new(
ctx.matter()
.with_state(|state| state.rtc.utc_time())
.reliable(),
))
}
fn granularity(&self, ctx: impl ReadContext) -> Result<GranularityEnum, Error> {
Ok(ctx
.matter()
.with_state(|state| state.rtc.utc_time_granularity()))
}
fn time_source(&self, ctx: impl ReadContext) -> Result<TimeSourceEnum, Error> {
Ok(ctx.matter().with_state(|state| state.rtc.utc_time_source()))
}
fn trusted_time_source<P: TLVBuilderParent>(
&self,
ctx: impl ReadContext,
builder: NullableBuilder<P, TrustedTimeSourceStructBuilder<P>>,
) -> Result<P, Error> {
match ctx
.matter()
.with_state(|state| state.rtc.trusted_time_source())
{
Some(tts) => builder
.non_null()?
.fabric_index(tts.fab_idx.get())?
.node_id(tts.node_id)?
.endpoint(tts.endpoint)?
.end(),
None => builder.null(),
}
}
fn default_ntp<P: TLVBuilderParent>(
&self,
_ctx: impl ReadContext,
builder: NullableBuilder<P, Utf8StrBuilder<P>>,
) -> Result<P, Error> {
match self
.ntp_client
.ok_or(ErrorCode::AttributeNotFound)?
.default_ntp()?
.into_option()
{
Some(s) => builder.non_null()?.set(s),
None => builder.null(),
}
}
fn supports_dns_resolve(&self, _ctx: impl ReadContext) -> Result<bool, Error> {
self.ntp_client
.ok_or(ErrorCode::AttributeNotFound)?
.supports_dns_resolve()
}
fn ntp_server_available(&self, _ctx: impl ReadContext) -> Result<bool, Error> {
self.ntp_server
.ok_or(ErrorCode::AttributeNotFound)?
.ntp_server_available()
}
fn time_zone<P: TLVBuilderParent>(
&self,
_ctx: impl ReadContext,
builder: ArrayAttributeRead<TimeZoneStructArrayBuilder<P>, TimeZoneStructBuilder<P>>,
) -> Result<P, Error> {
match builder {
ArrayAttributeRead::ReadAll(array) => {
let mut array_opt = Some(array);
self.time_zones
.ok_or(ErrorCode::AttributeNotFound)?
.time_zone(&mut |entry| {
let array = unwrap!(array_opt.take());
let next = array
.push()?
.offset(entry.offset)?
.valid_at(entry.valid_at)?
.name(entry.name)?
.end()?;
array_opt = Some(next);
Ok(())
})?;
unwrap!(array_opt.take()).end()
}
ArrayAttributeRead::ReadOne(index, item_builder) => {
let mut item_opt = Some(item_builder);
let mut returned: Option<P> = None;
let mut current = 0u16;
self.time_zones
.ok_or(ErrorCode::AttributeNotFound)?
.time_zone(&mut |entry| {
if returned.is_none() && current == index {
let b = unwrap!(item_opt.take());
returned = Some(
b.offset(entry.offset)?
.valid_at(entry.valid_at)?
.name(entry.name)?
.end()?,
);
}
current = current.saturating_add(1);
Ok(())
})?;
returned.ok_or_else(|| ErrorCode::ConstraintError.into())
}
ArrayAttributeRead::ReadNone(array) => array.end(),
}
}
fn dst_offset<P: TLVBuilderParent>(
&self,
_ctx: impl ReadContext,
builder: ArrayAttributeRead<DSTOffsetStructArrayBuilder<P>, DSTOffsetStructBuilder<P>>,
) -> Result<P, Error> {
match builder {
ArrayAttributeRead::ReadAll(array) => {
let mut array_opt = Some(array);
self.time_zones
.ok_or(ErrorCode::AttributeNotFound)?
.dst_offset(&mut |entry| {
let array = unwrap!(array_opt.take());
let next = array
.push()?
.offset(entry.offset)?
.valid_starting(entry.valid_starting)?
.valid_until(Nullable::new(entry.valid_until))?
.end()?;
array_opt = Some(next);
Ok(())
})?;
unwrap!(array_opt.take()).end()
}
ArrayAttributeRead::ReadOne(index, item_builder) => {
let mut item_opt = Some(item_builder);
let mut returned: Option<P> = None;
let mut current = 0u16;
self.time_zones
.ok_or(ErrorCode::AttributeNotFound)?
.dst_offset(&mut |entry| {
if returned.is_none() && current == index {
let b = unwrap!(item_opt.take());
returned = Some(
b.offset(entry.offset)?
.valid_starting(entry.valid_starting)?
.valid_until(Nullable::new(entry.valid_until))?
.end()?,
);
}
current = current.saturating_add(1);
Ok(())
})?;
returned.ok_or_else(|| ErrorCode::ConstraintError.into())
}
ArrayAttributeRead::ReadNone(array) => array.end(),
}
}
fn local_time(&self, ctx: impl ReadContext) -> Result<Nullable<u64>, Error> {
let Some(utc) = ctx
.matter()
.with_state(|state| state.rtc.utc_time())
.reliable()
else {
return Ok(Nullable::none());
};
let mut offset_secs: i64 = 0;
self.time_zones
.ok_or(ErrorCode::AttributeNotFound)?
.time_zone(&mut |entry| {
if entry.valid_at <= utc {
offset_secs = entry.offset as i64;
}
Ok(())
})?;
let mut usable = false;
let mut active: Option<i32> = None;
self.time_zones
.ok_or(ErrorCode::AttributeNotFound)?
.dst_offset(&mut |entry| {
let unexpired = entry.valid_until.map(|u| utc < u).unwrap_or(true);
if unexpired {
usable = true;
if entry.valid_starting <= utc {
active = Some(entry.offset);
}
}
Ok(())
})?;
if !usable {
return Ok(Nullable::none());
}
offset_secs += active.unwrap_or(0) as i64;
Ok(Nullable::some(utc.saturating_add_signed(
offset_secs.saturating_mul(1_000_000),
)))
}
fn time_zone_database(&self, _ctx: impl ReadContext) -> Result<TimeZoneDatabaseEnum, Error> {
self.time_zones
.ok_or(ErrorCode::AttributeNotFound)?
.time_zone_database()
}
fn time_zone_list_max_size(&self, _ctx: impl ReadContext) -> Result<u8, Error> {
self.time_zones
.ok_or(ErrorCode::AttributeNotFound)?
.time_zone_list_max_size()
}
fn dst_offset_list_max_size(&self, _ctx: impl ReadContext) -> Result<u8, Error> {
self.time_zones
.ok_or(ErrorCode::AttributeNotFound)?
.dst_offset_list_max_size()
}
fn handle_set_utc_time(
&self,
ctx: impl InvokeContext,
request: SetUTCTimeRequest<'_>,
) -> Result<(), Error> {
let utc_us = request.utc_time()?;
let granularity = request.granularity()?;
ctx.matter().with_state(|state| {
state
.rtc
.set_utc_time(utc_us, granularity, TimeSourceEnum::Admin, &ctx)
});
if let Some(store) = self.tz_store {
store.note_changed();
}
Ok(())
}
fn handle_set_trusted_time_source(
&self,
ctx: impl InvokeContext,
request: SetTrustedTimeSourceRequest<'_>,
) -> Result<(), Error> {
let fab_idx = NonZeroU8::new(ctx.cmd().fab_idx).ok_or(ErrorCode::InvalidCommand)?;
let source = request
.trusted_time_source()?
.into_option()
.map(|tts| {
Ok::<_, Error>(TrustedTimeSource {
fab_idx,
node_id: tts.node_id()?,
endpoint: tts.endpoint()?,
})
})
.transpose()?;
let mut persist = Persist::new(ctx.kv());
ctx.matter().with_state(|state| {
state
.rtc
.set_trusted_time_source_persist(source, &mut persist, &ctx, &ctx)
})?;
persist.run()?;
Ok(())
}
fn handle_set_time_zone<P: TLVBuilderParent>(
&self,
ctx: impl InvokeContext,
request: SetTimeZoneRequest<'_>,
response: SetTimeZoneResponseBuilder<P>,
) -> Result<P, Error> {
let dst_offset_required = self
.time_zones
.ok_or(ErrorCode::CommandNotFound)?
.set_time_zone(&request)?;
if let Some(store) = self.tz_store {
store.store_persist(ctx.kv())?;
}
ctx.notify_own_attr_changed(AttributeId::TimeZone as _);
ctx.notify_own_attr_changed(AttributeId::DSTOffset as _);
response.dst_offset_required(dst_offset_required)?.end()
}
fn handle_set_dst_offset(
&self,
ctx: impl InvokeContext,
request: SetDSTOffsetRequest<'_>,
) -> Result<(), Error> {
self.time_zones
.ok_or(ErrorCode::CommandNotFound)?
.set_dst_offset(&request)?;
if let Some(store) = self.tz_store {
store.store_persist(ctx.kv())?;
}
ctx.notify_own_attr_changed(AttributeId::DSTOffset as _);
Ok(())
}
fn handle_set_default_ntp(
&self,
_ctx: impl InvokeContext,
request: SetDefaultNTPRequest<'_>,
) -> Result<(), Error> {
self.ntp_client
.ok_or(ErrorCode::CommandNotFound)?
.set_default_ntp(&request)
}
}
impl core::fmt::Debug for TimeSyncHandler<'_> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("TimeSyncHandler")
.field("dataver", &self.dataver)
.finish()
}
}
#[cfg(feature = "defmt")]
impl defmt::Format for TimeSyncHandler<'_> {
fn format(&self, f: defmt::Formatter) {
defmt::write!(f, "TimeSyncHandler {{ dataver: {} }}", self.dataver.get());
}
}