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    fn supports_cov(&self) -> bool {
214        true
215    }
216}
217
218// ---------------------------------------------------------------------------
219// PulseConverterObject (type 24)
220// ---------------------------------------------------------------------------
221
222/// BACnet Pulse Converter object — converts accumulated pulses to an analog value.
223pub struct PulseConverterObject {
224    oid: ObjectIdentifier,
225    name: String,
226    description: String,
227    present_value: f32,
228    units: u32,
229    scale_factor: f32,
230    adjust_value: f32,
231    cov_increment: f32,
232    input_reference: Option<BACnetObjectPropertyReference>,
233    status_flags: StatusFlags,
234    event_state: u32,
235    out_of_service: bool,
236    reliability: u32,
237}
238
239impl PulseConverterObject {
240    /// Create a new Pulse Converter object with default values.
241    pub fn new(instance: u32, name: impl Into<String>, units: u32) -> Result<Self, Error> {
242        let oid = ObjectIdentifier::new(ObjectType::PULSE_CONVERTER, instance)?;
243        Ok(Self {
244            oid,
245            name: name.into(),
246            description: String::new(),
247            present_value: 0.0,
248            units,
249            scale_factor: 1.0,
250            adjust_value: 0.0,
251            cov_increment: 0.0,
252            input_reference: None,
253            status_flags: StatusFlags::empty(),
254            event_state: 0,
255            out_of_service: false,
256            reliability: 0,
257        })
258    }
259
260    /// Set the description string.
261    pub fn set_description(&mut self, desc: impl Into<String>) {
262        self.description = desc.into();
263    }
264
265    /// Set the input reference.
266    pub fn set_input_reference(&mut self, r: BACnetObjectPropertyReference) {
267        self.input_reference = Some(r);
268    }
269}
270
271impl BACnetObject for PulseConverterObject {
272    fn object_identifier(&self) -> ObjectIdentifier {
273        self.oid
274    }
275
276    fn object_name(&self) -> &str {
277        &self.name
278    }
279
280    fn read_property(
281        &self,
282        property: PropertyIdentifier,
283        array_index: Option<u32>,
284    ) -> Result<PropertyValue, Error> {
285        if let Some(result) = read_common_properties!(self, property, array_index) {
286            return result;
287        }
288        match property {
289            p if p == PropertyIdentifier::OBJECT_TYPE => Ok(PropertyValue::Enumerated(
290                ObjectType::PULSE_CONVERTER.to_raw(),
291            )),
292            p if p == PropertyIdentifier::PRESENT_VALUE => {
293                Ok(PropertyValue::Real(self.present_value))
294            }
295            p if p == PropertyIdentifier::UNITS => Ok(PropertyValue::Enumerated(self.units)),
296            p if p == PropertyIdentifier::SCALE_FACTOR => {
297                Ok(PropertyValue::Real(self.scale_factor))
298            }
299            p if p == PropertyIdentifier::ADJUST_VALUE => {
300                Ok(PropertyValue::Real(self.adjust_value))
301            }
302            p if p == PropertyIdentifier::COV_INCREMENT => {
303                Ok(PropertyValue::Real(self.cov_increment))
304            }
305            p if p == PropertyIdentifier::INPUT_REFERENCE => Ok(
306                crate::reference::reference_read_value(&self.input_reference),
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 !self.out_of_service {
337                    return Err(common::write_access_denied_error());
338                }
339                if let PropertyValue::Real(v) = value {
340                    common::reject_non_finite(v)?;
341                    self.present_value = v;
342                    Ok(())
343                } else {
344                    Err(common::invalid_data_type_error())
345                }
346            }
347            p if p == PropertyIdentifier::SCALE_FACTOR => {
348                if let PropertyValue::Real(v) = value {
349                    common::reject_non_finite(v)?;
350                    self.scale_factor = v;
351                    Ok(())
352                } else {
353                    Err(common::invalid_data_type_error())
354                }
355            }
356            p if p == PropertyIdentifier::ADJUST_VALUE => {
357                if let PropertyValue::Real(v) = value {
358                    common::reject_non_finite(v)?;
359                    self.adjust_value = v;
360                    Ok(())
361                } else {
362                    Err(common::invalid_data_type_error())
363                }
364            }
365            // Clause 12.23 Input_Reference (BACnetObjectPropertyReference):
366            // shared arm decode — legacy local List and framed network forms.
367            p if p == PropertyIdentifier::INPUT_REFERENCE => {
368                self.input_reference = crate::reference::decode_reference_write(
369                    &value,
370                    crate::reference::ReferenceFrame::Bare,
371                )?;
372                Ok(())
373            }
374            _ => Err(common::write_access_denied_error()),
375        }
376    }
377
378    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
379        static PROPS: &[PropertyIdentifier] = &[
380            PropertyIdentifier::OBJECT_IDENTIFIER,
381            PropertyIdentifier::OBJECT_NAME,
382            PropertyIdentifier::DESCRIPTION,
383            PropertyIdentifier::OBJECT_TYPE,
384            PropertyIdentifier::PRESENT_VALUE,
385            PropertyIdentifier::UNITS,
386            PropertyIdentifier::SCALE_FACTOR,
387            PropertyIdentifier::ADJUST_VALUE,
388            PropertyIdentifier::COV_INCREMENT,
389            PropertyIdentifier::INPUT_REFERENCE,
390            PropertyIdentifier::STATUS_FLAGS,
391            PropertyIdentifier::EVENT_STATE,
392            PropertyIdentifier::OUT_OF_SERVICE,
393            PropertyIdentifier::RELIABILITY,
394        ];
395        Cow::Borrowed(PROPS)
396    }
397
398    fn supports_cov(&self) -> bool {
399        true
400    }
401
402    fn cov_increment(&self) -> Option<f32> {
403        Some(self.cov_increment)
404    }
405
406    /// Mirror the static `write_property` routes (PICS truth invariant):
407    /// PRESENT_VALUE is advertised because its route is available while OOS;
408    /// this method does not report current-state authorization. OBJECT_NAME is
409    /// not advertised because no arm routes it.
410    fn is_writable_property(&self, property: PropertyIdentifier) -> bool {
411        matches!(
412            property,
413            PropertyIdentifier::PRESENT_VALUE
414                | PropertyIdentifier::SCALE_FACTOR
415                | PropertyIdentifier::ADJUST_VALUE
416                | PropertyIdentifier::INPUT_REFERENCE
417                | PropertyIdentifier::DESCRIPTION
418                | PropertyIdentifier::OUT_OF_SERVICE
419                | PropertyIdentifier::COV_INCREMENT
420        )
421    }
422}
423
424// ---------------------------------------------------------------------------
425// Tests
426// ---------------------------------------------------------------------------
427
428#[cfg(test)]
429#[path = "pulse_converter_policy_tests.rs"]
430mod pulse_converter_policy_tests;
431
432#[cfg(test)]
433mod tests {
434    use super::*;
435    use bacnet_types::constructed::BACnetPrescale;
436
437    // --- AccumulatorObject ---
438
439    #[test]
440    fn accumulator_create_and_read_defaults() {
441        let acc = AccumulatorObject::new(1, "ACC-1", 95).unwrap();
442        assert_eq!(acc.object_name(), "ACC-1");
443        assert_eq!(
444            acc.read_property(PropertyIdentifier::PRESENT_VALUE, None)
445                .unwrap(),
446            PropertyValue::Unsigned(0)
447        );
448        assert_eq!(
449            acc.read_property(PropertyIdentifier::UNITS, None).unwrap(),
450            PropertyValue::Enumerated(95)
451        );
452    }
453
454    #[test]
455    fn accumulator_read_present_value() {
456        let mut acc = AccumulatorObject::new(1, "ACC-1", 95).unwrap();
457        acc.set_present_value(42);
458        assert_eq!(
459            acc.read_property(PropertyIdentifier::PRESENT_VALUE, None)
460                .unwrap(),
461            PropertyValue::Unsigned(42)
462        );
463    }
464
465    #[test]
466    fn accumulator_present_value_read_only_from_network() {
467        let mut acc = AccumulatorObject::new(1, "ACC-1", 95).unwrap();
468        let result = acc.write_property(
469            PropertyIdentifier::PRESENT_VALUE,
470            None,
471            PropertyValue::Unsigned(10),
472            None,
473        );
474        assert!(result.is_err());
475    }
476
477    #[test]
478    fn accumulator_read_scale_float() {
479        let acc = AccumulatorObject::new(1, "ACC-1", 95).unwrap();
480        let val = acc.read_property(PropertyIdentifier::SCALE, None).unwrap();
481        assert_eq!(val, PropertyValue::List(vec![PropertyValue::Real(1.0)]));
482    }
483
484    #[test]
485    fn accumulator_read_scale_integer() {
486        let mut acc = AccumulatorObject::new(1, "ACC-1", 95).unwrap();
487        acc.set_scale(BACnetScale::IntegerScale(10));
488        let val = acc.read_property(PropertyIdentifier::SCALE, None).unwrap();
489        assert_eq!(val, PropertyValue::List(vec![PropertyValue::Signed(10)]));
490    }
491
492    #[test]
493    fn accumulator_read_prescale_none() {
494        let acc = AccumulatorObject::new(1, "ACC-1", 95).unwrap();
495        let val = acc
496            .read_property(PropertyIdentifier::PRESCALE, None)
497            .unwrap();
498        assert_eq!(val, PropertyValue::Null);
499    }
500
501    #[test]
502    fn accumulator_read_prescale_set() {
503        let mut acc = AccumulatorObject::new(1, "ACC-1", 95).unwrap();
504        acc.set_prescale(BACnetPrescale {
505            multiplier: 5,
506            modulo_divide: 100,
507        });
508        let val = acc
509            .read_property(PropertyIdentifier::PRESCALE, None)
510            .unwrap();
511        assert_eq!(
512            val,
513            PropertyValue::List(vec![
514                PropertyValue::Unsigned(5),
515                PropertyValue::Unsigned(100),
516            ])
517        );
518    }
519
520    #[test]
521    fn accumulator_object_type() {
522        let acc = AccumulatorObject::new(1, "ACC-1", 95).unwrap();
523        let val = acc
524            .read_property(PropertyIdentifier::OBJECT_TYPE, None)
525            .unwrap();
526        assert_eq!(
527            val,
528            PropertyValue::Enumerated(ObjectType::ACCUMULATOR.to_raw())
529        );
530    }
531
532    #[test]
533    fn accumulator_property_list() {
534        let acc = AccumulatorObject::new(1, "ACC-1", 95).unwrap();
535        let list = acc.property_list();
536        assert!(list.contains(&PropertyIdentifier::PRESENT_VALUE));
537        assert!(list.contains(&PropertyIdentifier::SCALE));
538        assert!(list.contains(&PropertyIdentifier::PRESCALE));
539        assert!(list.contains(&PropertyIdentifier::MAX_PRES_VALUE));
540        assert!(list.contains(&PropertyIdentifier::PULSE_RATE));
541    }
542
543    // --- PulseConverterObject ---
544
545    #[test]
546    fn pulse_converter_create_and_read_defaults() {
547        let pc = PulseConverterObject::new(1, "PC-1", 62).unwrap();
548        assert_eq!(pc.object_name(), "PC-1");
549        assert_eq!(
550            pc.read_property(PropertyIdentifier::PRESENT_VALUE, None)
551                .unwrap(),
552            PropertyValue::Real(0.0)
553        );
554        assert_eq!(
555            pc.read_property(PropertyIdentifier::UNITS, None).unwrap(),
556            PropertyValue::Enumerated(62)
557        );
558    }
559
560    #[test]
561    fn pulse_converter_read_write_present_value_while_out_of_service() {
562        let mut pc = PulseConverterObject::new(1, "PC-1", 62).unwrap();
563        pc.write_property(
564            PropertyIdentifier::OUT_OF_SERVICE,
565            None,
566            PropertyValue::Boolean(true),
567            None,
568        )
569        .unwrap();
570        pc.write_property(
571            PropertyIdentifier::PRESENT_VALUE,
572            None,
573            PropertyValue::Real(123.45),
574            None,
575        )
576        .unwrap();
577        assert_eq!(
578            pc.read_property(PropertyIdentifier::PRESENT_VALUE, None)
579                .unwrap(),
580            PropertyValue::Real(123.45)
581        );
582    }
583
584    #[test]
585    fn pulse_converter_read_scale_factor() {
586        let pc = PulseConverterObject::new(1, "PC-1", 62).unwrap();
587        let val = pc
588            .read_property(PropertyIdentifier::SCALE_FACTOR, None)
589            .unwrap();
590        assert_eq!(val, PropertyValue::Real(1.0));
591    }
592
593    #[test]
594    fn pulse_converter_write_scale_factor() {
595        let mut pc = PulseConverterObject::new(1, "PC-1", 62).unwrap();
596        pc.write_property(
597            PropertyIdentifier::SCALE_FACTOR,
598            None,
599            PropertyValue::Real(2.5),
600            None,
601        )
602        .unwrap();
603        assert_eq!(
604            pc.read_property(PropertyIdentifier::SCALE_FACTOR, None)
605                .unwrap(),
606            PropertyValue::Real(2.5)
607        );
608    }
609
610    #[test]
611    fn pulse_converter_cov_increment() {
612        let mut pc = PulseConverterObject::new(1, "PC-1", 62).unwrap();
613        assert_eq!(pc.cov_increment(), Some(0.0));
614        pc.write_property(
615            PropertyIdentifier::COV_INCREMENT,
616            None,
617            PropertyValue::Real(1.5),
618            None,
619        )
620        .unwrap();
621        assert_eq!(
622            pc.read_property(PropertyIdentifier::COV_INCREMENT, None)
623                .unwrap(),
624            PropertyValue::Real(1.5)
625        );
626        assert_eq!(pc.cov_increment(), Some(1.5));
627    }
628
629    #[test]
630    fn pulse_converter_object_type() {
631        let pc = PulseConverterObject::new(1, "PC-1", 62).unwrap();
632        let val = pc
633            .read_property(PropertyIdentifier::OBJECT_TYPE, None)
634            .unwrap();
635        assert_eq!(
636            val,
637            PropertyValue::Enumerated(ObjectType::PULSE_CONVERTER.to_raw())
638        );
639    }
640
641    #[test]
642    fn pulse_converter_write_wrong_type_rejected() {
643        let mut pc = PulseConverterObject::new(1, "PC-1", 62).unwrap();
644        let result = pc.write_property(
645            PropertyIdentifier::PRESENT_VALUE,
646            None,
647            PropertyValue::Unsigned(42),
648            None,
649        );
650        assert!(result.is_err());
651    }
652
653    #[test]
654    fn pulse_converter_input_reference_defaults_null() {
655        let pc = PulseConverterObject::new(1, "PC-1", 62).unwrap();
656        assert_eq!(
657            pc.read_property(PropertyIdentifier::INPUT_REFERENCE, None)
658                .unwrap(),
659            PropertyValue::Null
660        );
661    }
662
663    #[test]
664    fn pulse_converter_set_input_reference() {
665        let mut pc = PulseConverterObject::new(1, "PC-1", 62).unwrap();
666        let oid = ObjectIdentifier::new(ObjectType::ACCUMULATOR, 1).unwrap();
667        let prop_raw = PropertyIdentifier::PRESENT_VALUE.to_raw();
668        pc.set_input_reference(BACnetObjectPropertyReference::new(oid, prop_raw));
669        let val = pc
670            .read_property(PropertyIdentifier::INPUT_REFERENCE, None)
671            .unwrap();
672        assert_eq!(
673            val,
674            PropertyValue::List(vec![
675                PropertyValue::ObjectIdentifier(oid),
676                PropertyValue::Enumerated(prop_raw),
677            ])
678        );
679    }
680
681    #[test]
682    fn pulse_converter_property_list() {
683        let pc = PulseConverterObject::new(1, "PC-1", 62).unwrap();
684        let list = pc.property_list();
685        assert!(list.contains(&PropertyIdentifier::PRESENT_VALUE));
686        assert!(list.contains(&PropertyIdentifier::SCALE_FACTOR));
687        assert!(list.contains(&PropertyIdentifier::ADJUST_VALUE));
688        assert!(list.contains(&PropertyIdentifier::COV_INCREMENT));
689        assert!(list.contains(&PropertyIdentifier::INPUT_REFERENCE));
690    }
691
692    // --- #182: framed (context-tagged) Input_Reference writes ---
693
694    #[test]
695    fn pulse_converter_write_framed_indexed_input_reference_lands() {
696        let mut pc = PulseConverterObject::new(1, "PC-1", 62).unwrap();
697        let oid = ObjectIdentifier::new(ObjectType::ACCUMULATOR, 1).unwrap();
698        let prop_raw = PropertyIdentifier::PRESENT_VALUE.to_raw();
699        let r = BACnetObjectPropertyReference::new_indexed(oid, prop_raw, 4);
700        let mut buf = bytes::BytesMut::new();
701        bacnet_encoding::constructed::encode_object_property_reference(&mut buf, &r);
702        pc.write_property(
703            PropertyIdentifier::INPUT_REFERENCE,
704            None,
705            PropertyValue::ApplicationData(buf.to_vec()),
706            None,
707        )
708        .unwrap();
709        assert_eq!(
710            pc.read_property(PropertyIdentifier::INPUT_REFERENCE, None)
711                .unwrap(),
712            PropertyValue::List(vec![
713                PropertyValue::ObjectIdentifier(oid),
714                PropertyValue::Enumerated(prop_raw),
715                PropertyValue::Unsigned(4),
716            ])
717        );
718        // Null clears it again.
719        pc.write_property(
720            PropertyIdentifier::INPUT_REFERENCE,
721            None,
722            PropertyValue::Null,
723            None,
724        )
725        .unwrap();
726        assert_eq!(
727            pc.read_property(PropertyIdentifier::INPUT_REFERENCE, None)
728                .unwrap(),
729            PropertyValue::Null
730        );
731    }
732
733    #[test]
734    fn pulse_converter_write_malformed_framed_input_reference_rejected() {
735        let mut pc = PulseConverterObject::new(1, "PC-1", 62).unwrap();
736        // [0] object-identifier alone — property-identifier missing.
737        let mut buf = bytes::BytesMut::new();
738        bacnet_encoding::primitives::encode_ctx_object_id(
739            &mut buf,
740            0,
741            &ObjectIdentifier::new(ObjectType::ACCUMULATOR, 1).unwrap(),
742        );
743        match pc
744            .write_property(
745                PropertyIdentifier::INPUT_REFERENCE,
746                None,
747                PropertyValue::ApplicationData(buf.to_vec()),
748                None,
749            )
750            .unwrap_err()
751        {
752            Error::Protocol { class, code } => {
753                assert_eq!(
754                    class,
755                    bacnet_types::enums::ErrorClass::PROPERTY.to_raw() as u32
756                );
757                assert_eq!(
758                    code,
759                    bacnet_types::enums::ErrorCode::INVALID_DATA_ENCODING.to_raw() as u32
760                );
761            }
762            other => panic!("expected PROPERTY/INVALID_DATA_ENCODING, got {other:?}"),
763        }
764        assert_eq!(
765            pc.read_property(PropertyIdentifier::INPUT_REFERENCE, None)
766                .unwrap(),
767            PropertyValue::Null
768        );
769    }
770}