Skip to main content

bacnet_objects/
binary.rs

1//! Binary Input (type 3), Binary Output (type 4), and Binary Value (type 5)
2//! objects per ASHRAE 135-2020 Clauses 12.4, 12.5, 12.6.
3
4use bacnet_types::enums::{ObjectType, PropertyIdentifier};
5use bacnet_types::error::Error;
6use bacnet_types::primitives::{ObjectIdentifier, PropertyValue, StatusFlags};
7use std::borrow::Cow;
8
9use crate::common::{self, read_common_properties};
10use crate::traits::BACnetObject;
11
12// ---------------------------------------------------------------------------
13// BinaryInput (type 3)
14// ---------------------------------------------------------------------------
15
16/// BACnet Binary Input object.
17///
18/// Read-only binary point. Present_Value is writable only when out-of-service.
19/// Uses Enumerated values: 0 = inactive, 1 = active.
20pub struct BinaryInputObject {
21    oid: ObjectIdentifier,
22    name: String,
23    description: String,
24    present_value: u32,
25    out_of_service: bool,
26    status_flags: StatusFlags,
27    /// Polarity: 0 = normal, 1 = reverse.
28    polarity: u32,
29    /// Reliability: 0 = NO_FAULT_DETECTED.
30    reliability: u32,
31    active_text: String,
32    inactive_text: String,
33}
34
35impl BinaryInputObject {
36    pub fn new(instance: u32, name: impl Into<String>) -> Result<Self, Error> {
37        let oid = ObjectIdentifier::new(ObjectType::BINARY_INPUT, instance)?;
38        Ok(Self {
39            oid,
40            name: name.into(),
41            description: String::new(),
42            present_value: 0,
43            out_of_service: false,
44            status_flags: StatusFlags::empty(),
45            polarity: 0,
46            reliability: 0,
47            active_text: "Active".into(),
48            inactive_text: "Inactive".into(),
49        })
50    }
51
52    /// Set the present value (used by application to update input state).
53    pub fn set_present_value(&mut self, value: u32) {
54        self.present_value = value;
55    }
56
57    /// Set the description string.
58    pub fn set_description(&mut self, desc: impl Into<String>) {
59        self.description = desc.into();
60    }
61}
62
63impl BACnetObject for BinaryInputObject {
64    fn object_identifier(&self) -> ObjectIdentifier {
65        self.oid
66    }
67
68    fn object_name(&self) -> &str {
69        &self.name
70    }
71
72    fn read_property(
73        &self,
74        property: PropertyIdentifier,
75        array_index: Option<u32>,
76    ) -> Result<PropertyValue, Error> {
77        if let Some(result) = read_common_properties!(self, property, array_index) {
78            return result;
79        }
80        match property {
81            p if p == PropertyIdentifier::OBJECT_TYPE => {
82                Ok(PropertyValue::Enumerated(ObjectType::BINARY_INPUT.to_raw()))
83            }
84            p if p == PropertyIdentifier::PRESENT_VALUE => {
85                Ok(PropertyValue::Enumerated(self.present_value))
86            }
87            p if p == PropertyIdentifier::EVENT_STATE => Ok(PropertyValue::Enumerated(0)),
88            p if p == PropertyIdentifier::POLARITY => Ok(PropertyValue::Enumerated(self.polarity)),
89            p if p == PropertyIdentifier::ACTIVE_TEXT => {
90                Ok(PropertyValue::CharacterString(self.active_text.clone()))
91            }
92            p if p == PropertyIdentifier::INACTIVE_TEXT => {
93                Ok(PropertyValue::CharacterString(self.inactive_text.clone()))
94            }
95            _ => Err(common::unknown_property_error()),
96        }
97    }
98
99    fn write_property(
100        &mut self,
101        property: PropertyIdentifier,
102        _array_index: Option<u32>,
103        value: PropertyValue,
104        _priority: Option<u8>,
105    ) -> Result<(), Error> {
106        if property == PropertyIdentifier::PRESENT_VALUE {
107            if !self.out_of_service {
108                return Err(common::write_access_denied_error());
109            }
110            if let PropertyValue::Enumerated(v) = value {
111                if v > 1 {
112                    return Err(common::value_out_of_range_error());
113                }
114                self.present_value = v;
115                return Ok(());
116            }
117            return Err(common::invalid_data_type_error());
118        }
119        if property == PropertyIdentifier::ACTIVE_TEXT {
120            if let PropertyValue::CharacterString(s) = value {
121                self.active_text = s;
122                return Ok(());
123            }
124            return Err(common::invalid_data_type_error());
125        }
126        if property == PropertyIdentifier::INACTIVE_TEXT {
127            if let PropertyValue::CharacterString(s) = value {
128                self.inactive_text = s;
129                return Ok(());
130            }
131            return Err(common::invalid_data_type_error());
132        }
133        if let Some(result) =
134            common::write_out_of_service(&mut self.out_of_service, property, &value)
135        {
136            return result;
137        }
138        if let Some(result) = common::write_description(&mut self.description, property, &value) {
139            return result;
140        }
141        Err(common::write_access_denied_error())
142    }
143
144    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
145        static PROPS: &[PropertyIdentifier] = &[
146            PropertyIdentifier::OBJECT_IDENTIFIER,
147            PropertyIdentifier::OBJECT_NAME,
148            PropertyIdentifier::DESCRIPTION,
149            PropertyIdentifier::OBJECT_TYPE,
150            PropertyIdentifier::PRESENT_VALUE,
151            PropertyIdentifier::STATUS_FLAGS,
152            PropertyIdentifier::EVENT_STATE,
153            PropertyIdentifier::OUT_OF_SERVICE,
154            PropertyIdentifier::POLARITY,
155            PropertyIdentifier::RELIABILITY,
156            PropertyIdentifier::ACTIVE_TEXT,
157            PropertyIdentifier::INACTIVE_TEXT,
158        ];
159        Cow::Borrowed(PROPS)
160    }
161}
162
163// ---------------------------------------------------------------------------
164// BinaryOutput (type 4)
165// ---------------------------------------------------------------------------
166
167/// BACnet Binary Output object.
168///
169/// Commandable binary output with 16-level priority array.
170/// Uses Enumerated values: 0 = inactive, 1 = active.
171pub struct BinaryOutputObject {
172    oid: ObjectIdentifier,
173    name: String,
174    description: String,
175    present_value: u32,
176    out_of_service: bool,
177    status_flags: StatusFlags,
178    priority_array: [Option<u32>; 16],
179    relinquish_default: u32,
180    /// Polarity: 0 = normal, 1 = reverse.
181    polarity: u32,
182    /// Reliability: 0 = NO_FAULT_DETECTED.
183    reliability: u32,
184    active_text: String,
185    inactive_text: String,
186}
187
188impl BinaryOutputObject {
189    pub fn new(instance: u32, name: impl Into<String>) -> Result<Self, Error> {
190        let oid = ObjectIdentifier::new(ObjectType::BINARY_OUTPUT, instance)?;
191        Ok(Self {
192            oid,
193            name: name.into(),
194            description: String::new(),
195            present_value: 0,
196            out_of_service: false,
197            status_flags: StatusFlags::empty(),
198            priority_array: [None; 16],
199            relinquish_default: 0,
200            polarity: 0,
201            reliability: 0,
202            active_text: "Active".into(),
203            inactive_text: "Inactive".into(),
204        })
205    }
206
207    /// Set the description string.
208    pub fn set_description(&mut self, desc: impl Into<String>) {
209        self.description = desc.into();
210    }
211
212    fn recalculate_present_value(&mut self) {
213        self.present_value =
214            common::recalculate_from_priority_array(&self.priority_array, self.relinquish_default);
215    }
216}
217
218impl BACnetObject for BinaryOutputObject {
219    fn object_identifier(&self) -> ObjectIdentifier {
220        self.oid
221    }
222
223    fn object_name(&self) -> &str {
224        &self.name
225    }
226
227    fn read_property(
228        &self,
229        property: PropertyIdentifier,
230        array_index: Option<u32>,
231    ) -> Result<PropertyValue, Error> {
232        if let Some(result) = read_common_properties!(self, property, array_index) {
233            return result;
234        }
235        match property {
236            p if p == PropertyIdentifier::OBJECT_TYPE => Ok(PropertyValue::Enumerated(
237                ObjectType::BINARY_OUTPUT.to_raw(),
238            )),
239            p if p == PropertyIdentifier::PRESENT_VALUE => {
240                Ok(PropertyValue::Enumerated(self.present_value))
241            }
242            p if p == PropertyIdentifier::EVENT_STATE => Ok(PropertyValue::Enumerated(0)),
243            p if p == PropertyIdentifier::PRIORITY_ARRAY => {
244                common::read_priority_array!(self, array_index, PropertyValue::Enumerated)
245            }
246            p if p == PropertyIdentifier::RELINQUISH_DEFAULT => {
247                Ok(PropertyValue::Enumerated(self.relinquish_default))
248            }
249            p if p == PropertyIdentifier::POLARITY => Ok(PropertyValue::Enumerated(self.polarity)),
250            p if p == PropertyIdentifier::ACTIVE_TEXT => {
251                Ok(PropertyValue::CharacterString(self.active_text.clone()))
252            }
253            p if p == PropertyIdentifier::INACTIVE_TEXT => {
254                Ok(PropertyValue::CharacterString(self.inactive_text.clone()))
255            }
256            _ => Err(common::unknown_property_error()),
257        }
258    }
259
260    fn write_property(
261        &mut self,
262        property: PropertyIdentifier,
263        array_index: Option<u32>,
264        value: PropertyValue,
265        priority: Option<u8>,
266    ) -> Result<(), Error> {
267        common::write_priority_array_direct!(self, property, array_index, value, |v| {
268            if let PropertyValue::Enumerated(e) = v {
269                if e > 1 {
270                    Err(common::value_out_of_range_error())
271                } else {
272                    Ok(e)
273                }
274            } else {
275                Err(common::invalid_data_type_error())
276            }
277        });
278        if property == PropertyIdentifier::PRESENT_VALUE {
279            return common::write_priority_array!(self, value, priority, |v| {
280                if let PropertyValue::Enumerated(e) = v {
281                    if e > 1 {
282                        Err(common::value_out_of_range_error())
283                    } else {
284                        Ok(e)
285                    }
286                } else {
287                    Err(common::invalid_data_type_error())
288                }
289            });
290        }
291        if property == PropertyIdentifier::ACTIVE_TEXT {
292            if let PropertyValue::CharacterString(s) = value {
293                self.active_text = s;
294                return Ok(());
295            }
296            return Err(common::invalid_data_type_error());
297        }
298        if property == PropertyIdentifier::INACTIVE_TEXT {
299            if let PropertyValue::CharacterString(s) = value {
300                self.inactive_text = s;
301                return Ok(());
302            }
303            return Err(common::invalid_data_type_error());
304        }
305        if let Some(result) =
306            common::write_out_of_service(&mut self.out_of_service, property, &value)
307        {
308            return result;
309        }
310        if let Some(result) = common::write_description(&mut self.description, property, &value) {
311            return result;
312        }
313        Err(common::write_access_denied_error())
314    }
315
316    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
317        static PROPS: &[PropertyIdentifier] = &[
318            PropertyIdentifier::OBJECT_IDENTIFIER,
319            PropertyIdentifier::OBJECT_NAME,
320            PropertyIdentifier::DESCRIPTION,
321            PropertyIdentifier::OBJECT_TYPE,
322            PropertyIdentifier::PRESENT_VALUE,
323            PropertyIdentifier::STATUS_FLAGS,
324            PropertyIdentifier::EVENT_STATE,
325            PropertyIdentifier::OUT_OF_SERVICE,
326            PropertyIdentifier::PRIORITY_ARRAY,
327            PropertyIdentifier::RELINQUISH_DEFAULT,
328            PropertyIdentifier::POLARITY,
329            PropertyIdentifier::RELIABILITY,
330            PropertyIdentifier::ACTIVE_TEXT,
331            PropertyIdentifier::INACTIVE_TEXT,
332        ];
333        Cow::Borrowed(PROPS)
334    }
335}
336
337// ---------------------------------------------------------------------------
338// BinaryValue (type 5)
339// ---------------------------------------------------------------------------
340
341/// BACnet Binary Value object.
342///
343/// Commandable binary value with 16-level priority array.
344/// Uses Enumerated values: 0 = inactive, 1 = active.
345pub struct BinaryValueObject {
346    oid: ObjectIdentifier,
347    name: String,
348    description: String,
349    present_value: u32, // 0 = inactive, 1 = active
350    out_of_service: bool,
351    status_flags: StatusFlags,
352    priority_array: [Option<u32>; 16],
353    relinquish_default: u32,
354    /// Reliability: 0 = NO_FAULT_DETECTED.
355    reliability: u32,
356    active_text: String,
357    inactive_text: String,
358}
359
360impl BinaryValueObject {
361    /// Create a new Binary Value object.
362    pub fn new(instance: u32, name: impl Into<String>) -> Result<Self, Error> {
363        let oid = ObjectIdentifier::new(ObjectType::BINARY_VALUE, instance)?;
364        Ok(Self {
365            oid,
366            name: name.into(),
367            description: String::new(),
368            present_value: 0, // inactive
369            out_of_service: false,
370            status_flags: StatusFlags::empty(),
371            priority_array: [None; 16],
372            relinquish_default: 0,
373            reliability: 0,
374            active_text: "Active".into(),
375            inactive_text: "Inactive".into(),
376        })
377    }
378
379    /// Set the description string.
380    pub fn set_description(&mut self, desc: impl Into<String>) {
381        self.description = desc.into();
382    }
383
384    fn recalculate_present_value(&mut self) {
385        self.present_value =
386            common::recalculate_from_priority_array(&self.priority_array, self.relinquish_default);
387    }
388}
389
390impl BACnetObject for BinaryValueObject {
391    fn object_identifier(&self) -> ObjectIdentifier {
392        self.oid
393    }
394
395    fn object_name(&self) -> &str {
396        &self.name
397    }
398
399    fn read_property(
400        &self,
401        property: PropertyIdentifier,
402        array_index: Option<u32>,
403    ) -> Result<PropertyValue, Error> {
404        if let Some(result) = read_common_properties!(self, property, array_index) {
405            return result;
406        }
407        match property {
408            p if p == PropertyIdentifier::OBJECT_TYPE => {
409                Ok(PropertyValue::Enumerated(ObjectType::BINARY_VALUE.to_raw()))
410            }
411            p if p == PropertyIdentifier::PRESENT_VALUE => {
412                Ok(PropertyValue::Enumerated(self.present_value))
413            }
414            p if p == PropertyIdentifier::EVENT_STATE => {
415                Ok(PropertyValue::Enumerated(0)) // normal
416            }
417            p if p == PropertyIdentifier::PRIORITY_ARRAY => {
418                common::read_priority_array!(self, array_index, PropertyValue::Enumerated)
419            }
420            p if p == PropertyIdentifier::RELINQUISH_DEFAULT => {
421                Ok(PropertyValue::Enumerated(self.relinquish_default))
422            }
423            p if p == PropertyIdentifier::ACTIVE_TEXT => {
424                Ok(PropertyValue::CharacterString(self.active_text.clone()))
425            }
426            p if p == PropertyIdentifier::INACTIVE_TEXT => {
427                Ok(PropertyValue::CharacterString(self.inactive_text.clone()))
428            }
429            _ => Err(common::unknown_property_error()),
430        }
431    }
432
433    fn write_property(
434        &mut self,
435        property: PropertyIdentifier,
436        array_index: Option<u32>,
437        value: PropertyValue,
438        priority: Option<u8>,
439    ) -> Result<(), Error> {
440        common::write_priority_array_direct!(self, property, array_index, value, |v| {
441            if let PropertyValue::Enumerated(e) = v {
442                if e > 1 {
443                    Err(common::value_out_of_range_error())
444                } else {
445                    Ok(e)
446                }
447            } else {
448                Err(common::invalid_data_type_error())
449            }
450        });
451        if property == PropertyIdentifier::PRESENT_VALUE {
452            return common::write_priority_array!(self, value, priority, |v| {
453                if let PropertyValue::Enumerated(e) = v {
454                    if e > 1 {
455                        Err(common::value_out_of_range_error())
456                    } else {
457                        Ok(e)
458                    }
459                } else {
460                    Err(common::invalid_data_type_error())
461                }
462            });
463        }
464        if property == PropertyIdentifier::ACTIVE_TEXT {
465            if let PropertyValue::CharacterString(s) = value {
466                self.active_text = s;
467                return Ok(());
468            }
469            return Err(common::invalid_data_type_error());
470        }
471        if property == PropertyIdentifier::INACTIVE_TEXT {
472            if let PropertyValue::CharacterString(s) = value {
473                self.inactive_text = s;
474                return Ok(());
475            }
476            return Err(common::invalid_data_type_error());
477        }
478        if let Some(result) =
479            common::write_out_of_service(&mut self.out_of_service, property, &value)
480        {
481            return result;
482        }
483        if let Some(result) = common::write_description(&mut self.description, property, &value) {
484            return result;
485        }
486        Err(common::write_access_denied_error())
487    }
488
489    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
490        static PROPS: &[PropertyIdentifier] = &[
491            PropertyIdentifier::OBJECT_IDENTIFIER,
492            PropertyIdentifier::OBJECT_NAME,
493            PropertyIdentifier::DESCRIPTION,
494            PropertyIdentifier::OBJECT_TYPE,
495            PropertyIdentifier::PRESENT_VALUE,
496            PropertyIdentifier::STATUS_FLAGS,
497            PropertyIdentifier::EVENT_STATE,
498            PropertyIdentifier::OUT_OF_SERVICE,
499            PropertyIdentifier::PRIORITY_ARRAY,
500            PropertyIdentifier::RELINQUISH_DEFAULT,
501            PropertyIdentifier::RELIABILITY,
502            PropertyIdentifier::ACTIVE_TEXT,
503            PropertyIdentifier::INACTIVE_TEXT,
504        ];
505        Cow::Borrowed(PROPS)
506    }
507}
508
509#[cfg(test)]
510mod tests {
511    use super::*;
512
513    #[test]
514    fn bv_read_present_value_default() {
515        let bv = BinaryValueObject::new(1, "BV-1").unwrap();
516        let val = bv
517            .read_property(PropertyIdentifier::PRESENT_VALUE, None)
518            .unwrap();
519        assert_eq!(val, PropertyValue::Enumerated(0)); // inactive
520    }
521
522    #[test]
523    fn bv_write_present_value() {
524        let mut bv = BinaryValueObject::new(1, "BV-1").unwrap();
525        bv.write_property(
526            PropertyIdentifier::PRESENT_VALUE,
527            None,
528            PropertyValue::Enumerated(1), // active
529            Some(8),
530        )
531        .unwrap();
532        let val = bv
533            .read_property(PropertyIdentifier::PRESENT_VALUE, None)
534            .unwrap();
535        assert_eq!(val, PropertyValue::Enumerated(1));
536    }
537
538    #[test]
539    fn bv_write_invalid_value_rejected() {
540        let mut bv = BinaryValueObject::new(1, "BV-1").unwrap();
541        let result = bv.write_property(
542            PropertyIdentifier::PRESENT_VALUE,
543            None,
544            PropertyValue::Enumerated(2), // invalid -- only 0 or 1
545            Some(8),
546        );
547        assert!(result.is_err());
548    }
549
550    #[test]
551    fn bv_write_wrong_type_rejected() {
552        let mut bv = BinaryValueObject::new(1, "BV-1").unwrap();
553        let result = bv.write_property(
554            PropertyIdentifier::PRESENT_VALUE,
555            None,
556            PropertyValue::Real(1.0), // wrong type
557            Some(8),
558        );
559        assert!(result.is_err());
560    }
561
562    #[test]
563    fn bv_read_object_type() {
564        let bv = BinaryValueObject::new(1, "BV-1").unwrap();
565        let val = bv
566            .read_property(PropertyIdentifier::OBJECT_TYPE, None)
567            .unwrap();
568        assert_eq!(
569            val,
570            PropertyValue::Enumerated(ObjectType::BINARY_VALUE.to_raw())
571        );
572    }
573
574    #[test]
575    fn bv_read_reliability_default() {
576        let bv = BinaryValueObject::new(1, "BV-1").unwrap();
577        let val = bv
578            .read_property(PropertyIdentifier::RELIABILITY, None)
579            .unwrap();
580        assert_eq!(val, PropertyValue::Enumerated(0)); // NO_FAULT_DETECTED
581    }
582
583    // --- BinaryInput ---
584
585    #[test]
586    fn bi_read_present_value_default() {
587        let bi = BinaryInputObject::new(1, "BI-1").unwrap();
588        let val = bi
589            .read_property(PropertyIdentifier::PRESENT_VALUE, None)
590            .unwrap();
591        assert_eq!(val, PropertyValue::Enumerated(0));
592    }
593
594    #[test]
595    fn bi_write_denied_when_in_service() {
596        let mut bi = BinaryInputObject::new(1, "BI-1").unwrap();
597        let result = bi.write_property(
598            PropertyIdentifier::PRESENT_VALUE,
599            None,
600            PropertyValue::Enumerated(1),
601            None,
602        );
603        assert!(result.is_err());
604    }
605
606    #[test]
607    fn bi_write_allowed_when_out_of_service() {
608        let mut bi = BinaryInputObject::new(1, "BI-1").unwrap();
609        bi.write_property(
610            PropertyIdentifier::OUT_OF_SERVICE,
611            None,
612            PropertyValue::Boolean(true),
613            None,
614        )
615        .unwrap();
616        bi.write_property(
617            PropertyIdentifier::PRESENT_VALUE,
618            None,
619            PropertyValue::Enumerated(1),
620            None,
621        )
622        .unwrap();
623        let val = bi
624            .read_property(PropertyIdentifier::PRESENT_VALUE, None)
625            .unwrap();
626        assert_eq!(val, PropertyValue::Enumerated(1));
627    }
628
629    #[test]
630    fn bi_set_present_value() {
631        let mut bi = BinaryInputObject::new(1, "BI-1").unwrap();
632        bi.set_present_value(1);
633        let val = bi
634            .read_property(PropertyIdentifier::PRESENT_VALUE, None)
635            .unwrap();
636        assert_eq!(val, PropertyValue::Enumerated(1));
637    }
638
639    #[test]
640    fn bi_read_polarity_default() {
641        let bi = BinaryInputObject::new(1, "BI-1").unwrap();
642        let val = bi
643            .read_property(PropertyIdentifier::POLARITY, None)
644            .unwrap();
645        assert_eq!(val, PropertyValue::Enumerated(0)); // normal
646    }
647
648    #[test]
649    fn bi_read_reliability_default() {
650        let bi = BinaryInputObject::new(1, "BI-1").unwrap();
651        let val = bi
652            .read_property(PropertyIdentifier::RELIABILITY, None)
653            .unwrap();
654        assert_eq!(val, PropertyValue::Enumerated(0)); // NO_FAULT_DETECTED
655    }
656
657    // --- BinaryOutput ---
658
659    #[test]
660    fn bo_write_with_priority() {
661        let mut bo = BinaryOutputObject::new(1, "BO-1").unwrap();
662        bo.write_property(
663            PropertyIdentifier::PRESENT_VALUE,
664            None,
665            PropertyValue::Enumerated(1),
666            Some(8),
667        )
668        .unwrap();
669        let val = bo
670            .read_property(PropertyIdentifier::PRESENT_VALUE, None)
671            .unwrap();
672        assert_eq!(val, PropertyValue::Enumerated(1));
673        let slot = bo
674            .read_property(PropertyIdentifier::PRIORITY_ARRAY, Some(8))
675            .unwrap();
676        assert_eq!(slot, PropertyValue::Enumerated(1));
677    }
678
679    #[test]
680    fn bo_relinquish_falls_to_default() {
681        let mut bo = BinaryOutputObject::new(1, "BO-1").unwrap();
682        bo.write_property(
683            PropertyIdentifier::PRESENT_VALUE,
684            None,
685            PropertyValue::Enumerated(1),
686            Some(16),
687        )
688        .unwrap();
689        assert_eq!(
690            bo.read_property(PropertyIdentifier::PRESENT_VALUE, None)
691                .unwrap(),
692            PropertyValue::Enumerated(1)
693        );
694        bo.write_property(
695            PropertyIdentifier::PRESENT_VALUE,
696            None,
697            PropertyValue::Null,
698            Some(16),
699        )
700        .unwrap();
701        assert_eq!(
702            bo.read_property(PropertyIdentifier::PRESENT_VALUE, None)
703                .unwrap(),
704            PropertyValue::Enumerated(0)
705        );
706    }
707
708    #[test]
709    fn bo_invalid_value_rejected() {
710        let mut bo = BinaryOutputObject::new(1, "BO-1").unwrap();
711        let result = bo.write_property(
712            PropertyIdentifier::PRESENT_VALUE,
713            None,
714            PropertyValue::Enumerated(2),
715            None,
716        );
717        assert!(result.is_err());
718    }
719
720    #[test]
721    fn bo_read_polarity_default() {
722        let bo = BinaryOutputObject::new(1, "BO-1").unwrap();
723        let val = bo
724            .read_property(PropertyIdentifier::POLARITY, None)
725            .unwrap();
726        assert_eq!(val, PropertyValue::Enumerated(0)); // normal
727    }
728
729    #[test]
730    fn bo_read_reliability_default() {
731        let bo = BinaryOutputObject::new(1, "BO-1").unwrap();
732        let val = bo
733            .read_property(PropertyIdentifier::RELIABILITY, None)
734            .unwrap();
735        assert_eq!(val, PropertyValue::Enumerated(0)); // NO_FAULT_DETECTED
736    }
737
738    // --- Priority array bounds tests (BinaryOutput) ---
739
740    #[test]
741    fn bo_priority_array_index_zero_returns_size() {
742        let bo = BinaryOutputObject::new(1, "BO-1").unwrap();
743        let val = bo
744            .read_property(PropertyIdentifier::PRIORITY_ARRAY, Some(0))
745            .unwrap();
746        assert_eq!(val, PropertyValue::Unsigned(16));
747    }
748
749    #[test]
750    fn bo_priority_array_index_out_of_bounds() {
751        let bo = BinaryOutputObject::new(1, "BO-1").unwrap();
752        // Index 17 is out of bounds (valid: 0-16)
753        let result = bo.read_property(PropertyIdentifier::PRIORITY_ARRAY, Some(17));
754        assert!(result.is_err());
755    }
756
757    #[test]
758    fn bo_priority_array_index_far_out_of_bounds() {
759        let bo = BinaryOutputObject::new(1, "BO-1").unwrap();
760        let result = bo.read_property(PropertyIdentifier::PRIORITY_ARRAY, Some(100));
761        assert!(result.is_err());
762    }
763
764    // --- WriteProperty with invalid priority tests (BinaryOutput) ---
765
766    #[test]
767    fn bo_write_with_priority_zero_rejected() {
768        let mut bo = BinaryOutputObject::new(1, "BO-1").unwrap();
769        // Priority 0 is invalid (valid range is 1-16)
770        let result = bo.write_property(
771            PropertyIdentifier::PRESENT_VALUE,
772            None,
773            PropertyValue::Enumerated(1),
774            Some(0),
775        );
776        assert!(result.is_err());
777    }
778
779    #[test]
780    fn bo_write_with_priority_17_rejected() {
781        let mut bo = BinaryOutputObject::new(1, "BO-1").unwrap();
782        // Priority 17 is invalid (valid range is 1-16)
783        let result = bo.write_property(
784            PropertyIdentifier::PRESENT_VALUE,
785            None,
786            PropertyValue::Enumerated(1),
787            Some(17),
788        );
789        assert!(result.is_err());
790    }
791
792    #[test]
793    fn bo_write_with_priority_255_rejected() {
794        let mut bo = BinaryOutputObject::new(1, "BO-1").unwrap();
795        // Priority 255 is invalid
796        let result = bo.write_property(
797            PropertyIdentifier::PRESENT_VALUE,
798            None,
799            PropertyValue::Enumerated(1),
800            Some(255),
801        );
802        assert!(result.is_err());
803    }
804
805    // --- BinaryInput read-only properties ---
806
807    #[test]
808    fn bi_polarity_is_readable_as_enumerated() {
809        let bi = BinaryInputObject::new(1, "BI-1").unwrap();
810        let val = bi
811            .read_property(PropertyIdentifier::POLARITY, None)
812            .unwrap();
813        // Polarity default is 0 (normal), verify it comes back as Enumerated
814        match val {
815            PropertyValue::Enumerated(v) => assert_eq!(v, 0),
816            other => panic!("Expected Enumerated for POLARITY, got {:?}", other),
817        }
818    }
819
820    #[test]
821    fn bi_reliability_is_readable_as_enumerated() {
822        let bi = BinaryInputObject::new(1, "BI-1").unwrap();
823        let val = bi
824            .read_property(PropertyIdentifier::RELIABILITY, None)
825            .unwrap();
826        // Reliability default is 0 (NO_FAULT_DETECTED), verify correct type
827        match val {
828            PropertyValue::Enumerated(v) => assert_eq!(v, 0),
829            other => panic!("Expected Enumerated for RELIABILITY, got {:?}", other),
830        }
831    }
832
833    #[test]
834    fn bo_polarity_is_readable_as_enumerated() {
835        let bo = BinaryOutputObject::new(1, "BO-1").unwrap();
836        let val = bo
837            .read_property(PropertyIdentifier::POLARITY, None)
838            .unwrap();
839        match val {
840            PropertyValue::Enumerated(v) => assert_eq!(v, 0),
841            other => panic!("Expected Enumerated for POLARITY, got {:?}", other),
842        }
843    }
844
845    #[test]
846    fn bo_reliability_is_readable_as_enumerated() {
847        let bo = BinaryOutputObject::new(1, "BO-1").unwrap();
848        let val = bo
849            .read_property(PropertyIdentifier::RELIABILITY, None)
850            .unwrap();
851        match val {
852            PropertyValue::Enumerated(v) => assert_eq!(v, 0),
853            other => panic!("Expected Enumerated for RELIABILITY, got {:?}", other),
854        }
855    }
856
857    #[test]
858    fn bi_polarity_in_property_list() {
859        let bi = BinaryInputObject::new(1, "BI-1").unwrap();
860        let props = bi.property_list();
861        assert!(props.contains(&PropertyIdentifier::POLARITY));
862        assert!(props.contains(&PropertyIdentifier::RELIABILITY));
863    }
864
865    #[test]
866    fn bo_priority_array_read_all_slots_none_by_default() {
867        let bo = BinaryOutputObject::new(1, "BO-1").unwrap();
868        let val = bo
869            .read_property(PropertyIdentifier::PRIORITY_ARRAY, None)
870            .unwrap();
871        if let PropertyValue::List(elements) = val {
872            assert_eq!(elements.len(), 16);
873            for elem in &elements {
874                assert_eq!(elem, &PropertyValue::Null);
875            }
876        } else {
877            panic!("Expected List for priority array without index");
878        }
879    }
880
881    // --- Direct PRIORITY_ARRAY writes (Clause 15.9.1.1.3) ---
882
883    #[test]
884    fn bo_direct_priority_array_write_value() {
885        let mut bo = BinaryOutputObject::new(1, "BO-1").unwrap();
886        bo.write_property(
887            PropertyIdentifier::PRIORITY_ARRAY,
888            Some(5),
889            PropertyValue::Enumerated(1), // active
890            None,
891        )
892        .unwrap();
893        assert_eq!(
894            bo.read_property(PropertyIdentifier::PRESENT_VALUE, None)
895                .unwrap(),
896            PropertyValue::Enumerated(1)
897        );
898        assert_eq!(
899            bo.read_property(PropertyIdentifier::PRIORITY_ARRAY, Some(5))
900                .unwrap(),
901            PropertyValue::Enumerated(1)
902        );
903    }
904
905    #[test]
906    fn bo_direct_priority_array_relinquish() {
907        let mut bo = BinaryOutputObject::new(1, "BO-1").unwrap();
908        bo.write_property(
909            PropertyIdentifier::PRIORITY_ARRAY,
910            Some(5),
911            PropertyValue::Enumerated(1),
912            None,
913        )
914        .unwrap();
915        bo.write_property(
916            PropertyIdentifier::PRIORITY_ARRAY,
917            Some(5),
918            PropertyValue::Null,
919            None,
920        )
921        .unwrap();
922        // Fall back to relinquish default (0 = inactive)
923        assert_eq!(
924            bo.read_property(PropertyIdentifier::PRESENT_VALUE, None)
925                .unwrap(),
926            PropertyValue::Enumerated(0)
927        );
928    }
929
930    #[test]
931    fn bo_direct_priority_array_no_index_error() {
932        let mut bo = BinaryOutputObject::new(1, "BO-1").unwrap();
933        let result = bo.write_property(
934            PropertyIdentifier::PRIORITY_ARRAY,
935            None,
936            PropertyValue::Enumerated(1),
937            None,
938        );
939        assert!(result.is_err());
940    }
941
942    #[test]
943    fn bo_direct_priority_array_index_zero_error() {
944        let mut bo = BinaryOutputObject::new(1, "BO-1").unwrap();
945        let result = bo.write_property(
946            PropertyIdentifier::PRIORITY_ARRAY,
947            Some(0),
948            PropertyValue::Enumerated(1),
949            None,
950        );
951        assert!(result.is_err());
952    }
953
954    #[test]
955    fn bo_direct_priority_array_index_17_error() {
956        let mut bo = BinaryOutputObject::new(1, "BO-1").unwrap();
957        let result = bo.write_property(
958            PropertyIdentifier::PRIORITY_ARRAY,
959            Some(17),
960            PropertyValue::Enumerated(1),
961            None,
962        );
963        assert!(result.is_err());
964    }
965
966    // --- BinaryValue commandable tests ---
967
968    #[test]
969    fn bv_write_with_priority() {
970        let mut bv = BinaryValueObject::new(1, "BV-1").unwrap();
971        bv.write_property(
972            PropertyIdentifier::PRESENT_VALUE,
973            None,
974            PropertyValue::Enumerated(1),
975            Some(8),
976        )
977        .unwrap();
978        let val = bv
979            .read_property(PropertyIdentifier::PRESENT_VALUE, None)
980            .unwrap();
981        assert_eq!(val, PropertyValue::Enumerated(1));
982        let slot = bv
983            .read_property(PropertyIdentifier::PRIORITY_ARRAY, Some(8))
984            .unwrap();
985        assert_eq!(slot, PropertyValue::Enumerated(1));
986    }
987
988    #[test]
989    fn bv_relinquish_falls_to_default() {
990        let mut bv = BinaryValueObject::new(1, "BV-1").unwrap();
991        bv.write_property(
992            PropertyIdentifier::PRESENT_VALUE,
993            None,
994            PropertyValue::Enumerated(1),
995            Some(16),
996        )
997        .unwrap();
998        assert_eq!(
999            bv.read_property(PropertyIdentifier::PRESENT_VALUE, None)
1000                .unwrap(),
1001            PropertyValue::Enumerated(1)
1002        );
1003        bv.write_property(
1004            PropertyIdentifier::PRESENT_VALUE,
1005            None,
1006            PropertyValue::Null,
1007            Some(16),
1008        )
1009        .unwrap();
1010        assert_eq!(
1011            bv.read_property(PropertyIdentifier::PRESENT_VALUE, None)
1012                .unwrap(),
1013            PropertyValue::Enumerated(0) // relinquish_default
1014        );
1015    }
1016
1017    #[test]
1018    fn bv_read_priority_array_all_none() {
1019        let bv = BinaryValueObject::new(1, "BV-1").unwrap();
1020        let val = bv
1021            .read_property(PropertyIdentifier::PRIORITY_ARRAY, None)
1022            .unwrap();
1023        if let PropertyValue::List(elements) = val {
1024            assert_eq!(elements.len(), 16);
1025            for elem in &elements {
1026                assert_eq!(elem, &PropertyValue::Null);
1027            }
1028        } else {
1029            panic!("Expected List for priority array without index");
1030        }
1031    }
1032
1033    #[test]
1034    fn bv_read_relinquish_default() {
1035        let bv = BinaryValueObject::new(1, "BV-1").unwrap();
1036        let val = bv
1037            .read_property(PropertyIdentifier::RELINQUISH_DEFAULT, None)
1038            .unwrap();
1039        assert_eq!(val, PropertyValue::Enumerated(0));
1040    }
1041
1042    #[test]
1043    fn bv_priority_array_in_property_list() {
1044        let bv = BinaryValueObject::new(1, "BV-1").unwrap();
1045        let props = bv.property_list();
1046        assert!(props.contains(&PropertyIdentifier::PRIORITY_ARRAY));
1047        assert!(props.contains(&PropertyIdentifier::RELINQUISH_DEFAULT));
1048    }
1049
1050    #[test]
1051    fn bv_direct_priority_array_write() {
1052        let mut bv = BinaryValueObject::new(1, "BV-1").unwrap();
1053        bv.write_property(
1054            PropertyIdentifier::PRIORITY_ARRAY,
1055            Some(5),
1056            PropertyValue::Enumerated(1),
1057            None,
1058        )
1059        .unwrap();
1060        assert_eq!(
1061            bv.read_property(PropertyIdentifier::PRESENT_VALUE, None)
1062                .unwrap(),
1063            PropertyValue::Enumerated(1)
1064        );
1065        assert_eq!(
1066            bv.read_property(PropertyIdentifier::PRIORITY_ARRAY, Some(5))
1067                .unwrap(),
1068            PropertyValue::Enumerated(1)
1069        );
1070    }
1071
1072    #[test]
1073    fn bv_direct_priority_array_relinquish() {
1074        let mut bv = BinaryValueObject::new(1, "BV-1").unwrap();
1075        bv.write_property(
1076            PropertyIdentifier::PRIORITY_ARRAY,
1077            Some(5),
1078            PropertyValue::Enumerated(1),
1079            None,
1080        )
1081        .unwrap();
1082        bv.write_property(
1083            PropertyIdentifier::PRIORITY_ARRAY,
1084            Some(5),
1085            PropertyValue::Null,
1086            None,
1087        )
1088        .unwrap();
1089        assert_eq!(
1090            bv.read_property(PropertyIdentifier::PRESENT_VALUE, None)
1091                .unwrap(),
1092            PropertyValue::Enumerated(0) // relinquish_default
1093        );
1094    }
1095
1096    // --- Description tests ---
1097
1098    #[test]
1099    fn bv_description_read_write() {
1100        let mut bv = BinaryValueObject::new(1, "BV-1").unwrap();
1101        // Default is empty
1102        assert_eq!(
1103            bv.read_property(PropertyIdentifier::DESCRIPTION, None)
1104                .unwrap(),
1105            PropertyValue::CharacterString(String::new())
1106        );
1107        bv.write_property(
1108            PropertyIdentifier::DESCRIPTION,
1109            None,
1110            PropertyValue::CharacterString("Occupied/Unoccupied".into()),
1111            None,
1112        )
1113        .unwrap();
1114        assert_eq!(
1115            bv.read_property(PropertyIdentifier::DESCRIPTION, None)
1116                .unwrap(),
1117            PropertyValue::CharacterString("Occupied/Unoccupied".into())
1118        );
1119    }
1120
1121    #[test]
1122    fn bv_description_in_property_list() {
1123        let bv = BinaryValueObject::new(1, "BV-1").unwrap();
1124        assert!(bv
1125            .property_list()
1126            .contains(&PropertyIdentifier::DESCRIPTION));
1127    }
1128
1129    #[test]
1130    fn bi_description_read_write() {
1131        let mut bi = BinaryInputObject::new(1, "BI-1").unwrap();
1132        bi.write_property(
1133            PropertyIdentifier::DESCRIPTION,
1134            None,
1135            PropertyValue::CharacterString("Door contact".into()),
1136            None,
1137        )
1138        .unwrap();
1139        assert_eq!(
1140            bi.read_property(PropertyIdentifier::DESCRIPTION, None)
1141                .unwrap(),
1142            PropertyValue::CharacterString("Door contact".into())
1143        );
1144    }
1145
1146    #[test]
1147    fn bo_description_read_write() {
1148        let mut bo = BinaryOutputObject::new(1, "BO-1").unwrap();
1149        bo.write_property(
1150            PropertyIdentifier::DESCRIPTION,
1151            None,
1152            PropertyValue::CharacterString("Fan enable".into()),
1153            None,
1154        )
1155        .unwrap();
1156        assert_eq!(
1157            bo.read_property(PropertyIdentifier::DESCRIPTION, None)
1158                .unwrap(),
1159            PropertyValue::CharacterString("Fan enable".into())
1160        );
1161    }
1162
1163    #[test]
1164    fn bv_higher_priority_wins() {
1165        let mut bv = BinaryValueObject::new(1, "BV-1").unwrap();
1166        // Write inactive at priority 10
1167        bv.write_property(
1168            PropertyIdentifier::PRESENT_VALUE,
1169            None,
1170            PropertyValue::Enumerated(0),
1171            Some(10),
1172        )
1173        .unwrap();
1174        // Write active at priority 5 (higher)
1175        bv.write_property(
1176            PropertyIdentifier::PRESENT_VALUE,
1177            None,
1178            PropertyValue::Enumerated(1),
1179            Some(5),
1180        )
1181        .unwrap();
1182        assert_eq!(
1183            bv.read_property(PropertyIdentifier::PRESENT_VALUE, None)
1184                .unwrap(),
1185            PropertyValue::Enumerated(1) // priority 5 wins
1186        );
1187        // Relinquish priority 5, falls to priority 10
1188        bv.write_property(
1189            PropertyIdentifier::PRESENT_VALUE,
1190            None,
1191            PropertyValue::Null,
1192            Some(5),
1193        )
1194        .unwrap();
1195        assert_eq!(
1196            bv.read_property(PropertyIdentifier::PRESENT_VALUE, None)
1197                .unwrap(),
1198            PropertyValue::Enumerated(0) // priority 10 value
1199        );
1200    }
1201
1202    // --- active_text / inactive_text tests ---
1203
1204    #[test]
1205    fn bi_active_inactive_text_defaults() {
1206        let bi = BinaryInputObject::new(1, "BI-1").unwrap();
1207        assert_eq!(
1208            bi.read_property(PropertyIdentifier::ACTIVE_TEXT, None)
1209                .unwrap(),
1210            PropertyValue::CharacterString("Active".into())
1211        );
1212        assert_eq!(
1213            bi.read_property(PropertyIdentifier::INACTIVE_TEXT, None)
1214                .unwrap(),
1215            PropertyValue::CharacterString("Inactive".into())
1216        );
1217    }
1218
1219    #[test]
1220    fn bi_active_inactive_text_write_read() {
1221        let mut bi = BinaryInputObject::new(1, "BI-1").unwrap();
1222        bi.write_property(
1223            PropertyIdentifier::ACTIVE_TEXT,
1224            None,
1225            PropertyValue::CharacterString("On".into()),
1226            None,
1227        )
1228        .unwrap();
1229        bi.write_property(
1230            PropertyIdentifier::INACTIVE_TEXT,
1231            None,
1232            PropertyValue::CharacterString("Off".into()),
1233            None,
1234        )
1235        .unwrap();
1236        assert_eq!(
1237            bi.read_property(PropertyIdentifier::ACTIVE_TEXT, None)
1238                .unwrap(),
1239            PropertyValue::CharacterString("On".into())
1240        );
1241        assert_eq!(
1242            bi.read_property(PropertyIdentifier::INACTIVE_TEXT, None)
1243                .unwrap(),
1244            PropertyValue::CharacterString("Off".into())
1245        );
1246    }
1247
1248    #[test]
1249    fn bi_active_text_wrong_type_rejected() {
1250        let mut bi = BinaryInputObject::new(1, "BI-1").unwrap();
1251        assert!(bi
1252            .write_property(
1253                PropertyIdentifier::ACTIVE_TEXT,
1254                None,
1255                PropertyValue::Enumerated(1),
1256                None,
1257            )
1258            .is_err());
1259    }
1260
1261    #[test]
1262    fn bi_inactive_text_wrong_type_rejected() {
1263        let mut bi = BinaryInputObject::new(1, "BI-1").unwrap();
1264        assert!(bi
1265            .write_property(
1266                PropertyIdentifier::INACTIVE_TEXT,
1267                None,
1268                PropertyValue::Boolean(false),
1269                None,
1270            )
1271            .is_err());
1272    }
1273
1274    #[test]
1275    fn bi_active_inactive_text_in_property_list() {
1276        let bi = BinaryInputObject::new(1, "BI-1").unwrap();
1277        let props = bi.property_list();
1278        assert!(props.contains(&PropertyIdentifier::ACTIVE_TEXT));
1279        assert!(props.contains(&PropertyIdentifier::INACTIVE_TEXT));
1280    }
1281
1282    #[test]
1283    fn bo_active_inactive_text_defaults() {
1284        let bo = BinaryOutputObject::new(1, "BO-1").unwrap();
1285        assert_eq!(
1286            bo.read_property(PropertyIdentifier::ACTIVE_TEXT, None)
1287                .unwrap(),
1288            PropertyValue::CharacterString("Active".into())
1289        );
1290        assert_eq!(
1291            bo.read_property(PropertyIdentifier::INACTIVE_TEXT, None)
1292                .unwrap(),
1293            PropertyValue::CharacterString("Inactive".into())
1294        );
1295    }
1296
1297    #[test]
1298    fn bo_active_inactive_text_write_read() {
1299        let mut bo = BinaryOutputObject::new(1, "BO-1").unwrap();
1300        bo.write_property(
1301            PropertyIdentifier::ACTIVE_TEXT,
1302            None,
1303            PropertyValue::CharacterString("Running".into()),
1304            None,
1305        )
1306        .unwrap();
1307        bo.write_property(
1308            PropertyIdentifier::INACTIVE_TEXT,
1309            None,
1310            PropertyValue::CharacterString("Stopped".into()),
1311            None,
1312        )
1313        .unwrap();
1314        assert_eq!(
1315            bo.read_property(PropertyIdentifier::ACTIVE_TEXT, None)
1316                .unwrap(),
1317            PropertyValue::CharacterString("Running".into())
1318        );
1319        assert_eq!(
1320            bo.read_property(PropertyIdentifier::INACTIVE_TEXT, None)
1321                .unwrap(),
1322            PropertyValue::CharacterString("Stopped".into())
1323        );
1324    }
1325
1326    #[test]
1327    fn bo_active_inactive_text_in_property_list() {
1328        let bo = BinaryOutputObject::new(1, "BO-1").unwrap();
1329        let props = bo.property_list();
1330        assert!(props.contains(&PropertyIdentifier::ACTIVE_TEXT));
1331        assert!(props.contains(&PropertyIdentifier::INACTIVE_TEXT));
1332    }
1333
1334    #[test]
1335    fn bv_active_inactive_text_defaults() {
1336        let bv = BinaryValueObject::new(1, "BV-1").unwrap();
1337        assert_eq!(
1338            bv.read_property(PropertyIdentifier::ACTIVE_TEXT, None)
1339                .unwrap(),
1340            PropertyValue::CharacterString("Active".into())
1341        );
1342        assert_eq!(
1343            bv.read_property(PropertyIdentifier::INACTIVE_TEXT, None)
1344                .unwrap(),
1345            PropertyValue::CharacterString("Inactive".into())
1346        );
1347    }
1348
1349    #[test]
1350    fn bv_active_inactive_text_write_read() {
1351        let mut bv = BinaryValueObject::new(1, "BV-1").unwrap();
1352        bv.write_property(
1353            PropertyIdentifier::ACTIVE_TEXT,
1354            None,
1355            PropertyValue::CharacterString("Occupied".into()),
1356            None,
1357        )
1358        .unwrap();
1359        bv.write_property(
1360            PropertyIdentifier::INACTIVE_TEXT,
1361            None,
1362            PropertyValue::CharacterString("Unoccupied".into()),
1363            None,
1364        )
1365        .unwrap();
1366        assert_eq!(
1367            bv.read_property(PropertyIdentifier::ACTIVE_TEXT, None)
1368                .unwrap(),
1369            PropertyValue::CharacterString("Occupied".into())
1370        );
1371        assert_eq!(
1372            bv.read_property(PropertyIdentifier::INACTIVE_TEXT, None)
1373                .unwrap(),
1374            PropertyValue::CharacterString("Unoccupied".into())
1375        );
1376    }
1377
1378    #[test]
1379    fn bv_active_inactive_text_in_property_list() {
1380        let bv = BinaryValueObject::new(1, "BV-1").unwrap();
1381        let props = bv.property_list();
1382        assert!(props.contains(&PropertyIdentifier::ACTIVE_TEXT));
1383        assert!(props.contains(&PropertyIdentifier::INACTIVE_TEXT));
1384    }
1385
1386    #[test]
1387    fn bv_active_text_wrong_type_rejected() {
1388        let mut bv = BinaryValueObject::new(1, "BV-1").unwrap();
1389        assert!(bv
1390            .write_property(
1391                PropertyIdentifier::ACTIVE_TEXT,
1392                None,
1393                PropertyValue::Real(1.0),
1394                None,
1395            )
1396            .is_err());
1397    }
1398}