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