Skip to main content

bacnet_objects/
accumulator.rs

1//! Accumulator (type 23) and Pulse Converter (type 24) objects.
2//!
3//! Per ASHRAE 135-2020 Clauses 12.1 (Accumulator) and 12.2 (PulseConverter).
4
5use bacnet_types::constructed::{BACnetObjectPropertyReference, BACnetPrescale, BACnetScale};
6use bacnet_types::enums::{ObjectType, PropertyIdentifier};
7use bacnet_types::error::Error;
8use bacnet_types::primitives::{ObjectIdentifier, PropertyValue, StatusFlags};
9use std::borrow::Cow;
10
11use crate::common::{self, read_common_properties};
12use crate::traits::BACnetObject;
13
14// ---------------------------------------------------------------------------
15// AccumulatorObject (type 23)
16// ---------------------------------------------------------------------------
17
18/// BACnet Accumulator object — tracks a pulse count with scaling.
19pub struct AccumulatorObject {
20    oid: ObjectIdentifier,
21    name: String,
22    description: String,
23    present_value: u64,
24    max_pres_value: u64,
25    scale: BACnetScale,
26    prescale: Option<BACnetPrescale>,
27    pulse_rate: f32,
28    units: u32,
29    limit_monitoring_interval: u32,
30    status_flags: StatusFlags,
31    event_state: u32,
32    out_of_service: bool,
33    reliability: u32,
34    value_before_change: u64,
35    value_set: u64,
36}
37
38impl AccumulatorObject {
39    /// Create a new Accumulator object with default values.
40    pub fn new(instance: u32, name: impl Into<String>, units: u32) -> Result<Self, Error> {
41        let oid = ObjectIdentifier::new(ObjectType::ACCUMULATOR, instance)?;
42        Ok(Self {
43            oid,
44            name: name.into(),
45            description: String::new(),
46            present_value: 0,
47            max_pres_value: u64::MAX,
48            scale: BACnetScale::FloatScale(1.0),
49            prescale: None,
50            pulse_rate: 0.0,
51            units,
52            limit_monitoring_interval: 0,
53            status_flags: StatusFlags::empty(),
54            event_state: 0,
55            out_of_service: false,
56            reliability: 0,
57            value_before_change: 0,
58            value_set: 0,
59        })
60    }
61
62    /// Set the present value (application use).
63    pub fn set_present_value(&mut self, value: u64) {
64        self.present_value = value;
65    }
66
67    /// Set the description string.
68    pub fn set_description(&mut self, desc: impl Into<String>) {
69        self.description = desc.into();
70    }
71
72    /// Set the scale.
73    pub fn set_scale(&mut self, scale: BACnetScale) {
74        self.scale = scale;
75    }
76
77    /// Set the prescale.
78    pub fn set_prescale(&mut self, prescale: BACnetPrescale) {
79        self.prescale = Some(prescale);
80    }
81}
82
83impl BACnetObject for AccumulatorObject {
84    fn object_identifier(&self) -> ObjectIdentifier {
85        self.oid
86    }
87
88    fn object_name(&self) -> &str {
89        &self.name
90    }
91
92    fn read_property(
93        &self,
94        property: PropertyIdentifier,
95        array_index: Option<u32>,
96    ) -> Result<PropertyValue, Error> {
97        if let Some(result) = read_common_properties!(self, property, array_index) {
98            return result;
99        }
100        match property {
101            p if p == PropertyIdentifier::OBJECT_TYPE => {
102                Ok(PropertyValue::Enumerated(ObjectType::ACCUMULATOR.to_raw()))
103            }
104            p if p == PropertyIdentifier::PRESENT_VALUE => {
105                Ok(PropertyValue::Unsigned(self.present_value))
106            }
107            p if p == PropertyIdentifier::MAX_PRES_VALUE => {
108                Ok(PropertyValue::Unsigned(self.max_pres_value))
109            }
110            p if p == PropertyIdentifier::SCALE => match &self.scale {
111                BACnetScale::FloatScale(v) => {
112                    Ok(PropertyValue::List(vec![PropertyValue::Real(*v)]))
113                }
114                BACnetScale::IntegerScale(v) => {
115                    Ok(PropertyValue::List(vec![PropertyValue::Signed(*v)]))
116                }
117            },
118            p if p == PropertyIdentifier::PRESCALE => match &self.prescale {
119                Some(ps) => Ok(PropertyValue::List(vec![
120                    PropertyValue::Unsigned(ps.multiplier as u64),
121                    PropertyValue::Unsigned(ps.modulo_divide as u64),
122                ])),
123                None => Ok(PropertyValue::Null),
124            },
125            p if p == PropertyIdentifier::PULSE_RATE => Ok(PropertyValue::Real(self.pulse_rate)),
126            p if p == PropertyIdentifier::UNITS => Ok(PropertyValue::Enumerated(self.units)),
127            p if p == PropertyIdentifier::LIMIT_MONITORING_INTERVAL => Ok(PropertyValue::Unsigned(
128                self.limit_monitoring_interval as u64,
129            )),
130            p if p == PropertyIdentifier::EVENT_STATE => {
131                Ok(PropertyValue::Enumerated(self.event_state))
132            }
133            p if p == PropertyIdentifier::VALUE_BEFORE_CHANGE => {
134                Ok(PropertyValue::Unsigned(self.value_before_change))
135            }
136            p if p == PropertyIdentifier::VALUE_SET => Ok(PropertyValue::Unsigned(self.value_set)),
137            _ => Err(common::unknown_property_error()),
138        }
139    }
140
141    fn write_property(
142        &mut self,
143        property: PropertyIdentifier,
144        _array_index: Option<u32>,
145        value: PropertyValue,
146        _priority: Option<u8>,
147    ) -> Result<(), Error> {
148        // Present value is read-only from the network
149        if property == PropertyIdentifier::PRESENT_VALUE {
150            return Err(common::write_access_denied_error());
151        }
152        if let Some(result) =
153            common::write_out_of_service(&mut self.out_of_service, property, &value)
154        {
155            return result;
156        }
157        if let Some(result) = common::write_description(&mut self.description, property, &value) {
158            return result;
159        }
160        match property {
161            p if p == PropertyIdentifier::MAX_PRES_VALUE => {
162                if let PropertyValue::Unsigned(v) = value {
163                    self.max_pres_value = v;
164                    Ok(())
165                } else {
166                    Err(common::invalid_data_type_error())
167                }
168            }
169            p if p == PropertyIdentifier::PULSE_RATE => {
170                if let PropertyValue::Real(v) = value {
171                    common::reject_non_finite(v)?;
172                    self.pulse_rate = v;
173                    Ok(())
174                } else {
175                    Err(common::invalid_data_type_error())
176                }
177            }
178            p if p == PropertyIdentifier::LIMIT_MONITORING_INTERVAL => {
179                if let PropertyValue::Unsigned(v) = value {
180                    self.limit_monitoring_interval = common::u64_to_u32(v)?;
181                    Ok(())
182                } else {
183                    Err(common::invalid_data_type_error())
184                }
185            }
186            _ => Err(common::write_access_denied_error()),
187        }
188    }
189
190    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
191        static PROPS: &[PropertyIdentifier] = &[
192            PropertyIdentifier::OBJECT_IDENTIFIER,
193            PropertyIdentifier::OBJECT_NAME,
194            PropertyIdentifier::DESCRIPTION,
195            PropertyIdentifier::OBJECT_TYPE,
196            PropertyIdentifier::PRESENT_VALUE,
197            PropertyIdentifier::MAX_PRES_VALUE,
198            PropertyIdentifier::SCALE,
199            PropertyIdentifier::PRESCALE,
200            PropertyIdentifier::PULSE_RATE,
201            PropertyIdentifier::UNITS,
202            PropertyIdentifier::LIMIT_MONITORING_INTERVAL,
203            PropertyIdentifier::STATUS_FLAGS,
204            PropertyIdentifier::EVENT_STATE,
205            PropertyIdentifier::OUT_OF_SERVICE,
206            PropertyIdentifier::RELIABILITY,
207            PropertyIdentifier::VALUE_BEFORE_CHANGE,
208            PropertyIdentifier::VALUE_SET,
209        ];
210        Cow::Borrowed(PROPS)
211    }
212}
213
214// ---------------------------------------------------------------------------
215// PulseConverterObject (type 24)
216// ---------------------------------------------------------------------------
217
218/// BACnet Pulse Converter object — converts accumulated pulses to an analog value.
219pub struct PulseConverterObject {
220    oid: ObjectIdentifier,
221    name: String,
222    description: String,
223    present_value: f32,
224    units: u32,
225    scale_factor: f32,
226    adjust_value: f32,
227    cov_increment: f32,
228    input_reference: Option<BACnetObjectPropertyReference>,
229    status_flags: StatusFlags,
230    event_state: u32,
231    out_of_service: bool,
232    reliability: u32,
233}
234
235impl PulseConverterObject {
236    /// Create a new Pulse Converter object with default values.
237    pub fn new(instance: u32, name: impl Into<String>, units: u32) -> Result<Self, Error> {
238        let oid = ObjectIdentifier::new(ObjectType::PULSE_CONVERTER, instance)?;
239        Ok(Self {
240            oid,
241            name: name.into(),
242            description: String::new(),
243            present_value: 0.0,
244            units,
245            scale_factor: 1.0,
246            adjust_value: 0.0,
247            cov_increment: 0.0,
248            input_reference: None,
249            status_flags: StatusFlags::empty(),
250            event_state: 0,
251            out_of_service: false,
252            reliability: 0,
253        })
254    }
255
256    /// Set the description string.
257    pub fn set_description(&mut self, desc: impl Into<String>) {
258        self.description = desc.into();
259    }
260
261    /// Set the input reference.
262    pub fn set_input_reference(&mut self, r: BACnetObjectPropertyReference) {
263        self.input_reference = Some(r);
264    }
265}
266
267impl BACnetObject for PulseConverterObject {
268    fn object_identifier(&self) -> ObjectIdentifier {
269        self.oid
270    }
271
272    fn object_name(&self) -> &str {
273        &self.name
274    }
275
276    fn read_property(
277        &self,
278        property: PropertyIdentifier,
279        array_index: Option<u32>,
280    ) -> Result<PropertyValue, Error> {
281        if let Some(result) = read_common_properties!(self, property, array_index) {
282            return result;
283        }
284        match property {
285            p if p == PropertyIdentifier::OBJECT_TYPE => Ok(PropertyValue::Enumerated(
286                ObjectType::PULSE_CONVERTER.to_raw(),
287            )),
288            p if p == PropertyIdentifier::PRESENT_VALUE => {
289                Ok(PropertyValue::Real(self.present_value))
290            }
291            p if p == PropertyIdentifier::UNITS => Ok(PropertyValue::Enumerated(self.units)),
292            p if p == PropertyIdentifier::SCALE_FACTOR => {
293                Ok(PropertyValue::Real(self.scale_factor))
294            }
295            p if p == PropertyIdentifier::ADJUST_VALUE => {
296                Ok(PropertyValue::Real(self.adjust_value))
297            }
298            p if p == PropertyIdentifier::COV_INCREMENT => {
299                Ok(PropertyValue::Real(self.cov_increment))
300            }
301            p if p == PropertyIdentifier::INPUT_REFERENCE => match &self.input_reference {
302                Some(r) => Ok(PropertyValue::List(vec![
303                    PropertyValue::ObjectIdentifier(r.object_identifier),
304                    PropertyValue::Enumerated(r.property_identifier),
305                ])),
306                None => Ok(PropertyValue::Null),
307            },
308            p if p == PropertyIdentifier::EVENT_STATE => {
309                Ok(PropertyValue::Enumerated(self.event_state))
310            }
311            _ => Err(common::unknown_property_error()),
312        }
313    }
314
315    fn write_property(
316        &mut self,
317        property: PropertyIdentifier,
318        _array_index: Option<u32>,
319        value: PropertyValue,
320        _priority: Option<u8>,
321    ) -> Result<(), Error> {
322        if let Some(result) =
323            common::write_out_of_service(&mut self.out_of_service, property, &value)
324        {
325            return result;
326        }
327        if let Some(result) = common::write_description(&mut self.description, property, &value) {
328            return result;
329        }
330        if let Some(result) = common::write_cov_increment(&mut self.cov_increment, property, &value)
331        {
332            return result;
333        }
334        match property {
335            p if p == PropertyIdentifier::PRESENT_VALUE => {
336                if let PropertyValue::Real(v) = value {
337                    common::reject_non_finite(v)?;
338                    self.present_value = v;
339                    Ok(())
340                } else {
341                    Err(common::invalid_data_type_error())
342                }
343            }
344            p if p == PropertyIdentifier::SCALE_FACTOR => {
345                if let PropertyValue::Real(v) = value {
346                    common::reject_non_finite(v)?;
347                    self.scale_factor = v;
348                    Ok(())
349                } else {
350                    Err(common::invalid_data_type_error())
351                }
352            }
353            p if p == PropertyIdentifier::ADJUST_VALUE => {
354                if let PropertyValue::Real(v) = value {
355                    common::reject_non_finite(v)?;
356                    self.adjust_value = v;
357                    Ok(())
358                } else {
359                    Err(common::invalid_data_type_error())
360                }
361            }
362            p if p == PropertyIdentifier::INPUT_REFERENCE => match value {
363                PropertyValue::Null => {
364                    self.input_reference = None;
365                    Ok(())
366                }
367                PropertyValue::List(ref items) if items.len() >= 2 => {
368                    if let (PropertyValue::ObjectIdentifier(oid), PropertyValue::Enumerated(prop)) =
369                        (&items[0], &items[1])
370                    {
371                        self.input_reference =
372                            Some(BACnetObjectPropertyReference::new(*oid, *prop));
373                        Ok(())
374                    } else {
375                        Err(common::invalid_data_type_error())
376                    }
377                }
378                _ => Err(common::invalid_data_type_error()),
379            },
380            _ => Err(common::write_access_denied_error()),
381        }
382    }
383
384    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
385        static PROPS: &[PropertyIdentifier] = &[
386            PropertyIdentifier::OBJECT_IDENTIFIER,
387            PropertyIdentifier::OBJECT_NAME,
388            PropertyIdentifier::DESCRIPTION,
389            PropertyIdentifier::OBJECT_TYPE,
390            PropertyIdentifier::PRESENT_VALUE,
391            PropertyIdentifier::UNITS,
392            PropertyIdentifier::SCALE_FACTOR,
393            PropertyIdentifier::ADJUST_VALUE,
394            PropertyIdentifier::COV_INCREMENT,
395            PropertyIdentifier::INPUT_REFERENCE,
396            PropertyIdentifier::STATUS_FLAGS,
397            PropertyIdentifier::EVENT_STATE,
398            PropertyIdentifier::OUT_OF_SERVICE,
399            PropertyIdentifier::RELIABILITY,
400        ];
401        Cow::Borrowed(PROPS)
402    }
403
404    fn cov_increment(&self) -> Option<f32> {
405        Some(self.cov_increment)
406    }
407}
408
409// ---------------------------------------------------------------------------
410// Tests
411// ---------------------------------------------------------------------------
412
413#[cfg(test)]
414mod tests {
415    use super::*;
416    use bacnet_types::constructed::BACnetPrescale;
417
418    // --- AccumulatorObject ---
419
420    #[test]
421    fn accumulator_create_and_read_defaults() {
422        let acc = AccumulatorObject::new(1, "ACC-1", 95).unwrap();
423        assert_eq!(acc.object_name(), "ACC-1");
424        assert_eq!(
425            acc.read_property(PropertyIdentifier::PRESENT_VALUE, None)
426                .unwrap(),
427            PropertyValue::Unsigned(0)
428        );
429        assert_eq!(
430            acc.read_property(PropertyIdentifier::UNITS, None).unwrap(),
431            PropertyValue::Enumerated(95)
432        );
433    }
434
435    #[test]
436    fn accumulator_read_present_value() {
437        let mut acc = AccumulatorObject::new(1, "ACC-1", 95).unwrap();
438        acc.set_present_value(42);
439        assert_eq!(
440            acc.read_property(PropertyIdentifier::PRESENT_VALUE, None)
441                .unwrap(),
442            PropertyValue::Unsigned(42)
443        );
444    }
445
446    #[test]
447    fn accumulator_present_value_read_only_from_network() {
448        let mut acc = AccumulatorObject::new(1, "ACC-1", 95).unwrap();
449        let result = acc.write_property(
450            PropertyIdentifier::PRESENT_VALUE,
451            None,
452            PropertyValue::Unsigned(10),
453            None,
454        );
455        assert!(result.is_err());
456    }
457
458    #[test]
459    fn accumulator_read_scale_float() {
460        let acc = AccumulatorObject::new(1, "ACC-1", 95).unwrap();
461        let val = acc.read_property(PropertyIdentifier::SCALE, None).unwrap();
462        assert_eq!(val, PropertyValue::List(vec![PropertyValue::Real(1.0)]));
463    }
464
465    #[test]
466    fn accumulator_read_scale_integer() {
467        let mut acc = AccumulatorObject::new(1, "ACC-1", 95).unwrap();
468        acc.set_scale(BACnetScale::IntegerScale(10));
469        let val = acc.read_property(PropertyIdentifier::SCALE, None).unwrap();
470        assert_eq!(val, PropertyValue::List(vec![PropertyValue::Signed(10)]));
471    }
472
473    #[test]
474    fn accumulator_read_prescale_none() {
475        let acc = AccumulatorObject::new(1, "ACC-1", 95).unwrap();
476        let val = acc
477            .read_property(PropertyIdentifier::PRESCALE, None)
478            .unwrap();
479        assert_eq!(val, PropertyValue::Null);
480    }
481
482    #[test]
483    fn accumulator_read_prescale_set() {
484        let mut acc = AccumulatorObject::new(1, "ACC-1", 95).unwrap();
485        acc.set_prescale(BACnetPrescale {
486            multiplier: 5,
487            modulo_divide: 100,
488        });
489        let val = acc
490            .read_property(PropertyIdentifier::PRESCALE, None)
491            .unwrap();
492        assert_eq!(
493            val,
494            PropertyValue::List(vec![
495                PropertyValue::Unsigned(5),
496                PropertyValue::Unsigned(100),
497            ])
498        );
499    }
500
501    #[test]
502    fn accumulator_object_type() {
503        let acc = AccumulatorObject::new(1, "ACC-1", 95).unwrap();
504        let val = acc
505            .read_property(PropertyIdentifier::OBJECT_TYPE, None)
506            .unwrap();
507        assert_eq!(
508            val,
509            PropertyValue::Enumerated(ObjectType::ACCUMULATOR.to_raw())
510        );
511    }
512
513    #[test]
514    fn accumulator_property_list() {
515        let acc = AccumulatorObject::new(1, "ACC-1", 95).unwrap();
516        let list = acc.property_list();
517        assert!(list.contains(&PropertyIdentifier::PRESENT_VALUE));
518        assert!(list.contains(&PropertyIdentifier::SCALE));
519        assert!(list.contains(&PropertyIdentifier::PRESCALE));
520        assert!(list.contains(&PropertyIdentifier::MAX_PRES_VALUE));
521        assert!(list.contains(&PropertyIdentifier::PULSE_RATE));
522    }
523
524    // --- PulseConverterObject ---
525
526    #[test]
527    fn pulse_converter_create_and_read_defaults() {
528        let pc = PulseConverterObject::new(1, "PC-1", 62).unwrap();
529        assert_eq!(pc.object_name(), "PC-1");
530        assert_eq!(
531            pc.read_property(PropertyIdentifier::PRESENT_VALUE, None)
532                .unwrap(),
533            PropertyValue::Real(0.0)
534        );
535        assert_eq!(
536            pc.read_property(PropertyIdentifier::UNITS, None).unwrap(),
537            PropertyValue::Enumerated(62)
538        );
539    }
540
541    #[test]
542    fn pulse_converter_read_write_present_value() {
543        let mut pc = PulseConverterObject::new(1, "PC-1", 62).unwrap();
544        pc.write_property(
545            PropertyIdentifier::PRESENT_VALUE,
546            None,
547            PropertyValue::Real(123.45),
548            None,
549        )
550        .unwrap();
551        assert_eq!(
552            pc.read_property(PropertyIdentifier::PRESENT_VALUE, None)
553                .unwrap(),
554            PropertyValue::Real(123.45)
555        );
556    }
557
558    #[test]
559    fn pulse_converter_read_scale_factor() {
560        let pc = PulseConverterObject::new(1, "PC-1", 62).unwrap();
561        let val = pc
562            .read_property(PropertyIdentifier::SCALE_FACTOR, None)
563            .unwrap();
564        assert_eq!(val, PropertyValue::Real(1.0));
565    }
566
567    #[test]
568    fn pulse_converter_write_scale_factor() {
569        let mut pc = PulseConverterObject::new(1, "PC-1", 62).unwrap();
570        pc.write_property(
571            PropertyIdentifier::SCALE_FACTOR,
572            None,
573            PropertyValue::Real(2.5),
574            None,
575        )
576        .unwrap();
577        assert_eq!(
578            pc.read_property(PropertyIdentifier::SCALE_FACTOR, None)
579                .unwrap(),
580            PropertyValue::Real(2.5)
581        );
582    }
583
584    #[test]
585    fn pulse_converter_cov_increment() {
586        let mut pc = PulseConverterObject::new(1, "PC-1", 62).unwrap();
587        assert_eq!(pc.cov_increment(), Some(0.0));
588        pc.write_property(
589            PropertyIdentifier::COV_INCREMENT,
590            None,
591            PropertyValue::Real(1.5),
592            None,
593        )
594        .unwrap();
595        assert_eq!(
596            pc.read_property(PropertyIdentifier::COV_INCREMENT, None)
597                .unwrap(),
598            PropertyValue::Real(1.5)
599        );
600        assert_eq!(pc.cov_increment(), Some(1.5));
601    }
602
603    #[test]
604    fn pulse_converter_object_type() {
605        let pc = PulseConverterObject::new(1, "PC-1", 62).unwrap();
606        let val = pc
607            .read_property(PropertyIdentifier::OBJECT_TYPE, None)
608            .unwrap();
609        assert_eq!(
610            val,
611            PropertyValue::Enumerated(ObjectType::PULSE_CONVERTER.to_raw())
612        );
613    }
614
615    #[test]
616    fn pulse_converter_write_wrong_type_rejected() {
617        let mut pc = PulseConverterObject::new(1, "PC-1", 62).unwrap();
618        let result = pc.write_property(
619            PropertyIdentifier::PRESENT_VALUE,
620            None,
621            PropertyValue::Unsigned(42),
622            None,
623        );
624        assert!(result.is_err());
625    }
626
627    #[test]
628    fn pulse_converter_input_reference_defaults_null() {
629        let pc = PulseConverterObject::new(1, "PC-1", 62).unwrap();
630        assert_eq!(
631            pc.read_property(PropertyIdentifier::INPUT_REFERENCE, None)
632                .unwrap(),
633            PropertyValue::Null
634        );
635    }
636
637    #[test]
638    fn pulse_converter_set_input_reference() {
639        let mut pc = PulseConverterObject::new(1, "PC-1", 62).unwrap();
640        let oid = ObjectIdentifier::new(ObjectType::ACCUMULATOR, 1).unwrap();
641        let prop_raw = PropertyIdentifier::PRESENT_VALUE.to_raw();
642        pc.set_input_reference(BACnetObjectPropertyReference::new(oid, prop_raw));
643        let val = pc
644            .read_property(PropertyIdentifier::INPUT_REFERENCE, None)
645            .unwrap();
646        assert_eq!(
647            val,
648            PropertyValue::List(vec![
649                PropertyValue::ObjectIdentifier(oid),
650                PropertyValue::Enumerated(prop_raw),
651            ])
652        );
653    }
654
655    #[test]
656    fn pulse_converter_property_list() {
657        let pc = PulseConverterObject::new(1, "PC-1", 62).unwrap();
658        let list = pc.property_list();
659        assert!(list.contains(&PropertyIdentifier::PRESENT_VALUE));
660        assert!(list.contains(&PropertyIdentifier::SCALE_FACTOR));
661        assert!(list.contains(&PropertyIdentifier::ADJUST_VALUE));
662        assert!(list.contains(&PropertyIdentifier::COV_INCREMENT));
663        assert!(list.contains(&PropertyIdentifier::INPUT_REFERENCE));
664    }
665}