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