Skip to main content

bacnet_objects/lighting/
mod.rs

1//! Lighting Output (type 54) and Binary Lighting Output (type 55) objects per
2//! ASHRAE 135-2020 Clauses 12.54 and 12.55.
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    /// Set the Relinquish_Default (#270).
81    ///
82    /// Validated the same way a commanded Present_Value is (finite Real
83    /// within the 0..=100 light level); after the store, Present_Value is
84    /// resolved anew from the priority array so an empty array falls back to
85    /// the new default immediately.
86    pub fn set_relinquish_default(&mut self, value: f32) -> Result<(), Error> {
87        if !value.is_finite() || !(0.0..=100.0).contains(&value) {
88            return Err(common::value_out_of_range_error());
89        }
90        self.relinquish_default = value;
91        self.recalculate_present_value();
92        Ok(())
93    }
94}
95
96impl BACnetObject for LightingOutputObject {
97    fn object_identifier(&self) -> ObjectIdentifier {
98        self.oid
99    }
100
101    fn object_name(&self) -> &str {
102        &self.name
103    }
104
105    fn read_property(
106        &self,
107        property: PropertyIdentifier,
108        array_index: Option<u32>,
109    ) -> Result<PropertyValue, Error> {
110        if let Some(result) = read_common_properties!(self, property, array_index) {
111            return result;
112        }
113        match property {
114            p if p == PropertyIdentifier::OBJECT_TYPE => Ok(PropertyValue::Enumerated(
115                ObjectType::LIGHTING_OUTPUT.to_raw(),
116            )),
117            p if p == PropertyIdentifier::PRESENT_VALUE => {
118                Ok(PropertyValue::Real(self.present_value))
119            }
120            p if p == PropertyIdentifier::TRACKING_VALUE => {
121                Ok(PropertyValue::Real(self.tracking_value))
122            }
123            p if p == PropertyIdentifier::LIGHTING_COMMAND => {
124                Ok(PropertyValue::OctetString(self.lighting_command.clone()))
125            }
126            p if p == PropertyIdentifier::LIGHTING_COMMAND_DEFAULT_PRIORITY => Ok(
127                PropertyValue::Unsigned(self.lighting_command_default_priority as u64),
128            ),
129            p if p == PropertyIdentifier::IN_PROGRESS => {
130                Ok(PropertyValue::Enumerated(self.in_progress))
131            }
132            p if p == PropertyIdentifier::BLINK_WARN_ENABLE => {
133                Ok(PropertyValue::Boolean(self.blink_warn_enable))
134            }
135            p if p == PropertyIdentifier::EGRESS_TIME => {
136                Ok(PropertyValue::Unsigned(self.egress_time as u64))
137            }
138            p if p == PropertyIdentifier::EGRESS_ACTIVE => {
139                Ok(PropertyValue::Boolean(self.egress_active))
140            }
141            p if p == PropertyIdentifier::PRIORITY_ARRAY => {
142                read_priority_array!(self, array_index, PropertyValue::Real)
143            }
144            p if p == PropertyIdentifier::RELINQUISH_DEFAULT => {
145                Ok(PropertyValue::Real(self.relinquish_default))
146            }
147            p if p == PropertyIdentifier::DEFAULT_FADE_TIME => Ok(PropertyValue::Unsigned(0)),
148            _ => Err(common::unknown_property_error()),
149        }
150    }
151
152    fn write_property(
153        &mut self,
154        property: PropertyIdentifier,
155        array_index: Option<u32>,
156        value: PropertyValue,
157        priority: Option<u8>,
158    ) -> Result<(), Error> {
159        // Direct writes to PRIORITY_ARRAY[index]
160        write_priority_array_direct!(self, property, array_index, value, |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        // PRESENT_VALUE — commandable via priority array
174        if property == PropertyIdentifier::PRESENT_VALUE {
175            return write_priority_array!(self, value, priority, |v| {
176                match v {
177                    PropertyValue::Real(f) => {
178                        if !(0.0..=100.0).contains(&f) {
179                            Err(common::value_out_of_range_error())
180                        } else {
181                            Ok(f)
182                        }
183                    }
184                    _ => Err(common::invalid_data_type_error()),
185                }
186            });
187        }
188
189        // LIGHTING_COMMAND — stored as opaque bytes
190        if property == PropertyIdentifier::LIGHTING_COMMAND {
191            if let PropertyValue::OctetString(data) = value {
192                self.lighting_command = data;
193                return Ok(());
194            }
195            return Err(common::invalid_data_type_error());
196        }
197
198        // LIGHTING_COMMAND_DEFAULT_PRIORITY
199        if property == PropertyIdentifier::LIGHTING_COMMAND_DEFAULT_PRIORITY {
200            if let PropertyValue::Unsigned(v) = value {
201                if !(1..=16).contains(&v) {
202                    return Err(common::value_out_of_range_error());
203                }
204                self.lighting_command_default_priority = v as u32;
205                return Ok(());
206            }
207            return Err(common::invalid_data_type_error());
208        }
209
210        // RELINQUISH_DEFAULT — writable per Table 12-64 (R; the standard
211        // permits writability), validated the same way a commanded
212        // Present_Value is by the shared setter.
213        if property == PropertyIdentifier::RELINQUISH_DEFAULT {
214            if let PropertyValue::Real(f) = value {
215                return self.set_relinquish_default(f);
216            }
217            return Err(common::invalid_data_type_error());
218        }
219
220        // BLINK_WARN_ENABLE
221        if property == PropertyIdentifier::BLINK_WARN_ENABLE {
222            if let PropertyValue::Boolean(v) = value {
223                self.blink_warn_enable = v;
224                return Ok(());
225            }
226            return Err(common::invalid_data_type_error());
227        }
228
229        // EGRESS_TIME
230        if property == PropertyIdentifier::EGRESS_TIME {
231            if let PropertyValue::Unsigned(v) = value {
232                self.egress_time = common::u64_to_u32(v)?;
233                return Ok(());
234            }
235            return Err(common::invalid_data_type_error());
236        }
237
238        if let Some(result) =
239            common::write_out_of_service(&mut self.out_of_service, property, &value)
240        {
241            return result;
242        }
243        if let Some(result) = common::write_description(&mut self.description, property, &value) {
244            return result;
245        }
246        Err(common::write_access_denied_error())
247    }
248
249    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
250        static PROPS: &[PropertyIdentifier] = &[
251            PropertyIdentifier::OBJECT_IDENTIFIER,
252            PropertyIdentifier::OBJECT_NAME,
253            PropertyIdentifier::DESCRIPTION,
254            PropertyIdentifier::OBJECT_TYPE,
255            PropertyIdentifier::PRESENT_VALUE,
256            PropertyIdentifier::TRACKING_VALUE,
257            PropertyIdentifier::LIGHTING_COMMAND,
258            PropertyIdentifier::LIGHTING_COMMAND_DEFAULT_PRIORITY,
259            PropertyIdentifier::IN_PROGRESS,
260            PropertyIdentifier::BLINK_WARN_ENABLE,
261            PropertyIdentifier::EGRESS_TIME,
262            PropertyIdentifier::EGRESS_ACTIVE,
263            PropertyIdentifier::STATUS_FLAGS,
264            PropertyIdentifier::OUT_OF_SERVICE,
265            PropertyIdentifier::RELIABILITY,
266            PropertyIdentifier::PRIORITY_ARRAY,
267            PropertyIdentifier::RELINQUISH_DEFAULT,
268        ];
269        Cow::Borrowed(PROPS)
270    }
271
272    fn supports_cov(&self) -> bool {
273        true
274    }
275
276    fn is_writable_property(&self, property: PropertyIdentifier) -> bool {
277        // Mirrors the LightingOutputObject `write_property` arms so the PICS
278        // and runtime dispatch share one truth source.
279        matches!(
280            property,
281            PropertyIdentifier::PRIORITY_ARRAY
282                | PropertyIdentifier::PRESENT_VALUE
283                | PropertyIdentifier::RELINQUISH_DEFAULT
284                | PropertyIdentifier::LIGHTING_COMMAND
285                | PropertyIdentifier::LIGHTING_COMMAND_DEFAULT_PRIORITY
286                | PropertyIdentifier::BLINK_WARN_ENABLE
287                | PropertyIdentifier::EGRESS_TIME
288                | PropertyIdentifier::OUT_OF_SERVICE
289                | PropertyIdentifier::DESCRIPTION
290        )
291    }
292}
293
294mod binary;
295pub use binary::BinaryLightingOutputObject;
296
297// ---------------------------------------------------------------------------
298// Tests
299// ---------------------------------------------------------------------------
300
301#[cfg(test)]
302mod tests;