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