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