Skip to main content

bacnet_objects/
color.rs

1//! Color (type 63) and Color Temperature (type 64) objects.
2//!
3//! Per ASHRAE 135-2020 Addendum bj, Clauses 12.55-12.56.
4//!
5//! Color objects represent CIE 1931 xy color coordinates.
6//! Color Temperature objects represent correlated color temperature in Kelvin.
7//! Both support fade transitions via Color_Command.
8
9use bacnet_types::enums::{ObjectType, PropertyIdentifier};
10use bacnet_types::error::Error;
11use bacnet_types::primitives::{ObjectIdentifier, PropertyValue, StatusFlags};
12use std::borrow::Cow;
13
14use crate::common::{self, read_common_properties, read_property_list_property};
15use crate::traits::BACnetObject;
16
17// ---------------------------------------------------------------------------
18// ColorObject (type 63) — CIE 1931 xy color
19// ---------------------------------------------------------------------------
20
21/// BACnet Color object (type 63).
22///
23/// Represents a color as CIE 1931 xy coordinates. Supports FADE_TO_COLOR
24/// transitions via Color_Command. Non-commandable (no priority array).
25pub struct ColorObject {
26    oid: ObjectIdentifier,
27    name: String,
28    description: String,
29    /// Present_Value: BACnetxyColor encoded as (x: REAL, y: REAL).
30    /// Stored as two f32 values.
31    present_value_x: f32,
32    present_value_y: f32,
33    /// Tracking_Value: current actual color (may differ during fade).
34    tracking_value_x: f32,
35    tracking_value_y: f32,
36    /// Color_Command: last written command (opaque bytes for now).
37    color_command: Vec<u8>,
38    /// Default_Color: startup color (x, y).
39    default_color_x: f32,
40    default_color_y: f32,
41    /// Default_Fade_Time: milliseconds (100-86400000). 0 = use device default.
42    default_fade_time: u32,
43    /// Transition: 0=NONE, 1=FADE.
44    transition: u32,
45    /// In_Progress: 0=idle, 1=fade-active.
46    in_progress: u32,
47    status_flags: StatusFlags,
48    event_state: u32,
49    out_of_service: bool,
50    reliability: u32,
51}
52
53impl ColorObject {
54    /// Create a new Color object with default white color (x=0.3127, y=0.3290 ≈ D65).
55    pub fn new(instance: u32, name: impl Into<String>) -> Result<Self, Error> {
56        let oid = ObjectIdentifier::new(ObjectType::COLOR, instance)?;
57        Ok(Self {
58            oid,
59            name: name.into(),
60            description: String::new(),
61            present_value_x: 0.3127,
62            present_value_y: 0.3290,
63            tracking_value_x: 0.3127,
64            tracking_value_y: 0.3290,
65            color_command: Vec::new(),
66            default_color_x: 0.3127,
67            default_color_y: 0.3290,
68            default_fade_time: 0,
69            transition: 0,  // NONE
70            in_progress: 0, // idle
71            status_flags: StatusFlags::empty(),
72            event_state: 0, // NORMAL
73            out_of_service: false,
74            reliability: 0,
75        })
76    }
77
78    pub fn set_present_value(&mut self, x: f32, y: f32) {
79        self.present_value_x = x;
80        self.present_value_y = y;
81        self.tracking_value_x = x;
82        self.tracking_value_y = y;
83    }
84}
85
86impl BACnetObject for ColorObject {
87    fn object_identifier(&self) -> ObjectIdentifier {
88        self.oid
89    }
90
91    fn object_name(&self) -> &str {
92        &self.name
93    }
94
95    fn read_property(
96        &self,
97        property: PropertyIdentifier,
98        array_index: Option<u32>,
99    ) -> Result<PropertyValue, Error> {
100        if let Some(result) = read_common_properties!(self, property, array_index) {
101            return result;
102        }
103        match property {
104            p if p == PropertyIdentifier::OBJECT_TYPE => {
105                Ok(PropertyValue::Enumerated(ObjectType::COLOR.to_raw()))
106            }
107            p if p == PropertyIdentifier::PRESENT_VALUE => {
108                // BACnetxyColor encoded as a list of two REALs
109                Ok(PropertyValue::List(vec![
110                    PropertyValue::Real(self.present_value_x),
111                    PropertyValue::Real(self.present_value_y),
112                ]))
113            }
114            p if p == PropertyIdentifier::TRACKING_VALUE => Ok(PropertyValue::List(vec![
115                PropertyValue::Real(self.tracking_value_x),
116                PropertyValue::Real(self.tracking_value_y),
117            ])),
118            p if p == PropertyIdentifier::COLOR_COMMAND => {
119                Ok(PropertyValue::OctetString(self.color_command.clone()))
120            }
121            p if p == PropertyIdentifier::DEFAULT_COLOR => Ok(PropertyValue::List(vec![
122                PropertyValue::Real(self.default_color_x),
123                PropertyValue::Real(self.default_color_y),
124            ])),
125            p if p == PropertyIdentifier::DEFAULT_FADE_TIME => {
126                Ok(PropertyValue::Unsigned(self.default_fade_time as u64))
127            }
128            p if p == PropertyIdentifier::TRANSITION => {
129                Ok(PropertyValue::Enumerated(self.transition))
130            }
131            p if p == PropertyIdentifier::IN_PROGRESS => {
132                Ok(PropertyValue::Enumerated(self.in_progress))
133            }
134            p if p == PropertyIdentifier::EVENT_STATE => {
135                Ok(PropertyValue::Enumerated(self.event_state))
136            }
137            p if p == PropertyIdentifier::PROPERTY_LIST => {
138                read_property_list_property(&self.property_list(), array_index)
139            }
140            _ => Err(common::unknown_property_error()),
141        }
142    }
143
144    fn write_property(
145        &mut self,
146        property: PropertyIdentifier,
147        _array_index: Option<u32>,
148        value: PropertyValue,
149        _priority: Option<u8>,
150    ) -> Result<(), Error> {
151        if let Some(result) =
152            common::write_out_of_service(&mut self.out_of_service, property, &value)
153        {
154            return result;
155        }
156        if let Some(result) = common::write_description(&mut self.description, property, &value) {
157            return result;
158        }
159        match property {
160            p if p == PropertyIdentifier::COLOR_COMMAND => {
161                if let PropertyValue::OctetString(data) = value {
162                    self.color_command = data;
163                    Ok(())
164                } else {
165                    Err(common::invalid_data_type_error())
166                }
167            }
168            p if p == PropertyIdentifier::DEFAULT_FADE_TIME => {
169                if let PropertyValue::Unsigned(v) = value {
170                    if v > 86_400_000 {
171                        return Err(common::value_out_of_range_error());
172                    }
173                    self.default_fade_time = v as u32;
174                    Ok(())
175                } else {
176                    Err(common::invalid_data_type_error())
177                }
178            }
179            _ => Err(common::write_access_denied_error()),
180        }
181    }
182
183    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
184        static PROPS: &[PropertyIdentifier] = &[
185            PropertyIdentifier::OBJECT_IDENTIFIER,
186            PropertyIdentifier::OBJECT_NAME,
187            PropertyIdentifier::DESCRIPTION,
188            PropertyIdentifier::OBJECT_TYPE,
189            PropertyIdentifier::PRESENT_VALUE,
190            PropertyIdentifier::TRACKING_VALUE,
191            PropertyIdentifier::COLOR_COMMAND,
192            PropertyIdentifier::IN_PROGRESS,
193            PropertyIdentifier::DEFAULT_COLOR,
194            PropertyIdentifier::DEFAULT_FADE_TIME,
195            PropertyIdentifier::TRANSITION,
196            PropertyIdentifier::STATUS_FLAGS,
197            PropertyIdentifier::EVENT_STATE,
198            PropertyIdentifier::OUT_OF_SERVICE,
199            PropertyIdentifier::RELIABILITY,
200        ];
201        Cow::Borrowed(PROPS)
202    }
203
204    fn supports_cov(&self) -> bool {
205        true
206    }
207}
208
209// ---------------------------------------------------------------------------
210// ColorTemperatureObject (type 64) — Correlated Color Temperature
211// ---------------------------------------------------------------------------
212
213/// BACnet Color Temperature object (type 64).
214///
215/// Represents correlated color temperature in Kelvin (typically 1000-30000).
216/// Supports FADE, RAMP, and STEP transitions via Color_Command.
217pub struct ColorTemperatureObject {
218    oid: ObjectIdentifier,
219    name: String,
220    description: String,
221    /// Present_Value: Unsigned (Kelvin).
222    present_value: u32,
223    /// Tracking_Value: current actual color temperature.
224    tracking_value: u32,
225    /// Color_Command: last written command.
226    color_command: Vec<u8>,
227    /// Default_Color_Temperature: startup value.
228    default_color_temperature: u32,
229    /// Default_Fade_Time: milliseconds.
230    default_fade_time: u32,
231    /// Default_Ramp_Rate: Kelvin per second.
232    default_ramp_rate: u32,
233    /// Default_Step_Increment: Kelvin per step.
234    default_step_increment: u32,
235    /// Transition: 0=NONE, 1=FADE, 2=RAMP.
236    transition: u32,
237    /// In_Progress: 0=idle, 1=fade-active, 2=ramp-active.
238    in_progress: u32,
239    /// Min/Max present value bounds.
240    min_pres_value: Option<u32>,
241    max_pres_value: Option<u32>,
242    status_flags: StatusFlags,
243    event_state: u32,
244    out_of_service: bool,
245    reliability: u32,
246}
247
248impl ColorTemperatureObject {
249    /// Create a new Color Temperature object with default 4000K (neutral white).
250    pub fn new(instance: u32, name: impl Into<String>) -> Result<Self, Error> {
251        let oid = ObjectIdentifier::new(ObjectType::COLOR_TEMPERATURE, instance)?;
252        Ok(Self {
253            oid,
254            name: name.into(),
255            description: String::new(),
256            present_value: 4000,
257            tracking_value: 4000,
258            color_command: Vec::new(),
259            default_color_temperature: 4000,
260            default_fade_time: 0,
261            default_ramp_rate: 100,     // 100K/s
262            default_step_increment: 50, // 50K per step
263            transition: 0,              // NONE
264            in_progress: 0,             // idle
265            min_pres_value: Some(1000),
266            max_pres_value: Some(30000),
267            status_flags: StatusFlags::empty(),
268            event_state: 0,
269            out_of_service: false,
270            reliability: 0,
271        })
272    }
273
274    pub fn set_present_value(&mut self, kelvin: u32) {
275        self.present_value = kelvin;
276        self.tracking_value = kelvin;
277    }
278
279    pub fn set_min_max(&mut self, min: u32, max: u32) {
280        self.min_pres_value = Some(min);
281        self.max_pres_value = Some(max);
282    }
283}
284
285impl BACnetObject for ColorTemperatureObject {
286    fn object_identifier(&self) -> ObjectIdentifier {
287        self.oid
288    }
289
290    fn object_name(&self) -> &str {
291        &self.name
292    }
293
294    fn read_property(
295        &self,
296        property: PropertyIdentifier,
297        array_index: Option<u32>,
298    ) -> Result<PropertyValue, Error> {
299        if let Some(result) = read_common_properties!(self, property, array_index) {
300            return result;
301        }
302        match property {
303            p if p == PropertyIdentifier::OBJECT_TYPE => Ok(PropertyValue::Enumerated(
304                ObjectType::COLOR_TEMPERATURE.to_raw(),
305            )),
306            p if p == PropertyIdentifier::PRESENT_VALUE => {
307                Ok(PropertyValue::Unsigned(self.present_value as u64))
308            }
309            p if p == PropertyIdentifier::TRACKING_VALUE => {
310                Ok(PropertyValue::Unsigned(self.tracking_value as u64))
311            }
312            p if p == PropertyIdentifier::COLOR_COMMAND => {
313                Ok(PropertyValue::OctetString(self.color_command.clone()))
314            }
315            p if p == PropertyIdentifier::DEFAULT_COLOR_TEMPERATURE => Ok(PropertyValue::Unsigned(
316                self.default_color_temperature as u64,
317            )),
318            p if p == PropertyIdentifier::DEFAULT_FADE_TIME => {
319                Ok(PropertyValue::Unsigned(self.default_fade_time as u64))
320            }
321            p if p == PropertyIdentifier::DEFAULT_RAMP_RATE => {
322                Ok(PropertyValue::Unsigned(self.default_ramp_rate as u64))
323            }
324            p if p == PropertyIdentifier::DEFAULT_STEP_INCREMENT => {
325                Ok(PropertyValue::Unsigned(self.default_step_increment as u64))
326            }
327            p if p == PropertyIdentifier::TRANSITION => {
328                Ok(PropertyValue::Enumerated(self.transition))
329            }
330            p if p == PropertyIdentifier::IN_PROGRESS => {
331                Ok(PropertyValue::Enumerated(self.in_progress))
332            }
333            p if p == PropertyIdentifier::MIN_PRES_VALUE => match self.min_pres_value {
334                Some(v) => Ok(PropertyValue::Unsigned(v as u64)),
335                None => Err(common::unknown_property_error()),
336            },
337            p if p == PropertyIdentifier::MAX_PRES_VALUE => match self.max_pres_value {
338                Some(v) => Ok(PropertyValue::Unsigned(v as u64)),
339                None => Err(common::unknown_property_error()),
340            },
341            p if p == PropertyIdentifier::EVENT_STATE => {
342                Ok(PropertyValue::Enumerated(self.event_state))
343            }
344            p if p == PropertyIdentifier::PROPERTY_LIST => {
345                read_property_list_property(&self.property_list(), array_index)
346            }
347            _ => Err(common::unknown_property_error()),
348        }
349    }
350
351    fn write_property(
352        &mut self,
353        property: PropertyIdentifier,
354        _array_index: Option<u32>,
355        value: PropertyValue,
356        _priority: Option<u8>,
357    ) -> Result<(), Error> {
358        if let Some(result) =
359            common::write_out_of_service(&mut self.out_of_service, property, &value)
360        {
361            return result;
362        }
363        if let Some(result) = common::write_description(&mut self.description, property, &value) {
364            return result;
365        }
366        match property {
367            p if p == PropertyIdentifier::PRESENT_VALUE => {
368                if let PropertyValue::Unsigned(v) = value {
369                    let v32 = v as u32;
370                    // Clamp to min/max if supported
371                    if let Some(min) = self.min_pres_value {
372                        if v32 < min {
373                            return Err(common::value_out_of_range_error());
374                        }
375                    }
376                    if let Some(max) = self.max_pres_value {
377                        if v32 > max {
378                            return Err(common::value_out_of_range_error());
379                        }
380                    }
381                    self.present_value = v32;
382                    self.tracking_value = v32;
383                    Ok(())
384                } else {
385                    Err(common::invalid_data_type_error())
386                }
387            }
388            p if p == PropertyIdentifier::COLOR_COMMAND => {
389                if let PropertyValue::OctetString(data) = value {
390                    self.color_command = data;
391                    Ok(())
392                } else {
393                    Err(common::invalid_data_type_error())
394                }
395            }
396            _ => Err(common::write_access_denied_error()),
397        }
398    }
399
400    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
401        static PROPS: &[PropertyIdentifier] = &[
402            PropertyIdentifier::OBJECT_IDENTIFIER,
403            PropertyIdentifier::OBJECT_NAME,
404            PropertyIdentifier::DESCRIPTION,
405            PropertyIdentifier::OBJECT_TYPE,
406            PropertyIdentifier::PRESENT_VALUE,
407            PropertyIdentifier::TRACKING_VALUE,
408            PropertyIdentifier::COLOR_COMMAND,
409            PropertyIdentifier::IN_PROGRESS,
410            PropertyIdentifier::DEFAULT_COLOR_TEMPERATURE,
411            PropertyIdentifier::DEFAULT_FADE_TIME,
412            PropertyIdentifier::DEFAULT_RAMP_RATE,
413            PropertyIdentifier::DEFAULT_STEP_INCREMENT,
414            PropertyIdentifier::TRANSITION,
415            PropertyIdentifier::MIN_PRES_VALUE,
416            PropertyIdentifier::MAX_PRES_VALUE,
417            PropertyIdentifier::STATUS_FLAGS,
418            PropertyIdentifier::EVENT_STATE,
419            PropertyIdentifier::OUT_OF_SERVICE,
420            PropertyIdentifier::RELIABILITY,
421        ];
422        Cow::Borrowed(PROPS)
423    }
424
425    fn supports_cov(&self) -> bool {
426        true
427    }
428}