Skip to main content

bacnet_objects/
averaging.rs

1//! Averaging (type 18) object per ASHRAE 135-2020 Clause 12.4.
2//!
3//! Computes running statistics (min, max, average) over sampled values from
4//! a referenced object property.
5
6use bacnet_types::constructed::BACnetObjectPropertyReference;
7use bacnet_types::enums::{ObjectType, PropertyIdentifier};
8use bacnet_types::error::Error;
9use bacnet_types::primitives::{ObjectIdentifier, PropertyValue, StatusFlags};
10use std::borrow::Cow;
11
12use crate::common::{self, read_common_properties};
13use crate::traits::BACnetObject;
14
15/// BACnet Averaging object (type 18).
16///
17/// Accumulates sample values and computes min/max/average statistics.
18/// The `present_value` property reflects the current average.
19pub struct AveragingObject {
20    oid: ObjectIdentifier,
21    name: String,
22    description: String,
23    present_value: f32,
24    minimum_value: f32,
25    maximum_value: f32,
26    average_value: f32,
27    attempted_samples: u32,
28    valid_samples: u32,
29    object_property_reference: Option<BACnetObjectPropertyReference>,
30    status_flags: StatusFlags,
31    out_of_service: bool,
32    reliability: u32,
33}
34
35impl AveragingObject {
36    pub fn new(instance: u32, name: impl Into<String>) -> Result<Self, Error> {
37        let oid = ObjectIdentifier::new(ObjectType::AVERAGING, instance)?;
38        Ok(Self {
39            oid,
40            name: name.into(),
41            description: String::new(),
42            present_value: 0.0,
43            minimum_value: f32::MAX,
44            maximum_value: f32::MIN,
45            average_value: 0.0,
46            attempted_samples: 0,
47            valid_samples: 0,
48            object_property_reference: None,
49            status_flags: StatusFlags::empty(),
50            out_of_service: false,
51            reliability: 0,
52        })
53    }
54
55    /// Add a sample value, updating min/max/average and counts.
56    pub fn add_sample(&mut self, value: f32) {
57        self.attempted_samples += 1;
58        self.valid_samples += 1;
59
60        if value < self.minimum_value {
61            self.minimum_value = value;
62        }
63        if value > self.maximum_value {
64            self.maximum_value = value;
65        }
66
67        // Running average: avg = avg_prev + (value - avg_prev) / n
68        self.average_value += (value - self.average_value) / self.valid_samples as f32;
69        self.present_value = self.average_value;
70    }
71
72    /// Set the object property reference (the property being averaged).
73    pub fn set_object_property_reference(
74        &mut self,
75        reference: Option<BACnetObjectPropertyReference>,
76    ) {
77        self.object_property_reference = reference;
78    }
79
80    /// Set the description string.
81    pub fn set_description(&mut self, desc: impl Into<String>) {
82        self.description = desc.into();
83    }
84}
85
86impl BACnetObject for AveragingObject {
87    fn object_identifier(&self) -> ObjectIdentifier {
88        self.oid
89    }
90
91    fn object_name(&self) -> &str {
92        &self.name
93    }
94
95    fn read_property(
96        &self,
97        property: PropertyIdentifier,
98        array_index: Option<u32>,
99    ) -> Result<PropertyValue, Error> {
100        if let Some(result) = read_common_properties!(self, property, array_index) {
101            return result;
102        }
103        match property {
104            p if p == PropertyIdentifier::OBJECT_TYPE => {
105                Ok(PropertyValue::Enumerated(ObjectType::AVERAGING.to_raw()))
106            }
107            p if p == PropertyIdentifier::PRESENT_VALUE => {
108                Ok(PropertyValue::Real(self.present_value))
109            }
110            p if p == PropertyIdentifier::MINIMUM_VALUE => {
111                if self.valid_samples == 0 {
112                    Ok(PropertyValue::Real(0.0))
113                } else {
114                    Ok(PropertyValue::Real(self.minimum_value))
115                }
116            }
117            p if p == PropertyIdentifier::MAXIMUM_VALUE => {
118                if self.valid_samples == 0 {
119                    Ok(PropertyValue::Real(0.0))
120                } else {
121                    Ok(PropertyValue::Real(self.maximum_value))
122                }
123            }
124            p if p == PropertyIdentifier::AVERAGE_VALUE => {
125                Ok(PropertyValue::Real(self.average_value))
126            }
127            p if p == PropertyIdentifier::ATTEMPTED_SAMPLES => {
128                Ok(PropertyValue::Unsigned(self.attempted_samples as u64))
129            }
130            p if p == PropertyIdentifier::VALID_SAMPLES => {
131                Ok(PropertyValue::Unsigned(self.valid_samples as u64))
132            }
133            p if p == PropertyIdentifier::OBJECT_PROPERTY_REFERENCE => {
134                match &self.object_property_reference {
135                    None => Ok(PropertyValue::Null),
136                    Some(r) => {
137                        let mut fields = vec![
138                            PropertyValue::ObjectIdentifier(r.object_identifier),
139                            PropertyValue::Unsigned(r.property_identifier as u64),
140                        ];
141                        if let Some(idx) = r.property_array_index {
142                            fields.push(PropertyValue::Unsigned(idx as u64));
143                        }
144                        Ok(PropertyValue::List(fields))
145                    }
146                }
147            }
148            p if p == PropertyIdentifier::EVENT_STATE => Ok(PropertyValue::Enumerated(0)),
149            _ => Err(common::unknown_property_error()),
150        }
151    }
152
153    fn write_property(
154        &mut self,
155        property: PropertyIdentifier,
156        _array_index: Option<u32>,
157        value: PropertyValue,
158        _priority: Option<u8>,
159    ) -> Result<(), Error> {
160        if let Some(result) = common::write_description(&mut self.description, property, &value) {
161            return result;
162        }
163        if let Some(result) =
164            common::write_out_of_service(&mut self.out_of_service, property, &value)
165        {
166            return result;
167        }
168        if property == PropertyIdentifier::OBJECT_PROPERTY_REFERENCE {
169            match value {
170                PropertyValue::Null => {
171                    self.object_property_reference = None;
172                    return Ok(());
173                }
174                PropertyValue::List(ref items) if items.len() >= 2 => {
175                    if let (PropertyValue::ObjectIdentifier(oid), PropertyValue::Unsigned(prop)) =
176                        (&items[0], &items[1])
177                    {
178                        let array_index = if items.len() > 2 {
179                            if let PropertyValue::Unsigned(idx) = &items[2] {
180                                Some(*idx as u32)
181                            } else {
182                                None
183                            }
184                        } else {
185                            None
186                        };
187                        self.object_property_reference = Some(if let Some(idx) = array_index {
188                            BACnetObjectPropertyReference::new_indexed(*oid, *prop as u32, idx)
189                        } else {
190                            BACnetObjectPropertyReference::new(*oid, *prop as u32)
191                        });
192                        return Ok(());
193                    }
194                    return Err(common::invalid_data_type_error());
195                }
196                _ => return Err(common::invalid_data_type_error()),
197            }
198        }
199        Err(common::write_access_denied_error())
200    }
201
202    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
203        static PROPS: &[PropertyIdentifier] = &[
204            PropertyIdentifier::OBJECT_IDENTIFIER,
205            PropertyIdentifier::OBJECT_NAME,
206            PropertyIdentifier::DESCRIPTION,
207            PropertyIdentifier::OBJECT_TYPE,
208            PropertyIdentifier::PRESENT_VALUE,
209            PropertyIdentifier::MINIMUM_VALUE,
210            PropertyIdentifier::MAXIMUM_VALUE,
211            PropertyIdentifier::AVERAGE_VALUE,
212            PropertyIdentifier::ATTEMPTED_SAMPLES,
213            PropertyIdentifier::VALID_SAMPLES,
214            PropertyIdentifier::OBJECT_PROPERTY_REFERENCE,
215            PropertyIdentifier::STATUS_FLAGS,
216            PropertyIdentifier::OUT_OF_SERVICE,
217            PropertyIdentifier::RELIABILITY,
218            PropertyIdentifier::EVENT_STATE,
219        ];
220        Cow::Borrowed(PROPS)
221    }
222}
223
224#[cfg(test)]
225mod tests {
226    use super::*;
227    use bacnet_types::enums::ObjectType;
228
229    #[test]
230    fn averaging_create() {
231        let avg = AveragingObject::new(1, "AVG-1").unwrap();
232        assert_eq!(
233            avg.read_property(PropertyIdentifier::OBJECT_NAME, None)
234                .unwrap(),
235            PropertyValue::CharacterString("AVG-1".into())
236        );
237        assert_eq!(
238            avg.read_property(PropertyIdentifier::OBJECT_TYPE, None)
239                .unwrap(),
240            PropertyValue::Enumerated(ObjectType::AVERAGING.to_raw())
241        );
242        assert_eq!(
243            avg.read_property(PropertyIdentifier::PRESENT_VALUE, None)
244                .unwrap(),
245            PropertyValue::Real(0.0)
246        );
247    }
248
249    #[test]
250    fn averaging_add_samples() {
251        let mut avg = AveragingObject::new(1, "AVG-1").unwrap();
252        avg.add_sample(10.0);
253        avg.add_sample(20.0);
254        avg.add_sample(30.0);
255
256        assert_eq!(
257            avg.read_property(PropertyIdentifier::ATTEMPTED_SAMPLES, None)
258                .unwrap(),
259            PropertyValue::Unsigned(3)
260        );
261        assert_eq!(
262            avg.read_property(PropertyIdentifier::VALID_SAMPLES, None)
263                .unwrap(),
264            PropertyValue::Unsigned(3)
265        );
266    }
267
268    #[test]
269    fn averaging_min_max() {
270        let mut avg = AveragingObject::new(1, "AVG-1").unwrap();
271        avg.add_sample(15.0);
272        avg.add_sample(5.0);
273        avg.add_sample(25.0);
274
275        assert_eq!(
276            avg.read_property(PropertyIdentifier::MINIMUM_VALUE, None)
277                .unwrap(),
278            PropertyValue::Real(5.0)
279        );
280        assert_eq!(
281            avg.read_property(PropertyIdentifier::MAXIMUM_VALUE, None)
282                .unwrap(),
283            PropertyValue::Real(25.0)
284        );
285    }
286
287    #[test]
288    fn averaging_average_value() {
289        let mut avg = AveragingObject::new(1, "AVG-1").unwrap();
290        avg.add_sample(10.0);
291        avg.add_sample(20.0);
292        avg.add_sample(30.0);
293
294        let val = avg
295            .read_property(PropertyIdentifier::AVERAGE_VALUE, None)
296            .unwrap();
297        if let PropertyValue::Real(v) = val {
298            assert!((v - 20.0).abs() < 0.001);
299        } else {
300            panic!("Expected Real");
301        }
302
303        // present_value should equal average_value
304        let pv = avg
305            .read_property(PropertyIdentifier::PRESENT_VALUE, None)
306            .unwrap();
307        assert_eq!(pv, val);
308    }
309
310    #[test]
311    fn averaging_no_samples_defaults() {
312        let avg = AveragingObject::new(1, "AVG-1").unwrap();
313        // Before any samples, min/max return 0.0
314        assert_eq!(
315            avg.read_property(PropertyIdentifier::MINIMUM_VALUE, None)
316                .unwrap(),
317            PropertyValue::Real(0.0)
318        );
319        assert_eq!(
320            avg.read_property(PropertyIdentifier::MAXIMUM_VALUE, None)
321                .unwrap(),
322            PropertyValue::Real(0.0)
323        );
324        assert_eq!(
325            avg.read_property(PropertyIdentifier::AVERAGE_VALUE, None)
326                .unwrap(),
327            PropertyValue::Real(0.0)
328        );
329    }
330
331    #[test]
332    fn averaging_property_list() {
333        let avg = AveragingObject::new(1, "AVG-1").unwrap();
334        let props = avg.property_list();
335        assert!(props.contains(&PropertyIdentifier::PRESENT_VALUE));
336        assert!(props.contains(&PropertyIdentifier::MINIMUM_VALUE));
337        assert!(props.contains(&PropertyIdentifier::MAXIMUM_VALUE));
338        assert!(props.contains(&PropertyIdentifier::AVERAGE_VALUE));
339        assert!(props.contains(&PropertyIdentifier::ATTEMPTED_SAMPLES));
340        assert!(props.contains(&PropertyIdentifier::VALID_SAMPLES));
341        assert!(props.contains(&PropertyIdentifier::OBJECT_PROPERTY_REFERENCE));
342        assert!(props.contains(&PropertyIdentifier::STATUS_FLAGS));
343        assert!(props.contains(&PropertyIdentifier::OUT_OF_SERVICE));
344        assert!(props.contains(&PropertyIdentifier::RELIABILITY));
345    }
346
347    #[test]
348    fn averaging_object_property_reference_default_null() {
349        let avg = AveragingObject::new(1, "AVG-1").unwrap();
350        assert_eq!(
351            avg.read_property(PropertyIdentifier::OBJECT_PROPERTY_REFERENCE, None)
352                .unwrap(),
353            PropertyValue::Null
354        );
355    }
356
357    #[test]
358    fn averaging_set_object_property_reference() {
359        let mut avg = AveragingObject::new(1, "AVG-1").unwrap();
360        let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 5).unwrap();
361        let pv_raw = PropertyIdentifier::PRESENT_VALUE.to_raw();
362        avg.set_object_property_reference(Some(BACnetObjectPropertyReference::new(oid, pv_raw)));
363
364        let val = avg
365            .read_property(PropertyIdentifier::OBJECT_PROPERTY_REFERENCE, None)
366            .unwrap();
367        assert_eq!(
368            val,
369            PropertyValue::List(vec![
370                PropertyValue::ObjectIdentifier(oid),
371                PropertyValue::Unsigned(pv_raw as u64),
372            ])
373        );
374    }
375
376    #[test]
377    fn averaging_write_object_property_reference() {
378        let mut avg = AveragingObject::new(1, "AVG-1").unwrap();
379        let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 3).unwrap();
380        let pv_raw = PropertyIdentifier::PRESENT_VALUE.to_raw();
381
382        avg.write_property(
383            PropertyIdentifier::OBJECT_PROPERTY_REFERENCE,
384            None,
385            PropertyValue::List(vec![
386                PropertyValue::ObjectIdentifier(oid),
387                PropertyValue::Unsigned(pv_raw as u64),
388            ]),
389            None,
390        )
391        .unwrap();
392
393        assert_eq!(
394            avg.read_property(PropertyIdentifier::OBJECT_PROPERTY_REFERENCE, None)
395                .unwrap(),
396            PropertyValue::List(vec![
397                PropertyValue::ObjectIdentifier(oid),
398                PropertyValue::Unsigned(pv_raw as u64),
399            ])
400        );
401    }
402
403    #[test]
404    fn averaging_write_null_clears_reference() {
405        let mut avg = AveragingObject::new(1, "AVG-1").unwrap();
406        let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
407        avg.set_object_property_reference(Some(BACnetObjectPropertyReference::new(
408            oid,
409            PropertyIdentifier::PRESENT_VALUE.to_raw(),
410        )));
411
412        avg.write_property(
413            PropertyIdentifier::OBJECT_PROPERTY_REFERENCE,
414            None,
415            PropertyValue::Null,
416            None,
417        )
418        .unwrap();
419
420        assert_eq!(
421            avg.read_property(PropertyIdentifier::OBJECT_PROPERTY_REFERENCE, None)
422                .unwrap(),
423            PropertyValue::Null
424        );
425    }
426
427    #[test]
428    fn averaging_write_present_value_denied() {
429        let mut avg = AveragingObject::new(1, "AVG-1").unwrap();
430        let result = avg.write_property(
431            PropertyIdentifier::PRESENT_VALUE,
432            None,
433            PropertyValue::Real(42.0),
434            None,
435        );
436        assert!(result.is_err());
437    }
438
439    #[test]
440    fn averaging_description_read_write() {
441        let mut avg = AveragingObject::new(1, "AVG-1").unwrap();
442        assert_eq!(
443            avg.read_property(PropertyIdentifier::DESCRIPTION, None)
444                .unwrap(),
445            PropertyValue::CharacterString(String::new())
446        );
447        avg.write_property(
448            PropertyIdentifier::DESCRIPTION,
449            None,
450            PropertyValue::CharacterString("Zone temperature averaging".into()),
451            None,
452        )
453        .unwrap();
454        assert_eq!(
455            avg.read_property(PropertyIdentifier::DESCRIPTION, None)
456                .unwrap(),
457            PropertyValue::CharacterString("Zone temperature averaging".into())
458        );
459    }
460
461    #[test]
462    fn averaging_single_sample() {
463        let mut avg = AveragingObject::new(1, "AVG-1").unwrap();
464        avg.add_sample(42.0);
465
466        assert_eq!(
467            avg.read_property(PropertyIdentifier::MINIMUM_VALUE, None)
468                .unwrap(),
469            PropertyValue::Real(42.0)
470        );
471        assert_eq!(
472            avg.read_property(PropertyIdentifier::MAXIMUM_VALUE, None)
473                .unwrap(),
474            PropertyValue::Real(42.0)
475        );
476        assert_eq!(
477            avg.read_property(PropertyIdentifier::AVERAGE_VALUE, None)
478                .unwrap(),
479            PropertyValue::Real(42.0)
480        );
481        assert_eq!(
482            avg.read_property(PropertyIdentifier::PRESENT_VALUE, None)
483                .unwrap(),
484            PropertyValue::Real(42.0)
485        );
486    }
487}