Skip to main content

bacnet_objects/lighting/
mod.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;