Skip to main content

bacnet_objects/
clock.rs

1//! Dependency-neutral clock data exposed to BACnet objects.
2
3use bacnet_types::bitstring::DaysOfWeek;
4use bacnet_types::calendar::SpecificDate;
5use bacnet_types::primitives::{Date, Time};
6
7/// One coherent sample of the Device clock.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub struct ClockFrame {
10    /// Device local date.
11    pub local_date: Date,
12    /// Device local time.
13    pub local_time: Time,
14    /// Signed minutes west of UTC.
15    pub utc_offset: i16,
16    /// Whether daylight-saving time is currently applied.
17    pub daylight_savings_status: bool,
18}
19
20impl ClockFrame {
21    /// Return this frame's day of the week as a single [`DaysOfWeek`] flag,
22    /// or `None` for an unavailable/invalid day-of-week value.
23    pub fn day_of_week(self) -> Option<DaysOfWeek> {
24        (1..=7)
25            .contains(&self.local_date.day_of_week)
26            .then(|| DaysOfWeek::from_bits_truncate(1 << (self.local_date.day_of_week - 1)))
27    }
28
29    /// Whether this frame is a fully specified, internally consistent Device
30    /// DateTime suitable for timestamping notifications.
31    pub fn is_valid_actual_datetime(self) -> bool {
32        SpecificDate::from_date(&self.local_date)
33            .is_some_and(|day| day.weekday() == self.local_date.day_of_week)
34            && self.local_time.is_specific()
35    }
36}
37
38/// Synchronous read port for a coherent Device clock sample.
39///
40/// `None` means that no wall-clock frame is available. Implementations must
41/// return all four fields from the same sample.
42pub trait ClockReader: Send + Sync {
43    /// Read one coherent frame, or report that no wall clock is available.
44    fn read_clock(&self) -> Option<ClockFrame>;
45}
46
47/// The local date and time to stamp on a BACnetDateTime property when an
48/// object changes it: the Device clock's current frame, or a date and time
49/// with every field unspecified when there is no clock or its frame is not a
50/// valid actual date and time.
51pub(crate) fn stamp_datetime(clock: Option<&dyn ClockReader>) -> (Date, Time) {
52    current_datetime(clock).unwrap_or(UNSPECIFIED_DATETIME)
53}
54
55/// The Device clock's current local date and time, or `None` when there is
56/// no clock or its frame is not a valid actual date and time.
57pub(crate) fn current_datetime(clock: Option<&dyn ClockReader>) -> Option<(Date, Time)> {
58    clock
59        .and_then(ClockReader::read_clock)
60        .filter(|frame| frame.is_valid_actual_datetime())
61        .map(|frame| (frame.local_date, frame.local_time))
62}
63
64/// A BACnetDateTime with every field unspecified, the value of a timestamp
65/// that has never been set.
66pub(crate) const UNSPECIFIED_DATETIME: (Date, Time) = (
67    Date {
68        year: Date::UNSPECIFIED,
69        month: Date::UNSPECIFIED,
70        day: Date::UNSPECIFIED,
71        day_of_week: Date::UNSPECIFIED,
72    },
73    Time {
74        hour: Time::UNSPECIFIED,
75        minute: Time::UNSPECIFIED,
76        second: Time::UNSPECIFIED,
77        hundredths: Time::UNSPECIFIED,
78    },
79);
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    fn leap_day_frame() -> ClockFrame {
86        ClockFrame {
87            local_date: Date {
88                year: 124,
89                month: 2,
90                day: 29,
91                day_of_week: 4,
92            },
93            local_time: Time {
94                hour: 23,
95                minute: 59,
96                second: 59,
97                hundredths: 99,
98            },
99            utc_offset: 0,
100            daylight_savings_status: false,
101        }
102    }
103
104    #[test]
105    fn actual_datetime_validation_checks_calendar_weekday_and_time() {
106        let valid = leap_day_frame();
107        assert!(valid.is_valid_actual_datetime());
108
109        for invalid in [
110            ClockFrame {
111                local_date: Date {
112                    day: 30,
113                    ..valid.local_date
114                },
115                ..valid
116            },
117            ClockFrame {
118                local_date: Date {
119                    day_of_week: 5,
120                    ..valid.local_date
121                },
122                ..valid
123            },
124            ClockFrame {
125                local_time: Time {
126                    hour: Time::UNSPECIFIED,
127                    ..valid.local_time
128                },
129                ..valid
130            },
131        ] {
132            assert!(!invalid.is_valid_actual_datetime());
133        }
134    }
135}