Skip to main content

bacnet_objects/color/
mod.rs

1//! Color (type 63) and Color Temperature (type 64) objects, added by
2//! Addendum 135-2020ca.
3//!
4//! Color objects represent CIE 1931 xy color coordinates.
5//! Color Temperature objects represent correlated color temperature in Kelvin.
6//! Both take a typed `BACnetColorCommand` as Color_Command, checked against
7//! the operations each allows (see `command`), and serve the last one taken.
8//! Neither carries a command out: Present_Value, Tracking_Value and
9//! In_Progress stay as they are.
10
11use bacnet_types::constructed::{BACnetColorCommand, BACnetXyColor};
12use bacnet_types::enums::{
13    ColorOperation, EventState, ObjectType, PropertyIdentifier, Reliability,
14};
15use bacnet_types::error::Error;
16use bacnet_types::primitives::{ObjectIdentifier, PropertyValue, StatusFlags};
17use std::borrow::Cow;
18
19use crate::common::{self, read_common_properties, read_property_list_property};
20use crate::traits::BACnetObject;
21
22mod command;
23mod metadata;
24
25#[cfg(test)]
26mod command_tests;
27
28/// D65 white, where a new Color object starts.
29const D65: BACnetXyColor = BACnetXyColor::new(0.3127, 0.3290);
30
31/// The value an xy colour reads as: its two REALs, which encode as the
32/// BACnetxyColor SEQUENCE.
33fn xy_value(color: BACnetXyColor) -> PropertyValue {
34    PropertyValue::List(vec![
35        PropertyValue::Real(color.x),
36        PropertyValue::Real(color.y),
37    ])
38}
39
40// ---------------------------------------------------------------------------
41// ColorObject (type 63) — CIE 1931 xy color
42// ---------------------------------------------------------------------------
43
44/// BACnet Color object (type 63).
45///
46/// Represents a color as CIE 1931 xy coordinates. Non-commandable (no
47/// priority array).
48///
49/// Color_Command takes a [`BACnetColorCommand`] whose operation is
50/// FADE_TO_COLOR or STOP (see [`set_color_command`](Self::set_color_command))
51/// and serves the last one taken. The object doesn't carry commands out.
52pub struct ColorObject {
53    oid: ObjectIdentifier,
54    name: String,
55    description: String,
56    /// Present_Value: the target colour.
57    present_value: BACnetXyColor,
58    /// Tracking_Value: current actual color (may differ during fade).
59    tracking_value: BACnetXyColor,
60    /// The last command written; operation NONE until then.
61    color_command: BACnetColorCommand,
62    /// Default_Color: startup color.
63    default_color: BACnetXyColor,
64    /// Default_Fade_Time: milliseconds (100-86400000). 0 = use device default.
65    default_fade_time: u32,
66    /// Transition: 0=NONE, 1=FADE.
67    transition: u32,
68    /// In_Progress: 0=idle, 1=fade-active.
69    in_progress: u32,
70    status_flags: StatusFlags,
71    event_state: EventState,
72    out_of_service: bool,
73    reliability: Reliability,
74}
75
76impl ColorObject {
77    /// Create a new Color object with default white color (x=0.3127, y=0.3290 ≈ D65).
78    pub fn new(instance: u32, name: impl Into<String>) -> Result<Self, Error> {
79        let oid = ObjectIdentifier::new(ObjectType::COLOR, instance)?;
80        Ok(Self {
81            oid,
82            name: name.into(),
83            description: String::new(),
84            present_value: D65,
85            tracking_value: D65,
86            color_command: BACnetColorCommand::new(ColorOperation::NONE),
87            default_color: D65,
88            default_fade_time: 0,
89            transition: 0,  // NONE
90            in_progress: 0, // idle
91            status_flags: StatusFlags::empty(),
92            event_state: EventState::NORMAL,
93            out_of_service: false,
94            reliability: Reliability::NO_FAULT_DETECTED,
95        })
96    }
97
98    /// Set the CIE 1931 xy Present_Value and make Tracking_Value follow it immediately.
99    pub fn set_present_value(&mut self, x: f32, y: f32) {
100        self.present_value = BACnetXyColor::new(x, y);
101        self.tracking_value = self.present_value;
102    }
103
104    /// The last command written to Color_Command, or operation NONE with no
105    /// other field before any write.
106    pub fn color_command(&self) -> BACnetColorCommand {
107        self.color_command
108    }
109
110    /// Set Color_Command, as a WriteProperty of the encoded command would.
111    ///
112    /// The command is checked against the Color object's table of colour
113    /// commands (Addendum 135-2020ca): only FADE_TO_COLOR and STOP are taken.
114    /// FADE_TO_COLOR needs a target colour with both coordinates 0.0 to 1.0,
115    /// and a fade time, when it carries one, of 100 to 86,400,000 ms. Fields
116    /// the operation doesn't use are kept unchecked. A refusal is
117    /// VALUE_OUT_OF_RANGE and leaves the property unchanged. The object
118    /// stores the command without carrying it out.
119    pub fn set_color_command(&mut self, command: BACnetColorCommand) -> Result<(), Error> {
120        command::check_color(&command)?;
121        self.color_command = command;
122        Ok(())
123    }
124}
125
126impl BACnetObject for ColorObject {
127    fn object_identifier(&self) -> ObjectIdentifier {
128        self.oid
129    }
130
131    fn object_name(&self) -> &str {
132        &self.name
133    }
134
135    fn read_property(
136        &self,
137        property: PropertyIdentifier,
138        array_index: Option<u32>,
139    ) -> Result<PropertyValue, Error> {
140        if let Some(result) = read_common_properties!(self, property, array_index) {
141            return result;
142        }
143        match property {
144            p if p == PropertyIdentifier::OBJECT_TYPE => {
145                Ok(PropertyValue::Enumerated(ObjectType::COLOR.to_raw()))
146            }
147            p if p == PropertyIdentifier::PRESENT_VALUE => Ok(xy_value(self.present_value)),
148            p if p == PropertyIdentifier::TRACKING_VALUE => Ok(xy_value(self.tracking_value)),
149            p if p == PropertyIdentifier::COLOR_COMMAND => Ok(command::encode(&self.color_command)),
150            p if p == PropertyIdentifier::DEFAULT_COLOR => Ok(xy_value(self.default_color)),
151            p if p == PropertyIdentifier::DEFAULT_FADE_TIME => {
152                Ok(PropertyValue::Unsigned(self.default_fade_time as u64))
153            }
154            p if p == PropertyIdentifier::TRANSITION => {
155                Ok(PropertyValue::Enumerated(self.transition))
156            }
157            p if p == PropertyIdentifier::IN_PROGRESS => {
158                Ok(PropertyValue::Enumerated(self.in_progress))
159            }
160            p if p == PropertyIdentifier::EVENT_STATE => {
161                Ok(PropertyValue::Enumerated(self.event_state.to_raw()))
162            }
163            p if p == PropertyIdentifier::PROPERTY_LIST => {
164                read_property_list_property(&self.property_list(), array_index)
165            }
166            _ => Err(common::unknown_property_error()),
167        }
168    }
169
170    fn write_property(
171        &mut self,
172        property: PropertyIdentifier,
173        _array_index: Option<u32>,
174        value: PropertyValue,
175        _priority: Option<u8>,
176    ) -> Result<(), Error> {
177        if let Some(result) =
178            common::write_out_of_service(&mut self.out_of_service, property, &value)
179        {
180            return result;
181        }
182        if let Some(result) = common::write_description(&mut self.description, property, &value) {
183            return result;
184        }
185        match property {
186            p if p == PropertyIdentifier::COLOR_COMMAND => {
187                self.color_command = command::decode_write(value, command::check_color)?;
188                Ok(())
189            }
190            p if p == PropertyIdentifier::DEFAULT_FADE_TIME => {
191                if let PropertyValue::Unsigned(v) = value {
192                    if v > 86_400_000 {
193                        return Err(common::value_out_of_range_error());
194                    }
195                    self.default_fade_time = v as u32;
196                    Ok(())
197                } else {
198                    Err(common::invalid_data_type_error())
199                }
200            }
201            _ => Err(crate::common::unhandled_write_error(
202                self.property_metadata().as_ref(),
203                property,
204                _array_index,
205            )),
206        }
207    }
208
209    fn property_metadata(&self) -> Cow<'_, [crate::property_metadata::PropertyMetadata]> {
210        metadata::for_color_object(self)
211    }
212
213    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
214        crate::property_metadata::property_list_from_metadata(self.property_metadata().as_ref())
215    }
216
217    fn supports_cov(&self) -> bool {
218        true
219    }
220}
221
222// ---------------------------------------------------------------------------
223// ColorTemperatureObject (type 64) — Correlated Color Temperature
224// ---------------------------------------------------------------------------
225
226/// BACnet Color Temperature object (type 64).
227///
228/// Represents correlated color temperature in Kelvin (typically 1000-30000).
229///
230/// Color_Command takes a [`BACnetColorCommand`] whose operation is
231/// FADE_TO_CCT, RAMP_TO_CCT, STEP_UP_CCT, STEP_DOWN_CCT or STOP (see
232/// [`set_color_command`](Self::set_color_command)) and serves the last one
233/// taken. The object doesn't carry commands out.
234pub struct ColorTemperatureObject {
235    oid: ObjectIdentifier,
236    name: String,
237    description: String,
238    /// Present_Value: Unsigned (Kelvin).
239    present_value: u32,
240    /// Tracking_Value: current actual color temperature.
241    tracking_value: u32,
242    /// The last command written; operation NONE until then.
243    color_command: BACnetColorCommand,
244    /// Default_Color_Temperature: startup value.
245    default_color_temperature: u32,
246    /// Default_Fade_Time: milliseconds.
247    default_fade_time: u32,
248    /// Default_Ramp_Rate: Kelvin per second.
249    default_ramp_rate: u32,
250    /// Default_Step_Increment: Kelvin per step.
251    default_step_increment: u32,
252    /// Transition: 0=NONE, 1=FADE, 2=RAMP.
253    transition: u32,
254    /// In_Progress: 0=idle, 1=fade-active, 2=ramp-active.
255    in_progress: u32,
256    /// Min/Max present value bounds.
257    min_pres_value: Option<u32>,
258    max_pres_value: Option<u32>,
259    status_flags: StatusFlags,
260    event_state: EventState,
261    out_of_service: bool,
262    reliability: Reliability,
263}
264
265impl ColorTemperatureObject {
266    /// Create a new Color Temperature object with default 4000K (neutral white).
267    pub fn new(instance: u32, name: impl Into<String>) -> Result<Self, Error> {
268        let oid = ObjectIdentifier::new(ObjectType::COLOR_TEMPERATURE, instance)?;
269        Ok(Self {
270            oid,
271            name: name.into(),
272            description: String::new(),
273            present_value: 4000,
274            tracking_value: 4000,
275            color_command: BACnetColorCommand::new(ColorOperation::NONE),
276            default_color_temperature: 4000,
277            default_fade_time: 0,
278            default_ramp_rate: 100,     // 100K/s
279            default_step_increment: 50, // 50K per step
280            transition: 0,              // NONE
281            in_progress: 0,             // idle
282            min_pres_value: Some(1000),
283            max_pres_value: Some(30000),
284            status_flags: StatusFlags::empty(),
285            event_state: EventState::NORMAL,
286            out_of_service: false,
287            reliability: Reliability::NO_FAULT_DETECTED,
288        })
289    }
290
291    /// Set Present_Value in kelvin and make Tracking_Value follow it immediately.
292    pub fn set_present_value(&mut self, kelvin: u32) {
293        self.present_value = kelvin;
294        self.tracking_value = kelvin;
295    }
296
297    /// Set the Min_Pres_Value and Max_Pres_Value limits, in kelvin.
298    pub fn set_min_max(&mut self, min: u32, max: u32) {
299        self.min_pres_value = Some(min);
300        self.max_pres_value = Some(max);
301    }
302
303    /// The last command written to Color_Command, or operation NONE with no
304    /// other field before any write.
305    pub fn color_command(&self) -> BACnetColorCommand {
306        self.color_command
307    }
308
309    /// Set Color_Command, as a WriteProperty of the encoded command would.
310    ///
311    /// The command is checked against the Color Temperature object's table
312    /// of colour commands (Addendum 135-2020ca): FADE_TO_CCT, RAMP_TO_CCT,
313    /// STEP_UP_CCT, STEP_DOWN_CCT and STOP are taken. FADE_TO_CCT and
314    /// RAMP_TO_CCT need a target colour temperature of 1000 to 30000 K. A
315    /// field the operation uses must be in range: fade time (FADE_TO_CCT) 100
316    /// to 86,400,000 ms, ramp rate (RAMP_TO_CCT) 1 to 30000 K/s, step
317    /// increment (the step operations) 1 to 30000 K. Fields the operation
318    /// doesn't use are kept unchecked. A refusal is VALUE_OUT_OF_RANGE and
319    /// leaves the property unchanged. The object stores the command without
320    /// carrying it out.
321    pub fn set_color_command(&mut self, command: BACnetColorCommand) -> Result<(), Error> {
322        command::check_color_temperature(&command)?;
323        self.color_command = command;
324        Ok(())
325    }
326}
327
328impl BACnetObject for ColorTemperatureObject {
329    fn object_identifier(&self) -> ObjectIdentifier {
330        self.oid
331    }
332
333    fn object_name(&self) -> &str {
334        &self.name
335    }
336
337    fn read_property(
338        &self,
339        property: PropertyIdentifier,
340        array_index: Option<u32>,
341    ) -> Result<PropertyValue, Error> {
342        if let Some(result) = read_common_properties!(self, property, array_index) {
343            return result;
344        }
345        match property {
346            p if p == PropertyIdentifier::OBJECT_TYPE => Ok(PropertyValue::Enumerated(
347                ObjectType::COLOR_TEMPERATURE.to_raw(),
348            )),
349            p if p == PropertyIdentifier::PRESENT_VALUE => {
350                Ok(PropertyValue::Unsigned(self.present_value as u64))
351            }
352            p if p == PropertyIdentifier::TRACKING_VALUE => {
353                Ok(PropertyValue::Unsigned(self.tracking_value as u64))
354            }
355            p if p == PropertyIdentifier::COLOR_COMMAND => Ok(command::encode(&self.color_command)),
356            p if p == PropertyIdentifier::DEFAULT_COLOR_TEMPERATURE => Ok(PropertyValue::Unsigned(
357                self.default_color_temperature as u64,
358            )),
359            p if p == PropertyIdentifier::DEFAULT_FADE_TIME => {
360                Ok(PropertyValue::Unsigned(self.default_fade_time as u64))
361            }
362            p if p == PropertyIdentifier::DEFAULT_RAMP_RATE => {
363                Ok(PropertyValue::Unsigned(self.default_ramp_rate as u64))
364            }
365            p if p == PropertyIdentifier::DEFAULT_STEP_INCREMENT => {
366                Ok(PropertyValue::Unsigned(self.default_step_increment as u64))
367            }
368            p if p == PropertyIdentifier::TRANSITION => {
369                Ok(PropertyValue::Enumerated(self.transition))
370            }
371            p if p == PropertyIdentifier::IN_PROGRESS => {
372                Ok(PropertyValue::Enumerated(self.in_progress))
373            }
374            p if p == PropertyIdentifier::MIN_PRES_VALUE => match self.min_pres_value {
375                Some(v) => Ok(PropertyValue::Unsigned(v as u64)),
376                None => Err(common::unknown_property_error()),
377            },
378            p if p == PropertyIdentifier::MAX_PRES_VALUE => match self.max_pres_value {
379                Some(v) => Ok(PropertyValue::Unsigned(v as u64)),
380                None => Err(common::unknown_property_error()),
381            },
382            p if p == PropertyIdentifier::EVENT_STATE => {
383                Ok(PropertyValue::Enumerated(self.event_state.to_raw()))
384            }
385            p if p == PropertyIdentifier::PROPERTY_LIST => {
386                read_property_list_property(&self.property_list(), array_index)
387            }
388            _ => Err(common::unknown_property_error()),
389        }
390    }
391
392    fn write_property(
393        &mut self,
394        property: PropertyIdentifier,
395        _array_index: Option<u32>,
396        value: PropertyValue,
397        _priority: Option<u8>,
398    ) -> Result<(), Error> {
399        if let Some(result) =
400            common::write_out_of_service(&mut self.out_of_service, property, &value)
401        {
402            return result;
403        }
404        if let Some(result) = common::write_description(&mut self.description, property, &value) {
405            return result;
406        }
407        match property {
408            p if p == PropertyIdentifier::PRESENT_VALUE => {
409                if let PropertyValue::Unsigned(v) = value {
410                    let v32 = common::u64_to_u32(v)?;
411                    // Clamp to min/max if supported
412                    if let Some(min) = self.min_pres_value {
413                        if v32 < min {
414                            return Err(common::value_out_of_range_error());
415                        }
416                    }
417                    if let Some(max) = self.max_pres_value {
418                        if v32 > max {
419                            return Err(common::value_out_of_range_error());
420                        }
421                    }
422                    self.present_value = v32;
423                    self.tracking_value = v32;
424                    Ok(())
425                } else {
426                    Err(common::invalid_data_type_error())
427                }
428            }
429            p if p == PropertyIdentifier::COLOR_COMMAND => {
430                self.color_command =
431                    command::decode_write(value, command::check_color_temperature)?;
432                Ok(())
433            }
434            _ => Err(crate::common::unhandled_write_error(
435                self.property_metadata().as_ref(),
436                property,
437                _array_index,
438            )),
439        }
440    }
441
442    fn property_metadata(&self) -> Cow<'_, [crate::property_metadata::PropertyMetadata]> {
443        metadata::for_color_temperature_object(self)
444    }
445
446    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
447        crate::property_metadata::property_list_from_metadata(self.property_metadata().as_ref())
448    }
449
450    fn supports_cov(&self) -> bool {
451        true
452    }
453}
454
455#[cfg(test)]
456mod tests {
457    use super::*;
458    use bacnet_types::enums::{ErrorClass, ErrorCode};
459
460    fn assert_property_error(error: Error, expected_code: ErrorCode) {
461        match error {
462            Error::Protocol { class, code } => {
463                assert_eq!(class, ErrorClass::PROPERTY.to_raw() as u32);
464                assert_eq!(code, expected_code.to_raw() as u32);
465            }
466            other => panic!("expected PROPERTY/{expected_code:?}, got {other:?}"),
467        }
468    }
469
470    fn assert_present_and_tracking(object: &ColorTemperatureObject, expected: u64) {
471        assert_eq!(
472            object
473                .read_property(PropertyIdentifier::PRESENT_VALUE, None)
474                .unwrap(),
475            PropertyValue::Unsigned(expected)
476        );
477        assert_eq!(
478            object
479                .read_property(PropertyIdentifier::TRACKING_VALUE, None)
480                .unwrap(),
481            PropertyValue::Unsigned(expected)
482        );
483    }
484
485    #[test]
486    fn color_temperature_rejects_overwide_present_value_without_mutation() {
487        let mut object = ColorTemperatureObject::new(1, "CT-1").unwrap();
488
489        let error = object
490            .write_property(
491                PropertyIdentifier::PRESENT_VALUE,
492                None,
493                PropertyValue::Unsigned(0x1_0000_03E8),
494                None,
495            )
496            .expect_err("over-wide Present_Value must be rejected");
497
498        assert_property_error(error, ErrorCode::VALUE_OUT_OF_RANGE);
499        assert_present_and_tracking(&object, 4000);
500    }
501
502    #[test]
503    fn color_temperature_preserves_present_value_boundaries() {
504        let mut object = ColorTemperatureObject::new(1, "CT-1").unwrap();
505
506        for value in [1000, 30000] {
507            object
508                .write_property(
509                    PropertyIdentifier::PRESENT_VALUE,
510                    None,
511                    PropertyValue::Unsigned(value),
512                    None,
513                )
514                .unwrap();
515            assert_present_and_tracking(&object, value);
516        }
517
518        let error = object
519            .write_property(
520                PropertyIdentifier::PRESENT_VALUE,
521                None,
522                PropertyValue::Unsigned(u32::MAX as u64),
523                None,
524            )
525            .expect_err("u32::MAX remains outside the configured range");
526        assert_property_error(error, ErrorCode::VALUE_OUT_OF_RANGE);
527        assert_present_and_tracking(&object, 30000);
528
529        let error = object
530            .write_property(
531                PropertyIdentifier::PRESENT_VALUE,
532                None,
533                PropertyValue::Real(1000.0),
534                None,
535            )
536            .expect_err("wrong datatype must be rejected");
537        assert_property_error(error, ErrorCode::INVALID_DATA_TYPE);
538        assert_present_and_tracking(&object, 30000);
539    }
540}