Skip to main content

bacnet_objects/accumulator/
mod.rs

1//! Accumulator (type 23) and Pulse Converter (type 24) objects.
2//!
3//! Per ASHRAE 135-2020 §12.61 Table 12-79 (Accumulator) and §12.23 Table 12-27 (PulseConverter).
4
5use bacnet_encoding::constructed::{encode_prescale, encode_scale};
6use bacnet_types::constructed::{BACnetPrescale, BACnetScale};
7use bacnet_types::enums::{
8    EngineeringUnits, EventState, ObjectType, PropertyIdentifier, Reliability,
9};
10use bacnet_types::error::Error;
11use bacnet_types::primitives::{ObjectIdentifier, PropertyValue, StatusFlags};
12use bytes::BytesMut;
13use std::borrow::Cow;
14
15use crate::common::{self, read_common_properties};
16use crate::traits::BACnetObject;
17
18mod metadata;
19mod pulse_converter;
20
21#[doc(hidden)]
22pub use pulse_converter::InputReading;
23pub use pulse_converter::PulseConverterObject;
24
25// ---------------------------------------------------------------------------
26// AccumulatorObject (type 23)
27// ---------------------------------------------------------------------------
28
29/// BACnet Accumulator object — tracks a pulse count with scaling.
30pub struct AccumulatorObject {
31    oid: ObjectIdentifier,
32    name: String,
33    description: String,
34    present_value: u64,
35    max_pres_value: u64,
36    scale: BACnetScale,
37    prescale: Option<BACnetPrescale>,
38    pulse_rate: f32,
39    units: EngineeringUnits,
40    limit_monitoring_interval: u32,
41    status_flags: StatusFlags,
42    event_state: EventState,
43    out_of_service: bool,
44    reliability: Reliability,
45    value_before_change: u64,
46    value_set: u64,
47}
48
49impl AccumulatorObject {
50    /// Create a new Accumulator object with default values.
51    pub fn new(
52        instance: u32,
53        name: impl Into<String>,
54        units: EngineeringUnits,
55    ) -> Result<Self, Error> {
56        let oid = ObjectIdentifier::new(ObjectType::ACCUMULATOR, instance)?;
57        Ok(Self {
58            oid,
59            name: name.into(),
60            description: String::new(),
61            present_value: 0,
62            max_pres_value: u64::MAX,
63            scale: BACnetScale::FloatScale(1.0),
64            prescale: None,
65            pulse_rate: 0.0,
66            units,
67            limit_monitoring_interval: 0,
68            status_flags: StatusFlags::empty(),
69            event_state: EventState::NORMAL,
70            out_of_service: false,
71            reliability: Reliability::NO_FAULT_DETECTED,
72            value_before_change: 0,
73            value_set: 0,
74        })
75    }
76
77    /// Set the present value (application use).
78    pub fn set_present_value(&mut self, value: u64) {
79        self.present_value = value;
80    }
81
82    /// Set the description string.
83    pub fn set_description(&mut self, desc: impl Into<String>) {
84        self.description = desc.into();
85    }
86
87    /// Set the scale.
88    pub fn set_scale(&mut self, scale: BACnetScale) {
89        self.scale = scale;
90    }
91
92    /// Set the prescale. Prescale is optional (Table 12-79), and the object
93    /// serves it only once this has set one: until then it is absent from
94    /// Property_List and a read is UNKNOWN_PROPERTY, since BACnetPrescale
95    /// has no value standing for none.
96    pub fn set_prescale(&mut self, prescale: BACnetPrescale) {
97        self.prescale = Some(prescale);
98    }
99
100    /// Whether Prescale is present, for the metadata.
101    fn has_prescale(&self) -> bool {
102        self.prescale.is_some()
103    }
104}
105
106impl BACnetObject for AccumulatorObject {
107    fn object_identifier(&self) -> ObjectIdentifier {
108        self.oid
109    }
110
111    fn object_name(&self) -> &str {
112        &self.name
113    }
114
115    fn read_property(
116        &self,
117        property: PropertyIdentifier,
118        array_index: Option<u32>,
119    ) -> Result<PropertyValue, Error> {
120        if let Some(result) = read_common_properties!(self, property, array_index) {
121            return result;
122        }
123        match property {
124            p if p == PropertyIdentifier::OBJECT_TYPE => {
125                Ok(PropertyValue::Enumerated(ObjectType::ACCUMULATOR.to_raw()))
126            }
127            p if p == PropertyIdentifier::PRESENT_VALUE => {
128                Ok(PropertyValue::Unsigned(self.present_value))
129            }
130            p if p == PropertyIdentifier::MAX_PRES_VALUE => {
131                Ok(PropertyValue::Unsigned(self.max_pres_value))
132            }
133            // Both are constructed productions (Clause 21): Scale a CHOICE of
134            // a float [0] or an integer [1], Prescale its two Unsigneds under
135            // [0] and [1], served as their context-tagged octets (#1487).
136            p if p == PropertyIdentifier::SCALE => {
137                let mut encoded = BytesMut::new();
138                encode_scale(&mut encoded, &self.scale);
139                Ok(PropertyValue::ApplicationData(encoded.to_vec()))
140            }
141            p if p == PropertyIdentifier::PRESCALE => match &self.prescale {
142                Some(prescale) => {
143                    let mut encoded = BytesMut::new();
144                    encode_prescale(&mut encoded, prescale);
145                    Ok(PropertyValue::ApplicationData(encoded.to_vec()))
146                }
147                None => Err(common::unknown_property_error()),
148            },
149            p if p == PropertyIdentifier::PULSE_RATE => Ok(PropertyValue::Real(self.pulse_rate)),
150            p if p == PropertyIdentifier::UNITS => {
151                Ok(PropertyValue::Enumerated(self.units.to_raw()))
152            }
153            p if p == PropertyIdentifier::LIMIT_MONITORING_INTERVAL => Ok(PropertyValue::Unsigned(
154                self.limit_monitoring_interval as u64,
155            )),
156            p if p == PropertyIdentifier::EVENT_STATE => {
157                Ok(PropertyValue::Enumerated(self.event_state.to_raw()))
158            }
159            p if p == PropertyIdentifier::VALUE_BEFORE_CHANGE => {
160                Ok(PropertyValue::Unsigned(self.value_before_change))
161            }
162            p if p == PropertyIdentifier::VALUE_SET => Ok(PropertyValue::Unsigned(self.value_set)),
163            _ => Err(common::unknown_property_error()),
164        }
165    }
166
167    fn write_property(
168        &mut self,
169        property: PropertyIdentifier,
170        _array_index: Option<u32>,
171        value: PropertyValue,
172        _priority: Option<u8>,
173    ) -> Result<(), Error> {
174        // Present value is read-only from the network
175        if property == PropertyIdentifier::PRESENT_VALUE {
176            return Err(common::write_access_denied_error());
177        }
178        if let Some(result) =
179            common::write_out_of_service(&mut self.out_of_service, property, &value)
180        {
181            return result;
182        }
183        if let Some(result) = common::write_description(&mut self.description, property, &value) {
184            return result;
185        }
186        match property {
187            p if p == PropertyIdentifier::MAX_PRES_VALUE => {
188                if let PropertyValue::Unsigned(v) = value {
189                    self.max_pres_value = v;
190                    Ok(())
191                } else {
192                    Err(common::invalid_data_type_error())
193                }
194            }
195            p if p == PropertyIdentifier::PULSE_RATE => {
196                if let PropertyValue::Real(v) = value {
197                    common::reject_non_finite(v)?;
198                    self.pulse_rate = v;
199                    Ok(())
200                } else {
201                    Err(common::invalid_data_type_error())
202                }
203            }
204            p if p == PropertyIdentifier::LIMIT_MONITORING_INTERVAL => {
205                if let PropertyValue::Unsigned(v) = value {
206                    self.limit_monitoring_interval = common::u64_to_u32(v)?;
207                    Ok(())
208                } else {
209                    Err(common::invalid_data_type_error())
210                }
211            }
212            _ => Err(crate::common::unhandled_write_error(
213                self.property_metadata().as_ref(),
214                property,
215                _array_index,
216            )),
217        }
218    }
219
220    fn property_metadata(&self) -> Cow<'_, [crate::property_metadata::PropertyMetadata]> {
221        metadata::for_accumulator_object(self)
222    }
223
224    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
225        crate::property_metadata::property_list_from_metadata(self.property_metadata().as_ref())
226    }
227
228    fn supports_cov(&self) -> bool {
229        true
230    }
231}
232
233// ---------------------------------------------------------------------------
234// Tests
235// ---------------------------------------------------------------------------
236
237#[cfg(test)]
238mod tests {
239    use super::*;
240    use bacnet_types::constructed::BACnetPrescale;
241
242    // --- AccumulatorObject ---
243
244    #[test]
245    fn accumulator_create_and_read_defaults() {
246        let acc = AccumulatorObject::new(1, "ACC-1", EngineeringUnits::NO_UNITS).unwrap();
247        assert_eq!(acc.object_name(), "ACC-1");
248        assert_eq!(
249            acc.read_property(PropertyIdentifier::PRESENT_VALUE, None)
250                .unwrap(),
251            PropertyValue::Unsigned(0)
252        );
253        assert_eq!(
254            acc.read_property(PropertyIdentifier::UNITS, None).unwrap(),
255            PropertyValue::Enumerated(95)
256        );
257    }
258
259    #[test]
260    fn accumulator_read_present_value() {
261        let mut acc = AccumulatorObject::new(1, "ACC-1", EngineeringUnits::NO_UNITS).unwrap();
262        acc.set_present_value(42);
263        assert_eq!(
264            acc.read_property(PropertyIdentifier::PRESENT_VALUE, None)
265                .unwrap(),
266            PropertyValue::Unsigned(42)
267        );
268    }
269
270    #[test]
271    fn accumulator_present_value_read_only_from_network() {
272        let mut acc = AccumulatorObject::new(1, "ACC-1", EngineeringUnits::NO_UNITS).unwrap();
273        let result = acc.write_property(
274            PropertyIdentifier::PRESENT_VALUE,
275            None,
276            PropertyValue::Unsigned(10),
277            None,
278        );
279        assert!(result.is_err());
280    }
281
282    #[test]
283    fn accumulator_read_scale_float() {
284        // float-scale [0], a four-octet REAL (#1487).
285        let acc = AccumulatorObject::new(1, "ACC-1", EngineeringUnits::NO_UNITS).unwrap();
286        let val = acc.read_property(PropertyIdentifier::SCALE, None).unwrap();
287        assert_eq!(
288            val,
289            PropertyValue::ApplicationData(vec![0x0C, 0x3F, 0x80, 0x00, 0x00])
290        );
291    }
292
293    #[test]
294    fn accumulator_read_scale_integer() {
295        // integer-scale [1], a signed INTEGER (#1487).
296        let mut acc = AccumulatorObject::new(1, "ACC-1", EngineeringUnits::NO_UNITS).unwrap();
297        acc.set_scale(BACnetScale::IntegerScale(10));
298        let val = acc.read_property(PropertyIdentifier::SCALE, None).unwrap();
299        assert_eq!(val, PropertyValue::ApplicationData(vec![0x19, 0x0A]));
300        acc.set_scale(BACnetScale::IntegerScale(-2));
301        let val = acc.read_property(PropertyIdentifier::SCALE, None).unwrap();
302        assert_eq!(val, PropertyValue::ApplicationData(vec![0x19, 0xFE]));
303    }
304
305    #[test]
306    fn accumulator_without_prescale_does_not_serve_it() {
307        // Optional, and BACnetPrescale has no NULL: absent until set.
308        let acc = AccumulatorObject::new(1, "ACC-1", EngineeringUnits::NO_UNITS).unwrap();
309        let err = acc
310            .read_property(PropertyIdentifier::PRESCALE, None)
311            .unwrap_err();
312        assert!(
313            matches!(err, Error::Protocol { class, code }
314                if class == bacnet_types::enums::ErrorClass::PROPERTY
315                    && code == bacnet_types::enums::ErrorCode::UNKNOWN_PROPERTY),
316            "{err:?}"
317        );
318        assert!(!acc.property_list().contains(&PropertyIdentifier::PRESCALE));
319    }
320
321    #[test]
322    fn accumulator_read_prescale_set() {
323        let mut acc = AccumulatorObject::new(1, "ACC-1", EngineeringUnits::NO_UNITS).unwrap();
324        acc.set_prescale(BACnetPrescale {
325            multiplier: 5,
326            modulo_divide: 100,
327        });
328        let val = acc
329            .read_property(PropertyIdentifier::PRESCALE, None)
330            .unwrap();
331        // The multiplier under [0], the modulo divide under [1] (#1487).
332        assert_eq!(
333            val,
334            PropertyValue::ApplicationData(vec![0x09, 0x05, 0x19, 0x64])
335        );
336    }
337
338    #[test]
339    fn accumulator_object_type() {
340        let acc = AccumulatorObject::new(1, "ACC-1", EngineeringUnits::NO_UNITS).unwrap();
341        let val = acc
342            .read_property(PropertyIdentifier::OBJECT_TYPE, None)
343            .unwrap();
344        assert_eq!(
345            val,
346            PropertyValue::Enumerated(ObjectType::ACCUMULATOR.to_raw())
347        );
348    }
349
350    #[test]
351    fn accumulator_property_list() {
352        let mut acc = AccumulatorObject::new(1, "ACC-1", EngineeringUnits::NO_UNITS).unwrap();
353        acc.set_prescale(BACnetPrescale {
354            multiplier: 1,
355            modulo_divide: 1,
356        });
357        let list = acc.property_list();
358        assert!(list.contains(&PropertyIdentifier::PRESENT_VALUE));
359        assert!(list.contains(&PropertyIdentifier::SCALE));
360        assert!(list.contains(&PropertyIdentifier::PRESCALE));
361        assert!(list.contains(&PropertyIdentifier::MAX_PRES_VALUE));
362        assert!(list.contains(&PropertyIdentifier::PULSE_RATE));
363    }
364}