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