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