Skip to main content

bacnet_objects/color/
temperature.rs

1//! The Color Temperature object (type 64, Addendum 135-2020ca Clause 12.Y):
2//! a correlated colour temperature in kelvin.
3
4use crate::prelude::*;
5use alloc::borrow::Cow;
6use alloc::sync::Arc;
7use core::time::Duration;
8
9use bacnet_types::constructed::BACnetColorCommand;
10use bacnet_types::enums::{ColorOperation, ColorTransition, ObjectType, PropertyIdentifier};
11use bacnet_types::error::Error;
12use bacnet_types::primitives::{ObjectIdentifier, PropertyValue};
13
14use super::engine::Engine;
15use super::persistence::{ColorOutputPersistence, ColorOutputValue};
16use super::saving::{ColorOutputSaveCounters, OutputStorage, impl_color_durable};
17use super::{
18    command, metadata, milliseconds, noncommandable_audit_policy, written_transition,
19    written_unsigned,
20};
21use crate::audit::{AuditPolicyAuthority, ObjectAuditPolicy};
22use crate::command_source::{CommandOrigin, SingleValueSource};
23use crate::common::{self, read_identity_properties};
24use crate::object_profile::{ObjectProfile, ProfileState, TagsPersistence};
25use crate::traits::{BACnetObject, DeadlineWaker, MonotonicClock};
26use crate::transition::Transition;
27
28/// How far a fading or ramping colour temperature moves between COV samples
29/// of Tracking_Value, in kelvin. Ten kelvin is fine enough that a
30/// subscriber's own increment of a few tens of kelvin is met closely, and the
31/// 100 ms sample grid bounds how often a fast ramp is sampled.
32const KELVIN_SAMPLE_STEP: f64 = 10.0;
33
34/// BACnet Color Temperature object (type 64).
35///
36/// Holds a correlated colour temperature in kelvin, 1000 to 30000 and within
37/// Min_Pres_Value and Max_Pres_Value. It isn't commandable: Present_Value
38/// takes a write with no priority array, clamping a value between 1000 and
39/// Min_Pres_Value up to the minimum, and one between Max_Pres_Value and
40/// 30000 down to the maximum (Clause 12.Y.4).
41///
42/// Color_Command takes a [`BACnetColorCommand`] whose operation is
43/// FADE_TO_CCT, RAMP_TO_CCT, STEP_UP_CCT, STEP_DOWN_CCT or STOP (see
44/// [`set_color_command`](Self::set_color_command)) and carries it out
45/// (#1474). FADE_TO_CCT and RAMP_TO_CCT set Present_Value to the target,
46/// clamped to the limits, and move Tracking_Value to it from where it
47/// stands, over the command's fade time or at its ramp rate (the defaults
48/// when it carries none), with In_Progress FADE_ACTIVE or RAMP_ACTIVE until
49/// it arrives. The step operations set Present_Value at once to
50/// Tracking_Value plus or minus the step increment, clamped to the limits.
51/// STOP ends a fade or ramp there, and Present_Value takes the temperature it
52/// reached. A Present_Value write halts a fade or ramp too, then moves to the
53/// new temperature as Transition says: at once for NONE, a fade over
54/// Default_Fade_Time for FADE, or a ramp at Default_Ramp_Rate for RAMP.
55///
56/// Fades and ramps run on the monotonic clock the database binds, as a
57/// Color object's fades do.
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 ColorTemperatureObject {
64    oid: ObjectIdentifier,
65    name: String,
66    description: String,
67    /// Present_Value in kelvin: the temperature the output is at, or moving
68    /// to.
69    present_value: u32,
70    /// The last command written; operation NONE until then.
71    color_command: BACnetColorCommand,
72    /// Default_Color_Temperature in kelvin, or 0.
73    default_color_temperature: u32,
74    /// Default_Fade_Time in milliseconds, within 100..=86_400_000.
75    default_fade_time: u32,
76    /// Default_Ramp_Rate in kelvin per second, within 1..=30_000.
77    default_ramp_rate: u32,
78    /// Default_Step_Increment in kelvin, within 1..=30_000.
79    default_step_increment: u32,
80    /// Transition: NONE, FADE or RAMP.
81    transition: ColorTransition,
82    /// Min_Pres_Value and Max_Pres_Value, within 1000..=30_000.
83    min_pres_value: u32,
84    max_pres_value: u32,
85    /// Audit_Level and Auditable_Operations, once provisioned.
86    audit_policy: ObjectAuditPolicy,
87    /// Value_Source, once tracked (#1552).
88    value_source: SingleValueSource,
89    /// Tags, Profile_Location and Profile_Name, once provisioned (#1553).
90    profile: ProfileState,
91    /// Present_Value storage, once taken (#1526).
92    output: OutputStorage,
93    /// Whether Present_Value came back from that storage.
94    restored: bool,
95    /// The temperature storage brought back, until a start takes it.
96    to_restore: Option<u32>,
97    engine: Engine<u32>,
98}
99
100impl ColorTemperatureObject {
101    /// Create a Color Temperature object at 4000 K (neutral white), which is
102    /// also its Default_Color_Temperature.
103    ///
104    /// Min_Pres_Value and Max_Pres_Value start at the full 1000 to 30000 K
105    /// range, Default_Fade_Time at 100 ms (as a Color object's does),
106    /// Default_Ramp_Rate at 100 K/s, Default_Step_Increment at 50 K and
107    /// Transition at NONE.
108    pub fn new(instance: u32, name: impl Into<String>) -> Result<Self, Error> {
109        let oid = ObjectIdentifier::new(ObjectType::COLOR_TEMPERATURE, instance)?;
110        Ok(Self {
111            oid,
112            name: name.into(),
113            description: String::new(),
114            present_value: 4000,
115            color_command: BACnetColorCommand::new(ColorOperation::NONE),
116            default_color_temperature: 4000,
117            default_fade_time: *command::FADE_TIME_MS.start(),
118            default_ramp_rate: 100,
119            default_step_increment: 50,
120            transition: ColorTransition::NONE,
121            min_pres_value: *command::KELVIN.start(),
122            max_pres_value: *command::KELVIN.end(),
123            audit_policy: ObjectAuditPolicy::default(),
124            value_source: SingleValueSource::default(),
125            profile: ProfileState::default(),
126            output: OutputStorage::default(),
127            restored: false,
128            to_restore: None,
129            engine: Engine::new(KELVIN_SAMPLE_STEP),
130        })
131    }
132
133    /// Keep Present_Value in `persistence` (#1526), and take the temperature
134    /// it saved for this object, if any, as Present_Value, clamped to
135    /// Min_Pres_Value and Max_Pres_Value. Call it right after building the
136    /// object, before anything writes Present_Value.
137    ///
138    /// Saving works as on a Color object
139    /// ([`ColorObject::with_output_persistence`](super::ColorObject::with_output_persistence)).
140    /// With a Default_Color_Temperature of 0, the device's start keeps the
141    /// saved temperature (Clause 12.Y.4).
142    ///
143    /// Fails with storage's error when what it holds can't be loaded, and
144    /// when it holds an xy colour or a temperature outside 1000 to 30000 K.
145    pub fn with_output_persistence(
146        mut self,
147        persistence: Arc<dyn ColorOutputPersistence>,
148    ) -> Result<Self, Error> {
149        let current = ColorOutputValue::Kelvin(self.present_value);
150        match self.output.open(self.oid, current, persistence)? {
151            Some(ColorOutputValue::Kelvin(kelvin)) if command::KELVIN.contains(&kelvin) => {
152                self.present_value = self.clamped(kelvin);
153                self.restored = true;
154                self.to_restore = Some(kelvin);
155            }
156            Some(_) => {
157                return Err(Error::Encoding(
158                    "saved Color Temperature output is not a temperature the object can hold"
159                        .into(),
160                ));
161            }
162            None => {}
163        }
164        Ok(self)
165    }
166
167    /// Whether Present_Value came back from storage
168    /// ([`with_output_persistence`](Self::with_output_persistence)).
169    pub fn output_restored(&self) -> bool {
170        self.restored
171    }
172
173    /// The Present_Value save counters, shared with the object, or `None`
174    /// without storage. Take them before the object joins a database.
175    pub fn output_save_counters(&self) -> Option<ColorOutputSaveCounters> {
176        self.output.counters()
177    }
178
179    /// Block until the Present_Value saves queued so far have run. Dropping
180    /// the object waits for them too.
181    pub fn wait_for_output_saves(&self) {
182        self.output.wait_for_saves();
183    }
184
185    /// Default_Color_Temperature in kelvin: the temperature the device's
186    /// start drives the output to, or 0 to keep the one from before the
187    /// restart.
188    pub fn default_color_temperature(&self) -> u32 {
189        self.default_color_temperature
190    }
191
192    /// Set Default_Color_Temperature, as a WriteProperty of it would: 0, or
193    /// 1000 to 30000 K clamped to Min_Pres_Value and Max_Pres_Value (Clause
194    /// 12.Y.8). Any other value is refused with VALUE_OUT_OF_RANGE.
195    pub fn set_default_color_temperature(&mut self, kelvin: u32) -> Result<(), Error> {
196        self.default_color_temperature = match kelvin {
197            0 => 0,
198            kelvin => self.written_kelvin(u64::from(kelvin))?,
199        };
200        Ok(())
201    }
202
203    /// Drive the output as the device's start does (#1526), as a Color
204    /// object's [`start_output`](super::ColorObject::start_output) does.
205    ///
206    /// A non-zero Default_Color_Temperature becomes Present_Value at once,
207    /// whatever was set before the start. With a default of 0 the output
208    /// keeps, in order: the temperature restored from storage; else one set
209    /// before the start; else nothing, and In_Progress reads NOT_CONTROLLED
210    /// until a Present_Value or Color_Command write drives it (Clause
211    /// 12.Y.4). Either value is clamped to Min_Pres_Value and
212    /// Max_Pres_Value as they stand at the start.
213    pub fn start_output(&mut self) {
214        let restored = self.to_restore.take();
215        let target = match self.default_color_temperature {
216            0 => restored,
217            default => Some(default),
218        };
219        // Without a target, a temperature set before the start stays, and
220        // with none the output stays undriven.
221        if let Some(kelvin) = target.map(|kelvin| self.clamped(kelvin)) {
222            if kelvin != self.present_value {
223                self.value_source.forget();
224            }
225            self.present_value = kelvin;
226            self.engine.start(None);
227            self.output_changed();
228        }
229    }
230
231    /// Present_Value may have changed: save it, if the object keeps it.
232    fn output_changed(&mut self) {
233        self.output
234            .note(ColorOutputValue::Kelvin(self.present_value));
235    }
236
237    /// Track Value_Source (Clause 19.5, #1552), as a Color object's
238    /// [`set_value_source_tracking`](super::ColorObject::set_value_source_tracking)
239    /// does. A `set_min_max` that moves Present_Value leaves no source
240    /// either.
241    pub fn set_value_source_tracking(&mut self, enabled: bool) {
242        self.value_source.set_enabled(enabled);
243    }
244
245    pub(super) fn value_source(&self) -> &SingleValueSource {
246        &self.value_source
247    }
248
249    /// Build with application-owned Tags storage. Saved Tags override configured
250    /// Tags only while `set_profile` provisions the row. Loaded data must satisfy
251    /// the normal Tags rules and the opt-in 1 MiB snapshot limit.
252    pub fn with_tags_persistence(
253        instance: u32,
254        name: impl Into<String>,
255        persistence: Arc<dyn TagsPersistence>,
256    ) -> Result<Self, Error> {
257        let mut object = Self::new(instance, name)?;
258        object.profile = ProfileState::persistent(object.oid, persistence)?;
259        Ok(object)
260    }
261
262    /// Wait for queued save attempts to finish. This is not a success receipt;
263    /// writes report their own save outcomes. Unstaged local writes block.
264    pub fn wait_for_tag_saves(&self) {
265        self.profile.wait_for_saves();
266    }
267
268    /// Provision the optional Tags, Profile_Location and Profile_Name rows
269    /// before registration (#1553; see [`ObjectProfile`]). Tags takes
270    /// writes; the profile rows are read-only over the network. A profile
271    /// that fails [`ObjectProfile::check`] is refused and changes nothing.
272    pub fn set_profile(&mut self, profile: ObjectProfile) -> Result<(), Error> {
273        self.profile.provision(profile)
274    }
275
276    pub(super) fn profile(&self) -> &ObjectProfile {
277        self.profile.profile()
278    }
279
280    /// Provision the optional Audit_Level and Auditable_Operations rows
281    /// before registration, as a Color object's are (#1525). Table 12-Y has
282    /// no Audit_Priority_Filter either, so a priority filter in `policy` is
283    /// left out.
284    pub fn set_audit_policy(&mut self, policy: ObjectAuditPolicy) {
285        self.audit_policy = noncommandable_audit_policy(policy);
286    }
287
288    pub(super) fn audit_policy(&self) -> &ObjectAuditPolicy {
289        &self.audit_policy
290    }
291
292    /// Set Present_Value in kelvin, as a WriteProperty of it would.
293    ///
294    /// A value outside 1000 to 30000 is refused with VALUE_OUT_OF_RANGE and
295    /// changes nothing; one inside is clamped to Min_Pres_Value and
296    /// Max_Pres_Value (Clause 12.Y.4). A fade or ramp in progress is then
297    /// halted, and the output moves to the value as Transition says.
298    pub fn set_present_value(&mut self, kelvin: u32) -> Result<(), Error> {
299        let kelvin = self.written_kelvin(u64::from(kelvin))?;
300        self.write_present_value(kelvin);
301        self.value_source.forget();
302        Ok(())
303    }
304
305    /// Set Min_Pres_Value and Max_Pres_Value, in kelvin.
306    ///
307    /// Clauses 12.Y.13 and 12.Y.14 keep them within 1000 to 30000; a minimum
308    /// below 1000, a maximum above 30000 or a minimum above the maximum is
309    /// refused with VALUE_OUT_OF_RANGE and changes nothing. A Present_Value
310    /// outside the new limits moves to the nearer one at once, halting any
311    /// fade or ramp, and a non-zero Default_Color_Temperature is clamped the
312    /// same way.
313    pub fn set_min_max(&mut self, min: u32, max: u32) -> Result<(), Error> {
314        if !command::KELVIN.contains(&min) || !command::KELVIN.contains(&max) || min > max {
315            return Err(common::value_out_of_range_error());
316        }
317        self.min_pres_value = min;
318        self.max_pres_value = max;
319        let clamped = self.present_value.clamp(min, max);
320        if clamped != self.present_value {
321            self.engine.halt(self.engine.now());
322            self.present_value = clamped;
323            self.value_source.forget();
324            self.output_changed();
325        }
326        if self.default_color_temperature != 0 {
327            self.default_color_temperature = self.default_color_temperature.clamp(min, max);
328        }
329        Ok(())
330    }
331
332    /// The last command written to Color_Command, or operation NONE with no
333    /// other field before any write.
334    pub fn color_command(&self) -> BACnetColorCommand {
335        self.color_command
336    }
337
338    /// Set Color_Command and carry the command out, as a WriteProperty of
339    /// the encoded command would.
340    ///
341    /// The command is checked against the Color Temperature object's table
342    /// of colour commands (Addendum 135-2020ca, Table 12-Y2): FADE_TO_CCT,
343    /// RAMP_TO_CCT, STEP_UP_CCT, STEP_DOWN_CCT and STOP are taken.
344    /// FADE_TO_CCT and RAMP_TO_CCT need a target colour temperature of 1000
345    /// to 30000 K. A field the operation uses must be in range: fade time
346    /// (FADE_TO_CCT) 100 to 86,400,000 ms, ramp rate (RAMP_TO_CCT) 1 to
347    /// 30000 K/s, step increment (the step operations) 1 to 30000 K. Fields
348    /// the operation doesn't use are kept unchecked. A refusal is
349    /// VALUE_OUT_OF_RANGE and leaves the object unchanged.
350    pub fn set_color_command(&mut self, command: BACnetColorCommand) -> Result<(), Error> {
351        command::check_color_temperature(&command)?;
352        if self.take_color_command(command) {
353            self.value_source.forget();
354        }
355        Ok(())
356    }
357
358    /// A written temperature: refused outside 1000 to 30000 K, otherwise
359    /// clamped to Min_Pres_Value and Max_Pres_Value (Clause 12.Y.4).
360    fn written_kelvin(&self, kelvin: u64) -> Result<u32, Error> {
361        u32::try_from(kelvin)
362            .ok()
363            .filter(|kelvin| command::KELVIN.contains(kelvin))
364            .map(|kelvin| self.clamped(kelvin))
365            .ok_or_else(common::value_out_of_range_error)
366    }
367
368    fn clamped(&self, kelvin: u32) -> u32 {
369        kelvin.clamp(self.min_pres_value, self.max_pres_value)
370    }
371
372    /// Store a checked command and carry it out now (Table 12-Y2). Each
373    /// operation but STOP replaces the fade or ramp in progress, and STOP
374    /// ends it (Clause 12.Y.6.1). Whether it set Present_Value: a STOP with
375    /// nothing moving doesn't.
376    fn take_color_command(&mut self, command: BACnetColorCommand) -> bool {
377        let set = self.carry_out(command);
378        if set {
379            self.output_changed();
380        }
381        set
382    }
383
384    /// [`take_color_command`](Self::take_color_command) without the save.
385    fn carry_out(&mut self, command: BACnetColorCommand) -> bool {
386        self.color_command = command;
387        let now = self.engine.now();
388        let tracking = self.engine.tracking(self.present_value, now);
389        let target = command
390            .target_color_temperature
391            .map(|kelvin| self.clamped(kelvin));
392        match (command.operation, target) {
393            (ColorOperation::FADE_TO_CCT, Some(target)) => {
394                let fade_time = command.fade_time.unwrap_or(self.default_fade_time);
395                self.present_value = target;
396                self.engine.start(Transition::fade(
397                    tracking,
398                    target,
399                    now,
400                    milliseconds(fade_time),
401                ));
402                true
403            }
404            (ColorOperation::RAMP_TO_CCT, Some(target)) => {
405                let rate = command.ramp_rate.unwrap_or(self.default_ramp_rate);
406                self.present_value = target;
407                self.engine
408                    .start(Transition::ramp(tracking, target, now, f64::from(rate)));
409                true
410            }
411            (operation @ (ColorOperation::STEP_UP_CCT | ColorOperation::STEP_DOWN_CCT), _) => {
412                let increment = command
413                    .step_increment
414                    .unwrap_or(self.default_step_increment);
415                let stepped = if operation == ColorOperation::STEP_UP_CCT {
416                    tracking.saturating_add(increment)
417                } else {
418                    tracking.saturating_sub(increment)
419                };
420                self.present_value = self.clamped(stepped);
421                self.engine.start(None);
422                true
423            }
424            (ColorOperation::STOP, _) => match self.engine.halt(now) {
425                Some(reached) => {
426                    self.present_value = reached;
427                    true
428                }
429                None => false,
430            },
431            _ => false,
432        }
433    }
434
435    /// A Present_Value or Color_Command write, the two that can set
436    /// Present_Value, and whether it did.
437    fn write_output(
438        &mut self,
439        property: PropertyIdentifier,
440        value: PropertyValue,
441    ) -> Result<bool, Error> {
442        if property == PropertyIdentifier::PRESENT_VALUE {
443            let PropertyValue::Unsigned(kelvin) = value else {
444                return Err(common::invalid_data_type_error());
445            };
446            let kelvin = self.written_kelvin(kelvin)?;
447            self.write_present_value(kelvin);
448            return Ok(true);
449        }
450        let command = command::decode_write(value, command::check_color_temperature)?;
451        Ok(self.take_color_command(command))
452    }
453
454    /// Halt any fade or ramp and move to a written Present_Value as
455    /// Transition says (Clause 12.Y.15): at once, a fade over
456    /// Default_Fade_Time, or a ramp at Default_Ramp_Rate.
457    fn write_present_value(&mut self, kelvin: u32) {
458        let now = self.engine.now();
459        let tracking = self.engine.tracking(self.present_value, now);
460        self.present_value = kelvin;
461        let transition = match self.transition {
462            ColorTransition::FADE => {
463                Transition::fade(tracking, kelvin, now, milliseconds(self.default_fade_time))
464            }
465            ColorTransition::RAMP => {
466                Transition::ramp(tracking, kelvin, now, f64::from(self.default_ramp_rate))
467            }
468            _ => None,
469        };
470        self.engine.start(transition);
471        self.output_changed();
472    }
473}
474
475impl BACnetObject for ColorTemperatureObject {
476    fn object_identifier(&self) -> ObjectIdentifier {
477        self.oid
478    }
479
480    fn object_name(&self) -> &str {
481        &self.name
482    }
483
484    fn read_property(
485        &self,
486        property: PropertyIdentifier,
487        array_index: Option<u32>,
488    ) -> Result<PropertyValue, Error> {
489        if let Some(result) = read_identity_properties!(self, property, array_index) {
490            return result;
491        }
492        if let Some(result) = self.audit_policy.read(property, array_index) {
493            return result;
494        }
495        if let Some(result) = self.value_source.read(property, array_index) {
496            return result;
497        }
498        if let Some(result) = self.profile.read(property, array_index) {
499            return result;
500        }
501        let unsigned = |value: u32| Ok(PropertyValue::Unsigned(u64::from(value)));
502        match property {
503            p if p == PropertyIdentifier::OBJECT_TYPE => Ok(PropertyValue::Enumerated(
504                ObjectType::COLOR_TEMPERATURE.to_raw(),
505            )),
506            p if p == PropertyIdentifier::PRESENT_VALUE => unsigned(self.present_value),
507            p if p == PropertyIdentifier::TRACKING_VALUE => {
508                unsigned(self.engine.tracking(self.present_value, self.engine.now()))
509            }
510            p if p == PropertyIdentifier::COLOR_COMMAND => Ok(command::encode(&self.color_command)),
511            p if p == PropertyIdentifier::IN_PROGRESS => Ok(PropertyValue::Enumerated(
512                self.engine.in_progress(self.engine.now()).to_raw(),
513            )),
514            p if p == PropertyIdentifier::DEFAULT_COLOR_TEMPERATURE => {
515                unsigned(self.default_color_temperature)
516            }
517            p if p == PropertyIdentifier::DEFAULT_FADE_TIME => unsigned(self.default_fade_time),
518            p if p == PropertyIdentifier::DEFAULT_RAMP_RATE => unsigned(self.default_ramp_rate),
519            p if p == PropertyIdentifier::DEFAULT_STEP_INCREMENT => {
520                unsigned(self.default_step_increment)
521            }
522            p if p == PropertyIdentifier::MIN_PRES_VALUE => unsigned(self.min_pres_value),
523            p if p == PropertyIdentifier::MAX_PRES_VALUE => unsigned(self.max_pres_value),
524            p if p == PropertyIdentifier::TRANSITION => {
525                Ok(PropertyValue::Enumerated(self.transition.to_raw()))
526            }
527            _ => Err(common::unknown_property_error()),
528        }
529    }
530
531    fn write_property(
532        &mut self,
533        property: PropertyIdentifier,
534        array_index: Option<u32>,
535        value: PropertyValue,
536        priority: Option<u8>,
537    ) -> Result<(), Error> {
538        if let Some(result) = common::write_description(&mut self.description, property, &value) {
539            return result;
540        }
541        if let Some(result) = self
542            .audit_policy
543            .write(property, array_index, &value, priority)
544        {
545            return result;
546        }
547        if let Some(result) = self.profile.write(property, array_index, &value) {
548            return result;
549        }
550        match property {
551            p if p == PropertyIdentifier::PRESENT_VALUE
552                || p == PropertyIdentifier::COLOR_COMMAND =>
553            {
554                // A tracked source needs the writer (`write_property_from`).
555                self.value_source.unsourced()?;
556                self.write_output(property, value)?;
557            }
558            // Clause 12.Y.8 clamps a write as Present_Value's is, except 0,
559            // which the restart rule of Clause 12.Y.4 gives a meaning of its
560            // own: bring back the temperature from before the restart.
561            p if p == PropertyIdentifier::DEFAULT_COLOR_TEMPERATURE => {
562                self.default_color_temperature = match value {
563                    PropertyValue::Unsigned(0) => 0,
564                    PropertyValue::Unsigned(kelvin) => self.written_kelvin(kelvin)?,
565                    _ => return Err(common::invalid_data_type_error()),
566                };
567            }
568            p if p == PropertyIdentifier::DEFAULT_FADE_TIME => {
569                self.default_fade_time = written_unsigned(value, command::FADE_TIME_MS)?;
570            }
571            p if p == PropertyIdentifier::DEFAULT_RAMP_RATE => {
572                self.default_ramp_rate = written_unsigned(value, command::KELVIN_STEP)?;
573            }
574            p if p == PropertyIdentifier::DEFAULT_STEP_INCREMENT => {
575                self.default_step_increment = written_unsigned(value, command::KELVIN_STEP)?;
576            }
577            p if p == PropertyIdentifier::TRANSITION => {
578                self.transition = written_transition(value, ColorTransition::RAMP)?;
579            }
580            _ => {
581                return Err(common::unhandled_write_error(
582                    self.property_metadata().as_ref(),
583                    property,
584                    array_index,
585                ));
586            }
587        }
588        Ok(())
589    }
590
591    fn write_property_from(
592        &mut self,
593        property: PropertyIdentifier,
594        array_index: Option<u32>,
595        value: PropertyValue,
596        priority: Option<u8>,
597        origin: &CommandOrigin,
598    ) -> Result<(), Error> {
599        match property {
600            PropertyIdentifier::VALUE_SOURCE => {
601                self.value_source.correct(array_index, value, origin)
602            }
603            PropertyIdentifier::PRESENT_VALUE | PropertyIdentifier::COLOR_COMMAND => {
604                self.value_source.admit(origin)?;
605                if self.write_output(property, value)? {
606                    self.value_source.record(origin);
607                }
608                Ok(())
609            }
610            _ => self.write_property(property, array_index, value, priority),
611        }
612    }
613
614    fn property_metadata(&self) -> Cow<'_, [crate::property_metadata::PropertyMetadata]> {
615        metadata::for_color_temperature_object(self)
616    }
617
618    fn durable_writes_internal(&mut self) -> Option<&mut dyn crate::durable::DurableWrites> {
619        if self.output.is_some() {
620            return Some(self);
621        }
622        self.profile.capability()
623    }
624
625    fn start_output_internal(&mut self) {
626        self.start_output();
627    }
628
629    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
630        crate::property_metadata::property_list_from_metadata(self.property_metadata().as_ref())
631    }
632
633    fn supports_cov(&self) -> bool {
634        true
635    }
636
637    fn audit_object_policy_internal(&self) -> ObjectAuditPolicy {
638        self.audit_policy
639    }
640
641    fn audit_policy_authority_internal(&mut self) -> Option<AuditPolicyAuthority<'_>> {
642        Some(AuditPolicyAuthority::new(&mut self.audit_policy))
643    }
644
645    fn advance_time_internal(&mut self, elapsed: Duration) -> bool {
646        self.engine.advance_by(elapsed)
647    }
648
649    fn bind_monotonic_clock_internal(&mut self, clock: Option<Arc<MonotonicClock>>) {
650        self.engine.bind_clock(clock);
651    }
652
653    fn bind_deadline_waker_internal(&mut self, waker: Option<Arc<DeadlineWaker>>) {
654        self.engine.bind_waker(waker);
655    }
656
657    fn advance_monotonic_time_internal(&mut self, now: Duration) -> bool {
658        self.profile.expire(now);
659        self.engine.advance_to(now)
660    }
661
662    fn next_monotonic_deadline_internal(&self) -> Option<Duration> {
663        self.engine.deadline()
664    }
665
666    fn set_tracking_cov_increment_internal(&mut self, finest: Option<f64>) {
667        self.engine.set_finest_increment(finest);
668    }
669
670    /// A copy that reads as the object does now; see the Color object's.
671    fn cov_snapshot_internal(&self) -> Option<Box<dyn BACnetObject>> {
672        Some(Box::new(Self {
673            engine: self.engine.frozen(),
674            ..self.clone()
675        }))
676    }
677}
678
679impl_color_durable!(ColorTemperatureObject);