Skip to main content

bacnet_objects/
averaging.rs

1//! Averaging (type 18) object per ASHRAE 135-2020 Clause 12.5.
2//!
3//! Computes minimum, maximum and average statistics over a sliding window of
4//! samples taken from a referenced object property. The object keeps the
5//! schedule those samples follow; the database reads the referenced property
6//! when a sample falls due (`ObjectDatabase::sample_due_averaging_objects`),
7//! and the application can feed samples of its own.
8
9use crate::prelude::*;
10use alloc::borrow::Cow;
11use alloc::sync::Arc;
12use bacnet_types::constructed::{
13    BACnetDeviceObjectPropertyReference, BACnetObjectPropertyReference,
14};
15use bacnet_types::enums::{ObjectType, PropertyIdentifier};
16use bacnet_types::error::Error;
17use bacnet_types::primitives::{ObjectIdentifier, PropertyValue};
18use core::time::Duration;
19
20use crate::common::{self, read_identity_properties};
21use crate::device_reference;
22use crate::traits::{BACnetObject, MonotonicClock};
23
24mod metadata;
25mod schedule;
26mod window;
27
28pub use schedule::MIN_SAMPLE_PERIOD;
29pub use window::{DEFAULT_WINDOW_INTERVAL, DEFAULT_WINDOW_SAMPLES, MAX_WINDOW_SAMPLES};
30
31/// BACnet Averaging object (type 18).
32///
33/// Keeps the most recent Window_Samples sample attempts and serves the
34/// minimum, maximum and average of the valid ones. Table 12-5 has no
35/// Present_Value, Status_Flags, Event_State, Reliability or Out_Of_Service,
36/// so the object serves none of them (#1064).
37///
38/// Once the object is in a running server and holds an
39/// Object_Property_Reference, the server samples that property every
40/// Window_Interval / Window_Samples seconds (Clauses 12.5.14 and 12.5.15), at
41/// most every [`MIN_SAMPLE_PERIOD`]. A failed read, or a value the object can't
42/// average, is recorded as a missed attempt. Without a reference the
43/// application feeds the samples (see [`add_sample`](Self::add_sample) and
44/// `BACnetServer::add_averaging_sample_local`); with one, an application
45/// sample is one more attempt in the window and doesn't move the schedule.
46///
47/// Changing Window_Interval, Window_Samples or Object_Property_Reference, or
48/// writing zero to Attempted_Samples, discards the samples: both counts read
49/// zero and the statistics read positive infinity, NaN and negative infinity
50/// until the next valid sample. The schedule starts over at the same moment,
51/// so the next server sample is one spacing later.
52pub struct AveragingObject {
53    oid: ObjectIdentifier,
54    name: String,
55    description: String,
56    window: window::SampleWindow,
57    object_property_reference: Option<BACnetObjectPropertyReference>,
58}
59
60impl AveragingObject {
61    /// Create a new Averaging object; fails if `instance` exceeds the object-identifier range.
62    ///
63    /// The window starts at [`DEFAULT_WINDOW_SAMPLES`] samples over
64    /// [`DEFAULT_WINDOW_INTERVAL`] seconds, with no samples taken.
65    pub fn new(instance: u32, name: impl Into<String>) -> Result<Self, Error> {
66        let oid = ObjectIdentifier::new(ObjectType::AVERAGING, instance)?;
67        Ok(Self {
68            oid,
69            name: name.into(),
70            description: String::new(),
71            window: window::SampleWindow::new(),
72            object_property_reference: None,
73        })
74    }
75
76    /// Add a sample value as the newest slot of the window, dropping the
77    /// oldest once the window is full, and recompute the statistics.
78    ///
79    /// NaN or an infinity fails with PROPERTY / VALUE_OUT_OF_RANGE and leaves
80    /// the window unchanged. Like other direct setters this bypasses the
81    /// server's COV fanout; once the object is in a running server, its
82    /// application calls `BACnetServer::add_averaging_sample_local` instead.
83    pub fn add_sample(&mut self, value: f32) -> Result<(), Error> {
84        common::reject_non_finite(value)?;
85        self.window.push(Some(value));
86        Ok(())
87    }
88
89    /// Record a sample attempt that produced no value, such as a failed read
90    /// of the referenced property.
91    ///
92    /// The attempt takes a slot in the window, so it counts toward
93    /// Attempted_Samples but not Valid_Samples, and the statistics cover the
94    /// valid slots left.
95    pub fn add_missed_sample(&mut self) {
96        self.window.push(None);
97    }
98
99    /// Window_Interval: the period, in seconds, the window is expected to span.
100    pub fn window_interval(&self) -> u32 {
101        self.window.interval()
102    }
103
104    /// Window_Samples: how many sample attempts the window holds.
105    pub fn window_samples(&self) -> u32 {
106        self.window.capacity()
107    }
108
109    /// Set Window_Interval and discard the samples, as a network write does.
110    ///
111    /// Zero fails with PROPERTY / VALUE_OUT_OF_RANGE and changes nothing.
112    pub fn set_window_interval(&mut self, seconds: u32) -> Result<(), Error> {
113        self.window.set_interval(seconds)
114    }
115
116    /// Set Window_Samples and discard the samples, as a network write does.
117    ///
118    /// Zero, or more than [`MAX_WINDOW_SAMPLES`], fails with PROPERTY /
119    /// VALUE_OUT_OF_RANGE and changes nothing.
120    pub fn set_window_samples(&mut self, samples: u32) -> Result<(), Error> {
121        self.window.set_capacity(samples)
122    }
123
124    /// Set the object property reference (the property being averaged) and
125    /// discard the samples taken from the previous one. `None`, or a
126    /// reference to the reserved instance 4194303, leaves the object without
127    /// one (#1417).
128    pub fn set_object_property_reference(
129        &mut self,
130        reference: Option<BACnetObjectPropertyReference>,
131    ) {
132        self.object_property_reference = reference.filter(|reference| !reference.is_unset());
133        self.window.reset();
134    }
135
136    /// Set the description string.
137    pub fn set_description(&mut self, desc: impl Into<String>) {
138        self.description = desc.into();
139    }
140}
141
142/// Convert an application sample to the REAL the statistics are kept in.
143///
144/// Clause 12.5 names five datatypes a sampled property may have (BOOLEAN,
145/// INTEGER, Unsigned, Enumerated and REAL) and computes in REAL. FALSE and
146/// TRUE count as 0 and 1, and an Enumerated counts as its Unsigned value.
147/// Anything else, Double included, fails with PROPERTY / INVALID_DATA_TYPE.
148fn sample_as_real(value: &PropertyValue) -> Result<f32, Error> {
149    match *value {
150        PropertyValue::Boolean(active) => Ok(if active { 1.0 } else { 0.0 }),
151        PropertyValue::Signed(v) => Ok(v as f32),
152        PropertyValue::Unsigned(v) => Ok(v as f32),
153        PropertyValue::Enumerated(v) => Ok(v as f32),
154        PropertyValue::Real(v) => Ok(v),
155        _ => Err(common::invalid_data_type_error()),
156    }
157}
158
159/// The Unsigned a window-property write carries, as a u32. Another datatype
160/// fails with PROPERTY / INVALID_DATA_TYPE and a value past u32 with PROPERTY
161/// / VALUE_OUT_OF_RANGE.
162fn unsigned_u32(value: &PropertyValue) -> Result<u32, Error> {
163    match *value {
164        PropertyValue::Unsigned(v) => common::u64_to_u32(v),
165        _ => Err(common::invalid_data_type_error()),
166    }
167}
168
169impl BACnetObject for AveragingObject {
170    fn object_identifier(&self) -> ObjectIdentifier {
171        self.oid
172    }
173
174    fn object_name(&self) -> &str {
175        &self.name
176    }
177
178    fn read_property(
179        &self,
180        property: PropertyIdentifier,
181        array_index: Option<u32>,
182    ) -> Result<PropertyValue, Error> {
183        if let Some(result) = read_identity_properties!(self, property, array_index) {
184            return result;
185        }
186        match property {
187            p if p == PropertyIdentifier::OBJECT_TYPE => {
188                Ok(PropertyValue::Enumerated(ObjectType::AVERAGING.to_raw()))
189            }
190            p if p == PropertyIdentifier::MINIMUM_VALUE => {
191                Ok(PropertyValue::Real(self.window.minimum()))
192            }
193            p if p == PropertyIdentifier::MAXIMUM_VALUE => {
194                Ok(PropertyValue::Real(self.window.maximum()))
195            }
196            p if p == PropertyIdentifier::AVERAGE_VALUE => {
197                Ok(PropertyValue::Real(self.window.average()))
198            }
199            p if p == PropertyIdentifier::ATTEMPTED_SAMPLES => {
200                Ok(PropertyValue::Unsigned(self.window.attempted().into()))
201            }
202            p if p == PropertyIdentifier::VALID_SAMPLES => {
203                Ok(PropertyValue::Unsigned(self.window.valid().into()))
204            }
205            p if p == PropertyIdentifier::WINDOW_INTERVAL => {
206                Ok(PropertyValue::Unsigned(self.window.interval().into()))
207            }
208            p if p == PropertyIdentifier::WINDOW_SAMPLES => {
209                Ok(PropertyValue::Unsigned(self.window.capacity().into()))
210            }
211            // Table 12-5 types the property as a
212            // BACnetDeviceObjectPropertyReference (#1182). The object keeps
213            // only local references, so what it serves never has a Device
214            // member; with no reference it reads as the unset form, an
215            // Analog Input's Present_Value at the reserved instance (#1417).
216            p if p == PropertyIdentifier::OBJECT_PROPERTY_REFERENCE => {
217                Ok(device_reference::optional_reference_value(
218                    self.object_property_reference
219                        .as_ref()
220                        .map(device_reference::local_property_reference)
221                        .as_ref(),
222                    ObjectType::ANALOG_INPUT,
223                ))
224            }
225            _ => Err(common::unknown_property_error()),
226        }
227    }
228
229    fn write_property(
230        &mut self,
231        property: PropertyIdentifier,
232        _array_index: Option<u32>,
233        value: PropertyValue,
234        _priority: Option<u8>,
235    ) -> Result<(), Error> {
236        if let Some(result) = common::write_description(&mut self.description, property, &value) {
237            return result;
238        }
239        // Footnote 1 of Table 12-5 attaches the same reset to a write of any
240        // of the four rows below: each accepted write discards the samples,
241        // even when the value doesn't change, and a refused one leaves the
242        // window alone.
243        match property {
244            // Clause 12.5 Table 12-5 types Object_Property_Reference as
245            // BACnetDeviceObjectPropertyReference, and 12.5.13 leaves
246            // sampling a property on another BACnet device optional. This
247            // implementation samples local objects only, so the shared
248            // helpers decode the reference and refuse a Device member: one
249            // that isn't a Device identifier with VALUE_OUT_OF_RANGE (#1182),
250            // any other with OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED rather than
251            // silently dropping the device. The object can't tell which
252            // Device holds it; the bundled server drops a Device member naming
253            // its own Device before the value gets here (#1153). The unset
254            // form, whatever its Device member, clears the reference (#1417);
255            // Null and the flat application-tagged list are INVALID_DATA_TYPE.
256            p if p == PropertyIdentifier::OBJECT_PROPERTY_REFERENCE => {
257                let reference: BACnetDeviceObjectPropertyReference =
258                    device_reference::decode_reference(&value)?;
259                device_reference::check_device_member(reference.device_identifier)?;
260                let reference = device_reference::set_or_unset(reference)
261                    .map(device_reference::into_local_property_reference)
262                    .transpose()?;
263                self.set_object_property_reference(reference);
264                Ok(())
265            }
266            p if p == PropertyIdentifier::WINDOW_INTERVAL => {
267                self.window.set_interval(unsigned_u32(&value)?)
268            }
269            p if p == PropertyIdentifier::WINDOW_SAMPLES => {
270                self.window.set_capacity(unsigned_u32(&value)?)
271            }
272            // Zero is the only value Clause 12.5.11 lets a client write.
273            p if p == PropertyIdentifier::ATTEMPTED_SAMPLES => match value {
274                PropertyValue::Unsigned(0) => {
275                    self.window.reset();
276                    Ok(())
277                }
278                PropertyValue::Unsigned(_) => Err(common::value_out_of_range_error()),
279                _ => Err(common::invalid_data_type_error()),
280            },
281            _ => Err(common::unhandled_write_error(
282                self.property_metadata().as_ref(),
283                property,
284                _array_index,
285            )),
286        }
287    }
288
289    fn property_metadata(&self) -> Cow<'_, [crate::property_metadata::PropertyMetadata]> {
290        metadata::for_object(self)
291    }
292
293    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
294        crate::property_metadata::property_list_from_metadata(self.property_metadata().as_ref())
295    }
296
297    fn supports_subscribe_cov_property(&self) -> bool {
298        // Table 13-1 has no Averaging row, so SubscribeCOV stays refused
299        // (supports_cov is false). A property subscription uses the Table
300        // 13-1a criterion for its datatype; the object has no Status_Flags to
301        // add to the report.
302        true
303    }
304
305    fn add_averaging_sample_internal(
306        &mut self,
307        sample: Option<PropertyValue>,
308    ) -> Result<(), Error> {
309        match sample {
310            Some(value) => self.add_sample(sample_as_real(&value)?),
311            None => {
312                self.add_missed_sample();
313                Ok(())
314            }
315        }
316    }
317
318    fn bind_monotonic_clock_internal(&mut self, clock: Option<Arc<MonotonicClock>>) {
319        self.window.bind_clock(clock);
320    }
321
322    fn next_monotonic_deadline_internal(&self) -> Option<Duration> {
323        // Only an object with a reference has anything for the server to read.
324        self.object_property_reference.as_ref()?;
325        self.window.next_due()
326    }
327
328    fn take_due_averaging_sample_internal(
329        &mut self,
330        now: Duration,
331    ) -> Option<BACnetObjectPropertyReference> {
332        let reference = self.object_property_reference.clone()?;
333        self.window.take_due(now).then_some(reference)
334    }
335}
336
337#[cfg(test)]
338mod sample_tests;
339
340#[cfg(test)]
341mod schedule_tests;
342
343#[cfg(test)]
344mod tests;
345
346#[cfg(test)]
347mod window_tests;