Skip to main content

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}