1use 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
28const D65: BACnetXyColor = BACnetXyColor::new(0.3127, 0.3290);
30
31fn xy_value(color: BACnetXyColor) -> PropertyValue {
34 PropertyValue::List(vec![
35 PropertyValue::Real(color.x),
36 PropertyValue::Real(color.y),
37 ])
38}
39
40pub struct ColorObject {
53 oid: ObjectIdentifier,
54 name: String,
55 description: String,
56 present_value: BACnetXyColor,
58 tracking_value: BACnetXyColor,
60 color_command: BACnetColorCommand,
62 default_color: BACnetXyColor,
64 default_fade_time: u32,
66 transition: u32,
68 in_progress: u32,
70 status_flags: StatusFlags,
71 event_state: EventState,
72 out_of_service: bool,
73 reliability: Reliability,
74}
75
76impl ColorObject {
77 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, in_progress: 0, status_flags: StatusFlags::empty(),
92 event_state: EventState::NORMAL,
93 out_of_service: false,
94 reliability: Reliability::NO_FAULT_DETECTED,
95 })
96 }
97
98 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 pub fn color_command(&self) -> BACnetColorCommand {
107 self.color_command
108 }
109
110 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
222pub struct ColorTemperatureObject {
235 oid: ObjectIdentifier,
236 name: String,
237 description: String,
238 present_value: u32,
240 tracking_value: u32,
242 color_command: BACnetColorCommand,
244 default_color_temperature: u32,
246 default_fade_time: u32,
248 default_ramp_rate: u32,
250 default_step_increment: u32,
252 transition: u32,
254 in_progress: u32,
256 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 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, default_step_increment: 50, transition: 0, in_progress: 0, 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 pub fn set_present_value(&mut self, kelvin: u32) {
293 self.present_value = kelvin;
294 self.tracking_value = kelvin;
295 }
296
297 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 pub fn color_command(&self) -> BACnetColorCommand {
306 self.color_command
307 }
308
309 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 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}