1use 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
25const KELVIN_SAMPLE_STEP: f64 = 10.0;
30
31#[derive(Clone)]
60pub struct ColorTemperatureObject {
61 oid: ObjectIdentifier,
62 name: String,
63 description: String,
64 present_value: u32,
67 color_command: BACnetColorCommand,
69 default_color_temperature: u32,
71 default_fade_time: u32,
73 default_ramp_rate: u32,
75 default_step_increment: u32,
77 transition: ColorTransition,
79 min_pres_value: u32,
81 max_pres_value: u32,
82 audit_policy: ObjectAuditPolicy,
84 value_source: SingleValueSource,
86 profile: ProfileState,
88 engine: Engine<u32>,
89}
90
91impl ColorTemperatureObject {
92 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 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 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 pub fn wait_for_tag_saves(&self) {
149 self.profile.wait_for_saves();
150 }
151
152 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 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 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 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 pub fn color_command(&self) -> BACnetColorCommand {
218 self.color_command
219 }
220
221 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 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 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 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 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 self.value_source.unsourced()?;
429 self.write_output(property, value)?;
430 }
431 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 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}