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