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        // Clause 12.5 Table 12-5 types Object_Property_Reference as
169        // BACnetDeviceObjectPropertyReference, and the object text leaves
170        // referencing an object in a DIFFERENT device explicit ("Optionally,
171        // the object property to be sampled may exist in a different BACnet
172        // device") — this implementation samples local objects only, so the
173        // shared arm helper decodes with the local-only
174        // BACnetObjectPropertyReference framing: a device-qualified member
175        // [3] refuses INVALID_DATA_ENCODING instead of silently dropping the
176        // device, and the flat form keeps its historical Unsigned members
177        // (both Unsigned and Enumerated are accepted there; see
178        // reference.rs).
179        if property == PropertyIdentifier::OBJECT_PROPERTY_REFERENCE {
180            self.object_property_reference = crate::reference::decode_reference_write(
181                &value,
182                crate::reference::ReferenceFrame::Bare,
183            )?;
184            return Ok(());
185        }
186        Err(common::write_access_denied_error())
187    }
188
189    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
190        static PROPS: &[PropertyIdentifier] = &[
191            PropertyIdentifier::OBJECT_IDENTIFIER,
192            PropertyIdentifier::OBJECT_NAME,
193            PropertyIdentifier::DESCRIPTION,
194            PropertyIdentifier::OBJECT_TYPE,
195            PropertyIdentifier::PRESENT_VALUE,
196            PropertyIdentifier::MINIMUM_VALUE,
197            PropertyIdentifier::MAXIMUM_VALUE,
198            PropertyIdentifier::AVERAGE_VALUE,
199            PropertyIdentifier::ATTEMPTED_SAMPLES,
200            PropertyIdentifier::VALID_SAMPLES,
201            PropertyIdentifier::OBJECT_PROPERTY_REFERENCE,
202            PropertyIdentifier::STATUS_FLAGS,
203            PropertyIdentifier::OUT_OF_SERVICE,
204            PropertyIdentifier::RELIABILITY,
205            PropertyIdentifier::EVENT_STATE,
206        ];
207        Cow::Borrowed(PROPS)
208    }
209
210    /// Mirror the `write_property` arms exactly (PICS truth invariant):
211    /// Averaging accepts OBJECT_PROPERTY_REFERENCE plus the shared
212    /// DESCRIPTION / OUT_OF_SERVICE routes. OBJECT_NAME is NOT advertised:
213    /// unlike the historical default's blanket claim, no arm routes it (a
214    /// network write falls through to WRITE_ACCESS_DENIED).
215    fn is_writable_property(&self, property: PropertyIdentifier) -> bool {
216        matches!(
217            property,
218            PropertyIdentifier::OBJECT_PROPERTY_REFERENCE
219                | PropertyIdentifier::DESCRIPTION
220                | PropertyIdentifier::OUT_OF_SERVICE
221        )
222    }
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228    use bacnet_types::enums::ObjectType;
229
230    #[test]
231    fn averaging_create() {
232        let avg = AveragingObject::new(1, "AVG-1").unwrap();
233        assert_eq!(
234            avg.read_property(PropertyIdentifier::OBJECT_NAME, None)
235                .unwrap(),
236            PropertyValue::CharacterString("AVG-1".into())
237        );
238        assert_eq!(
239            avg.read_property(PropertyIdentifier::OBJECT_TYPE, None)
240                .unwrap(),
241            PropertyValue::Enumerated(ObjectType::AVERAGING.to_raw())
242        );
243        assert_eq!(
244            avg.read_property(PropertyIdentifier::PRESENT_VALUE, None)
245                .unwrap(),
246            PropertyValue::Real(0.0)
247        );
248    }
249
250    #[test]
251    fn averaging_add_samples() {
252        let mut avg = AveragingObject::new(1, "AVG-1").unwrap();
253        avg.add_sample(10.0);
254        avg.add_sample(20.0);
255        avg.add_sample(30.0);
256
257        assert_eq!(
258            avg.read_property(PropertyIdentifier::ATTEMPTED_SAMPLES, None)
259                .unwrap(),
260            PropertyValue::Unsigned(3)
261        );
262        assert_eq!(
263            avg.read_property(PropertyIdentifier::VALID_SAMPLES, None)
264                .unwrap(),
265            PropertyValue::Unsigned(3)
266        );
267    }
268
269    #[test]
270    fn averaging_min_max() {
271        let mut avg = AveragingObject::new(1, "AVG-1").unwrap();
272        avg.add_sample(15.0);
273        avg.add_sample(5.0);
274        avg.add_sample(25.0);
275
276        assert_eq!(
277            avg.read_property(PropertyIdentifier::MINIMUM_VALUE, None)
278                .unwrap(),
279            PropertyValue::Real(5.0)
280        );
281        assert_eq!(
282            avg.read_property(PropertyIdentifier::MAXIMUM_VALUE, None)
283                .unwrap(),
284            PropertyValue::Real(25.0)
285        );
286    }
287
288    #[test]
289    fn averaging_average_value() {
290        let mut avg = AveragingObject::new(1, "AVG-1").unwrap();
291        avg.add_sample(10.0);
292        avg.add_sample(20.0);
293        avg.add_sample(30.0);
294
295        let val = avg
296            .read_property(PropertyIdentifier::AVERAGE_VALUE, None)
297            .unwrap();
298        if let PropertyValue::Real(v) = val {
299            assert!((v - 20.0).abs() < 0.001);
300        } else {
301            panic!("Expected Real");
302        }
303
304        // present_value should equal average_value
305        let pv = avg
306            .read_property(PropertyIdentifier::PRESENT_VALUE, None)
307            .unwrap();
308        assert_eq!(pv, val);
309    }
310
311    #[test]
312    fn averaging_no_samples_defaults() {
313        let avg = AveragingObject::new(1, "AVG-1").unwrap();
314        // Before any samples, min/max return 0.0
315        assert_eq!(
316            avg.read_property(PropertyIdentifier::MINIMUM_VALUE, None)
317                .unwrap(),
318            PropertyValue::Real(0.0)
319        );
320        assert_eq!(
321            avg.read_property(PropertyIdentifier::MAXIMUM_VALUE, None)
322                .unwrap(),
323            PropertyValue::Real(0.0)
324        );
325        assert_eq!(
326            avg.read_property(PropertyIdentifier::AVERAGE_VALUE, None)
327                .unwrap(),
328            PropertyValue::Real(0.0)
329        );
330    }
331
332    #[test]
333    fn averaging_property_list() {
334        let avg = AveragingObject::new(1, "AVG-1").unwrap();
335        let props = avg.property_list();
336        assert!(props.contains(&PropertyIdentifier::PRESENT_VALUE));
337        assert!(props.contains(&PropertyIdentifier::MINIMUM_VALUE));
338        assert!(props.contains(&PropertyIdentifier::MAXIMUM_VALUE));
339        assert!(props.contains(&PropertyIdentifier::AVERAGE_VALUE));
340        assert!(props.contains(&PropertyIdentifier::ATTEMPTED_SAMPLES));
341        assert!(props.contains(&PropertyIdentifier::VALID_SAMPLES));
342        assert!(props.contains(&PropertyIdentifier::OBJECT_PROPERTY_REFERENCE));
343        assert!(props.contains(&PropertyIdentifier::STATUS_FLAGS));
344        assert!(props.contains(&PropertyIdentifier::OUT_OF_SERVICE));
345        assert!(props.contains(&PropertyIdentifier::RELIABILITY));
346    }
347
348    #[test]
349    fn averaging_object_property_reference_default_null() {
350        let avg = AveragingObject::new(1, "AVG-1").unwrap();
351        assert_eq!(
352            avg.read_property(PropertyIdentifier::OBJECT_PROPERTY_REFERENCE, None)
353                .unwrap(),
354            PropertyValue::Null
355        );
356    }
357
358    #[test]
359    fn averaging_set_object_property_reference() {
360        let mut avg = AveragingObject::new(1, "AVG-1").unwrap();
361        let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 5).unwrap();
362        let pv_raw = PropertyIdentifier::PRESENT_VALUE.to_raw();
363        avg.set_object_property_reference(Some(BACnetObjectPropertyReference::new(oid, pv_raw)));
364
365        let val = avg
366            .read_property(PropertyIdentifier::OBJECT_PROPERTY_REFERENCE, None)
367            .unwrap();
368        assert_eq!(
369            val,
370            PropertyValue::List(vec![
371                PropertyValue::ObjectIdentifier(oid),
372                PropertyValue::Unsigned(pv_raw as u64),
373            ])
374        );
375    }
376
377    #[test]
378    fn averaging_write_object_property_reference() {
379        let mut avg = AveragingObject::new(1, "AVG-1").unwrap();
380        let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 3).unwrap();
381        let pv_raw = PropertyIdentifier::PRESENT_VALUE.to_raw();
382
383        avg.write_property(
384            PropertyIdentifier::OBJECT_PROPERTY_REFERENCE,
385            None,
386            PropertyValue::List(vec![
387                PropertyValue::ObjectIdentifier(oid),
388                PropertyValue::Unsigned(pv_raw as u64),
389            ]),
390            None,
391        )
392        .unwrap();
393
394        assert_eq!(
395            avg.read_property(PropertyIdentifier::OBJECT_PROPERTY_REFERENCE, None)
396                .unwrap(),
397            PropertyValue::List(vec![
398                PropertyValue::ObjectIdentifier(oid),
399                PropertyValue::Unsigned(pv_raw as u64),
400            ])
401        );
402    }
403
404    #[test]
405    fn averaging_write_null_clears_reference() {
406        let mut avg = AveragingObject::new(1, "AVG-1").unwrap();
407        let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
408        avg.set_object_property_reference(Some(BACnetObjectPropertyReference::new(
409            oid,
410            PropertyIdentifier::PRESENT_VALUE.to_raw(),
411        )));
412
413        avg.write_property(
414            PropertyIdentifier::OBJECT_PROPERTY_REFERENCE,
415            None,
416            PropertyValue::Null,
417            None,
418        )
419        .unwrap();
420
421        assert_eq!(
422            avg.read_property(PropertyIdentifier::OBJECT_PROPERTY_REFERENCE, None)
423                .unwrap(),
424            PropertyValue::Null
425        );
426    }
427
428    #[test]
429    fn averaging_write_present_value_denied() {
430        let mut avg = AveragingObject::new(1, "AVG-1").unwrap();
431        let result = avg.write_property(
432            PropertyIdentifier::PRESENT_VALUE,
433            None,
434            PropertyValue::Real(42.0),
435            None,
436        );
437        assert!(result.is_err());
438    }
439
440    #[test]
441    fn averaging_description_read_write() {
442        let mut avg = AveragingObject::new(1, "AVG-1").unwrap();
443        assert_eq!(
444            avg.read_property(PropertyIdentifier::DESCRIPTION, None)
445                .unwrap(),
446            PropertyValue::CharacterString(String::new())
447        );
448        avg.write_property(
449            PropertyIdentifier::DESCRIPTION,
450            None,
451            PropertyValue::CharacterString("Zone temperature averaging".into()),
452            None,
453        )
454        .unwrap();
455        assert_eq!(
456            avg.read_property(PropertyIdentifier::DESCRIPTION, None)
457                .unwrap(),
458            PropertyValue::CharacterString("Zone temperature averaging".into())
459        );
460    }
461
462    #[test]
463    fn averaging_single_sample() {
464        let mut avg = AveragingObject::new(1, "AVG-1").unwrap();
465        avg.add_sample(42.0);
466
467        assert_eq!(
468            avg.read_property(PropertyIdentifier::MINIMUM_VALUE, None)
469                .unwrap(),
470            PropertyValue::Real(42.0)
471        );
472        assert_eq!(
473            avg.read_property(PropertyIdentifier::MAXIMUM_VALUE, None)
474                .unwrap(),
475            PropertyValue::Real(42.0)
476        );
477        assert_eq!(
478            avg.read_property(PropertyIdentifier::AVERAGE_VALUE, None)
479                .unwrap(),
480            PropertyValue::Real(42.0)
481        );
482        assert_eq!(
483            avg.read_property(PropertyIdentifier::PRESENT_VALUE, None)
484                .unwrap(),
485            PropertyValue::Real(42.0)
486        );
487    }
488
489    // --- #182 adversary blocker: strict shared decode on the reference arm ---
490
491    #[test]
492    fn averaging_reference_write_accepts_exact_shapes_and_both_member_typings() {
493        let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 7).unwrap();
494        let pv_raw = PropertyIdentifier::PRESENT_VALUE.to_raw();
495        for (label, members, expect_indexed) in [
496            (
497                "2-member Unsigned (historical flat form)",
498                vec![
499                    PropertyValue::ObjectIdentifier(oid),
500                    PropertyValue::Unsigned(pv_raw as u64),
501                ],
502                None,
503            ),
504            (
505                "2-member Enumerated (Loop-family flat form)",
506                vec![
507                    PropertyValue::ObjectIdentifier(oid),
508                    PropertyValue::Enumerated(pv_raw),
509                ],
510                None,
511            ),
512            (
513                "3-member indexed",
514                vec![
515                    PropertyValue::ObjectIdentifier(oid),
516                    PropertyValue::Unsigned(pv_raw as u64),
517                    PropertyValue::Unsigned(4),
518                ],
519                Some(4),
520            ),
521        ] {
522            let mut avg = AveragingObject::new(1, "AVG-1").unwrap();
523            avg.write_property(
524                PropertyIdentifier::OBJECT_PROPERTY_REFERENCE,
525                None,
526                PropertyValue::List(members),
527                None,
528            )
529            .unwrap_or_else(|e| panic!("{label}: must be accepted: {e:?}"));
530            let mut expected = vec![
531                PropertyValue::ObjectIdentifier(oid),
532                PropertyValue::Unsigned(pv_raw as u64),
533            ];
534            if let Some(idx) = expect_indexed {
535                expected.push(PropertyValue::Unsigned(idx as u64));
536            }
537            assert_eq!(
538                avg.read_property(PropertyIdentifier::OBJECT_PROPERTY_REFERENCE, None)
539                    .unwrap(),
540                PropertyValue::List(expected),
541                "{label}: read-back fidelity"
542            );
543        }
544    }
545
546    #[test]
547    fn averaging_reference_write_rejects_bad_shapes_and_preserves_state() {
548        let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 7).unwrap();
549        let pv_raw = PropertyIdentifier::PRESENT_VALUE.to_raw();
550        let baseline = PropertyValue::List(vec![
551            PropertyValue::ObjectIdentifier(oid),
552            PropertyValue::Unsigned(pv_raw as u64),
553        ]);
554
555        // A framed device-qualified write ([3] device-identifier): the
556        // Clause 12.5 typing is BACnetDeviceObjectPropertyReference but the
557        // remote-sample path is the standard's OPTIONAL branch, unmodeled
558        // here — refused INVALID_DATA_ENCODING rather than silently
559        // local-izing.
560        let mut framed_device = bytes::BytesMut::new();
561        bacnet_encoding::constructed::encode_object_property_reference(
562            &mut framed_device,
563            &BACnetObjectPropertyReference::new(oid, pv_raw),
564        );
565        bacnet_encoding::primitives::encode_ctx_object_id(
566            &mut framed_device,
567            3,
568            &ObjectIdentifier::new(ObjectType::DEVICE, 42).unwrap(),
569        );
570
571        let cases: Vec<(PropertyValue, bacnet_types::enums::ErrorCode, &str)> = vec![
572            (
573                PropertyValue::List(vec![
574                    PropertyValue::ObjectIdentifier(oid),
575                    PropertyValue::Unsigned(pv_raw as u64),
576                    PropertyValue::Unsigned(2),
577                    PropertyValue::Unsigned(9), // 4th member: silently dropped pre-fix
578                ]),
579                bacnet_types::enums::ErrorCode::INVALID_DATA_TYPE,
580                "4-member list",
581            ),
582            (
583                PropertyValue::List(vec![
584                    PropertyValue::ObjectIdentifier(oid),
585                    PropertyValue::Unsigned(pv_raw as u64),
586                    PropertyValue::Real(2.0), // non-Unsigned 3rd: retyped to no-index pre-fix
587                ]),
588                bacnet_types::enums::ErrorCode::INVALID_DATA_TYPE,
589                "wrong-typed third member",
590            ),
591            (
592                PropertyValue::List(vec![
593                    PropertyValue::Unsigned(1),
594                    PropertyValue::Unsigned(pv_raw as u64),
595                ]),
596                bacnet_types::enums::ErrorCode::INVALID_DATA_TYPE,
597                "non-ObjectIdentifier first member",
598            ),
599            (
600                PropertyValue::List(vec![
601                    PropertyValue::ObjectIdentifier(oid),
602                    PropertyValue::Unsigned(u64::MAX), // > 4 octets: `as u32` truncated pre-fix
603                ]),
604                bacnet_types::enums::ErrorCode::INVALID_DATA_TYPE,
605                "oversized Unsigned property member",
606            ),
607            (
608                PropertyValue::ApplicationData(framed_device.to_vec()),
609                bacnet_types::enums::ErrorCode::INVALID_DATA_ENCODING,
610                "device-qualified framed reference",
611            ),
612        ];
613
614        for (value, expected_code, label) in cases {
615            let mut avg = AveragingObject::new(1, "AVG-1").unwrap();
616            avg.write_property(
617                PropertyIdentifier::OBJECT_PROPERTY_REFERENCE,
618                None,
619                baseline.clone(),
620                None,
621            )
622            .unwrap();
623            let err = avg
624                .write_property(
625                    PropertyIdentifier::OBJECT_PROPERTY_REFERENCE,
626                    None,
627                    value,
628                    None,
629                )
630                .expect_err(label);
631            match err {
632                Error::Protocol { class, code } => {
633                    assert_eq!(
634                        class,
635                        bacnet_types::enums::ErrorClass::PROPERTY.to_raw() as u32,
636                        "{label}: wrong class"
637                    );
638                    assert_eq!(code, expected_code.to_raw() as u32, "{label}: wrong code");
639                }
640                other => panic!("{label}: expected Property error, got {other:?}"),
641            }
642            assert_eq!(
643                avg.read_property(PropertyIdentifier::OBJECT_PROPERTY_REFERENCE, None)
644                    .unwrap(),
645                baseline,
646                "{label}: refused write must leave the stored reference untouched"
647            );
648        }
649    }
650
651    #[test]
652    fn averaging_reference_write_accepts_the_framed_local_form() {
653        let mut avg = AveragingObject::new(1, "AVG-1").unwrap();
654        let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 7).unwrap();
655        let pv_raw = PropertyIdentifier::PRESENT_VALUE.to_raw();
656        let mut framed = bytes::BytesMut::new();
657        bacnet_encoding::constructed::encode_object_property_reference(
658            &mut framed,
659            &BACnetObjectPropertyReference::new_indexed(oid, pv_raw, 2),
660        );
661        avg.write_property(
662            PropertyIdentifier::OBJECT_PROPERTY_REFERENCE,
663            None,
664            PropertyValue::ApplicationData(framed.to_vec()),
665            None,
666        )
667        .unwrap();
668        assert_eq!(
669            avg.read_property(PropertyIdentifier::OBJECT_PROPERTY_REFERENCE, None)
670                .unwrap(),
671            PropertyValue::List(vec![
672                PropertyValue::ObjectIdentifier(oid),
673                PropertyValue::Unsigned(pv_raw as u64),
674                PropertyValue::Unsigned(2),
675            ])
676        );
677    }
678}