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;