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;