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