1use 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
15pub 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 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 self.average_value += (value - self.average_value) / self.valid_samples as f32;
69 self.present_value = self.average_value;
70 }
71
72 pub fn set_object_property_reference(
74 &mut self,
75 reference: Option<BACnetObjectPropertyReference>,
76 ) {
77 self.object_property_reference = reference;
78 }
79
80 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 {
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 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 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 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 #[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 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), ]),
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), ]),
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), ]),
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}