Skip to main content

bacnet_objects/schedule/
mod.rs

1//! Schedule (type 17) and Calendar (type 6) objects per ASHRAE 135-2020.
2
3use crate::prelude::*;
4use alloc::borrow::Cow;
5use bacnet_encoding::constructed::{
6    encode_daily_schedule, encode_date_range, encode_special_event,
7};
8use bacnet_types::calendar::SpecificDate;
9use bacnet_types::constructed::{
10    BACnetDateRange, BACnetObjectPropertyReference, BACnetSpecialEvent, BACnetTimeValue,
11};
12use bacnet_types::enums::{
13    ErrorClass, ErrorCode, EventState, ObjectType, PropertyIdentifier, Reliability,
14};
15use bacnet_types::error::Error;
16use bacnet_types::primitives::{Date, ObjectIdentifier, PropertyValue, StatusFlags, Time};
17use bytes::BytesMut;
18
19use crate::common::{self, read_property_list_property};
20use crate::traits::BACnetObject;
21
22mod calendar;
23mod calendar_metadata;
24mod date_list;
25mod evaluation;
26mod metadata;
27mod out_of_service;
28mod reliability;
29mod targets;
30mod writes;
31
32pub use calendar::CalendarObject;
33pub use evaluation::{ScheduleTargetOutcome, ScheduleWrite};
34
35// ---------------------------------------------------------------------------
36// Schedule (type 17)
37// ---------------------------------------------------------------------------
38
39/// BACnet Schedule object.
40///
41/// Present_Value is calculated as Clause 12.24.4 describes (see
42/// [`evaluate`](Self::evaluate)): within Effective_Period, the best special
43/// event in effect, then today's weekly entry, then Schedule_Default. The
44/// bundled server evaluates every Schedule once a minute against the Device
45/// clock and writes a changed value, in its own datatype, to every member of
46/// List_Of_Object_Property_References at Priority_For_Writing. Entering the
47/// Effective_Period, the first evaluation after start-up included, writes the
48/// value even when it has not changed.
49///
50/// Time-values and Schedule_Default hold values of a primitive datatype; the
51/// setters refuse anything else. Weekly_Schedule, Exception_Schedule,
52/// Effective_Period, Schedule_Default, List_Of_Object_Property_References and
53/// Priority_For_Writing are network-writable, through the same checks as the
54/// setters; after such a write the bundled server runs the evaluation again
55/// at once. AddListElement and RemoveListElement edit the reference list and
56/// write it back whole the same way. A change to the references or the
57/// priority sends the current value to the new targets and relinquishes the
58/// slots it leaves behind (see
59/// [`set_object_property_references`](Self::set_object_property_references)).
60///
61/// Reliability is CONFIGURATION_ERROR while the non-NULL values in the two
62/// schedules and Schedule_Default are not all of one datatype, or while a
63/// referenced property refused the schedule's datatype, or the reference
64/// itself, at its last write (Clause 12.24.13). Such a schedule still
65/// evaluates and writes its references, and each pass that has nothing else
66/// to send offers the current value again to the references that refused it,
67/// so the fault clears once its cause is gone.
68///
69/// While Out_Of_Service is TRUE the calculation leaves Present_Value alone and
70/// a client may write it instead; each such write goes on to the references
71/// as a calculated change would (Clause 12.24.14). Back in service, the
72/// calculation takes over again.
73pub struct ScheduleObject {
74    oid: ObjectIdentifier,
75    name: String,
76    description: String,
77    present_value: PropertyValue,
78    schedule_default: PropertyValue,
79    out_of_service: bool,
80    reliability: Reliability,
81    /// Evaluated Reliability saved while a client simulation owns the property
82    /// (Out_Of_Service TRUE); restored on the return to service.
83    reliability_before_out_of_service: Option<Reliability>,
84    /// Whether the current CONFIGURATION_ERROR was raised by this object's
85    /// own consistency check, either half, which may then clear it.
86    owns_configuration_error: bool,
87    /// References whose last write of a value in the schedule's datatype
88    /// was refused, for its datatype or as a reference: the reference half of
89    /// the consistency check (`reliability.rs`, #1086, #1433), and what a
90    /// pass with nothing else to send retries (#1436).
91    refusing_references: Vec<BACnetObjectPropertyReference>,
92    status_flags: StatusFlags,
93    /// 7-day weekly schedule: index 0 = Monday, index 6 = Sunday.
94    weekly_schedule: [Vec<BACnetTimeValue>; 7],
95    exception_schedule: Vec<BACnetSpecialEvent>,
96    effective_period: BACnetDateRange,
97    list_of_object_property_references: Vec<BACnetObjectPropertyReference>,
98    /// Priority for writing to referenced objects (1-16).
99    priority_for_writing: u8,
100    /// Whether the last evaluation found today inside Effective_Period. The
101    /// first evaluation inside it after one outside it (or after start-up)
102    /// writes the references even if Present_Value is unchanged (Clause
103    /// 12.24.6).
104    in_effective_period: bool,
105    /// A Present_Value written while Out_Of_Service is TRUE that has not been
106    /// sent to the references yet.
107    simulated_write: Option<PropertyValue>,
108    /// The slots this Schedule's value holds on its targets (`targets.rs`).
109    held: Option<targets::HeldCommand>,
110    /// NULL writes owed to the slots a change of the references or the
111    /// priority left behind.
112    relinquish_owed: Vec<ScheduleWrite>,
113    /// A change of the references or the priority owes the current
114    /// Present_Value to the new list at the new priority.
115    rewrite_owed: bool,
116}
117
118impl ScheduleObject {
119    /// Create a new Schedule object; `schedule_default` is both Schedule_Default and the initial
120    /// Present_Value, and must be of a primitive datatype (INVALID_DATA_TYPE otherwise).
121    /// Effective_Period starts with both dates unspecified, a range that covers every date, and
122    /// Priority_For_Writing at 16.
123    pub fn new(
124        instance: u32,
125        name: impl Into<String>,
126        schedule_default: PropertyValue,
127    ) -> Result<Self, Error> {
128        let oid = ObjectIdentifier::new(ObjectType::SCHEDULE, instance)?;
129        if !schedule_default.is_primitive() {
130            return Err(common::invalid_data_type_error());
131        }
132        Ok(Self {
133            oid,
134            name: name.into(),
135            description: String::new(),
136            present_value: schedule_default.clone(),
137            schedule_default,
138            out_of_service: false,
139            reliability: Reliability::NO_FAULT_DETECTED,
140            reliability_before_out_of_service: None,
141            owns_configuration_error: false,
142            refusing_references: Vec::new(),
143            status_flags: StatusFlags::empty(),
144            weekly_schedule: [vec![], vec![], vec![], vec![], vec![], vec![], vec![]],
145            exception_schedule: Vec::new(),
146            effective_period: BACnetDateRange {
147                start_date: unspecified_date(),
148                end_date: unspecified_date(),
149            },
150            list_of_object_property_references: Vec::new(),
151            priority_for_writing: 16, // default: lowest priority
152            in_effective_period: false,
153            simulated_write: None,
154            held: None,
155            relinquish_owed: Vec::new(),
156            rewrite_owed: false,
157        })
158    }
159
160    /// Set the description string.
161    pub fn set_description(&mut self, desc: impl Into<String>) {
162        self.description = desc.into();
163    }
164
165    /// Set time-value entries for a given day (0=Monday .. 6=Sunday).
166    ///
167    /// Refuses, leaving the day unchanged, a day index past 6 or a time that
168    /// is not specific (VALUE_OUT_OF_RANGE), a value that is not of a
169    /// primitive datatype (INVALID_DATA_TYPE), and a time given twice
170    /// (DUPLICATE_ENTRY).
171    pub fn set_weekly_schedule(
172        &mut self,
173        day_index: usize,
174        entries: Vec<BACnetTimeValue>,
175    ) -> Result<(), Error> {
176        let Some(day) = self.weekly_schedule.get_mut(day_index) else {
177            return Err(common::value_out_of_range_error());
178        };
179        evaluation::check_time_values(&entries)?;
180        *day = entries;
181        self.contents_changed();
182        Ok(())
183    }
184
185    /// Append a special event to the exception schedule.
186    ///
187    /// Refuses an event priority outside 1 to 16 or an inline calendar entry
188    /// with an out-of-range value (VALUE_OUT_OF_RANGE), time-values that
189    /// [`set_weekly_schedule`](Self::set_weekly_schedule) would refuse, and an
190    /// event past the 1,024-event cap (RESOURCES /
191    /// NO_SPACE_TO_WRITE_PROPERTY).
192    pub fn add_exception(&mut self, event: BACnetSpecialEvent) -> Result<(), Error> {
193        evaluation::check_special_event(&event)?;
194        if self.exception_schedule.len() >= writes::MAX_EXCEPTIONS {
195            return Err(writes::no_space_error());
196        }
197        self.exception_schedule.push(event);
198        self.contents_changed();
199        Ok(())
200    }
201
202    /// Set the effective period for this schedule.
203    ///
204    /// Each date must be a specific date or wholly unspecified, an open end
205    /// (VALUE_OUT_OF_RANGE otherwise).
206    pub fn set_effective_period(&mut self, period: BACnetDateRange) -> Result<(), Error> {
207        if !period.is_valid() {
208            return Err(common::value_out_of_range_error());
209        }
210        self.effective_period = period;
211        Ok(())
212    }
213
214    /// Set Priority_For_Writing, 1 (highest) to 16 (VALUE_OUT_OF_RANGE
215    /// otherwise).
216    ///
217    /// The current value then goes to the references at the new priority,
218    /// and the slots held at the old one are relinquished, as for
219    /// [`set_object_property_references`](Self::set_object_property_references).
220    pub fn set_priority_for_writing(&mut self, priority: u8) -> Result<(), Error> {
221        if !(1..=16).contains(&priority) {
222            return Err(common::value_out_of_range_error());
223        }
224        self.priority_for_writing = priority;
225        self.targets_changed();
226        Ok(())
227    }
228
229    /// Append a local target reference, retaining its optional array index.
230    ///
231    /// Refuses a reference past the 1,024-member cap (RESOURCES /
232    /// NO_SPACE_TO_WRITE_PROPERTY). The new member gets the current value as
233    /// [`set_object_property_references`](Self::set_object_property_references)
234    /// describes.
235    pub fn add_object_property_reference(
236        &mut self,
237        r: BACnetObjectPropertyReference,
238    ) -> Result<(), Error> {
239        if self.list_of_object_property_references.len() >= targets::MAX_REFERENCES {
240            return Err(writes::no_space_error());
241        }
242        self.list_of_object_property_references.push(r);
243        self.targets_changed();
244        Ok(())
245    }
246
247    /// Replace List_Of_Object_Property_References with local target
248    /// references, each retaining its optional array index.
249    ///
250    /// Refuses more than 1,024 references (RESOURCES /
251    /// NO_SPACE_TO_WRITE_PROPERTY), leaving the list unchanged. Once
252    /// accepted, the next schedule pass sends the current Present_Value to
253    /// every reference at Priority_For_Writing, if the object is writing at
254    /// all: in service only inside Effective_Period. It also relinquishes, with
255    /// a NULL at the priority it filled, each slot this Schedule holds on a
256    /// reference the change drops. A Schedule holds a slot from a write of a
257    /// value other than NULL until it leaves its Effective_Period; one outside
258    /// it relinquishes nothing, so as not to clear the command of another
259    /// Schedule in season on the same targets (Clause 12.24.6).
260    pub fn set_object_property_references(
261        &mut self,
262        references: Vec<BACnetObjectPropertyReference>,
263    ) -> Result<(), Error> {
264        if references.len() > targets::MAX_REFERENCES {
265            return Err(writes::no_space_error());
266        }
267        self.list_of_object_property_references = references;
268        self.targets_changed();
269        Ok(())
270    }
271
272    /// Read the current present_value.
273    pub fn present_value(&self) -> &PropertyValue {
274        &self.present_value
275    }
276
277    /// Weekly_Schedule, Exception_Schedule or Schedule_Default changed:
278    /// re-check Reliability.
279    fn contents_changed(&mut self) {
280        let _ = self.recompute_reliability();
281    }
282}
283
284/// A Date with every octet unspecified.
285fn unspecified_date() -> Date {
286    Date {
287        year: Date::UNSPECIFIED,
288        month: Date::UNSPECIFIED,
289        day: Date::UNSPECIFIED,
290        day_of_week: Date::UNSPECIFIED,
291    }
292}
293
294/// One Weekly_Schedule element: a BACnetDailySchedule.
295fn daily_schedule(time_values: &[BACnetTimeValue]) -> Result<PropertyValue, Error> {
296    let mut encoded = BytesMut::new();
297    encode_daily_schedule(&mut encoded, time_values)?;
298    Ok(PropertyValue::ApplicationData(encoded.to_vec()))
299}
300
301/// One Exception_Schedule element: a BACnetSpecialEvent.
302fn special_event(event: &BACnetSpecialEvent) -> Result<PropertyValue, Error> {
303    let mut encoded = BytesMut::new();
304    encode_special_event(&mut encoded, event)?;
305    Ok(PropertyValue::ApplicationData(encoded.to_vec()))
306}
307
308impl BACnetObject for ScheduleObject {
309    fn object_identifier(&self) -> ObjectIdentifier {
310        self.oid
311    }
312
313    fn object_name(&self) -> &str {
314        &self.name
315    }
316
317    fn read_property(
318        &self,
319        property: PropertyIdentifier,
320        array_index: Option<u32>,
321    ) -> Result<PropertyValue, Error> {
322        match property {
323            p if p == PropertyIdentifier::OBJECT_IDENTIFIER => {
324                Ok(PropertyValue::ObjectIdentifier(self.oid))
325            }
326            p if p == PropertyIdentifier::OBJECT_NAME => {
327                Ok(PropertyValue::CharacterString(self.name.clone()))
328            }
329            p if p == PropertyIdentifier::DESCRIPTION => {
330                Ok(PropertyValue::CharacterString(self.description.clone()))
331            }
332            p if p == PropertyIdentifier::OBJECT_TYPE => {
333                Ok(PropertyValue::Enumerated(ObjectType::SCHEDULE.to_raw()))
334            }
335            p if p == PropertyIdentifier::PRESENT_VALUE => Ok(self.present_value.clone()),
336            p if p == PropertyIdentifier::SCHEDULE_DEFAULT => Ok(self.schedule_default.clone()),
337            // FAULT follows Reliability and OUT_OF_SERVICE follows
338            // Out_Of_Service; IN_ALARM follows the fixed NORMAL Event_State
339            // this object reports.
340            p if p == PropertyIdentifier::STATUS_FLAGS => Ok(common::compute_status_flags(
341                self.status_flags,
342                self.reliability,
343                self.out_of_service,
344                EventState::NORMAL,
345            )),
346            p if p == PropertyIdentifier::EVENT_STATE => {
347                Ok(PropertyValue::Enumerated(EventState::NORMAL.to_raw()))
348            }
349            p if p == PropertyIdentifier::RELIABILITY => {
350                Ok(PropertyValue::Enumerated(self.reliability.to_raw()))
351            }
352            p if p == PropertyIdentifier::OUT_OF_SERVICE => {
353                Ok(PropertyValue::Boolean(self.out_of_service))
354            }
355            // Clause 21 wire forms, one list element per array element so
356            // the services concatenate them for a whole-array read.
357            p if p == PropertyIdentifier::WEEKLY_SCHEDULE => match array_index {
358                None => Ok(PropertyValue::List(
359                    self.weekly_schedule
360                        .iter()
361                        .map(|day| daily_schedule(day))
362                        .collect::<Result<_, _>>()?,
363                )),
364                Some(0) => Ok(PropertyValue::Unsigned(7)),
365                Some(idx) if (1..=7).contains(&idx) => {
366                    daily_schedule(&self.weekly_schedule[(idx - 1) as usize])
367                }
368                _ => Err(common::invalid_array_index_error()),
369            },
370            p if p == PropertyIdentifier::EXCEPTION_SCHEDULE => match array_index {
371                None => Ok(PropertyValue::List(
372                    self.exception_schedule
373                        .iter()
374                        .map(special_event)
375                        .collect::<Result<_, _>>()?,
376                )),
377                Some(0) => Ok(PropertyValue::Unsigned(self.exception_schedule.len() as u64)),
378                Some(i) => (i as usize)
379                    .checked_sub(1)
380                    .and_then(|idx| self.exception_schedule.get(idx))
381                    .ok_or_else(common::invalid_array_index_error)
382                    .and_then(special_event),
383            },
384            p if p == PropertyIdentifier::EFFECTIVE_PERIOD => {
385                let mut encoded = BytesMut::new();
386                encode_date_range(&mut encoded, &self.effective_period);
387                Ok(PropertyValue::ApplicationData(encoded.to_vec()))
388            }
389            // The local references back to back, each with no Device member,
390            // in the run form the server's list services edit.
391            p if p == PropertyIdentifier::LIST_OF_OBJECT_PROPERTY_REFERENCES => {
392                Ok(crate::device_reference::reference_run(
393                    &self
394                        .list_of_object_property_references
395                        .iter()
396                        .map(crate::device_reference::local_property_reference)
397                        .collect::<Vec<_>>(),
398                ))
399            }
400            p if p == PropertyIdentifier::PRIORITY_FOR_WRITING => {
401                Ok(PropertyValue::Unsigned(self.priority_for_writing as u64))
402            }
403            p if p == PropertyIdentifier::PROPERTY_LIST => {
404                read_property_list_property(&self.property_list(), array_index)
405            }
406            _ => Err(Error::protocol(
407                ErrorClass::PROPERTY,
408                ErrorCode::UNKNOWN_PROPERTY,
409            )),
410        }
411    }
412
413    fn write_property(
414        &mut self,
415        property: PropertyIdentifier,
416        array_index: Option<u32>,
417        value: PropertyValue,
418        _priority: Option<u8>,
419    ) -> Result<(), Error> {
420        if property == PropertyIdentifier::PRESENT_VALUE {
421            return self.write_present_value(array_index, value);
422        }
423        if property == PropertyIdentifier::WEEKLY_SCHEDULE {
424            return self.write_weekly_schedule(array_index, value);
425        }
426        if property == PropertyIdentifier::EXCEPTION_SCHEDULE {
427            return self.write_exception_schedule(array_index, value);
428        }
429        if property == PropertyIdentifier::EFFECTIVE_PERIOD {
430            if array_index.is_some() {
431                return Err(common::property_is_not_an_array_error());
432            }
433            return self.write_effective_period(value);
434        }
435        if property == PropertyIdentifier::LIST_OF_OBJECT_PROPERTY_REFERENCES {
436            if array_index.is_some() {
437                return Err(common::property_is_not_an_array_error());
438            }
439            return self.write_object_property_references(value);
440        }
441        if property == PropertyIdentifier::PRIORITY_FOR_WRITING {
442            if array_index.is_some() {
443                return Err(common::property_is_not_an_array_error());
444            }
445            return self.write_priority_for_writing(value);
446        }
447        if property == PropertyIdentifier::SCHEDULE_DEFAULT {
448            if array_index.is_some() {
449                return Err(common::property_is_not_an_array_error());
450            }
451            // Clause 12.24.9: any primitive datatype, NULL included.
452            if !value.is_primitive() {
453                return Err(common::invalid_data_type_error());
454            }
455            self.schedule_default = value;
456            self.contents_changed();
457            return Ok(());
458        }
459        // Clause 12.24 Table 12-28 carries no writable footnote on Reliability
460        // (plain R), but the object text still anticipates client simulation:
461        // the Reliability_Evaluation_Inhibit description states the property
462        // holds NO_FAULT_DETECTED while evaluation is disabled, except when a
463        // client has supplied a replacement Reliability value while Out_Of_Service
464        // is TRUE. In service the property reports the object's
465        // own consistency evaluation (CONFIGURATION_ERROR et al.), so a network
466        // write is refused; the internal route is `set_reliability_internal`
467        // with the complementary guard.
468        if property == PropertyIdentifier::RELIABILITY {
469            if !self.out_of_service {
470                return Err(common::write_access_denied_error());
471            }
472            if let PropertyValue::Enumerated(raw) = value {
473                let v = Reliability::from_raw(raw);
474                if !common::is_reliability_value_valid(v) {
475                    return Err(common::value_out_of_range_error());
476                }
477                self.reliability = v;
478                return Ok(());
479            }
480            return Err(Error::protocol(
481                ErrorClass::PROPERTY,
482                ErrorCode::INVALID_DATA_TYPE,
483            ));
484        }
485        if let Some(result) = common::write_out_of_service_with_reliability_restore(
486            &mut self.out_of_service,
487            &mut self.reliability,
488            &mut self.reliability_before_out_of_service,
489            property,
490            &value,
491        ) {
492            result?;
493            // Back in service, the restored value may predate changes made
494            // while the client owned Reliability.
495            let _ = self.recompute_reliability();
496            return Ok(());
497        }
498        if let Some(result) = common::write_description(&mut self.description, property, &value) {
499            return result;
500        }
501        Err(crate::common::unhandled_write_error(
502            self.property_metadata().as_ref(),
503            property,
504            array_index,
505        ))
506    }
507
508    fn property_metadata(&self) -> Cow<'_, [crate::property_metadata::PropertyMetadata]> {
509        metadata::for_object(self)
510    }
511
512    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
513        crate::property_metadata::property_list_from_metadata(self.property_metadata().as_ref())
514    }
515
516    fn set_reliability_internal(&mut self, reliability: Reliability) -> Result<(), Error> {
517        // While Out_Of_Service is TRUE the client owns the simulated value;
518        // refusing here keeps the internal consistency evaluation from
519        // clobbering the simulation.
520        if self.out_of_service {
521            return Err(common::write_access_denied_error());
522        }
523        if !common::is_reliability_value_valid(reliability) {
524            return Err(common::value_out_of_range_error());
525        }
526        self.reliability = reliability;
527        // The caller owns this value; the consistency check leaves it alone.
528        self.owns_configuration_error = false;
529        Ok(())
530    }
531
532    fn evaluate_reliability_internal(
533        &mut self,
534    ) -> Result<crate::traits::ReliabilityEvaluation, Error> {
535        Ok(self.recompute_reliability())
536    }
537
538    fn take_owed_schedule_writes(&mut self) -> Vec<ScheduleWrite> {
539        self.take_owed_writes()
540    }
541
542    fn complete_schedule_write(
543        &mut self,
544        write: &ScheduleWrite,
545        outcomes: &[ScheduleTargetOutcome],
546    ) -> bool {
547        self.complete_write(write, outcomes)
548    }
549
550    fn retry_refusals_naming(&self, target: ObjectIdentifier) -> Option<ScheduleWrite> {
551        self.retry_refused_naming(target)
552    }
553
554    fn tick_schedule(
555        &mut self,
556        today: SpecificDate,
557        time: Time,
558        calendar_active: &dyn Fn(ObjectIdentifier) -> bool,
559    ) -> Option<ScheduleWrite> {
560        // Out_Of_Service decouples Present_Value from the calculation; a
561        // client's write takes its place (`out_of_service.rs`).
562        if self.out_of_service {
563            return None;
564        }
565        let Some(value) = self.evaluate(today, time, calendar_active) else {
566            // Inactive: nothing is written, and the slots the last value
567            // filled are no longer this Schedule's to relinquish
568            // (`targets.rs`). Entering the period writes anyway.
569            self.in_effective_period = false;
570            self.rewrite_owed = false;
571            self.held = None;
572            return None;
573        };
574        let entered = !core::mem::replace(&mut self.in_effective_period, true);
575        if !entered && !self.rewrite_owed && value == self.present_value {
576            // Nothing new for the list; offer the value again only to the
577            // references that refused it (`reliability.rs`, #1436).
578            return self.retry_refused();
579        }
580        self.present_value = value.clone();
581        self.command(value)
582    }
583}
584
585#[cfg(test)]
586mod tests;
587
588#[cfg(test)]
589mod calendar_tests;
590
591#[cfg(test)]
592mod evaluation_tests;
593
594#[cfg(test)]
595mod out_of_service_tests;
596
597#[cfg(test)]
598mod reliability_tests;
599
600#[cfg(test)]
601mod retry_tests;
602
603#[cfg(test)]
604mod targets_tests;
605
606#[cfg(test)]
607mod write_tests;