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