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::object_profile::{ObjectProfile, ProfileState, TagsPersistence};
14use crate::traits::{BACnetObject, DeadlineWaker, MonotonicClock};
15
16// ---------------------------------------------------------------------------
17// LightingOutput (type 54)
18// ---------------------------------------------------------------------------
19
20/// BACnet Lighting Output object.
21///
22/// Commandable output with a 16-level priority array controlling a
23/// floating-point present-value (0.0 to 100.0 percent). A commanded level
24/// above 0.0 and below 1.0 is stored as 1.0, and so is such a
25/// Relinquish_Default (see [`set_relinquish_default`](Self::set_relinquish_default)).
26///
27/// Lighting_Command takes a [`BACnetLightingCommand`] checked against its
28/// operation (see [`set_lighting_command`](Self::set_lighting_command)),
29/// serves the last one taken and carries it out (#1384): fades and ramps move
30/// Tracking_Value to a new level over time with In_Progress showing which,
31/// steps change the level at once, and the warn operations blink and hold
32/// the level for Egress_Time before relinquishing or turning it off.
33/// Present_Value takes the blink-warn values -1.0, -2.0 and -3.0 as the
34/// three warn commands. Tracking_Value equals Present_Value whenever no fade
35/// or ramp is moving it and no trim holds it.
36///
37/// Color_Reference, Color_Override and Override_Color_Reference are absent
38/// until [`set_color_link`](Self::set_color_link) links the output to its
39/// colour objects (#1527).
40///
41/// High_End_Trim, Low_End_Trim and Trim_Fade_Time are absent until set
42/// (#1528; see [`set_high_end_trim`](Self::set_high_end_trim)). Once set,
43/// Tracking_Value is held between the trims, In_Progress reads TRIM_ACTIVE
44/// while that keeps it from Present_Value, and a trim change takes
45/// Trim_Fade_Time to show.
46///
47/// Fades, ramps and egress timers run on the monotonic clock the database
48/// binds; the server's monotonic task advances them and fans their COV out.
49/// With no clock bound they wait on `advance_time_internal`.
50///
51/// Cloning copies served state into an in-memory object. Tags persistence and
52/// pending saves remain solely with the original; cloning or dropping the copy
53/// never saves, corrects, settles, or waits on the original writer.
54#[derive(Clone)]
55pub struct LightingOutputObject {
56    oid: ObjectIdentifier,
57    name: String,
58    description: String,
59    present_value: f32,
60    /// The last command written; operation NONE until then.
61    lighting_command: BACnetLightingCommand,
62    lighting_command_default_priority: u32,
63    /// The fade, ramp or egress in progress, if any.
64    operation: Option<engine::Operation>,
65    /// Blink-warn notifications requested so far, for tests to observe.
66    blink_request_count: u64,
67    blink_warn_enable: bool,
68    /// Egress_Time in seconds.
69    egress_time: u32,
70    /// Default_Fade_Time in milliseconds, within 100..=86_400_000.
71    default_fade_time: u32,
72    /// Default_Ramp_Rate in percent per second, within 0.1..=100.0.
73    default_ramp_rate: f32,
74    /// Default_Step_Increment in percent, within 0.1..=100.0.
75    default_step_increment: f32,
76    /// COV_Increment: the Present_Value change that triggers a notification.
77    cov_increment: f32,
78    /// The finest COV increment a Tracking_Value subscriber asks for, as the
79    /// server last passed it (#1510).
80    finest_tracking_increment: Option<f64>,
81    out_of_service: bool,
82    status_flags: StatusFlags,
83    /// Reliability; NO_FAULT_DETECTED until a fault is evaluated or simulated.
84    reliability: Reliability,
85    priority_array: [Option<f32>; 16],
86    relinquish_default: f32,
87    /// High_End_Trim, Low_End_Trim and Trim_Fade_Time, and a trim change
88    /// under way.
89    trims: trim::Trims,
90    /// Color_Reference, with Color_Override and Override_Color_Reference
91    /// when overridable; absent until set.
92    color_link: Option<ColorLink>,
93    /// Tags, Profile_Location and Profile_Name, once provisioned (#1553).
94    profile: ProfileState,
95    monotonic_clock: Option<Arc<MonotonicClock>>,
96    deadline_waker: Option<Arc<DeadlineWaker>>,
97    /// The time an object with no clock bound has been advanced to.
98    logical_now: Duration,
99}
100
101impl LightingOutputObject {
102    /// Create a new Lighting Output object.
103    pub fn new(instance: u32, name: impl Into<String>) -> Result<Self, Error> {
104        let oid = ObjectIdentifier::new(ObjectType::LIGHTING_OUTPUT, instance)?;
105        Ok(Self {
106            oid,
107            name: name.into(),
108            description: String::new(),
109            present_value: 0.0,
110            lighting_command: BACnetLightingCommand::new(LightingOperation::NONE),
111            lighting_command_default_priority: 16,
112            operation: None,
113            blink_request_count: 0,
114            blink_warn_enable: false,
115            egress_time: 0,
116            default_fade_time: *DEFAULT_FADE_TIME_MS.start(),
117            default_ramp_rate: 100.0,
118            default_step_increment: 1.0,
119            cov_increment: 0.0,
120            finest_tracking_increment: None,
121            out_of_service: false,
122            status_flags: StatusFlags::empty(),
123            reliability: Reliability::NO_FAULT_DETECTED,
124            priority_array: [None; 16],
125            relinquish_default: 0.0,
126            trims: trim::Trims::NONE,
127            color_link: None,
128            profile: ProfileState::default(),
129            monotonic_clock: None,
130            deadline_waker: None,
131            logical_now: Duration::ZERO,
132        })
133    }
134
135    /// Set the description string.
136    pub fn set_description(&mut self, desc: impl Into<String>) {
137        self.description = desc.into();
138    }
139
140    /// Recalculate present-value from the priority array.
141    ///
142    /// Tracking_Value isn't stored: it reads as Present_Value while
143    /// In_Progress is IDLE and as the fade or ramp's current value while one
144    /// runs (Clause 12.54.5), so a new Present_Value shows in it only once
145    /// nothing is moving it.
146    fn recalculate_present_value(&mut self) {
147        self.present_value =
148            common::recalculate_from_priority_array(&self.priority_array, self.relinquish_default);
149    }
150
151    /// Set the Relinquish_Default (#270).
152    ///
153    /// Checked the same way a commanded Present_Value is: a value outside
154    /// 0.0 to 100.0, or NaN, is refused with VALUE_OUT_OF_RANGE, and one
155    /// above 0.0 and below 1.0 is stored as 1.0, so Present_Value never falls
156    /// back to a level between off and the dimmest on level. After the store,
157    /// Present_Value is resolved anew from the priority array so an empty
158    /// array falls back to the new default immediately.
159    pub fn set_relinquish_default(&mut self, value: f32) -> Result<(), Error> {
160        self.relinquish_default = normalized_level(value)?;
161        self.recalculate_present_value();
162        Ok(())
163    }
164
165    /// The last command written to Lighting_Command, or operation NONE with no
166    /// other field before any write.
167    pub fn lighting_command(&self) -> BACnetLightingCommand {
168        self.lighting_command
169    }
170
171    /// Set Lighting_Command and carry the command out, as a WriteProperty of
172    /// the encoded command would.
173    ///
174    /// The command is checked against its operation (Clause 12.54, Table
175    /// 12-67). NONE, a reserved operation (11 to 255) and anything past
176    /// 65,535 are refused; FADE_TO and RAMP_TO need a target level. A field
177    /// the operation uses must be in range: target level 0.0 to 100.0, fade
178    /// time 100 to 86,400,000 ms, ramp rate and step increment 0.1 to 100.0,
179    /// priority 1 to 16. Fields it doesn't use are kept unchecked, and a
180    /// proprietary operation (256 to 65,535) has only its priority checked. A
181    /// refusal is VALUE_OUT_OF_RANGE and leaves the object unchanged.
182    ///
183    /// A command taken is stored as written and carried out at its priority,
184    /// or at Lighting_Command_Default_Priority when it has none. A level it
185    /// puts in a priority slot is normalized as a commanded Present_Value is
186    /// (Clause 12.54.4), so a FADE_TO 0.5 fades to 1.0 while Lighting_Command
187    /// still reads 0.5. A proprietary operation is stored and does nothing
188    /// else.
189    pub fn set_lighting_command(&mut self, command: BACnetLightingCommand) -> Result<(), Error> {
190        command::check(&command)?;
191        self.take_lighting_command(command);
192        Ok(())
193    }
194
195    /// Store a checked command and carry it out now.
196    fn take_lighting_command(&mut self, command: BACnetLightingCommand) {
197        self.lighting_command = command;
198        let now = self.now();
199        self.execute(&command, now);
200    }
201
202    /// Link the output to the colour objects that set its colour, or take
203    /// the link away with `None` (#1527, Addendum 135-2020ca part 4).
204    ///
205    /// A link serves Color_Reference, and with a [`ColorOverride`] also
206    /// Color_Override and Override_Color_Reference; all of them take
207    /// writes. Each reference must name a colour object, Color or Color
208    /// Temperature (instance 4194303 names none); anything else is refused
209    /// with VALUE_OUT_OF_RANGE and changes nothing. The object only stores
210    /// the references:
211    /// [`ObjectDatabase::lighting_color`](crate::database::ObjectDatabase::lighting_color)
212    /// follows them.
213    pub fn set_color_link(&mut self, link: Option<ColorLink>) -> Result<(), Error> {
214        self.color_link = link.map(ColorLink::checked).transpose()?;
215        Ok(())
216    }
217
218    /// The colour link, if one is set.
219    pub fn color_link(&self) -> Option<&ColorLink> {
220        self.color_link.as_ref()
221    }
222
223    /// Build with application-owned Tags storage. Saved Tags override configured
224    /// Tags only while `set_profile` provisions the row. Loaded data must satisfy
225    /// the normal Tags rules and the opt-in 1 MiB snapshot limit.
226    pub fn with_tags_persistence(
227        instance: u32,
228        name: impl Into<String>,
229        persistence: Arc<dyn TagsPersistence>,
230    ) -> Result<Self, Error> {
231        let mut object = Self::new(instance, name)?;
232        object.profile = ProfileState::persistent(object.oid, persistence)?;
233        Ok(object)
234    }
235
236    /// Wait for queued save attempts to finish. This is not a success receipt;
237    /// writes report their own save outcomes. Unstaged local writes block.
238    pub fn wait_for_tag_saves(&self) {
239        self.profile.wait_for_saves();
240    }
241
242    /// Provision the optional Tags, Profile_Location and Profile_Name rows
243    /// before registration (#1553), as a Color object's
244    /// [`set_profile`](crate::color::ColorObject::set_profile) does.
245    pub fn set_profile(&mut self, profile: ObjectProfile) -> Result<(), Error> {
246        self.profile.provision(profile)
247    }
248
249    /// Set Default_Fade_Time, the milliseconds a fade request without its own
250    /// fade time takes. A new object uses 100, the shortest fade the clause
251    /// allows, as Default_Ramp_Rate starts at its fastest rate.
252    ///
253    /// Clause 12.54.16 bounds it to 100..=86_400_000 (one day); a value
254    /// outside that range is refused with VALUE_OUT_OF_RANGE and the property
255    /// is left unchanged. WriteProperty applies the same check.
256    pub fn set_default_fade_time(&mut self, milliseconds: u32) -> Result<(), Error> {
257        if !DEFAULT_FADE_TIME_MS.contains(&milliseconds) {
258            return Err(common::value_out_of_range_error());
259        }
260        self.default_fade_time = milliseconds;
261        Ok(())
262    }
263
264    /// Set Default_Ramp_Rate, the percent-per-second rate a ramp request
265    /// without its own rate uses. A new object uses 100.0.
266    ///
267    /// Clause 12.54.17 bounds it to 0.1..=100.0; a value outside that range,
268    /// or a non-finite one, is refused with VALUE_OUT_OF_RANGE and the
269    /// property is left unchanged. WriteProperty applies the same check.
270    pub fn set_default_ramp_rate(&mut self, value: f32) -> Result<(), Error> {
271        self.default_ramp_rate = lighting_percent(value)?;
272        Ok(())
273    }
274
275    /// Set Default_Step_Increment, the percent a step request without its own
276    /// increment adds. A new object uses 1.0.
277    ///
278    /// Clause 12.54.18 bounds it to 0.1..=100.0; a value outside that range,
279    /// or a non-finite one, is refused with VALUE_OUT_OF_RANGE and the
280    /// property is left unchanged. WriteProperty applies the same check.
281    pub fn set_default_step_increment(&mut self, value: f32) -> Result<(), Error> {
282        self.default_step_increment = lighting_percent(value)?;
283        Ok(())
284    }
285}
286
287/// Check a light level written to Present_Value or Relinquish_Default.
288///
289/// Both are percentages on the Clause 12.54 normalized scale: 0.0 is off,
290/// 1.0 is the dimmest on level and 100.0 the brightest, and no level lies
291/// strictly between 0.0 and 1.0. Clause 12.54.4 has a Present_Value write in
292/// that gap taken as 1.0, so such a level comes back as 1.0. 0.0 and levels
293/// from 1.0 to 100.0 come back unchanged. A level below 0.0 or above 100.0,
294/// NaN included, is VALUE_OUT_OF_RANGE; a Present_Value write tells the
295/// blink-warn values -1.0 to -3.0 apart before it gets here. -0.0 is off, so
296/// it comes back as 0.0 rather than keeping its sign on the wire.
297fn normalized_level(value: f32) -> Result<f32, Error> {
298    if !(0.0..=100.0).contains(&value) {
299        return Err(common::value_out_of_range_error());
300    }
301    Ok(if value == 0.0 {
302        0.0
303    } else if value < 1.0 {
304        1.0
305    } else {
306        value
307    })
308}
309
310/// The Default_Fade_Time range of Clause 12.54.16, in milliseconds, which a
311/// lighting command's fade time shares (Table 12-66).
312const DEFAULT_FADE_TIME_MS: std::ops::RangeInclusive<u32> = 100..=86_400_000;
313
314/// Check a Default_Ramp_Rate or Default_Step_Increment value, or a lighting
315/// command's ramp rate or step increment, which share the 0.1..=100.0 range.
316fn lighting_percent(value: f32) -> Result<f32, Error> {
317    if (0.1..=100.0).contains(&value) {
318        Ok(value)
319    } else {
320        Err(common::value_out_of_range_error())
321    }
322}
323
324impl BACnetObject for LightingOutputObject {
325    fn object_identifier(&self) -> ObjectIdentifier {
326        self.oid
327    }
328
329    fn object_name(&self) -> &str {
330        &self.name
331    }
332
333    fn read_property(
334        &self,
335        property: PropertyIdentifier,
336        array_index: Option<u32>,
337    ) -> Result<PropertyValue, Error> {
338        if let Some(result) = read_common_properties!(self, property, array_index) {
339            return result;
340        }
341        match property {
342            p if p == PropertyIdentifier::OBJECT_TYPE => Ok(PropertyValue::Enumerated(
343                ObjectType::LIGHTING_OUTPUT.to_raw(),
344            )),
345            p if p == PropertyIdentifier::PRESENT_VALUE => {
346                Ok(PropertyValue::Real(self.present_value))
347            }
348            // Each read takes its own instant, so one ReadPropertyMultiple
349            // that straddles a fade's end may pair a Tracking_Value just short
350            // of the level with In_Progress IDLE.
351            p if p == PropertyIdentifier::TRACKING_VALUE => {
352                Ok(PropertyValue::Real(self.tracking_value_at(self.now())))
353            }
354            p if p == PropertyIdentifier::LIGHTING_COMMAND => {
355                Ok(command::encode(&self.lighting_command))
356            }
357            p if p == PropertyIdentifier::LIGHTING_COMMAND_DEFAULT_PRIORITY => Ok(
358                PropertyValue::Unsigned(self.lighting_command_default_priority as u64),
359            ),
360            p if p == PropertyIdentifier::IN_PROGRESS => Ok(PropertyValue::Enumerated(
361                self.in_progress_at(self.now()).to_raw(),
362            )),
363            p if p == PropertyIdentifier::BLINK_WARN_ENABLE => {
364                Ok(PropertyValue::Boolean(self.blink_warn_enable))
365            }
366            p if p == PropertyIdentifier::EGRESS_TIME => {
367                Ok(PropertyValue::Unsigned(self.egress_time as u64))
368            }
369            p if p == PropertyIdentifier::EGRESS_ACTIVE => {
370                Ok(PropertyValue::Boolean(self.egress_active()))
371            }
372            p if p == PropertyIdentifier::PRIORITY_ARRAY => {
373                read_priority_array!(self, array_index, PropertyValue::Real)
374            }
375            p if p == PropertyIdentifier::RELINQUISH_DEFAULT => {
376                Ok(PropertyValue::Real(self.relinquish_default))
377            }
378            p if p == PropertyIdentifier::DEFAULT_FADE_TIME => {
379                Ok(PropertyValue::Unsigned(u64::from(self.default_fade_time)))
380            }
381            p if p == PropertyIdentifier::DEFAULT_RAMP_RATE => {
382                Ok(PropertyValue::Real(self.default_ramp_rate))
383            }
384            p if p == PropertyIdentifier::DEFAULT_STEP_INCREMENT => {
385                Ok(PropertyValue::Real(self.default_step_increment))
386            }
387            p if p == PropertyIdentifier::COV_INCREMENT => {
388                Ok(PropertyValue::Real(self.cov_increment))
389            }
390            p if p == PropertyIdentifier::CURRENT_COMMAND_PRIORITY => {
391                Ok(common::current_command_priority(&self.priority_array))
392            }
393            p => color_link::read(self.color_link.as_ref(), p)
394                .or_else(|| self.read_trim(p))
395                .or_else(|| self.profile.read(p, array_index))
396                .unwrap_or_else(|| Err(common::unknown_property_error())),
397        }
398    }
399
400    fn write_property(
401        &mut self,
402        property: PropertyIdentifier,
403        array_index: Option<u32>,
404        value: PropertyValue,
405        priority: Option<u8>,
406    ) -> Result<(), Error> {
407        // A level is normalized before it reaches the slot, so the slot,
408        // Present_Value, Tracking_Value and a COV report all see 1.0 for a
409        // write between 0.0 and 1.0. -1.0, -2.0 and -3.0 are the warn
410        // commands (Table 12-65) and never reach a slot themselves.
411        if property == PropertyIdentifier::PRESENT_VALUE {
412            let priority = priority.unwrap_or(16);
413            if !(1..=16).contains(&priority) {
414                return Err(common::value_out_of_range_error());
415            }
416            let write = engine::PresentValueWrite::decode(value)?;
417            let now = self.now();
418            self.write_present_value(priority, write, now);
419            return Ok(());
420        }
421
422        // LIGHTING_COMMAND: a BACnetLightingCommand, checked against its
423        // operation, then carried out.
424        if property == PropertyIdentifier::LIGHTING_COMMAND {
425            let command = command::decode_write(value)?;
426            self.take_lighting_command(command);
427            return Ok(());
428        }
429
430        // LIGHTING_COMMAND_DEFAULT_PRIORITY: 1 to 16, but not 6. Clause
431        // 12.54.27 keeps 6 out: Table 19-1 gives that slot to Minimum On/Off,
432        // and a command written without a priority acts at this one.
433        if property == PropertyIdentifier::LIGHTING_COMMAND_DEFAULT_PRIORITY {
434            if let PropertyValue::Unsigned(v) = value {
435                if !(1..=16).contains(&v) || v == 6 {
436                    return Err(common::value_out_of_range_error());
437                }
438                self.lighting_command_default_priority = v as u32;
439                return Ok(());
440            }
441            return Err(common::invalid_data_type_error());
442        }
443
444        // RELINQUISH_DEFAULT — writable per Table 12-64 (R; the standard
445        // permits writability), validated the same way a commanded
446        // Present_Value is by the shared setter.
447        if property == PropertyIdentifier::RELINQUISH_DEFAULT {
448            if let PropertyValue::Real(f) = value {
449                return self.set_relinquish_default(f);
450            }
451            return Err(common::invalid_data_type_error());
452        }
453
454        // BLINK_WARN_ENABLE
455        if property == PropertyIdentifier::BLINK_WARN_ENABLE {
456            if let PropertyValue::Boolean(v) = value {
457                self.blink_warn_enable = v;
458                return Ok(());
459            }
460            return Err(common::invalid_data_type_error());
461        }
462
463        // EGRESS_TIME
464        if property == PropertyIdentifier::EGRESS_TIME {
465            if let PropertyValue::Unsigned(v) = value {
466                self.egress_time = common::u64_to_u32(v)?;
467                return Ok(());
468            }
469            return Err(common::invalid_data_type_error());
470        }
471
472        // DEFAULT_FADE_TIME, DEFAULT_RAMP_RATE and DEFAULT_STEP_INCREMENT go
473        // through the range-checked setters. A fade time too large for the
474        // setter's u32 is past the range too.
475        if property == PropertyIdentifier::DEFAULT_FADE_TIME {
476            if let PropertyValue::Unsigned(v) = value {
477                return self.set_default_fade_time(common::u64_to_u32(v)?);
478            }
479            return Err(common::invalid_data_type_error());
480        }
481        if property == PropertyIdentifier::DEFAULT_RAMP_RATE {
482            if let PropertyValue::Real(v) = value {
483                return self.set_default_ramp_rate(v);
484            }
485            return Err(common::invalid_data_type_error());
486        }
487        if property == PropertyIdentifier::DEFAULT_STEP_INCREMENT {
488            if let PropertyValue::Real(v) = value {
489                return self.set_default_step_increment(v);
490            }
491            return Err(common::invalid_data_type_error());
492        }
493
494        // HIGH_END_TRIM, LOW_END_TRIM and TRIM_FADE_TIME, and the colour
495        // links, once present.
496        if let Some(result) = self.write_trim(property, &value) {
497            return result;
498        }
499        if let Some(result) = color_link::write(&mut self.color_link, property, &value) {
500            return result;
501        }
502        if let Some(result) = self.profile.write(property, array_index, &value) {
503            return result;
504        }
505        if let Some(result) = common::write_cov_increment(&mut self.cov_increment, property, &value)
506        {
507            return result;
508        }
509        if let Some(result) =
510            common::write_out_of_service(&mut self.out_of_service, property, &value)
511        {
512            return result;
513        }
514        if let Some(result) = common::write_description(&mut self.description, property, &value) {
515            return result;
516        }
517        Err(crate::common::unhandled_write_error(
518            self.property_metadata().as_ref(),
519            property,
520            array_index,
521        ))
522    }
523
524    fn durable_writes_internal(&mut self) -> Option<&mut dyn crate::durable::DurableWrites> {
525        self.profile.capability()
526    }
527
528    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
529        crate::property_metadata::property_list_from_metadata(self.property_metadata().as_ref())
530    }
531
532    fn property_metadata(&self) -> Cow<'_, [crate::property_metadata::PropertyMetadata]> {
533        metadata::for_lighting_output_object(self)
534    }
535
536    fn cov_increment(&self) -> Option<f64> {
537        Some(f64::from(self.cov_increment))
538    }
539
540    fn supports_cov(&self) -> bool {
541        true
542    }
543
544    fn advance_time_internal(&mut self, elapsed: Duration) -> bool {
545        self.logical_now = self.logical_now.saturating_add(elapsed);
546        self.advance_to(self.logical_now)
547    }
548
549    fn bind_monotonic_clock_internal(&mut self, clock: Option<Arc<MonotonicClock>>) {
550        self.monotonic_clock = clock;
551    }
552
553    fn bind_deadline_waker_internal(&mut self, waker: Option<Arc<DeadlineWaker>>) {
554        self.deadline_waker = waker;
555    }
556
557    fn advance_monotonic_time_internal(&mut self, now: Duration) -> bool {
558        self.profile.expire(now);
559        self.advance_to(now)
560    }
561
562    fn next_monotonic_deadline_internal(&self) -> Option<Duration> {
563        self.next_deadline()
564    }
565
566    fn set_tracking_cov_increment_internal(&mut self, finest: Option<f64>) {
567        self.finest_tracking_increment = finest;
568    }
569
570    /// A copy that reads as the object does now: its clock stops at this
571    /// instant, so a fade's Tracking_Value in a COV report is the value at
572    /// the moment the report was taken.
573    fn cov_snapshot_internal(&self) -> Option<Box<dyn BACnetObject>> {
574        let mut snapshot = self.clone();
575        snapshot.logical_now = self.now();
576        snapshot.monotonic_clock = None;
577        snapshot.deadline_waker = None;
578        Some(Box::new(snapshot))
579    }
580
581    fn lighting_blink_count_internal(&self) -> u64 {
582        self.blink_request_count
583    }
584}
585
586mod binary;
587mod color_link;
588mod command;
589mod engine;
590mod metadata;
591mod trim;
592pub use binary::BinaryLightingOutputObject;
593pub use color_link::{ColorLink, ColorOverride, LightingColor, OutputColor};
594
595// ---------------------------------------------------------------------------
596// Tests
597// ---------------------------------------------------------------------------
598
599#[cfg(test)]
600mod tests;
601
602#[cfg(test)]
603mod required_rows_tests;
604
605#[cfg(test)]
606mod command_tests;
607
608#[cfg(test)]
609mod present_value_tests;
610
611#[cfg(test)]
612mod engine_tests;
613
614#[cfg(test)]
615mod warn_tests;
616
617#[cfg(test)]
618mod trim_tests;
619
620#[cfg(test)]
621mod color_link_tests;