Skip to main content

bacnet_objects/schedule/
mod.rs

1//! Schedule (type 17) and Calendar (type 6) objects per ASHRAE 135-2020.
2
3use bacnet_types::constructed::{
4    BACnetCalendarEntry, BACnetDateRange, BACnetObjectPropertyReference, BACnetSpecialEvent,
5    BACnetTimeValue,
6};
7use bacnet_types::enums::{ErrorClass, ErrorCode, ObjectType, PropertyIdentifier};
8use bacnet_types::error::Error;
9use bacnet_types::primitives::{ObjectIdentifier, PropertyValue, StatusFlags};
10use std::borrow::Cow;
11
12use crate::common::read_property_list_property;
13use crate::traits::BACnetObject;
14
15// ---------------------------------------------------------------------------
16// Calendar (type 6)
17// ---------------------------------------------------------------------------
18
19/// BACnet Calendar object.
20///
21/// Present_Value is Boolean — true when today matches one of the date_list
22/// entries. The application is responsible for evaluating the date_list and
23/// calling `set_present_value()`.
24pub struct CalendarObject {
25    oid: ObjectIdentifier,
26    name: String,
27    description: String,
28    present_value: bool,
29    status_flags: StatusFlags,
30    date_list: Vec<BACnetCalendarEntry>,
31}
32
33impl CalendarObject {
34    pub fn new(instance: u32, name: impl Into<String>) -> Result<Self, Error> {
35        let oid = ObjectIdentifier::new(ObjectType::CALENDAR, instance)?;
36        Ok(Self {
37            oid,
38            name: name.into(),
39            description: String::new(),
40            present_value: false,
41            status_flags: StatusFlags::empty(),
42            date_list: Vec::new(),
43        })
44    }
45
46    /// Application sets this based on date-list evaluation.
47    pub fn set_present_value(&mut self, value: bool) {
48        self.present_value = value;
49    }
50
51    /// Set the description string.
52    pub fn set_description(&mut self, desc: impl Into<String>) {
53        self.description = desc.into();
54    }
55
56    /// Append a calendar entry to the date_list.
57    pub fn add_date_entry(&mut self, entry: BACnetCalendarEntry) {
58        self.date_list.push(entry);
59    }
60
61    /// Remove all entries from the date_list.
62    pub fn clear_date_list(&mut self) {
63        self.date_list.clear();
64    }
65}
66
67impl BACnetObject for CalendarObject {
68    fn object_identifier(&self) -> ObjectIdentifier {
69        self.oid
70    }
71
72    fn object_name(&self) -> &str {
73        &self.name
74    }
75
76    fn read_property(
77        &self,
78        property: PropertyIdentifier,
79        array_index: Option<u32>,
80    ) -> Result<PropertyValue, Error> {
81        match property {
82            p if p == PropertyIdentifier::OBJECT_IDENTIFIER => {
83                Ok(PropertyValue::ObjectIdentifier(self.oid))
84            }
85            p if p == PropertyIdentifier::OBJECT_NAME => {
86                Ok(PropertyValue::CharacterString(self.name.clone()))
87            }
88            p if p == PropertyIdentifier::DESCRIPTION => {
89                Ok(PropertyValue::CharacterString(self.description.clone()))
90            }
91            p if p == PropertyIdentifier::OBJECT_TYPE => {
92                Ok(PropertyValue::Enumerated(ObjectType::CALENDAR.to_raw()))
93            }
94            p if p == PropertyIdentifier::PRESENT_VALUE => {
95                Ok(PropertyValue::Boolean(self.present_value))
96            }
97            p if p == PropertyIdentifier::STATUS_FLAGS => Ok(PropertyValue::BitString {
98                unused_bits: 4,
99                data: vec![self.status_flags.bits() << 4],
100            }),
101            p if p == PropertyIdentifier::EVENT_STATE => Ok(PropertyValue::Enumerated(0)),
102            p if p == PropertyIdentifier::OUT_OF_SERVICE => Ok(PropertyValue::Boolean(false)),
103            p if p == PropertyIdentifier::DATE_LIST => Ok(PropertyValue::List(
104                self.date_list
105                    .iter()
106                    .map(|entry| match entry {
107                        BACnetCalendarEntry::Date(d) => PropertyValue::Date(*d),
108                        BACnetCalendarEntry::DateRange(dr) => {
109                            PropertyValue::OctetString(dr.encode().to_vec())
110                        }
111                        BACnetCalendarEntry::WeekNDay(wnd) => {
112                            PropertyValue::OctetString(wnd.encode().to_vec())
113                        }
114                    })
115                    .collect(),
116            )),
117            p if p == PropertyIdentifier::PROPERTY_LIST => {
118                read_property_list_property(&self.property_list(), array_index)
119            }
120            _ => Err(Error::Protocol {
121                class: ErrorClass::PROPERTY.to_raw() as u32,
122                code: ErrorCode::UNKNOWN_PROPERTY.to_raw() as u32,
123            }),
124        }
125    }
126
127    fn write_property(
128        &mut self,
129        property: PropertyIdentifier,
130        _array_index: Option<u32>,
131        value: PropertyValue,
132        _priority: Option<u8>,
133    ) -> Result<(), Error> {
134        if property == PropertyIdentifier::DESCRIPTION {
135            if let PropertyValue::CharacterString(s) = value {
136                self.description = s;
137                return Ok(());
138            }
139            return Err(Error::Protocol {
140                class: ErrorClass::PROPERTY.to_raw() as u32,
141                code: ErrorCode::INVALID_DATA_TYPE.to_raw() as u32,
142            });
143        }
144        if property == PropertyIdentifier::PRESENT_VALUE {
145            return Err(Error::Protocol {
146                class: ErrorClass::PROPERTY.to_raw() as u32,
147                code: ErrorCode::WRITE_ACCESS_DENIED.to_raw() as u32,
148            });
149        }
150        Err(Error::Protocol {
151            class: ErrorClass::PROPERTY.to_raw() as u32,
152            code: ErrorCode::WRITE_ACCESS_DENIED.to_raw() as u32,
153        })
154    }
155
156    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
157        static PROPS: &[PropertyIdentifier] = &[
158            PropertyIdentifier::OBJECT_IDENTIFIER,
159            PropertyIdentifier::OBJECT_NAME,
160            PropertyIdentifier::DESCRIPTION,
161            PropertyIdentifier::OBJECT_TYPE,
162            PropertyIdentifier::PRESENT_VALUE,
163            PropertyIdentifier::DATE_LIST,
164            PropertyIdentifier::STATUS_FLAGS,
165            PropertyIdentifier::EVENT_STATE,
166            PropertyIdentifier::OUT_OF_SERVICE,
167        ];
168        Cow::Borrowed(PROPS)
169    }
170}
171
172// ---------------------------------------------------------------------------
173// Schedule (type 17)
174// ---------------------------------------------------------------------------
175
176/// BACnet Schedule object.
177///
178/// Stores schedule configuration. The application is responsible for
179/// evaluating the weekly/exception schedule and calling `set_present_value()`.
180/// Present_Value data type matches schedule_default.
181pub struct ScheduleObject {
182    oid: ObjectIdentifier,
183    name: String,
184    description: String,
185    present_value: PropertyValue,
186    schedule_default: PropertyValue,
187    out_of_service: bool,
188    reliability: u32,
189    status_flags: StatusFlags,
190    /// 7-day weekly schedule: index 0 = Monday, index 6 = Sunday.
191    weekly_schedule: [Vec<BACnetTimeValue>; 7],
192    exception_schedule: Vec<BACnetSpecialEvent>,
193    effective_period: Option<BACnetDateRange>,
194    list_of_object_property_references: Vec<BACnetObjectPropertyReference>,
195    /// Priority for writing to referenced objects (1-16).
196    priority_for_writing: u8,
197}
198
199impl ScheduleObject {
200    pub fn new(
201        instance: u32,
202        name: impl Into<String>,
203        schedule_default: PropertyValue,
204    ) -> Result<Self, Error> {
205        let oid = ObjectIdentifier::new(ObjectType::SCHEDULE, instance)?;
206        Ok(Self {
207            oid,
208            name: name.into(),
209            description: String::new(),
210            present_value: schedule_default.clone(),
211            schedule_default,
212            out_of_service: false,
213            reliability: 0,
214            status_flags: StatusFlags::empty(),
215            weekly_schedule: [vec![], vec![], vec![], vec![], vec![], vec![], vec![]],
216            exception_schedule: Vec::new(),
217            effective_period: None,
218            list_of_object_property_references: Vec::new(),
219            priority_for_writing: 16, // default: lowest priority
220        })
221    }
222
223    /// Application sets this based on schedule evaluation.
224    pub fn set_present_value(&mut self, value: PropertyValue) {
225        self.present_value = value;
226    }
227
228    /// Set the description string.
229    pub fn set_description(&mut self, desc: impl Into<String>) {
230        self.description = desc.into();
231    }
232
233    /// Set time-value entries for a given day (0=Monday .. 6=Sunday).
234    pub fn set_weekly_schedule(&mut self, day_index: usize, entries: Vec<BACnetTimeValue>) {
235        if day_index < 7 {
236            self.weekly_schedule[day_index] = entries;
237        }
238    }
239
240    /// Append a special event to the exception schedule.
241    pub fn add_exception(&mut self, event: BACnetSpecialEvent) {
242        self.exception_schedule.push(event);
243    }
244
245    /// Set the effective period for this schedule.
246    pub fn set_effective_period(&mut self, period: BACnetDateRange) {
247        self.effective_period = Some(period);
248    }
249
250    /// Append an object property reference to the list.
251    pub fn add_object_property_reference(&mut self, r: BACnetObjectPropertyReference) {
252        self.list_of_object_property_references.push(r);
253    }
254
255    /// Read the current present_value.
256    pub fn present_value(&self) -> &PropertyValue {
257        &self.present_value
258    }
259
260    /// Evaluate the schedule for the given day and time.
261    ///
262    /// Returns the current effective value (from exception, weekly, or default).
263    /// `day_of_week`: 0=Monday .. 6=Sunday.
264    pub fn evaluate(&self, day_of_week: u8, hour: u8, minute: u8) -> PropertyValue {
265        if self.out_of_service {
266            return self.present_value.clone();
267        }
268
269        // 1. Check exception_schedule first (highest priority = lowest number)
270        let mut best_exception: Option<(u8, &[u8])> = None;
271        for event in &self.exception_schedule {
272            if let Some(raw) = find_active_time_value(&event.list_of_time_values, hour, minute) {
273                match best_exception {
274                    None => best_exception = Some((event.event_priority, raw)),
275                    Some((p, _)) if event.event_priority < p => {
276                        best_exception = Some((event.event_priority, raw));
277                    }
278                    _ => {}
279                }
280            }
281        }
282        if let Some((_, raw)) = best_exception {
283            return PropertyValue::OctetString(raw.to_vec());
284        }
285
286        // 2. Check weekly_schedule[day_of_week]
287        if (day_of_week as usize) < 7 {
288            if let Some(raw) =
289                find_active_time_value(&self.weekly_schedule[day_of_week as usize], hour, minute)
290            {
291                return PropertyValue::OctetString(raw.to_vec());
292            }
293        }
294
295        // 3. Fall back to schedule_default
296        self.schedule_default.clone()
297    }
298}
299
300/// Find the last time-value entry whose time is at or before (hour, minute).
301///
302/// Entries are expected to be in chronological order per the BACnet spec.
303fn find_active_time_value(entries: &[BACnetTimeValue], hour: u8, minute: u8) -> Option<&[u8]> {
304    let mut result = None;
305    for tv in entries {
306        let t = &tv.time;
307        if t.hour < hour || (t.hour == hour && t.minute <= minute) {
308            result = Some(tv.value.as_slice());
309        }
310    }
311    result
312}
313
314impl BACnetObject for ScheduleObject {
315    fn object_identifier(&self) -> ObjectIdentifier {
316        self.oid
317    }
318
319    fn object_name(&self) -> &str {
320        &self.name
321    }
322
323    fn read_property(
324        &self,
325        property: PropertyIdentifier,
326        array_index: Option<u32>,
327    ) -> Result<PropertyValue, Error> {
328        match property {
329            p if p == PropertyIdentifier::OBJECT_IDENTIFIER => {
330                Ok(PropertyValue::ObjectIdentifier(self.oid))
331            }
332            p if p == PropertyIdentifier::OBJECT_NAME => {
333                Ok(PropertyValue::CharacterString(self.name.clone()))
334            }
335            p if p == PropertyIdentifier::DESCRIPTION => {
336                Ok(PropertyValue::CharacterString(self.description.clone()))
337            }
338            p if p == PropertyIdentifier::OBJECT_TYPE => {
339                Ok(PropertyValue::Enumerated(ObjectType::SCHEDULE.to_raw()))
340            }
341            p if p == PropertyIdentifier::PRESENT_VALUE => Ok(self.present_value.clone()),
342            p if p == PropertyIdentifier::SCHEDULE_DEFAULT => Ok(self.schedule_default.clone()),
343            p if p == PropertyIdentifier::STATUS_FLAGS => Ok(PropertyValue::BitString {
344                unused_bits: 4,
345                data: vec![self.status_flags.bits() << 4],
346            }),
347            p if p == PropertyIdentifier::EVENT_STATE => Ok(PropertyValue::Enumerated(0)),
348            p if p == PropertyIdentifier::RELIABILITY => {
349                Ok(PropertyValue::Enumerated(self.reliability))
350            }
351            p if p == PropertyIdentifier::OUT_OF_SERVICE => {
352                Ok(PropertyValue::Boolean(self.out_of_service))
353            }
354            p if p == PropertyIdentifier::WEEKLY_SCHEDULE => match array_index {
355                None => {
356                    let days: Vec<PropertyValue> = self
357                        .weekly_schedule
358                        .iter()
359                        .map(|day| {
360                            PropertyValue::List(
361                                day.iter()
362                                    .map(|tv| {
363                                        PropertyValue::List(vec![
364                                            PropertyValue::Time(tv.time),
365                                            PropertyValue::OctetString(tv.value.clone()),
366                                        ])
367                                    })
368                                    .collect(),
369                            )
370                        })
371                        .collect();
372                    Ok(PropertyValue::List(days))
373                }
374                Some(0) => Ok(PropertyValue::Unsigned(7)),
375                Some(idx) if (1..=7).contains(&idx) => {
376                    let day = &self.weekly_schedule[(idx - 1) as usize];
377                    Ok(PropertyValue::List(
378                        day.iter()
379                            .map(|tv| {
380                                PropertyValue::List(vec![
381                                    PropertyValue::Time(tv.time),
382                                    PropertyValue::OctetString(tv.value.clone()),
383                                ])
384                            })
385                            .collect(),
386                    ))
387                }
388                _ => Err(Error::Protocol {
389                    class: ErrorClass::PROPERTY.to_raw() as u32,
390                    code: ErrorCode::INVALID_ARRAY_INDEX.to_raw() as u32,
391                }),
392            },
393            p if p == PropertyIdentifier::EXCEPTION_SCHEDULE => match array_index {
394                None => {
395                    let events: Vec<PropertyValue> = self
396                        .exception_schedule
397                        .iter()
398                        .map(|ev| {
399                            let tvs: Vec<PropertyValue> = ev
400                                .list_of_time_values
401                                .iter()
402                                .map(|tv| {
403                                    PropertyValue::List(vec![
404                                        PropertyValue::Time(tv.time),
405                                        PropertyValue::OctetString(tv.value.clone()),
406                                    ])
407                                })
408                                .collect();
409                            PropertyValue::List(vec![
410                                PropertyValue::Unsigned(ev.event_priority as u64),
411                                PropertyValue::List(tvs),
412                            ])
413                        })
414                        .collect();
415                    Ok(PropertyValue::List(events))
416                }
417                Some(0) => Ok(PropertyValue::Unsigned(self.exception_schedule.len() as u64)),
418                Some(i) => {
419                    let idx = (i as usize).checked_sub(1).ok_or(Error::Protocol {
420                        class: ErrorClass::PROPERTY.to_raw() as u32,
421                        code: ErrorCode::INVALID_ARRAY_INDEX.to_raw() as u32,
422                    })?;
423                    let ev = self.exception_schedule.get(idx).ok_or(Error::Protocol {
424                        class: ErrorClass::PROPERTY.to_raw() as u32,
425                        code: ErrorCode::INVALID_ARRAY_INDEX.to_raw() as u32,
426                    })?;
427                    let tvs: Vec<PropertyValue> = ev
428                        .list_of_time_values
429                        .iter()
430                        .map(|tv| {
431                            PropertyValue::List(vec![
432                                PropertyValue::Time(tv.time),
433                                PropertyValue::OctetString(tv.value.clone()),
434                            ])
435                        })
436                        .collect();
437                    Ok(PropertyValue::List(vec![
438                        PropertyValue::Unsigned(ev.event_priority as u64),
439                        PropertyValue::List(tvs),
440                    ]))
441                }
442            },
443            p if p == PropertyIdentifier::EFFECTIVE_PERIOD => match &self.effective_period {
444                Some(dr) => Ok(PropertyValue::OctetString(dr.encode().to_vec())),
445                None => Ok(PropertyValue::Null),
446            },
447            p if p == PropertyIdentifier::LIST_OF_OBJECT_PROPERTY_REFERENCES => {
448                Ok(PropertyValue::List(
449                    self.list_of_object_property_references
450                        .iter()
451                        .map(|r| {
452                            PropertyValue::List(vec![
453                                PropertyValue::ObjectIdentifier(r.object_identifier),
454                                PropertyValue::Enumerated(r.property_identifier),
455                            ])
456                        })
457                        .collect(),
458                ))
459            }
460            p if p == PropertyIdentifier::PRIORITY_FOR_WRITING => {
461                Ok(PropertyValue::Unsigned(self.priority_for_writing as u64))
462            }
463            p if p == PropertyIdentifier::PROPERTY_LIST => {
464                read_property_list_property(&self.property_list(), array_index)
465            }
466            _ => Err(Error::Protocol {
467                class: ErrorClass::PROPERTY.to_raw() as u32,
468                code: ErrorCode::UNKNOWN_PROPERTY.to_raw() as u32,
469            }),
470        }
471    }
472
473    fn write_property(
474        &mut self,
475        property: PropertyIdentifier,
476        _array_index: Option<u32>,
477        value: PropertyValue,
478        _priority: Option<u8>,
479    ) -> Result<(), Error> {
480        if property == PropertyIdentifier::SCHEDULE_DEFAULT {
481            self.schedule_default = value;
482            return Ok(());
483        }
484        if property == PropertyIdentifier::RELIABILITY {
485            if let PropertyValue::Enumerated(v) = value {
486                self.reliability = v;
487                return Ok(());
488            }
489            return Err(Error::Protocol {
490                class: ErrorClass::PROPERTY.to_raw() as u32,
491                code: ErrorCode::INVALID_DATA_TYPE.to_raw() as u32,
492            });
493        }
494        if property == PropertyIdentifier::OUT_OF_SERVICE {
495            if let PropertyValue::Boolean(v) = value {
496                self.out_of_service = v;
497                return Ok(());
498            }
499            return Err(Error::Protocol {
500                class: ErrorClass::PROPERTY.to_raw() as u32,
501                code: ErrorCode::INVALID_DATA_TYPE.to_raw() as u32,
502            });
503        }
504        if property == PropertyIdentifier::DESCRIPTION {
505            if let PropertyValue::CharacterString(s) = value {
506                self.description = s;
507                return Ok(());
508            }
509            return Err(Error::Protocol {
510                class: ErrorClass::PROPERTY.to_raw() as u32,
511                code: ErrorCode::INVALID_DATA_TYPE.to_raw() as u32,
512            });
513        }
514        Err(Error::Protocol {
515            class: ErrorClass::PROPERTY.to_raw() as u32,
516            code: ErrorCode::WRITE_ACCESS_DENIED.to_raw() as u32,
517        })
518    }
519
520    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
521        static PROPS: &[PropertyIdentifier] = &[
522            PropertyIdentifier::OBJECT_IDENTIFIER,
523            PropertyIdentifier::OBJECT_NAME,
524            PropertyIdentifier::DESCRIPTION,
525            PropertyIdentifier::OBJECT_TYPE,
526            PropertyIdentifier::PRESENT_VALUE,
527            PropertyIdentifier::SCHEDULE_DEFAULT,
528            PropertyIdentifier::WEEKLY_SCHEDULE,
529            PropertyIdentifier::EXCEPTION_SCHEDULE,
530            PropertyIdentifier::EFFECTIVE_PERIOD,
531            PropertyIdentifier::LIST_OF_OBJECT_PROPERTY_REFERENCES,
532            PropertyIdentifier::STATUS_FLAGS,
533            PropertyIdentifier::EVENT_STATE,
534            PropertyIdentifier::RELIABILITY,
535            PropertyIdentifier::OUT_OF_SERVICE,
536        ];
537        Cow::Borrowed(PROPS)
538    }
539
540    fn tick_schedule(
541        &mut self,
542        day_of_week: u8,
543        hour: u8,
544        minute: u8,
545    ) -> Option<(PropertyValue, Vec<(ObjectIdentifier, u32)>)> {
546        if self.out_of_service || self.list_of_object_property_references.is_empty() {
547            return None;
548        }
549
550        let new_value = self.evaluate(day_of_week, hour, minute);
551        if new_value == self.present_value {
552            return None;
553        }
554
555        self.present_value = new_value.clone();
556
557        let refs = self
558            .list_of_object_property_references
559            .iter()
560            .map(|r| (r.object_identifier, r.property_identifier))
561            .collect();
562
563        Some((new_value, refs))
564    }
565}
566
567#[cfg(test)]
568mod tests;