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::{self, 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    /// Evaluated Reliability saved while a client simulation owns the property
190    /// (Out_Of_Service TRUE); restored on the return to service.
191    reliability_before_out_of_service: Option<u32>,
192    status_flags: StatusFlags,
193    /// 7-day weekly schedule: index 0 = Monday, index 6 = Sunday.
194    weekly_schedule: [Vec<BACnetTimeValue>; 7],
195    exception_schedule: Vec<BACnetSpecialEvent>,
196    effective_period: Option<BACnetDateRange>,
197    list_of_object_property_references: Vec<BACnetObjectPropertyReference>,
198    /// Priority for writing to referenced objects (1-16).
199    priority_for_writing: u8,
200}
201
202impl ScheduleObject {
203    pub fn new(
204        instance: u32,
205        name: impl Into<String>,
206        schedule_default: PropertyValue,
207    ) -> Result<Self, Error> {
208        let oid = ObjectIdentifier::new(ObjectType::SCHEDULE, instance)?;
209        Ok(Self {
210            oid,
211            name: name.into(),
212            description: String::new(),
213            present_value: schedule_default.clone(),
214            schedule_default,
215            out_of_service: false,
216            reliability: 0,
217            reliability_before_out_of_service: None,
218            status_flags: StatusFlags::empty(),
219            weekly_schedule: [vec![], vec![], vec![], vec![], vec![], vec![], vec![]],
220            exception_schedule: Vec::new(),
221            effective_period: None,
222            list_of_object_property_references: Vec::new(),
223            priority_for_writing: 16, // default: lowest priority
224        })
225    }
226
227    /// Application sets this based on schedule evaluation.
228    pub fn set_present_value(&mut self, value: PropertyValue) {
229        self.present_value = value;
230    }
231
232    /// Set the description string.
233    pub fn set_description(&mut self, desc: impl Into<String>) {
234        self.description = desc.into();
235    }
236
237    /// Set time-value entries for a given day (0=Monday .. 6=Sunday).
238    pub fn set_weekly_schedule(&mut self, day_index: usize, entries: Vec<BACnetTimeValue>) {
239        if day_index < 7 {
240            self.weekly_schedule[day_index] = entries;
241        }
242    }
243
244    /// Append a special event to the exception schedule.
245    pub fn add_exception(&mut self, event: BACnetSpecialEvent) {
246        self.exception_schedule.push(event);
247    }
248
249    /// Set the effective period for this schedule.
250    pub fn set_effective_period(&mut self, period: BACnetDateRange) {
251        self.effective_period = Some(period);
252    }
253
254    /// Append an object property reference to the list.
255    pub fn add_object_property_reference(&mut self, r: BACnetObjectPropertyReference) {
256        self.list_of_object_property_references.push(r);
257    }
258
259    /// Read the current present_value.
260    pub fn present_value(&self) -> &PropertyValue {
261        &self.present_value
262    }
263
264    /// Evaluate the schedule for the given day and time.
265    ///
266    /// Returns the current effective value (from exception, weekly, or default).
267    /// `day_of_week`: 0=Monday .. 6=Sunday.
268    pub fn evaluate(&self, day_of_week: u8, hour: u8, minute: u8) -> PropertyValue {
269        if self.out_of_service {
270            return self.present_value.clone();
271        }
272
273        // 1. Check exception_schedule first (highest priority = lowest number)
274        let mut best_exception: Option<(u8, &[u8])> = None;
275        for event in &self.exception_schedule {
276            if let Some(raw) = find_active_time_value(&event.list_of_time_values, hour, minute) {
277                match best_exception {
278                    None => best_exception = Some((event.event_priority, raw)),
279                    Some((p, _)) if event.event_priority < p => {
280                        best_exception = Some((event.event_priority, raw));
281                    }
282                    _ => {}
283                }
284            }
285        }
286        if let Some((_, raw)) = best_exception {
287            return PropertyValue::OctetString(raw.to_vec());
288        }
289
290        // 2. Check weekly_schedule[day_of_week]
291        if (day_of_week as usize) < 7 {
292            if let Some(raw) =
293                find_active_time_value(&self.weekly_schedule[day_of_week as usize], hour, minute)
294            {
295                return PropertyValue::OctetString(raw.to_vec());
296            }
297        }
298
299        // 3. Fall back to schedule_default
300        self.schedule_default.clone()
301    }
302}
303
304/// Find the last time-value entry whose time is at or before (hour, minute).
305///
306/// Entries are expected to be in chronological order per the BACnet spec.
307fn find_active_time_value(entries: &[BACnetTimeValue], hour: u8, minute: u8) -> Option<&[u8]> {
308    let mut result = None;
309    for tv in entries {
310        let t = &tv.time;
311        if t.hour < hour || (t.hour == hour && t.minute <= minute) {
312            result = Some(tv.value.as_slice());
313        }
314    }
315    result
316}
317
318impl BACnetObject for ScheduleObject {
319    fn object_identifier(&self) -> ObjectIdentifier {
320        self.oid
321    }
322
323    fn object_name(&self) -> &str {
324        &self.name
325    }
326
327    fn read_property(
328        &self,
329        property: PropertyIdentifier,
330        array_index: Option<u32>,
331    ) -> Result<PropertyValue, Error> {
332        match property {
333            p if p == PropertyIdentifier::OBJECT_IDENTIFIER => {
334                Ok(PropertyValue::ObjectIdentifier(self.oid))
335            }
336            p if p == PropertyIdentifier::OBJECT_NAME => {
337                Ok(PropertyValue::CharacterString(self.name.clone()))
338            }
339            p if p == PropertyIdentifier::DESCRIPTION => {
340                Ok(PropertyValue::CharacterString(self.description.clone()))
341            }
342            p if p == PropertyIdentifier::OBJECT_TYPE => {
343                Ok(PropertyValue::Enumerated(ObjectType::SCHEDULE.to_raw()))
344            }
345            p if p == PropertyIdentifier::PRESENT_VALUE => Ok(self.present_value.clone()),
346            p if p == PropertyIdentifier::SCHEDULE_DEFAULT => Ok(self.schedule_default.clone()),
347            p if p == PropertyIdentifier::STATUS_FLAGS => Ok(PropertyValue::BitString {
348                unused_bits: 4,
349                data: vec![self.status_flags.bits() << 4],
350            }),
351            p if p == PropertyIdentifier::EVENT_STATE => Ok(PropertyValue::Enumerated(0)),
352            p if p == PropertyIdentifier::RELIABILITY => {
353                Ok(PropertyValue::Enumerated(self.reliability))
354            }
355            p if p == PropertyIdentifier::OUT_OF_SERVICE => {
356                Ok(PropertyValue::Boolean(self.out_of_service))
357            }
358            p if p == PropertyIdentifier::WEEKLY_SCHEDULE => match array_index {
359                None => {
360                    let days: Vec<PropertyValue> = self
361                        .weekly_schedule
362                        .iter()
363                        .map(|day| {
364                            PropertyValue::List(
365                                day.iter()
366                                    .map(|tv| {
367                                        PropertyValue::List(vec![
368                                            PropertyValue::Time(tv.time),
369                                            PropertyValue::OctetString(tv.value.clone()),
370                                        ])
371                                    })
372                                    .collect(),
373                            )
374                        })
375                        .collect();
376                    Ok(PropertyValue::List(days))
377                }
378                Some(0) => Ok(PropertyValue::Unsigned(7)),
379                Some(idx) if (1..=7).contains(&idx) => {
380                    let day = &self.weekly_schedule[(idx - 1) as usize];
381                    Ok(PropertyValue::List(
382                        day.iter()
383                            .map(|tv| {
384                                PropertyValue::List(vec![
385                                    PropertyValue::Time(tv.time),
386                                    PropertyValue::OctetString(tv.value.clone()),
387                                ])
388                            })
389                            .collect(),
390                    ))
391                }
392                _ => Err(Error::Protocol {
393                    class: ErrorClass::PROPERTY.to_raw() as u32,
394                    code: ErrorCode::INVALID_ARRAY_INDEX.to_raw() as u32,
395                }),
396            },
397            p if p == PropertyIdentifier::EXCEPTION_SCHEDULE => match array_index {
398                None => {
399                    let events: Vec<PropertyValue> = self
400                        .exception_schedule
401                        .iter()
402                        .map(|ev| {
403                            let tvs: Vec<PropertyValue> = ev
404                                .list_of_time_values
405                                .iter()
406                                .map(|tv| {
407                                    PropertyValue::List(vec![
408                                        PropertyValue::Time(tv.time),
409                                        PropertyValue::OctetString(tv.value.clone()),
410                                    ])
411                                })
412                                .collect();
413                            PropertyValue::List(vec![
414                                PropertyValue::Unsigned(ev.event_priority as u64),
415                                PropertyValue::List(tvs),
416                            ])
417                        })
418                        .collect();
419                    Ok(PropertyValue::List(events))
420                }
421                Some(0) => Ok(PropertyValue::Unsigned(self.exception_schedule.len() as u64)),
422                Some(i) => {
423                    let idx = (i as usize).checked_sub(1).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 ev = self.exception_schedule.get(idx).ok_or(Error::Protocol {
428                        class: ErrorClass::PROPERTY.to_raw() as u32,
429                        code: ErrorCode::INVALID_ARRAY_INDEX.to_raw() as u32,
430                    })?;
431                    let tvs: Vec<PropertyValue> = ev
432                        .list_of_time_values
433                        .iter()
434                        .map(|tv| {
435                            PropertyValue::List(vec![
436                                PropertyValue::Time(tv.time),
437                                PropertyValue::OctetString(tv.value.clone()),
438                            ])
439                        })
440                        .collect();
441                    Ok(PropertyValue::List(vec![
442                        PropertyValue::Unsigned(ev.event_priority as u64),
443                        PropertyValue::List(tvs),
444                    ]))
445                }
446            },
447            p if p == PropertyIdentifier::EFFECTIVE_PERIOD => match &self.effective_period {
448                Some(dr) => Ok(PropertyValue::OctetString(dr.encode().to_vec())),
449                None => Ok(PropertyValue::Null),
450            },
451            p if p == PropertyIdentifier::LIST_OF_OBJECT_PROPERTY_REFERENCES => {
452                Ok(PropertyValue::List(
453                    self.list_of_object_property_references
454                        .iter()
455                        .map(|r| {
456                            PropertyValue::List(vec![
457                                PropertyValue::ObjectIdentifier(r.object_identifier),
458                                PropertyValue::Enumerated(r.property_identifier),
459                            ])
460                        })
461                        .collect(),
462                ))
463            }
464            p if p == PropertyIdentifier::PRIORITY_FOR_WRITING => {
465                Ok(PropertyValue::Unsigned(self.priority_for_writing as u64))
466            }
467            p if p == PropertyIdentifier::PROPERTY_LIST => {
468                read_property_list_property(&self.property_list(), array_index)
469            }
470            _ => Err(Error::Protocol {
471                class: ErrorClass::PROPERTY.to_raw() as u32,
472                code: ErrorCode::UNKNOWN_PROPERTY.to_raw() as u32,
473            }),
474        }
475    }
476
477    fn write_property(
478        &mut self,
479        property: PropertyIdentifier,
480        _array_index: Option<u32>,
481        value: PropertyValue,
482        _priority: Option<u8>,
483    ) -> Result<(), Error> {
484        if property == PropertyIdentifier::SCHEDULE_DEFAULT {
485            self.schedule_default = value;
486            return Ok(());
487        }
488        // Clause 12.24 Table 12-28 carries no writable footnote on Reliability
489        // (plain R), but the object text still anticipates client simulation:
490        // the Reliability_Evaluation_Inhibit description states the property
491        // holds NO_FAULT_DETECTED while evaluation is disabled "unless
492        // Out_Of_Service is TRUE and an alternate value has been written to the
493        // Reliability property". In service the property reports the object's
494        // own consistency evaluation (CONFIGURATION_ERROR et al.), so a network
495        // write is refused; the internal route is `set_reliability_internal`
496        // with the complementary guard.
497        if property == PropertyIdentifier::RELIABILITY {
498            if !self.out_of_service {
499                return Err(common::write_access_denied_error());
500            }
501            if let PropertyValue::Enumerated(v) = value {
502                if !common::is_reliability_value_valid(v) {
503                    return Err(common::value_out_of_range_error());
504                }
505                self.reliability = v;
506                return Ok(());
507            }
508            return Err(Error::Protocol {
509                class: ErrorClass::PROPERTY.to_raw() as u32,
510                code: ErrorCode::INVALID_DATA_TYPE.to_raw() as u32,
511            });
512        }
513        if let Some(result) = common::write_out_of_service_with_reliability_restore(
514            &mut self.out_of_service,
515            &mut self.reliability,
516            &mut self.reliability_before_out_of_service,
517            property,
518            &value,
519        ) {
520            return result;
521        }
522        if property == PropertyIdentifier::DESCRIPTION {
523            if let PropertyValue::CharacterString(s) = value {
524                self.description = s;
525                return Ok(());
526            }
527            return Err(Error::Protocol {
528                class: ErrorClass::PROPERTY.to_raw() as u32,
529                code: ErrorCode::INVALID_DATA_TYPE.to_raw() as u32,
530            });
531        }
532        Err(Error::Protocol {
533            class: ErrorClass::PROPERTY.to_raw() as u32,
534            code: ErrorCode::WRITE_ACCESS_DENIED.to_raw() as u32,
535        })
536    }
537
538    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
539        static PROPS: &[PropertyIdentifier] = &[
540            PropertyIdentifier::OBJECT_IDENTIFIER,
541            PropertyIdentifier::OBJECT_NAME,
542            PropertyIdentifier::DESCRIPTION,
543            PropertyIdentifier::OBJECT_TYPE,
544            PropertyIdentifier::PRESENT_VALUE,
545            PropertyIdentifier::SCHEDULE_DEFAULT,
546            PropertyIdentifier::WEEKLY_SCHEDULE,
547            PropertyIdentifier::EXCEPTION_SCHEDULE,
548            PropertyIdentifier::EFFECTIVE_PERIOD,
549            PropertyIdentifier::LIST_OF_OBJECT_PROPERTY_REFERENCES,
550            PropertyIdentifier::STATUS_FLAGS,
551            PropertyIdentifier::EVENT_STATE,
552            PropertyIdentifier::RELIABILITY,
553            PropertyIdentifier::OUT_OF_SERVICE,
554        ];
555        Cow::Borrowed(PROPS)
556    }
557
558    fn set_reliability_internal(&mut self, reliability: u32) -> Result<(), Error> {
559        // While Out_Of_Service is TRUE the client owns the simulated value;
560        // refusing here keeps the internal consistency evaluation from
561        // clobbering the simulation.
562        if self.out_of_service {
563            return Err(common::write_access_denied_error());
564        }
565        if !common::is_reliability_value_valid(reliability) {
566            return Err(common::value_out_of_range_error());
567        }
568        self.reliability = reliability;
569        Ok(())
570    }
571
572    fn is_writable_property(&self, property: PropertyIdentifier) -> bool {
573        // Mirrors the ScheduleObject `write_property` arms so the PICS and
574        // runtime dispatch share one truth source.
575        matches!(
576            property,
577            PropertyIdentifier::SCHEDULE_DEFAULT
578                | PropertyIdentifier::RELIABILITY
579                | PropertyIdentifier::OUT_OF_SERVICE
580                | PropertyIdentifier::DESCRIPTION
581        )
582    }
583
584    fn tick_schedule(
585        &mut self,
586        day_of_week: u8,
587        hour: u8,
588        minute: u8,
589    ) -> Option<(PropertyValue, Vec<(ObjectIdentifier, u32)>)> {
590        if self.out_of_service || self.list_of_object_property_references.is_empty() {
591            return None;
592        }
593
594        let new_value = self.evaluate(day_of_week, hour, minute);
595        if new_value == self.present_value {
596            return None;
597        }
598
599        self.present_value = new_value.clone();
600
601        let refs = self
602            .list_of_object_property_references
603            .iter()
604            .map(|r| (r.object_identifier, r.property_identifier))
605            .collect();
606
607        Some((new_value, refs))
608    }
609}
610
611#[cfg(test)]
612mod tests;