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::constructed::BACnetLightingCommand;
5use bacnet_types::enums::{LightingOperation, ObjectType, PropertyIdentifier, Reliability};
6use bacnet_types::error::Error;
7use bacnet_types::primitives::{ObjectIdentifier, PropertyValue, StatusFlags};
8use std::borrow::Cow;
9use std::sync::Arc;
10use std::time::Duration;
11
12use crate::common::{self, read_common_properties, read_priority_array};
13use crate::traits::{BACnetObject, DeadlineWaker, MonotonicClock};
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). A commanded level
23/// above 0.0 and below 1.0 is stored as 1.0, and so is such a
24/// Relinquish_Default (see [`set_relinquish_default`](Self::set_relinquish_default)).
25///
26/// Lighting_Command takes a [`BACnetLightingCommand`] checked against its
27/// operation (see [`set_lighting_command`](Self::set_lighting_command)),
28/// serves the last one taken and carries it out (#1384): fades and ramps move
29/// Tracking_Value to a new level over time with In_Progress showing which,
30/// steps change the level at once, and the warn operations blink and hold
31/// the level for Egress_Time before relinquishing or turning it off.
32/// Present_Value takes the blink-warn values -1.0, -2.0 and -3.0 as the
33/// three warn commands. Tracking_Value equals Present_Value whenever no fade
34/// or ramp is moving it.
35///
36/// Fades, ramps and egress timers run on the monotonic clock the database
37/// binds; the server's monotonic task advances them and fans their COV out.
38/// With no clock bound they wait on `advance_time_internal`.
39#[derive(Clone)]
40pub struct LightingOutputObject {
41    oid: ObjectIdentifier,
42    name: String,
43    description: String,
44    present_value: f32,
45    /// The last command written; operation NONE until then.
46    lighting_command: BACnetLightingCommand,
47    lighting_command_default_priority: u32,
48    /// The fade, ramp or egress in progress, if any.
49    operation: Option<engine::Operation>,
50    /// Blink-warn notifications requested so far, for tests to observe.
51    blink_request_count: u64,
52    blink_warn_enable: bool,
53    /// Egress_Time in seconds.
54    egress_time: u32,
55    /// Default_Fade_Time in milliseconds, within 100..=86_400_000.
56    default_fade_time: u32,
57    /// Default_Ramp_Rate in percent per second, within 0.1..=100.0.
58    default_ramp_rate: f32,
59    /// Default_Step_Increment in percent, within 0.1..=100.0.
60    default_step_increment: f32,
61    /// COV_Increment: the Present_Value change that triggers a notification.
62    cov_increment: f32,
63    out_of_service: bool,
64    status_flags: StatusFlags,
65    /// Reliability; NO_FAULT_DETECTED until a fault is evaluated or simulated.
66    reliability: Reliability,
67    priority_array: [Option<f32>; 16],
68    relinquish_default: f32,
69    monotonic_clock: Option<Arc<MonotonicClock>>,
70    deadline_waker: Option<Arc<DeadlineWaker>>,
71    /// The time an object with no clock bound has been advanced to.
72    logical_now: Duration,
73}
74
75impl LightingOutputObject {
76    /// Create a new Lighting Output object.
77    pub fn new(instance: u32, name: impl Into<String>) -> Result<Self, Error> {
78        let oid = ObjectIdentifier::new(ObjectType::LIGHTING_OUTPUT, instance)?;
79        Ok(Self {
80            oid,
81            name: name.into(),
82            description: String::new(),
83            present_value: 0.0,
84            lighting_command: BACnetLightingCommand::new(LightingOperation::NONE),
85            lighting_command_default_priority: 16,
86            operation: None,
87            blink_request_count: 0,
88            blink_warn_enable: false,
89            egress_time: 0,
90            default_fade_time: *DEFAULT_FADE_TIME_MS.start(),
91            default_ramp_rate: 100.0,
92            default_step_increment: 1.0,
93            cov_increment: 0.0,
94            out_of_service: false,
95            status_flags: StatusFlags::empty(),
96            reliability: Reliability::NO_FAULT_DETECTED,
97            priority_array: [None; 16],
98            relinquish_default: 0.0,
99            monotonic_clock: None,
100            deadline_waker: None,
101            logical_now: Duration::ZERO,
102        })
103    }
104
105    /// Set the description string.
106    pub fn set_description(&mut self, desc: impl Into<String>) {
107        self.description = desc.into();
108    }
109
110    /// Recalculate present-value from the priority array.
111    ///
112    /// Tracking_Value isn't stored: it reads as Present_Value while
113    /// In_Progress is IDLE and as the fade or ramp's current value while one
114    /// runs (Clause 12.54.5), so a new Present_Value shows in it only once
115    /// nothing is moving it.
116    fn recalculate_present_value(&mut self) {
117        self.present_value =
118            common::recalculate_from_priority_array(&self.priority_array, self.relinquish_default);
119    }
120
121    /// Set the Relinquish_Default (#270).
122    ///
123    /// Checked the same way a commanded Present_Value is: a value outside
124    /// 0.0 to 100.0, or NaN, is refused with VALUE_OUT_OF_RANGE, and one
125    /// above 0.0 and below 1.0 is stored as 1.0, so Present_Value never falls
126    /// back to a level between off and the dimmest on level. After the store,
127    /// Present_Value is resolved anew from the priority array so an empty
128    /// array falls back to the new default immediately.
129    pub fn set_relinquish_default(&mut self, value: f32) -> Result<(), Error> {
130        self.relinquish_default = normalized_level(value)?;
131        self.recalculate_present_value();
132        Ok(())
133    }
134
135    /// The last command written to Lighting_Command, or operation NONE with no
136    /// other field before any write.
137    pub fn lighting_command(&self) -> BACnetLightingCommand {
138        self.lighting_command
139    }
140
141    /// Set Lighting_Command and carry the command out, as a WriteProperty of
142    /// the encoded command would.
143    ///
144    /// The command is checked against its operation (Clause 12.54, Table
145    /// 12-67). NONE, a reserved operation (11 to 255) and anything past
146    /// 65,535 are refused; FADE_TO and RAMP_TO need a target level. A field
147    /// the operation uses must be in range: target level 0.0 to 100.0, fade
148    /// time 100 to 86,400,000 ms, ramp rate and step increment 0.1 to 100.0,
149    /// priority 1 to 16. Fields it doesn't use are kept unchecked, and a
150    /// proprietary operation (256 to 65,535) has only its priority checked. A
151    /// refusal is VALUE_OUT_OF_RANGE and leaves the object unchanged.
152    ///
153    /// A command taken is stored as written and carried out at its priority,
154    /// or at Lighting_Command_Default_Priority when it has none. A level it
155    /// puts in a priority slot is normalized as a commanded Present_Value is
156    /// (Clause 12.54.4), so a FADE_TO 0.5 fades to 1.0 while Lighting_Command
157    /// still reads 0.5. A proprietary operation is stored and does nothing
158    /// else.
159    pub fn set_lighting_command(&mut self, command: BACnetLightingCommand) -> Result<(), Error> {
160        command::check(&command)?;
161        self.take_lighting_command(command);
162        Ok(())
163    }
164
165    /// Store a checked command and carry it out now.
166    fn take_lighting_command(&mut self, command: BACnetLightingCommand) {
167        self.lighting_command = command;
168        let now = self.now();
169        self.execute(&command, now);
170    }
171
172    /// Set Default_Fade_Time, the milliseconds a fade request without its own
173    /// fade time takes. A new object uses 100, the shortest fade the clause
174    /// allows, as Default_Ramp_Rate starts at its fastest rate.
175    ///
176    /// Clause 12.54.16 bounds it to 100..=86_400_000 (one day); a value
177    /// outside that range is refused with VALUE_OUT_OF_RANGE and the property
178    /// is left unchanged. WriteProperty applies the same check.
179    pub fn set_default_fade_time(&mut self, milliseconds: u32) -> Result<(), Error> {
180        if !DEFAULT_FADE_TIME_MS.contains(&milliseconds) {
181            return Err(common::value_out_of_range_error());
182        }
183        self.default_fade_time = milliseconds;
184        Ok(())
185    }
186
187    /// Set Default_Ramp_Rate, the percent-per-second rate a ramp request
188    /// without its own rate uses. A new object uses 100.0.
189    ///
190    /// Clause 12.54.17 bounds it to 0.1..=100.0; a value outside that range,
191    /// or a non-finite one, is refused with VALUE_OUT_OF_RANGE and the
192    /// property is left unchanged. WriteProperty applies the same check.
193    pub fn set_default_ramp_rate(&mut self, value: f32) -> Result<(), Error> {
194        self.default_ramp_rate = lighting_percent(value)?;
195        Ok(())
196    }
197
198    /// Set Default_Step_Increment, the percent a step request without its own
199    /// increment adds. A new object uses 1.0.
200    ///
201    /// Clause 12.54.18 bounds it to 0.1..=100.0; a value outside that range,
202    /// or a non-finite one, is refused with VALUE_OUT_OF_RANGE and the
203    /// property is left unchanged. WriteProperty applies the same check.
204    pub fn set_default_step_increment(&mut self, value: f32) -> Result<(), Error> {
205        self.default_step_increment = lighting_percent(value)?;
206        Ok(())
207    }
208}
209
210/// Check a light level written to Present_Value or Relinquish_Default.
211///
212/// Both are percentages on the Clause 12.54 normalized scale: 0.0 is off,
213/// 1.0 is the dimmest on level and 100.0 the brightest, and no level lies
214/// strictly between 0.0 and 1.0. Clause 12.54.4 has a Present_Value write in
215/// that gap taken as 1.0, so such a level comes back as 1.0. 0.0 and levels
216/// from 1.0 to 100.0 come back unchanged. A level below 0.0 or above 100.0,
217/// NaN included, is VALUE_OUT_OF_RANGE; a Present_Value write tells the
218/// blink-warn values -1.0 to -3.0 apart before it gets here. -0.0 is off, so
219/// it comes back as 0.0 rather than keeping its sign on the wire.
220fn normalized_level(value: f32) -> Result<f32, Error> {
221    if !(0.0..=100.0).contains(&value) {
222        return Err(common::value_out_of_range_error());
223    }
224    Ok(if value == 0.0 {
225        0.0
226    } else if value < 1.0 {
227        1.0
228    } else {
229        value
230    })
231}
232
233/// The Default_Fade_Time range of Clause 12.54.16, in milliseconds, which a
234/// lighting command's fade time shares (Table 12-66).
235const DEFAULT_FADE_TIME_MS: std::ops::RangeInclusive<u32> = 100..=86_400_000;
236
237/// Check a Default_Ramp_Rate or Default_Step_Increment value, or a lighting
238/// command's ramp rate or step increment, which share the 0.1..=100.0 range.
239fn lighting_percent(value: f32) -> Result<f32, Error> {
240    if (0.1..=100.0).contains(&value) {
241        Ok(value)
242    } else {
243        Err(common::value_out_of_range_error())
244    }
245}
246
247impl BACnetObject for LightingOutputObject {
248    fn object_identifier(&self) -> ObjectIdentifier {
249        self.oid
250    }
251
252    fn object_name(&self) -> &str {
253        &self.name
254    }
255
256    fn read_property(
257        &self,
258        property: PropertyIdentifier,
259        array_index: Option<u32>,
260    ) -> Result<PropertyValue, Error> {
261        if let Some(result) = read_common_properties!(self, property, array_index) {
262            return result;
263        }
264        match property {
265            p if p == PropertyIdentifier::OBJECT_TYPE => Ok(PropertyValue::Enumerated(
266                ObjectType::LIGHTING_OUTPUT.to_raw(),
267            )),
268            p if p == PropertyIdentifier::PRESENT_VALUE => {
269                Ok(PropertyValue::Real(self.present_value))
270            }
271            // Each read takes its own instant, so one ReadPropertyMultiple
272            // that straddles a fade's end may pair a Tracking_Value just short
273            // of the level with In_Progress IDLE.
274            p if p == PropertyIdentifier::TRACKING_VALUE => {
275                Ok(PropertyValue::Real(self.tracking_value_at(self.now())))
276            }
277            p if p == PropertyIdentifier::LIGHTING_COMMAND => {
278                Ok(command::encode(&self.lighting_command))
279            }
280            p if p == PropertyIdentifier::LIGHTING_COMMAND_DEFAULT_PRIORITY => Ok(
281                PropertyValue::Unsigned(self.lighting_command_default_priority as u64),
282            ),
283            p if p == PropertyIdentifier::IN_PROGRESS => Ok(PropertyValue::Enumerated(
284                self.in_progress_at(self.now()).to_raw(),
285            )),
286            p if p == PropertyIdentifier::BLINK_WARN_ENABLE => {
287                Ok(PropertyValue::Boolean(self.blink_warn_enable))
288            }
289            p if p == PropertyIdentifier::EGRESS_TIME => {
290                Ok(PropertyValue::Unsigned(self.egress_time as u64))
291            }
292            p if p == PropertyIdentifier::EGRESS_ACTIVE => {
293                Ok(PropertyValue::Boolean(self.egress_active()))
294            }
295            p if p == PropertyIdentifier::PRIORITY_ARRAY => {
296                read_priority_array!(self, array_index, PropertyValue::Real)
297            }
298            p if p == PropertyIdentifier::RELINQUISH_DEFAULT => {
299                Ok(PropertyValue::Real(self.relinquish_default))
300            }
301            p if p == PropertyIdentifier::DEFAULT_FADE_TIME => {
302                Ok(PropertyValue::Unsigned(u64::from(self.default_fade_time)))
303            }
304            p if p == PropertyIdentifier::DEFAULT_RAMP_RATE => {
305                Ok(PropertyValue::Real(self.default_ramp_rate))
306            }
307            p if p == PropertyIdentifier::DEFAULT_STEP_INCREMENT => {
308                Ok(PropertyValue::Real(self.default_step_increment))
309            }
310            p if p == PropertyIdentifier::COV_INCREMENT => {
311                Ok(PropertyValue::Real(self.cov_increment))
312            }
313            p if p == PropertyIdentifier::CURRENT_COMMAND_PRIORITY => {
314                Ok(common::current_command_priority(&self.priority_array))
315            }
316            _ => Err(common::unknown_property_error()),
317        }
318    }
319
320    fn write_property(
321        &mut self,
322        property: PropertyIdentifier,
323        _array_index: Option<u32>,
324        value: PropertyValue,
325        priority: Option<u8>,
326    ) -> Result<(), Error> {
327        // A level is normalized before it reaches the slot, so the slot,
328        // Present_Value, Tracking_Value and a COV report all see 1.0 for a
329        // write between 0.0 and 1.0. -1.0, -2.0 and -3.0 are the warn
330        // commands (Table 12-65) and never reach a slot themselves.
331        if property == PropertyIdentifier::PRESENT_VALUE {
332            let priority = priority.unwrap_or(16);
333            if !(1..=16).contains(&priority) {
334                return Err(common::value_out_of_range_error());
335            }
336            let write = engine::PresentValueWrite::decode(value)?;
337            let now = self.now();
338            self.write_present_value(priority, write, now);
339            return Ok(());
340        }
341
342        // LIGHTING_COMMAND: a BACnetLightingCommand, checked against its
343        // operation, then carried out.
344        if property == PropertyIdentifier::LIGHTING_COMMAND {
345            let command = command::decode_write(value)?;
346            self.take_lighting_command(command);
347            return Ok(());
348        }
349
350        // LIGHTING_COMMAND_DEFAULT_PRIORITY: 1 to 16, but not 6. Clause
351        // 12.54.27 keeps 6 out: Table 19-1 gives that slot to Minimum On/Off,
352        // and a command written without a priority acts at this one.
353        if property == PropertyIdentifier::LIGHTING_COMMAND_DEFAULT_PRIORITY {
354            if let PropertyValue::Unsigned(v) = value {
355                if !(1..=16).contains(&v) || v == 6 {
356                    return Err(common::value_out_of_range_error());
357                }
358                self.lighting_command_default_priority = v as u32;
359                return Ok(());
360            }
361            return Err(common::invalid_data_type_error());
362        }
363
364        // RELINQUISH_DEFAULT — writable per Table 12-64 (R; the standard
365        // permits writability), validated the same way a commanded
366        // Present_Value is by the shared setter.
367        if property == PropertyIdentifier::RELINQUISH_DEFAULT {
368            if let PropertyValue::Real(f) = value {
369                return self.set_relinquish_default(f);
370            }
371            return Err(common::invalid_data_type_error());
372        }
373
374        // BLINK_WARN_ENABLE
375        if property == PropertyIdentifier::BLINK_WARN_ENABLE {
376            if let PropertyValue::Boolean(v) = value {
377                self.blink_warn_enable = v;
378                return Ok(());
379            }
380            return Err(common::invalid_data_type_error());
381        }
382
383        // EGRESS_TIME
384        if property == PropertyIdentifier::EGRESS_TIME {
385            if let PropertyValue::Unsigned(v) = value {
386                self.egress_time = common::u64_to_u32(v)?;
387                return Ok(());
388            }
389            return Err(common::invalid_data_type_error());
390        }
391
392        // DEFAULT_FADE_TIME, DEFAULT_RAMP_RATE and DEFAULT_STEP_INCREMENT go
393        // through the range-checked setters. A fade time too large for the
394        // setter's u32 is past the range too.
395        if property == PropertyIdentifier::DEFAULT_FADE_TIME {
396            if let PropertyValue::Unsigned(v) = value {
397                return self.set_default_fade_time(common::u64_to_u32(v)?);
398            }
399            return Err(common::invalid_data_type_error());
400        }
401        if property == PropertyIdentifier::DEFAULT_RAMP_RATE {
402            if let PropertyValue::Real(v) = value {
403                return self.set_default_ramp_rate(v);
404            }
405            return Err(common::invalid_data_type_error());
406        }
407        if property == PropertyIdentifier::DEFAULT_STEP_INCREMENT {
408            if let PropertyValue::Real(v) = value {
409                return self.set_default_step_increment(v);
410            }
411            return Err(common::invalid_data_type_error());
412        }
413
414        if let Some(result) = common::write_cov_increment(&mut self.cov_increment, property, &value)
415        {
416            return result;
417        }
418        if let Some(result) =
419            common::write_out_of_service(&mut self.out_of_service, property, &value)
420        {
421            return result;
422        }
423        if let Some(result) = common::write_description(&mut self.description, property, &value) {
424            return result;
425        }
426        Err(crate::common::unhandled_write_error(
427            self.property_metadata().as_ref(),
428            property,
429            _array_index,
430        ))
431    }
432
433    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
434        crate::property_metadata::property_list_from_metadata(self.property_metadata().as_ref())
435    }
436
437    fn property_metadata(&self) -> Cow<'_, [crate::property_metadata::PropertyMetadata]> {
438        metadata::for_lighting_output_object(self)
439    }
440
441    fn cov_increment(&self) -> Option<f64> {
442        Some(f64::from(self.cov_increment))
443    }
444
445    fn supports_cov(&self) -> bool {
446        true
447    }
448
449    fn advance_time_internal(&mut self, elapsed: Duration) -> bool {
450        self.logical_now = self.logical_now.saturating_add(elapsed);
451        self.advance_to(self.logical_now)
452    }
453
454    fn bind_monotonic_clock_internal(&mut self, clock: Option<Arc<MonotonicClock>>) {
455        self.monotonic_clock = clock;
456    }
457
458    fn bind_deadline_waker_internal(&mut self, waker: Option<Arc<DeadlineWaker>>) {
459        self.deadline_waker = waker;
460    }
461
462    fn advance_monotonic_time_internal(&mut self, now: Duration) -> bool {
463        self.advance_to(now)
464    }
465
466    fn next_monotonic_deadline_internal(&self) -> Option<Duration> {
467        self.next_deadline()
468    }
469
470    /// A copy that reads as the object does now: its clock stops at this
471    /// instant, so a fade's Tracking_Value in a COV report is the value at
472    /// the moment the report was taken.
473    fn cov_snapshot_internal(&self) -> Option<Box<dyn BACnetObject>> {
474        let mut snapshot = self.clone();
475        snapshot.logical_now = self.now();
476        snapshot.monotonic_clock = None;
477        snapshot.deadline_waker = None;
478        Some(Box::new(snapshot))
479    }
480
481    fn lighting_blink_count_internal(&self) -> u64 {
482        self.blink_request_count
483    }
484}
485
486mod binary;
487mod command;
488mod engine;
489mod metadata;
490pub use binary::BinaryLightingOutputObject;
491
492// ---------------------------------------------------------------------------
493// Tests
494// ---------------------------------------------------------------------------
495
496#[cfg(test)]
497mod tests;
498
499#[cfg(test)]
500mod required_rows_tests;
501
502#[cfg(test)]
503mod command_tests;
504
505#[cfg(test)]
506mod present_value_tests;
507
508#[cfg(test)]
509mod engine_tests;
510
511#[cfg(test)]
512mod warn_tests;