Skip to main content

bacnet_objects/
lighting.rs

1//! Lighting Output (type 54), Binary Lighting Output (type 55), and Channel
2//! (type 53) objects per ASHRAE 135-2020 Clauses 12.55, 12.56, and 12.53.
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::{
10    self, read_common_properties, read_priority_array, write_priority_array,
11    write_priority_array_direct,
12};
13use crate::traits::BACnetObject;
14
15// ---------------------------------------------------------------------------
16// LightingOutput (type 54)
17// ---------------------------------------------------------------------------
18
19/// BACnet Lighting Output object.
20///
21/// Commandable output with a 16-level priority array controlling a
22/// floating-point present-value (0.0 to 100.0 percent).
23pub struct LightingOutputObject {
24    oid: ObjectIdentifier,
25    name: String,
26    description: String,
27    present_value: f32,
28    tracking_value: f32,
29    /// Stored as opaque OctetString for now (BACnetLightingCommand encoding).
30    lighting_command: Vec<u8>,
31    lighting_command_default_priority: u32,
32    /// LightingInProgress enumeration: 0=idle, 1=fade-active, 2=ramp-active, 3=not-controlled, etc.
33    in_progress: u32,
34    blink_warn_enable: bool,
35    egress_time: u32,
36    egress_active: bool,
37    out_of_service: bool,
38    status_flags: StatusFlags,
39    /// Reliability: 0 = NO_FAULT_DETECTED.
40    reliability: u32,
41    priority_array: [Option<f32>; 16],
42    relinquish_default: f32,
43}
44
45impl LightingOutputObject {
46    /// Create a new Lighting Output object.
47    pub fn new(instance: u32, name: impl Into<String>) -> Result<Self, Error> {
48        let oid = ObjectIdentifier::new(ObjectType::LIGHTING_OUTPUT, instance)?;
49        Ok(Self {
50            oid,
51            name: name.into(),
52            description: String::new(),
53            present_value: 0.0,
54            tracking_value: 0.0,
55            lighting_command: Vec::new(),
56            lighting_command_default_priority: 16,
57            in_progress: 0, // idle
58            blink_warn_enable: false,
59            egress_time: 0,
60            egress_active: false,
61            out_of_service: false,
62            status_flags: StatusFlags::empty(),
63            reliability: 0,
64            priority_array: [None; 16],
65            relinquish_default: 0.0,
66        })
67    }
68
69    /// Set the description string.
70    pub fn set_description(&mut self, desc: impl Into<String>) {
71        self.description = desc.into();
72    }
73
74    /// Recalculate present-value from the priority array.
75    fn recalculate_present_value(&mut self) {
76        self.present_value =
77            common::recalculate_from_priority_array(&self.priority_array, self.relinquish_default);
78    }
79}
80
81impl BACnetObject for LightingOutputObject {
82    fn object_identifier(&self) -> ObjectIdentifier {
83        self.oid
84    }
85
86    fn object_name(&self) -> &str {
87        &self.name
88    }
89
90    fn read_property(
91        &self,
92        property: PropertyIdentifier,
93        array_index: Option<u32>,
94    ) -> Result<PropertyValue, Error> {
95        if let Some(result) = read_common_properties!(self, property, array_index) {
96            return result;
97        }
98        match property {
99            p if p == PropertyIdentifier::OBJECT_TYPE => Ok(PropertyValue::Enumerated(
100                ObjectType::LIGHTING_OUTPUT.to_raw(),
101            )),
102            p if p == PropertyIdentifier::PRESENT_VALUE => {
103                Ok(PropertyValue::Real(self.present_value))
104            }
105            p if p == PropertyIdentifier::TRACKING_VALUE => {
106                Ok(PropertyValue::Real(self.tracking_value))
107            }
108            p if p == PropertyIdentifier::LIGHTING_COMMAND => {
109                Ok(PropertyValue::OctetString(self.lighting_command.clone()))
110            }
111            p if p == PropertyIdentifier::LIGHTING_COMMAND_DEFAULT_PRIORITY => Ok(
112                PropertyValue::Unsigned(self.lighting_command_default_priority as u64),
113            ),
114            p if p == PropertyIdentifier::IN_PROGRESS => {
115                Ok(PropertyValue::Enumerated(self.in_progress))
116            }
117            p if p == PropertyIdentifier::BLINK_WARN_ENABLE => {
118                Ok(PropertyValue::Boolean(self.blink_warn_enable))
119            }
120            p if p == PropertyIdentifier::EGRESS_TIME => {
121                Ok(PropertyValue::Unsigned(self.egress_time as u64))
122            }
123            p if p == PropertyIdentifier::EGRESS_ACTIVE => {
124                Ok(PropertyValue::Boolean(self.egress_active))
125            }
126            p if p == PropertyIdentifier::PRIORITY_ARRAY => {
127                read_priority_array!(self, array_index, PropertyValue::Real)
128            }
129            p if p == PropertyIdentifier::RELINQUISH_DEFAULT => {
130                Ok(PropertyValue::Real(self.relinquish_default))
131            }
132            p if p == PropertyIdentifier::DEFAULT_FADE_TIME => Ok(PropertyValue::Unsigned(0)),
133            _ => Err(common::unknown_property_error()),
134        }
135    }
136
137    fn write_property(
138        &mut self,
139        property: PropertyIdentifier,
140        array_index: Option<u32>,
141        value: PropertyValue,
142        priority: Option<u8>,
143    ) -> Result<(), Error> {
144        // Direct writes to PRIORITY_ARRAY[index]
145        write_priority_array_direct!(self, property, array_index, value, |v| {
146            match v {
147                PropertyValue::Real(f) => {
148                    if !(0.0..=100.0).contains(&f) {
149                        Err(common::value_out_of_range_error())
150                    } else {
151                        Ok(f)
152                    }
153                }
154                _ => Err(common::invalid_data_type_error()),
155            }
156        });
157
158        // PRESENT_VALUE — commandable via priority array
159        if property == PropertyIdentifier::PRESENT_VALUE {
160            return write_priority_array!(self, value, priority, |v| {
161                match v {
162                    PropertyValue::Real(f) => {
163                        if !(0.0..=100.0).contains(&f) {
164                            Err(common::value_out_of_range_error())
165                        } else {
166                            Ok(f)
167                        }
168                    }
169                    _ => Err(common::invalid_data_type_error()),
170                }
171            });
172        }
173
174        // LIGHTING_COMMAND — stored as opaque bytes
175        if property == PropertyIdentifier::LIGHTING_COMMAND {
176            if let PropertyValue::OctetString(data) = value {
177                self.lighting_command = data;
178                return Ok(());
179            }
180            return Err(common::invalid_data_type_error());
181        }
182
183        // LIGHTING_COMMAND_DEFAULT_PRIORITY
184        if property == PropertyIdentifier::LIGHTING_COMMAND_DEFAULT_PRIORITY {
185            if let PropertyValue::Unsigned(v) = value {
186                if !(1..=16).contains(&v) {
187                    return Err(common::value_out_of_range_error());
188                }
189                self.lighting_command_default_priority = v as u32;
190                return Ok(());
191            }
192            return Err(common::invalid_data_type_error());
193        }
194
195        // BLINK_WARN_ENABLE
196        if property == PropertyIdentifier::BLINK_WARN_ENABLE {
197            if let PropertyValue::Boolean(v) = value {
198                self.blink_warn_enable = v;
199                return Ok(());
200            }
201            return Err(common::invalid_data_type_error());
202        }
203
204        // EGRESS_TIME
205        if property == PropertyIdentifier::EGRESS_TIME {
206            if let PropertyValue::Unsigned(v) = value {
207                self.egress_time = common::u64_to_u32(v)?;
208                return Ok(());
209            }
210            return Err(common::invalid_data_type_error());
211        }
212
213        if let Some(result) =
214            common::write_out_of_service(&mut self.out_of_service, property, &value)
215        {
216            return result;
217        }
218        if let Some(result) = common::write_description(&mut self.description, property, &value) {
219            return result;
220        }
221        Err(common::write_access_denied_error())
222    }
223
224    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
225        static PROPS: &[PropertyIdentifier] = &[
226            PropertyIdentifier::OBJECT_IDENTIFIER,
227            PropertyIdentifier::OBJECT_NAME,
228            PropertyIdentifier::DESCRIPTION,
229            PropertyIdentifier::OBJECT_TYPE,
230            PropertyIdentifier::PRESENT_VALUE,
231            PropertyIdentifier::TRACKING_VALUE,
232            PropertyIdentifier::LIGHTING_COMMAND,
233            PropertyIdentifier::LIGHTING_COMMAND_DEFAULT_PRIORITY,
234            PropertyIdentifier::IN_PROGRESS,
235            PropertyIdentifier::BLINK_WARN_ENABLE,
236            PropertyIdentifier::EGRESS_TIME,
237            PropertyIdentifier::EGRESS_ACTIVE,
238            PropertyIdentifier::STATUS_FLAGS,
239            PropertyIdentifier::OUT_OF_SERVICE,
240            PropertyIdentifier::RELIABILITY,
241            PropertyIdentifier::PRIORITY_ARRAY,
242            PropertyIdentifier::RELINQUISH_DEFAULT,
243        ];
244        Cow::Borrowed(PROPS)
245    }
246
247    fn supports_cov(&self) -> bool {
248        true
249    }
250}
251
252// ---------------------------------------------------------------------------
253// BinaryLightingOutput (type 55)
254// ---------------------------------------------------------------------------
255
256/// BACnet Binary Lighting Output object.
257///
258/// Commandable output with a 16-level priority array controlling an
259/// Enumerated present-value: 0=off, 1=on, 2=warn, 3=warn-off, 4=fade-on.
260pub struct BinaryLightingOutputObject {
261    oid: ObjectIdentifier,
262    name: String,
263    description: String,
264    present_value: u32,
265    blink_warn_enable: bool,
266    egress_time: u32,
267    egress_active: bool,
268    out_of_service: bool,
269    status_flags: StatusFlags,
270    /// Reliability: 0 = NO_FAULT_DETECTED.
271    reliability: u32,
272    priority_array: [Option<u32>; 16],
273    relinquish_default: u32,
274}
275
276impl BinaryLightingOutputObject {
277    /// Valid BinaryLightingPV values: off=0, on=1, warn=2, warn-off=3, fade-on=4.
278    const MAX_PV: u32 = 4;
279
280    /// Create a new Binary Lighting Output object.
281    pub fn new(instance: u32, name: impl Into<String>) -> Result<Self, Error> {
282        let oid = ObjectIdentifier::new(ObjectType::BINARY_LIGHTING_OUTPUT, instance)?;
283        Ok(Self {
284            oid,
285            name: name.into(),
286            description: String::new(),
287            present_value: 0, // off
288            blink_warn_enable: false,
289            egress_time: 0,
290            egress_active: false,
291            out_of_service: false,
292            status_flags: StatusFlags::empty(),
293            reliability: 0,
294            priority_array: [None; 16],
295            relinquish_default: 0,
296        })
297    }
298
299    /// Set the description string.
300    pub fn set_description(&mut self, desc: impl Into<String>) {
301        self.description = desc.into();
302    }
303
304    /// Recalculate present-value from the priority array.
305    fn recalculate_present_value(&mut self) {
306        self.present_value =
307            common::recalculate_from_priority_array(&self.priority_array, self.relinquish_default);
308    }
309}
310
311impl BACnetObject for BinaryLightingOutputObject {
312    fn object_identifier(&self) -> ObjectIdentifier {
313        self.oid
314    }
315
316    fn object_name(&self) -> &str {
317        &self.name
318    }
319
320    fn read_property(
321        &self,
322        property: PropertyIdentifier,
323        array_index: Option<u32>,
324    ) -> Result<PropertyValue, Error> {
325        if let Some(result) = read_common_properties!(self, property, array_index) {
326            return result;
327        }
328        match property {
329            p if p == PropertyIdentifier::OBJECT_TYPE => Ok(PropertyValue::Enumerated(
330                ObjectType::BINARY_LIGHTING_OUTPUT.to_raw(),
331            )),
332            p if p == PropertyIdentifier::PRESENT_VALUE => {
333                Ok(PropertyValue::Enumerated(self.present_value))
334            }
335            p if p == PropertyIdentifier::BLINK_WARN_ENABLE => {
336                Ok(PropertyValue::Boolean(self.blink_warn_enable))
337            }
338            p if p == PropertyIdentifier::EGRESS_TIME => {
339                Ok(PropertyValue::Unsigned(self.egress_time as u64))
340            }
341            p if p == PropertyIdentifier::EGRESS_ACTIVE => {
342                Ok(PropertyValue::Boolean(self.egress_active))
343            }
344            p if p == PropertyIdentifier::PRIORITY_ARRAY => {
345                read_priority_array!(self, array_index, PropertyValue::Enumerated)
346            }
347            p if p == PropertyIdentifier::RELINQUISH_DEFAULT => {
348                Ok(PropertyValue::Enumerated(self.relinquish_default))
349            }
350            _ => Err(common::unknown_property_error()),
351        }
352    }
353
354    fn write_property(
355        &mut self,
356        property: PropertyIdentifier,
357        array_index: Option<u32>,
358        value: PropertyValue,
359        priority: Option<u8>,
360    ) -> Result<(), Error> {
361        // Direct writes to PRIORITY_ARRAY[index]
362        write_priority_array_direct!(self, property, array_index, value, |v| {
363            if let PropertyValue::Enumerated(e) = v {
364                if e > Self::MAX_PV {
365                    Err(common::value_out_of_range_error())
366                } else {
367                    Ok(e)
368                }
369            } else {
370                Err(common::invalid_data_type_error())
371            }
372        });
373
374        // PRESENT_VALUE — commandable via priority array
375        if property == PropertyIdentifier::PRESENT_VALUE {
376            return write_priority_array!(self, value, priority, |v| {
377                if let PropertyValue::Enumerated(e) = v {
378                    if e > Self::MAX_PV {
379                        Err(common::value_out_of_range_error())
380                    } else {
381                        Ok(e)
382                    }
383                } else {
384                    Err(common::invalid_data_type_error())
385                }
386            });
387        }
388
389        // BLINK_WARN_ENABLE
390        if property == PropertyIdentifier::BLINK_WARN_ENABLE {
391            if let PropertyValue::Boolean(v) = value {
392                self.blink_warn_enable = v;
393                return Ok(());
394            }
395            return Err(common::invalid_data_type_error());
396        }
397
398        // EGRESS_TIME
399        if property == PropertyIdentifier::EGRESS_TIME {
400            if let PropertyValue::Unsigned(v) = value {
401                self.egress_time = common::u64_to_u32(v)?;
402                return Ok(());
403            }
404            return Err(common::invalid_data_type_error());
405        }
406
407        if let Some(result) =
408            common::write_out_of_service(&mut self.out_of_service, property, &value)
409        {
410            return result;
411        }
412        if let Some(result) = common::write_description(&mut self.description, property, &value) {
413            return result;
414        }
415        Err(common::write_access_denied_error())
416    }
417
418    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
419        static PROPS: &[PropertyIdentifier] = &[
420            PropertyIdentifier::OBJECT_IDENTIFIER,
421            PropertyIdentifier::OBJECT_NAME,
422            PropertyIdentifier::DESCRIPTION,
423            PropertyIdentifier::OBJECT_TYPE,
424            PropertyIdentifier::PRESENT_VALUE,
425            PropertyIdentifier::BLINK_WARN_ENABLE,
426            PropertyIdentifier::EGRESS_TIME,
427            PropertyIdentifier::EGRESS_ACTIVE,
428            PropertyIdentifier::STATUS_FLAGS,
429            PropertyIdentifier::OUT_OF_SERVICE,
430            PropertyIdentifier::RELIABILITY,
431            PropertyIdentifier::PRIORITY_ARRAY,
432            PropertyIdentifier::RELINQUISH_DEFAULT,
433        ];
434        Cow::Borrowed(PROPS)
435    }
436
437    fn supports_cov(&self) -> bool {
438        true
439    }
440}
441
442// ---------------------------------------------------------------------------
443// Channel (type 53)
444// ---------------------------------------------------------------------------
445
446/// BACnet Channel object.
447///
448/// A channel aggregates multiple objects for group control. The present-value
449/// represents the current channel value, and writes propagate to members.
450pub struct ChannelObject {
451    oid: ObjectIdentifier,
452    name: String,
453    description: String,
454    /// Present value — the current channel value (Unsigned).
455    present_value: u32,
456    /// Last priority used for the most recent write (Unsigned).
457    last_priority: u32,
458    /// Write status: 0=idle, 1=inProgress, 2=successful, 3=failed.
459    write_status: u32,
460    /// Channel number (Unsigned).
461    channel_number: u32,
462    /// Count of object-property references in this channel's member list.
463    list_of_object_property_references_count: u32,
464    out_of_service: bool,
465    status_flags: StatusFlags,
466    /// Reliability: 0 = NO_FAULT_DETECTED.
467    reliability: u32,
468}
469
470impl ChannelObject {
471    /// Create a new Channel object.
472    pub fn new(instance: u32, name: impl Into<String>, channel_number: u32) -> Result<Self, Error> {
473        let oid = ObjectIdentifier::new(ObjectType::CHANNEL, instance)?;
474        Ok(Self {
475            oid,
476            name: name.into(),
477            description: String::new(),
478            present_value: 0,
479            last_priority: 16,
480            write_status: 0, // idle
481            channel_number,
482            list_of_object_property_references_count: 0,
483            out_of_service: false,
484            status_flags: StatusFlags::empty(),
485            reliability: 0,
486        })
487    }
488
489    /// Set the description string.
490    pub fn set_description(&mut self, desc: impl Into<String>) {
491        self.description = desc.into();
492    }
493}
494
495impl BACnetObject for ChannelObject {
496    fn object_identifier(&self) -> ObjectIdentifier {
497        self.oid
498    }
499
500    fn object_name(&self) -> &str {
501        &self.name
502    }
503
504    fn read_property(
505        &self,
506        property: PropertyIdentifier,
507        array_index: Option<u32>,
508    ) -> Result<PropertyValue, Error> {
509        if let Some(result) = read_common_properties!(self, property, array_index) {
510            return result;
511        }
512        match property {
513            p if p == PropertyIdentifier::OBJECT_TYPE => {
514                Ok(PropertyValue::Enumerated(ObjectType::CHANNEL.to_raw()))
515            }
516            p if p == PropertyIdentifier::PRESENT_VALUE => {
517                Ok(PropertyValue::Unsigned(self.present_value as u64))
518            }
519            p if p == PropertyIdentifier::LAST_PRIORITY => {
520                Ok(PropertyValue::Unsigned(self.last_priority as u64))
521            }
522            p if p == PropertyIdentifier::WRITE_STATUS => {
523                Ok(PropertyValue::Enumerated(self.write_status))
524            }
525            p if p == PropertyIdentifier::CHANNEL_NUMBER => {
526                Ok(PropertyValue::Unsigned(self.channel_number as u64))
527            }
528            p if p == PropertyIdentifier::LIST_OF_OBJECT_PROPERTY_REFERENCES => Ok(
529                PropertyValue::Unsigned(self.list_of_object_property_references_count as u64),
530            ),
531            _ => Err(common::unknown_property_error()),
532        }
533    }
534
535    fn write_property(
536        &mut self,
537        property: PropertyIdentifier,
538        _array_index: Option<u32>,
539        value: PropertyValue,
540        priority: Option<u8>,
541    ) -> Result<(), Error> {
542        // PRESENT_VALUE — write the channel value and update last_priority
543        if property == PropertyIdentifier::PRESENT_VALUE {
544            if let PropertyValue::Unsigned(v) = value {
545                self.present_value = common::u64_to_u32(v)?;
546                self.last_priority = priority.unwrap_or(16) as u32;
547                return Ok(());
548            }
549            return Err(common::invalid_data_type_error());
550        }
551
552        // CHANNEL_NUMBER
553        if property == PropertyIdentifier::CHANNEL_NUMBER {
554            if let PropertyValue::Unsigned(v) = value {
555                self.channel_number = common::u64_to_u32(v)?;
556                return Ok(());
557            }
558            return Err(common::invalid_data_type_error());
559        }
560
561        if let Some(result) =
562            common::write_out_of_service(&mut self.out_of_service, property, &value)
563        {
564            return result;
565        }
566        if let Some(result) = common::write_description(&mut self.description, property, &value) {
567            return result;
568        }
569        Err(common::write_access_denied_error())
570    }
571
572    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
573        static PROPS: &[PropertyIdentifier] = &[
574            PropertyIdentifier::OBJECT_IDENTIFIER,
575            PropertyIdentifier::OBJECT_NAME,
576            PropertyIdentifier::DESCRIPTION,
577            PropertyIdentifier::OBJECT_TYPE,
578            PropertyIdentifier::PRESENT_VALUE,
579            PropertyIdentifier::LAST_PRIORITY,
580            PropertyIdentifier::WRITE_STATUS,
581            PropertyIdentifier::CHANNEL_NUMBER,
582            PropertyIdentifier::LIST_OF_OBJECT_PROPERTY_REFERENCES,
583            PropertyIdentifier::STATUS_FLAGS,
584            PropertyIdentifier::OUT_OF_SERVICE,
585            PropertyIdentifier::RELIABILITY,
586        ];
587        Cow::Borrowed(PROPS)
588    }
589}
590
591// ---------------------------------------------------------------------------
592// Tests
593// ---------------------------------------------------------------------------
594
595#[cfg(test)]
596mod tests {
597    use super::*;
598
599    // --- LightingOutputObject ---
600
601    #[test]
602    fn lighting_output_create() {
603        let obj = LightingOutputObject::new(1, "LO-1").unwrap();
604        assert_eq!(obj.object_name(), "LO-1");
605        assert_eq!(
606            obj.object_identifier().object_type(),
607            ObjectType::LIGHTING_OUTPUT
608        );
609        assert_eq!(obj.object_identifier().instance_number(), 1);
610    }
611
612    #[test]
613    fn lighting_output_read_present_value() {
614        let obj = LightingOutputObject::new(1, "LO-1").unwrap();
615        let pv = obj.read_property(PropertyIdentifier::PRESENT_VALUE, None);
616        assert_eq!(pv.unwrap(), PropertyValue::Real(0.0));
617    }
618
619    #[test]
620    fn lighting_output_read_object_type() {
621        let obj = LightingOutputObject::new(1, "LO-1").unwrap();
622        let ot = obj
623            .read_property(PropertyIdentifier::OBJECT_TYPE, None)
624            .unwrap();
625        assert_eq!(
626            ot,
627            PropertyValue::Enumerated(ObjectType::LIGHTING_OUTPUT.to_raw())
628        );
629    }
630
631    #[test]
632    fn lighting_output_write_pv_commandable() {
633        let mut obj = LightingOutputObject::new(1, "LO-1").unwrap();
634        // Write at priority 8
635        obj.write_property(
636            PropertyIdentifier::PRESENT_VALUE,
637            None,
638            PropertyValue::Real(75.0),
639            Some(8),
640        )
641        .unwrap();
642        let pv = obj
643            .read_property(PropertyIdentifier::PRESENT_VALUE, None)
644            .unwrap();
645        assert_eq!(pv, PropertyValue::Real(75.0));
646
647        // Write at priority 1 (higher) overrides
648        obj.write_property(
649            PropertyIdentifier::PRESENT_VALUE,
650            None,
651            PropertyValue::Real(50.0),
652            Some(1),
653        )
654        .unwrap();
655        let pv = obj
656            .read_property(PropertyIdentifier::PRESENT_VALUE, None)
657            .unwrap();
658        assert_eq!(pv, PropertyValue::Real(50.0));
659
660        // Relinquish priority 1 — falls back to priority 8 value
661        obj.write_property(
662            PropertyIdentifier::PRESENT_VALUE,
663            None,
664            PropertyValue::Null,
665            Some(1),
666        )
667        .unwrap();
668        let pv = obj
669            .read_property(PropertyIdentifier::PRESENT_VALUE, None)
670            .unwrap();
671        assert_eq!(pv, PropertyValue::Real(75.0));
672    }
673
674    #[test]
675    fn lighting_output_pv_out_of_range() {
676        let mut obj = LightingOutputObject::new(1, "LO-1").unwrap();
677        let result = obj.write_property(
678            PropertyIdentifier::PRESENT_VALUE,
679            None,
680            PropertyValue::Real(101.0),
681            Some(16),
682        );
683        assert!(result.is_err());
684
685        let result = obj.write_property(
686            PropertyIdentifier::PRESENT_VALUE,
687            None,
688            PropertyValue::Real(-1.0),
689            Some(16),
690        );
691        assert!(result.is_err());
692    }
693
694    #[test]
695    fn lighting_output_priority_array_read() {
696        let mut obj = LightingOutputObject::new(1, "LO-1").unwrap();
697        obj.write_property(
698            PropertyIdentifier::PRESENT_VALUE,
699            None,
700            PropertyValue::Real(50.0),
701            Some(8),
702        )
703        .unwrap();
704
705        // Read array size (index 0)
706        let size = obj
707            .read_property(PropertyIdentifier::PRIORITY_ARRAY, Some(0))
708            .unwrap();
709        assert_eq!(size, PropertyValue::Unsigned(16));
710
711        // Read slot 8
712        let slot = obj
713            .read_property(PropertyIdentifier::PRIORITY_ARRAY, Some(8))
714            .unwrap();
715        assert_eq!(slot, PropertyValue::Real(50.0));
716
717        // Read empty slot 1
718        let slot = obj
719            .read_property(PropertyIdentifier::PRIORITY_ARRAY, Some(1))
720            .unwrap();
721        assert_eq!(slot, PropertyValue::Null);
722    }
723
724    #[test]
725    fn lighting_output_priority_array_direct_write() {
726        let mut obj = LightingOutputObject::new(1, "LO-1").unwrap();
727        obj.write_property(
728            PropertyIdentifier::PRIORITY_ARRAY,
729            Some(5),
730            PropertyValue::Real(33.0),
731            None,
732        )
733        .unwrap();
734        let pv = obj
735            .read_property(PropertyIdentifier::PRESENT_VALUE, None)
736            .unwrap();
737        assert_eq!(pv, PropertyValue::Real(33.0));
738    }
739
740    #[test]
741    fn lighting_output_relinquish_default() {
742        let obj = LightingOutputObject::new(1, "LO-1").unwrap();
743        let rd = obj
744            .read_property(PropertyIdentifier::RELINQUISH_DEFAULT, None)
745            .unwrap();
746        assert_eq!(rd, PropertyValue::Real(0.0));
747    }
748
749    #[test]
750    fn lighting_output_lighting_properties() {
751        let mut obj = LightingOutputObject::new(1, "LO-1").unwrap();
752
753        // TRACKING_VALUE
754        let tv = obj
755            .read_property(PropertyIdentifier::TRACKING_VALUE, None)
756            .unwrap();
757        assert_eq!(tv, PropertyValue::Real(0.0));
758
759        // LIGHTING_COMMAND
760        let lc = obj
761            .read_property(PropertyIdentifier::LIGHTING_COMMAND, None)
762            .unwrap();
763        assert_eq!(lc, PropertyValue::OctetString(vec![]));
764
765        // Write LIGHTING_COMMAND
766        obj.write_property(
767            PropertyIdentifier::LIGHTING_COMMAND,
768            None,
769            PropertyValue::OctetString(vec![0x01, 0x02]),
770            None,
771        )
772        .unwrap();
773        let lc = obj
774            .read_property(PropertyIdentifier::LIGHTING_COMMAND, None)
775            .unwrap();
776        assert_eq!(lc, PropertyValue::OctetString(vec![0x01, 0x02]));
777
778        // LIGHTING_COMMAND_DEFAULT_PRIORITY
779        let lcdp = obj
780            .read_property(PropertyIdentifier::LIGHTING_COMMAND_DEFAULT_PRIORITY, None)
781            .unwrap();
782        assert_eq!(lcdp, PropertyValue::Unsigned(16));
783
784        // IN_PROGRESS
785        let ip = obj
786            .read_property(PropertyIdentifier::IN_PROGRESS, None)
787            .unwrap();
788        assert_eq!(ip, PropertyValue::Enumerated(0));
789
790        // BLINK_WARN_ENABLE
791        let bwe = obj
792            .read_property(PropertyIdentifier::BLINK_WARN_ENABLE, None)
793            .unwrap();
794        assert_eq!(bwe, PropertyValue::Boolean(false));
795
796        // EGRESS_TIME
797        let et = obj
798            .read_property(PropertyIdentifier::EGRESS_TIME, None)
799            .unwrap();
800        assert_eq!(et, PropertyValue::Unsigned(0));
801
802        // EGRESS_ACTIVE
803        let ea = obj
804            .read_property(PropertyIdentifier::EGRESS_ACTIVE, None)
805            .unwrap();
806        assert_eq!(ea, PropertyValue::Boolean(false));
807    }
808
809    #[test]
810    fn lighting_output_property_list() {
811        let obj = LightingOutputObject::new(1, "LO-1").unwrap();
812        let props = obj.property_list();
813        assert!(props.contains(&PropertyIdentifier::PRESENT_VALUE));
814        assert!(props.contains(&PropertyIdentifier::TRACKING_VALUE));
815        assert!(props.contains(&PropertyIdentifier::LIGHTING_COMMAND));
816        assert!(props.contains(&PropertyIdentifier::PRIORITY_ARRAY));
817        assert!(props.contains(&PropertyIdentifier::RELINQUISH_DEFAULT));
818    }
819
820    // --- BinaryLightingOutputObject ---
821
822    #[test]
823    fn binary_lighting_output_create() {
824        let obj = BinaryLightingOutputObject::new(1, "BLO-1").unwrap();
825        assert_eq!(obj.object_name(), "BLO-1");
826        assert_eq!(
827            obj.object_identifier().object_type(),
828            ObjectType::BINARY_LIGHTING_OUTPUT
829        );
830        assert_eq!(obj.object_identifier().instance_number(), 1);
831    }
832
833    #[test]
834    fn binary_lighting_output_read_present_value() {
835        let obj = BinaryLightingOutputObject::new(1, "BLO-1").unwrap();
836        let pv = obj.read_property(PropertyIdentifier::PRESENT_VALUE, None);
837        assert_eq!(pv.unwrap(), PropertyValue::Enumerated(0)); // off
838    }
839
840    #[test]
841    fn binary_lighting_output_read_object_type() {
842        let obj = BinaryLightingOutputObject::new(1, "BLO-1").unwrap();
843        let ot = obj
844            .read_property(PropertyIdentifier::OBJECT_TYPE, None)
845            .unwrap();
846        assert_eq!(
847            ot,
848            PropertyValue::Enumerated(ObjectType::BINARY_LIGHTING_OUTPUT.to_raw())
849        );
850    }
851
852    #[test]
853    fn binary_lighting_output_write_pv_commandable() {
854        let mut obj = BinaryLightingOutputObject::new(1, "BLO-1").unwrap();
855        // Write on (1) at priority 8
856        obj.write_property(
857            PropertyIdentifier::PRESENT_VALUE,
858            None,
859            PropertyValue::Enumerated(1),
860            Some(8),
861        )
862        .unwrap();
863        let pv = obj
864            .read_property(PropertyIdentifier::PRESENT_VALUE, None)
865            .unwrap();
866        assert_eq!(pv, PropertyValue::Enumerated(1));
867
868        // Write warn (2) at priority 1 overrides
869        obj.write_property(
870            PropertyIdentifier::PRESENT_VALUE,
871            None,
872            PropertyValue::Enumerated(2),
873            Some(1),
874        )
875        .unwrap();
876        let pv = obj
877            .read_property(PropertyIdentifier::PRESENT_VALUE, None)
878            .unwrap();
879        assert_eq!(pv, PropertyValue::Enumerated(2));
880
881        // Relinquish priority 1 — falls back to priority 8 (on)
882        obj.write_property(
883            PropertyIdentifier::PRESENT_VALUE,
884            None,
885            PropertyValue::Null,
886            Some(1),
887        )
888        .unwrap();
889        let pv = obj
890            .read_property(PropertyIdentifier::PRESENT_VALUE, None)
891            .unwrap();
892        assert_eq!(pv, PropertyValue::Enumerated(1));
893    }
894
895    #[test]
896    fn binary_lighting_output_pv_out_of_range() {
897        let mut obj = BinaryLightingOutputObject::new(1, "BLO-1").unwrap();
898        let result = obj.write_property(
899            PropertyIdentifier::PRESENT_VALUE,
900            None,
901            PropertyValue::Enumerated(5), // > MAX_PV
902            Some(16),
903        );
904        assert!(result.is_err());
905    }
906
907    #[test]
908    fn binary_lighting_output_all_valid_pv_values() {
909        let mut obj = BinaryLightingOutputObject::new(1, "BLO-1").unwrap();
910        for val in 0..=4 {
911            obj.write_property(
912                PropertyIdentifier::PRESENT_VALUE,
913                None,
914                PropertyValue::Enumerated(val),
915                Some(16),
916            )
917            .unwrap();
918            let pv = obj
919                .read_property(PropertyIdentifier::PRESENT_VALUE, None)
920                .unwrap();
921            assert_eq!(pv, PropertyValue::Enumerated(val));
922        }
923    }
924
925    #[test]
926    fn binary_lighting_output_priority_array() {
927        let mut obj = BinaryLightingOutputObject::new(1, "BLO-1").unwrap();
928        obj.write_property(
929            PropertyIdentifier::PRESENT_VALUE,
930            None,
931            PropertyValue::Enumerated(1),
932            Some(5),
933        )
934        .unwrap();
935
936        // Read array size
937        let size = obj
938            .read_property(PropertyIdentifier::PRIORITY_ARRAY, Some(0))
939            .unwrap();
940        assert_eq!(size, PropertyValue::Unsigned(16));
941
942        // Read slot 5
943        let slot = obj
944            .read_property(PropertyIdentifier::PRIORITY_ARRAY, Some(5))
945            .unwrap();
946        assert_eq!(slot, PropertyValue::Enumerated(1));
947
948        // Read empty slot 1
949        let slot = obj
950            .read_property(PropertyIdentifier::PRIORITY_ARRAY, Some(1))
951            .unwrap();
952        assert_eq!(slot, PropertyValue::Null);
953    }
954
955    #[test]
956    fn binary_lighting_output_priority_array_direct_write() {
957        let mut obj = BinaryLightingOutputObject::new(1, "BLO-1").unwrap();
958        obj.write_property(
959            PropertyIdentifier::PRIORITY_ARRAY,
960            Some(3),
961            PropertyValue::Enumerated(4), // fade-on
962            None,
963        )
964        .unwrap();
965        let pv = obj
966            .read_property(PropertyIdentifier::PRESENT_VALUE, None)
967            .unwrap();
968        assert_eq!(pv, PropertyValue::Enumerated(4));
969    }
970
971    #[test]
972    fn binary_lighting_output_property_list() {
973        let obj = BinaryLightingOutputObject::new(1, "BLO-1").unwrap();
974        let props = obj.property_list();
975        assert!(props.contains(&PropertyIdentifier::PRESENT_VALUE));
976        assert!(props.contains(&PropertyIdentifier::BLINK_WARN_ENABLE));
977        assert!(props.contains(&PropertyIdentifier::EGRESS_TIME));
978        assert!(props.contains(&PropertyIdentifier::PRIORITY_ARRAY));
979        assert!(props.contains(&PropertyIdentifier::RELINQUISH_DEFAULT));
980    }
981
982    // --- ChannelObject ---
983
984    #[test]
985    fn channel_create() {
986        let obj = ChannelObject::new(1, "CH-1", 5).unwrap();
987        assert_eq!(obj.object_name(), "CH-1");
988        assert_eq!(obj.object_identifier().object_type(), ObjectType::CHANNEL);
989        assert_eq!(obj.object_identifier().instance_number(), 1);
990    }
991
992    #[test]
993    fn channel_read_present_value() {
994        let obj = ChannelObject::new(1, "CH-1", 5).unwrap();
995        let pv = obj.read_property(PropertyIdentifier::PRESENT_VALUE, None);
996        assert_eq!(pv.unwrap(), PropertyValue::Unsigned(0));
997    }
998
999    #[test]
1000    fn channel_read_object_type() {
1001        let obj = ChannelObject::new(1, "CH-1", 5).unwrap();
1002        let ot = obj
1003            .read_property(PropertyIdentifier::OBJECT_TYPE, None)
1004            .unwrap();
1005        assert_eq!(ot, PropertyValue::Enumerated(ObjectType::CHANNEL.to_raw()));
1006    }
1007
1008    #[test]
1009    fn channel_write_present_value() {
1010        let mut obj = ChannelObject::new(1, "CH-1", 5).unwrap();
1011        obj.write_property(
1012            PropertyIdentifier::PRESENT_VALUE,
1013            None,
1014            PropertyValue::Unsigned(42),
1015            Some(8),
1016        )
1017        .unwrap();
1018        let pv = obj
1019            .read_property(PropertyIdentifier::PRESENT_VALUE, None)
1020            .unwrap();
1021        assert_eq!(pv, PropertyValue::Unsigned(42));
1022
1023        // Verify last_priority was updated
1024        let lp = obj
1025            .read_property(PropertyIdentifier::LAST_PRIORITY, None)
1026            .unwrap();
1027        assert_eq!(lp, PropertyValue::Unsigned(8));
1028    }
1029
1030    #[test]
1031    fn channel_read_channel_number() {
1032        let obj = ChannelObject::new(1, "CH-1", 5).unwrap();
1033        let cn = obj
1034            .read_property(PropertyIdentifier::CHANNEL_NUMBER, None)
1035            .unwrap();
1036        assert_eq!(cn, PropertyValue::Unsigned(5));
1037    }
1038
1039    #[test]
1040    fn channel_write_channel_number() {
1041        let mut obj = ChannelObject::new(1, "CH-1", 5).unwrap();
1042        obj.write_property(
1043            PropertyIdentifier::CHANNEL_NUMBER,
1044            None,
1045            PropertyValue::Unsigned(10),
1046            None,
1047        )
1048        .unwrap();
1049        let cn = obj
1050            .read_property(PropertyIdentifier::CHANNEL_NUMBER, None)
1051            .unwrap();
1052        assert_eq!(cn, PropertyValue::Unsigned(10));
1053    }
1054
1055    #[test]
1056    fn channel_read_write_status() {
1057        let obj = ChannelObject::new(1, "CH-1", 5).unwrap();
1058        let ws = obj
1059            .read_property(PropertyIdentifier::WRITE_STATUS, None)
1060            .unwrap();
1061        assert_eq!(ws, PropertyValue::Enumerated(0)); // idle
1062    }
1063
1064    #[test]
1065    fn channel_read_last_priority_default() {
1066        let obj = ChannelObject::new(1, "CH-1", 5).unwrap();
1067        let lp = obj
1068            .read_property(PropertyIdentifier::LAST_PRIORITY, None)
1069            .unwrap();
1070        assert_eq!(lp, PropertyValue::Unsigned(16)); // default priority
1071    }
1072
1073    #[test]
1074    fn channel_property_list() {
1075        let obj = ChannelObject::new(1, "CH-1", 5).unwrap();
1076        let props = obj.property_list();
1077        assert!(props.contains(&PropertyIdentifier::PRESENT_VALUE));
1078        assert!(props.contains(&PropertyIdentifier::LAST_PRIORITY));
1079        assert!(props.contains(&PropertyIdentifier::WRITE_STATUS));
1080        assert!(props.contains(&PropertyIdentifier::CHANNEL_NUMBER));
1081        assert!(props.contains(&PropertyIdentifier::LIST_OF_OBJECT_PROPERTY_REFERENCES));
1082    }
1083
1084    #[test]
1085    fn channel_write_pv_default_priority() {
1086        let mut obj = ChannelObject::new(1, "CH-1", 5).unwrap();
1087        // Write without explicit priority — defaults to 16
1088        obj.write_property(
1089            PropertyIdentifier::PRESENT_VALUE,
1090            None,
1091            PropertyValue::Unsigned(99),
1092            None,
1093        )
1094        .unwrap();
1095        let lp = obj
1096            .read_property(PropertyIdentifier::LAST_PRIORITY, None)
1097            .unwrap();
1098        assert_eq!(lp, PropertyValue::Unsigned(16));
1099    }
1100}