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