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 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}