Skip to main content

rustbac_client/
schedule.rs

1//! Convenience types and helpers for BACnet Schedule and Calendar objects.
2//!
3//! Provides typed representations of weekly schedules, exception schedules,
4//! and calendar entries that wrap the lower-level [`ClientDataValue`] encoding.
5
6use crate::ClientDataValue;
7use rustbac_core::types::{Date, Time};
8
9/// A single time-value pair in a daily schedule.
10#[derive(Debug, Clone, PartialEq)]
11pub struct TimeValue {
12    pub time: Time,
13    pub value: ClientDataValue,
14}
15
16/// A date range for exception schedules and calendar entries.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub struct DateRange {
19    pub start: Date,
20    pub end: Date,
21}
22
23/// An entry in a BACnet Calendar's date-list property.
24#[derive(Debug, Clone, PartialEq)]
25pub enum CalendarEntry {
26    Date(Date),
27    Range(DateRange),
28    WeekNDay {
29        month: u8,
30        week_of_month: u8,
31        day_of_week: u8,
32    },
33}
34
35/// Decode a weekly schedule from a [`ClientDataValue::Constructed`].
36///
37/// A BACnet weekly schedule is a sequence of 7 daily schedules (Sun–Sat),
38/// each containing a list of [`TimeValue`] pairs.
39pub fn decode_weekly_schedule(value: &ClientDataValue) -> Option<Vec<Vec<TimeValue>>> {
40    let days = match value {
41        ClientDataValue::Constructed { values, .. } => values,
42        _ => return None,
43    };
44
45    let mut week = Vec::with_capacity(7);
46    for day in days {
47        let day_values = match day {
48            ClientDataValue::Constructed { values, .. } => values,
49            _ => {
50                week.push(Vec::new());
51                continue;
52            }
53        };
54
55        let mut entries = Vec::new();
56        let mut i = 0;
57        while i + 1 < day_values.len() {
58            if let ClientDataValue::Time(t) = &day_values[i] {
59                entries.push(TimeValue {
60                    time: *t,
61                    value: day_values[i + 1].clone(),
62                });
63                i += 2;
64            } else {
65                i += 1;
66            }
67        }
68        week.push(entries);
69    }
70
71    Some(week)
72}
73
74/// Encode a weekly schedule into a [`ClientDataValue::Constructed`].
75pub fn encode_weekly_schedule(week: &[Vec<TimeValue>]) -> ClientDataValue {
76    let mut days = Vec::with_capacity(week.len());
77    for (i, day) in week.iter().enumerate() {
78        let mut values = Vec::with_capacity(day.len() * 2);
79        for entry in day {
80            values.push(ClientDataValue::Time(entry.time));
81            values.push(entry.value.clone());
82        }
83        days.push(ClientDataValue::Constructed {
84            tag_num: i as u8,
85            values,
86        });
87    }
88    ClientDataValue::Constructed {
89        tag_num: 0,
90        values: days,
91    }
92}
93
94/// Decode a date-list from a [`ClientDataValue::Constructed`] into calendar entries.
95pub fn decode_date_list(value: &ClientDataValue) -> Option<Vec<CalendarEntry>> {
96    let items = match value {
97        ClientDataValue::Constructed { values, .. } => values,
98        _ => return None,
99    };
100
101    let mut entries = Vec::new();
102    for item in items {
103        match item {
104            ClientDataValue::Date(d) => entries.push(CalendarEntry::Date(*d)),
105            ClientDataValue::Constructed { tag_num: 1, values } if values.len() == 2 => {
106                if let (ClientDataValue::Date(start), ClientDataValue::Date(end)) =
107                    (&values[0], &values[1])
108                {
109                    entries.push(CalendarEntry::Range(DateRange {
110                        start: *start,
111                        end: *end,
112                    }));
113                }
114            }
115            ClientDataValue::Constructed { tag_num: 2, values } if values.len() == 3 => {
116                if let (
117                    ClientDataValue::Unsigned(month),
118                    ClientDataValue::Unsigned(week),
119                    ClientDataValue::Unsigned(day),
120                ) = (&values[0], &values[1], &values[2])
121                {
122                    entries.push(CalendarEntry::WeekNDay {
123                        month: *month as u8,
124                        week_of_month: *week as u8,
125                        day_of_week: *day as u8,
126                    });
127                }
128            }
129            _ => {}
130        }
131    }
132
133    Some(entries)
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139    use rustbac_core::types::Time;
140
141    #[test]
142    fn weekly_schedule_roundtrip() {
143        let monday = vec![
144            TimeValue {
145                time: Time {
146                    hour: 8,
147                    minute: 0,
148                    second: 0,
149                    hundredths: 0,
150                },
151                value: ClientDataValue::Real(72.0),
152            },
153            TimeValue {
154                time: Time {
155                    hour: 18,
156                    minute: 0,
157                    second: 0,
158                    hundredths: 0,
159                },
160                value: ClientDataValue::Real(65.0),
161            },
162        ];
163
164        let mut week = vec![Vec::new(); 7];
165        week[1] = monday.clone();
166
167        let encoded = encode_weekly_schedule(&week);
168        let decoded = decode_weekly_schedule(&encoded).unwrap();
169        assert_eq!(decoded.len(), 7);
170        assert!(decoded[0].is_empty());
171        assert_eq!(decoded[1].len(), 2);
172        assert_eq!(decoded[1][0].time.hour, 8);
173        assert_eq!(decoded[1][1].time.hour, 18);
174    }
175
176    #[test]
177    fn decode_date_list_entries() {
178        let date = Date {
179            year_since_1900: 124,
180            month: 12,
181            day: 25,
182            weekday: 0xFF,
183        };
184
185        let value = ClientDataValue::Constructed {
186            tag_num: 0,
187            values: vec![ClientDataValue::Date(date)],
188        };
189
190        let entries = decode_date_list(&value).unwrap();
191        assert_eq!(entries.len(), 1);
192        assert_eq!(entries[0], CalendarEntry::Date(date));
193    }
194}