bacnet_objects/schedule/evaluation.rs
1//! Present_Value calculation for the Schedule object (Clause 12.24.4) and the
2//! value checks its schedules share (#1028).
3//!
4//! Within Effective_Period, the value comes from the first of these sources
5//! that has one:
6//!
7//! 1. Exception_Schedule: among the special events in effect today, the one
8//! with the best event priority (1 is best; the lower array index breaks a
9//! tie) whose current value is not NULL. An event is in effect when its
10//! inline calendar entry matches today or the Calendar it references is
11//! TRUE today.
12//! 2. Today's Weekly_Schedule entry, if its current value is not NULL.
13//! 3. Schedule_Default, which may itself be NULL.
14//!
15//! A list of time-values' current value is the value of its latest entry at
16//! or before the current time; before the first entry it has none. A NULL
17//! value therefore ends a scheduled period and hands control to the next
18//! source. Outside Effective_Period the object is inactive: Present_Value
19//! keeps its last value and nothing is written.
20
21use std::collections::HashSet;
22
23use bacnet_types::calendar::SpecificDate;
24use bacnet_types::constructed::{
25 BACnetObjectPropertyReference, BACnetSpecialEvent, BACnetTimeValue, SpecialEventPeriod,
26};
27use bacnet_types::enums::{ErrorClass, ErrorCode};
28use bacnet_types::error::Error;
29use bacnet_types::primitives::{ObjectIdentifier, PropertyValue, Time};
30
31use super::ScheduleObject;
32use crate::common;
33
34/// The writes one Schedule evaluation owes to List_Of_Object_Property_References.
35#[derive(Debug, Clone, PartialEq)]
36pub struct ScheduleWrite {
37 /// The new Present_Value in the scheduled value's own datatype. A NULL
38 /// relinquishes the slot at `priority` in a commandable target.
39 pub value: PropertyValue,
40 /// Priority_For_Writing, 1 to 16: the priority each target is written at.
41 pub priority: u8,
42 /// The complete local references, target array indices included.
43 pub references: Vec<BACnetObjectPropertyReference>,
44 /// Whether this write only offers the value the references already owe
45 /// again, to those whose last write was refused (#1436). A Schedule makes
46 /// it on every pass with nothing else to send while a refusal stands, so
47 /// the server logs a target's failure of it at debug, not warn.
48 pub retry: bool,
49}
50
51/// How one target took a [`ScheduleWrite`], as the server reports it back
52/// through
53/// [`complete_schedule_write`](crate::traits::BACnetObject::complete_schedule_write)
54/// (#1086).
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub enum ScheduleTargetOutcome {
57 /// The target took the value.
58 Accepted,
59 /// The target refused the value's datatype: INVALID_DATA_TYPE or
60 /// DATATYPE_NOT_SUPPORTED.
61 DatatypeRefused,
62 /// The reference names nothing the target can write, whatever the value
63 /// (#1433): UNKNOWN_OBJECT (a missing target object included),
64 /// UNKNOWN_PROPERTY, PROPERTY_IS_NOT_AN_ARRAY or INVALID_ARRAY_INDEX.
65 ReferenceRefused,
66 /// Any other failure. It says nothing about the configuration:
67 /// WRITE_ACCESS_DENIED, for one, can come from the target's state
68 /// (Out_Of_Service, a lock) and pass with it. On a
69 /// [`retry`](ScheduleWrite::retry) it ends the member's refusal, since
70 /// the target no longer refuses it (#1436).
71 Failed,
72}
73
74impl ScheduleTargetOutcome {
75 /// Classify the result of one target write.
76 pub fn of(result: &Result<(), Error>) -> Self {
77 const DATATYPE: [ErrorCode; 2] = [
78 ErrorCode::INVALID_DATA_TYPE,
79 ErrorCode::DATATYPE_NOT_SUPPORTED,
80 ];
81 const REFERENCE: [ErrorCode; 4] = [
82 ErrorCode::UNKNOWN_OBJECT,
83 ErrorCode::UNKNOWN_PROPERTY,
84 ErrorCode::PROPERTY_IS_NOT_AN_ARRAY,
85 ErrorCode::INVALID_ARRAY_INDEX,
86 ];
87 let code = match result {
88 Ok(()) => return Self::Accepted,
89 Err(Error::Protocol { code, .. } | Error::Structured { code, .. }) => *code,
90 Err(_) => return Self::Failed,
91 };
92 let names = |codes: &[ErrorCode]| codes.iter().any(|known| known.to_raw() as u32 == code);
93 if names(&DATATYPE) {
94 Self::DatatypeRefused
95 } else if names(&REFERENCE) {
96 Self::ReferenceRefused
97 } else {
98 Self::Failed
99 }
100 }
101}
102
103impl ScheduleObject {
104 /// Present_Value per Clause 12.24.4 at `time` on `today`, or `None` when
105 /// `today` is outside Effective_Period.
106 ///
107 /// `calendar_active` answers for a special event whose period references
108 /// a Calendar: whether that Calendar is TRUE on `today`. A reference to a
109 /// missing object or to anything but a Calendar should answer `false`.
110 /// Out_Of_Service does not change the calculation.
111 pub fn evaluate(
112 &self,
113 today: SpecificDate,
114 time: Time,
115 calendar_active: &dyn Fn(ObjectIdentifier) -> bool,
116 ) -> Option<PropertyValue> {
117 if !self.effective_period.contains(today) {
118 return None;
119 }
120 let mut exception: Option<(u64, &PropertyValue)> = None;
121 for event in &self.exception_schedule {
122 // Iterating in array order, a later event wins only with a
123 // strictly better priority.
124 if exception.is_some_and(|(best, _)| best <= event.event_priority) {
125 continue;
126 }
127 let in_effect = match &event.period {
128 SpecialEventPeriod::CalendarEntry(entry) => entry.matches(today),
129 SpecialEventPeriod::CalendarReference(calendar) => calendar_active(*calendar),
130 };
131 if !in_effect {
132 continue;
133 }
134 if let Some(value) = current_value(&event.list_of_time_values, time) {
135 exception = Some((event.event_priority, value));
136 }
137 }
138 if let Some((_, value)) = exception {
139 return Some(value.clone());
140 }
141 let weekly = &self.weekly_schedule[usize::from(today.weekday() - 1)];
142 Some(
143 current_value(weekly, time)
144 .unwrap_or(&self.schedule_default)
145 .clone(),
146 )
147 }
148}
149
150/// The current value of a list of time-values at `time`: that of the latest
151/// entry at or before `time`, or `None` before the first entry or when that
152/// entry's value is NULL.
153fn current_value(entries: &[BACnetTimeValue], time: Time) -> Option<&PropertyValue> {
154 let key = |t: &Time| (t.hour, t.minute, t.second, t.hundredths);
155 entries
156 .iter()
157 .filter(|entry| key(&entry.time) <= key(&time))
158 .max_by_key(|entry| key(&entry.time))
159 .map(|entry| &entry.value)
160 .filter(|value| **value != PropertyValue::Null)
161}
162
163/// Check one list of time-values: each time specific (VALUE_OUT_OF_RANGE),
164/// each value primitive (INVALID_DATA_TYPE), and no time twice
165/// (DUPLICATE_ENTRY, as Clauses 12.24.7 and 12.24.8 ask of a written day).
166pub(super) fn check_time_values(entries: &[BACnetTimeValue]) -> Result<(), Error> {
167 let mut times = HashSet::with_capacity(entries.len());
168 for entry in entries {
169 if !entry.time.is_specific() {
170 return Err(common::value_out_of_range_error());
171 }
172 if !entry.value.is_primitive() {
173 return Err(common::invalid_data_type_error());
174 }
175 if !times.insert(entry.time) {
176 return Err(common::protocol_error(
177 ErrorClass::PROPERTY,
178 ErrorCode::DUPLICATE_ENTRY,
179 ));
180 }
181 }
182 Ok(())
183}
184
185/// Check a special event: event priority 1 to 16 and an inline calendar entry
186/// in range (VALUE_OUT_OF_RANGE), then its time-values.
187pub(super) fn check_special_event(event: &BACnetSpecialEvent) -> Result<(), Error> {
188 let period_ok = match &event.period {
189 SpecialEventPeriod::CalendarEntry(entry) => entry.is_valid(),
190 SpecialEventPeriod::CalendarReference(_) => true,
191 };
192 if !(1..=16).contains(&event.event_priority) || !period_ok {
193 return Err(common::value_out_of_range_error());
194 }
195 check_time_values(&event.list_of_time_values)
196}