1use crate::prelude::*;
5use alloc::borrow::Cow;
6use alloc::sync::Arc;
7use bacnet_types::constructed::BACnetLightingCommand;
8use bacnet_types::enums::{LightingOperation, ObjectType, PropertyIdentifier, Reliability};
9use bacnet_types::error::Error;
10use bacnet_types::primitives::{ObjectIdentifier, PropertyValue, StatusFlags};
11use core::time::Duration;
12
13use crate::command_source::{CommandOrigin, ValueSourceTracking};
14use crate::common::{self, read_common_properties, read_priority_array};
15use crate::object_profile::{ObjectProfile, ProfileState, TagsPersistence};
16use crate::traits::{BACnetObject, DeadlineWaker, MonotonicClock};
17
18#[derive(Clone)]
64pub struct LightingOutputObject {
65 oid: ObjectIdentifier,
66 name: String,
67 description: String,
68 present_value: f32,
69 lighting_command: BACnetLightingCommand,
71 lighting_command_default_priority: u32,
72 operation: Option<engine::Operation>,
74 blink_request_count: u64,
76 blink_warn_enable: bool,
77 egress_time: u32,
79 default_fade_time: u32,
81 default_ramp_rate: f32,
83 default_step_increment: f32,
85 cov_increment: f32,
87 finest_tracking_increment: Option<f64>,
90 out_of_service: bool,
91 status_flags: StatusFlags,
92 reliability: Reliability,
94 priority_array: [Option<f32>; 16],
95 relinquish_default: f32,
96 value_source: ValueSourceTracking,
98 trims: trim::Trims,
101 color_link: Option<ColorLink>,
104 profile: ProfileState,
106 monotonic_clock: Option<Arc<MonotonicClock>>,
107 deadline_waker: Option<Arc<DeadlineWaker>>,
108 logical_now: Duration,
110}
111
112impl LightingOutputObject {
113 pub fn new(instance: u32, name: impl Into<String>) -> Result<Self, Error> {
115 let oid = ObjectIdentifier::new(ObjectType::LIGHTING_OUTPUT, instance)?;
116 Ok(Self {
117 oid,
118 name: name.into(),
119 description: String::new(),
120 present_value: 0.0,
121 lighting_command: BACnetLightingCommand::new(LightingOperation::NONE),
122 lighting_command_default_priority: 16,
123 operation: None,
124 blink_request_count: 0,
125 blink_warn_enable: false,
126 egress_time: 0,
127 default_fade_time: *DEFAULT_FADE_TIME_MS.start(),
128 default_ramp_rate: 100.0,
129 default_step_increment: 1.0,
130 cov_increment: 0.0,
131 finest_tracking_increment: None,
132 out_of_service: false,
133 status_flags: StatusFlags::empty(),
134 reliability: Reliability::NO_FAULT_DETECTED,
135 priority_array: [None; 16],
136 relinquish_default: 0.0,
137 value_source: ValueSourceTracking::default(),
138 trims: trim::Trims::NONE,
139 color_link: None,
140 profile: ProfileState::default(),
141 monotonic_clock: None,
142 deadline_waker: None,
143 logical_now: Duration::ZERO,
144 })
145 }
146
147 pub fn set_description(&mut self, desc: impl Into<String>) {
149 self.description = desc.into();
150 }
151
152 fn recalculate_present_value(&mut self) {
159 self.present_value =
160 common::recalculate_from_priority_array(&self.priority_array, self.relinquish_default);
161 }
162
163 pub fn set_relinquish_default(&mut self, value: f32) -> Result<(), Error> {
172 self.relinquish_default = normalized_level(value)?;
173 self.recalculate_present_value();
174 Ok(())
175 }
176
177 pub fn lighting_command(&self) -> BACnetLightingCommand {
180 self.lighting_command
181 }
182
183 pub fn set_lighting_command(&mut self, command: BACnetLightingCommand) -> Result<(), Error> {
207 command::check(&command)?;
208 let priority = self.command_priority(&command);
209 self.sourced(priority, None, |object| {
210 object.take_lighting_command(command)
211 });
212 Ok(())
213 }
214
215 fn take_lighting_command(&mut self, command: BACnetLightingCommand) -> bool {
218 self.lighting_command = command;
219 let now = self.now();
220 self.execute(&command, now)
221 }
222
223 fn sourced(
228 &mut self,
229 priority: u8,
230 origin: Option<&CommandOrigin>,
231 act: impl FnOnce(&mut Self) -> bool,
232 ) {
233 let before = self
234 .value_source
235 .effect(&self.priority_array, self.relinquish_default);
236 if act(self) {
237 let index = usize::from(priority - 1);
238 match origin {
239 Some(origin) => self.value_source.record(index, origin),
240 None => self.value_source.record_unsourced(index),
241 }
242 }
243 let after = self
244 .value_source
245 .effect(&self.priority_array, self.relinquish_default);
246 self.value_source.settle(before, after);
247 }
248
249 fn advance_sourced(&mut self, now: Duration) -> bool {
252 let before = self
253 .value_source
254 .effect(&self.priority_array, self.relinquish_default);
255 let changed = self.advance_to(now);
256 let after = self
257 .value_source
258 .effect(&self.priority_array, self.relinquish_default);
259 self.value_source.settle(before, after);
260 changed
261 }
262
263 pub fn set_color_link(&mut self, link: Option<ColorLink>) -> Result<(), Error> {
275 self.color_link = link.map(ColorLink::checked).transpose()?;
276 Ok(())
277 }
278
279 pub fn color_link(&self) -> Option<&ColorLink> {
281 self.color_link.as_ref()
282 }
283
284 pub fn with_tags_persistence(
288 instance: u32,
289 name: impl Into<String>,
290 persistence: Arc<dyn TagsPersistence>,
291 ) -> Result<Self, Error> {
292 let mut object = Self::new(instance, name)?;
293 object.profile = ProfileState::persistent(object.oid, persistence)?;
294 Ok(object)
295 }
296
297 pub fn wait_for_tag_saves(&self) {
300 self.profile.wait_for_saves();
301 }
302
303 pub fn set_profile(&mut self, profile: ObjectProfile) -> Result<(), Error> {
307 self.profile.provision(profile)
308 }
309
310 pub fn set_default_fade_time(&mut self, milliseconds: u32) -> Result<(), Error> {
318 if !DEFAULT_FADE_TIME_MS.contains(&milliseconds) {
319 return Err(common::value_out_of_range_error());
320 }
321 self.default_fade_time = milliseconds;
322 Ok(())
323 }
324
325 pub fn set_default_ramp_rate(&mut self, value: f32) -> Result<(), Error> {
332 self.default_ramp_rate = lighting_percent(value)?;
333 Ok(())
334 }
335
336 pub fn set_default_step_increment(&mut self, value: f32) -> Result<(), Error> {
343 self.default_step_increment = lighting_percent(value)?;
344 Ok(())
345 }
346}
347
348fn normalized_level(value: f32) -> Result<f32, Error> {
359 if !(0.0..=100.0).contains(&value) {
360 return Err(common::value_out_of_range_error());
361 }
362 Ok(if value == 0.0 {
363 0.0
364 } else if value < 1.0 {
365 1.0
366 } else {
367 value
368 })
369}
370
371const DEFAULT_FADE_TIME_MS: core::ops::RangeInclusive<u32> = 100..=86_400_000;
374
375fn lighting_percent(value: f32) -> Result<f32, Error> {
378 if (0.1..=100.0).contains(&value) {
379 Ok(value)
380 } else {
381 Err(common::value_out_of_range_error())
382 }
383}
384
385impl BACnetObject for LightingOutputObject {
386 fn object_identifier(&self) -> ObjectIdentifier {
387 self.oid
388 }
389
390 fn object_name(&self) -> &str {
391 &self.name
392 }
393
394 fn read_property(
395 &self,
396 property: PropertyIdentifier,
397 array_index: Option<u32>,
398 ) -> Result<PropertyValue, Error> {
399 if let Some(result) = self
400 .value_source
401 .read(property, array_index, &self.priority_array)
402 {
403 return result;
404 }
405 if let Some(result) = read_common_properties!(self, property, array_index) {
406 return result;
407 }
408 match property {
409 p if p == PropertyIdentifier::OBJECT_TYPE => Ok(PropertyValue::Enumerated(
410 ObjectType::LIGHTING_OUTPUT.to_raw(),
411 )),
412 p if p == PropertyIdentifier::PRESENT_VALUE => {
413 Ok(PropertyValue::Real(self.present_value))
414 }
415 p if p == PropertyIdentifier::TRACKING_VALUE => {
419 Ok(PropertyValue::Real(self.tracking_value_at(self.now())))
420 }
421 p if p == PropertyIdentifier::LIGHTING_COMMAND => {
422 Ok(command::encode(&self.lighting_command))
423 }
424 p if p == PropertyIdentifier::LIGHTING_COMMAND_DEFAULT_PRIORITY => Ok(
425 PropertyValue::Unsigned(self.lighting_command_default_priority as u64),
426 ),
427 p if p == PropertyIdentifier::IN_PROGRESS => Ok(PropertyValue::Enumerated(
428 self.in_progress_at(self.now()).to_raw(),
429 )),
430 p if p == PropertyIdentifier::BLINK_WARN_ENABLE => {
431 Ok(PropertyValue::Boolean(self.blink_warn_enable))
432 }
433 p if p == PropertyIdentifier::EGRESS_TIME => {
434 Ok(PropertyValue::Unsigned(self.egress_time as u64))
435 }
436 p if p == PropertyIdentifier::EGRESS_ACTIVE => {
437 Ok(PropertyValue::Boolean(self.egress_active()))
438 }
439 p if p == PropertyIdentifier::PRIORITY_ARRAY => {
440 read_priority_array!(self, array_index, PropertyValue::Real)
441 }
442 p if p == PropertyIdentifier::RELINQUISH_DEFAULT => {
443 Ok(PropertyValue::Real(self.relinquish_default))
444 }
445 p if p == PropertyIdentifier::DEFAULT_FADE_TIME => {
446 Ok(PropertyValue::Unsigned(u64::from(self.default_fade_time)))
447 }
448 p if p == PropertyIdentifier::DEFAULT_RAMP_RATE => {
449 Ok(PropertyValue::Real(self.default_ramp_rate))
450 }
451 p if p == PropertyIdentifier::DEFAULT_STEP_INCREMENT => {
452 Ok(PropertyValue::Real(self.default_step_increment))
453 }
454 p if p == PropertyIdentifier::COV_INCREMENT => {
455 Ok(PropertyValue::Real(self.cov_increment))
456 }
457 p if p == PropertyIdentifier::CURRENT_COMMAND_PRIORITY => {
458 Ok(common::current_command_priority(&self.priority_array))
459 }
460 p => color_link::read(self.color_link.as_ref(), p)
461 .or_else(|| self.read_trim(p))
462 .or_else(|| self.profile.read(p, array_index))
463 .unwrap_or_else(|| Err(common::unknown_property_error())),
464 }
465 }
466
467 fn write_property_from(
468 &mut self,
469 property: PropertyIdentifier,
470 array_index: Option<u32>,
471 value: PropertyValue,
472 priority: Option<u8>,
473 origin: &CommandOrigin,
474 ) -> Result<(), Error> {
475 if matches!(
476 property,
477 PropertyIdentifier::PRESENT_VALUE | PropertyIdentifier::VALUE_SOURCE
478 ) && array_index.is_some()
479 {
480 return Err(common::property_is_not_an_array_error());
481 }
482 if property == PropertyIdentifier::VALUE_SOURCE {
483 return self.value_source.correct(value, priority, origin);
484 }
485 if property == PropertyIdentifier::PRESENT_VALUE {
490 let priority = priority.unwrap_or(16);
491 if !(1..=16).contains(&priority) {
492 return Err(common::value_out_of_range_error());
493 }
494 let write = engine::PresentValueWrite::decode(value)?;
495 ValueSourceTracking::admit(Some(priority), origin)?;
496 let now = self.now();
497 self.sourced(priority, Some(origin), |object| {
498 object.write_present_value(priority, write, now)
499 });
500 return Ok(());
501 }
502 if property == PropertyIdentifier::LIGHTING_COMMAND {
505 let command = command::decode_write(value)?;
506 let priority = self.command_priority(&command);
507 ValueSourceTracking::admit(Some(priority), origin)?;
508 self.sourced(priority, Some(origin), |object| {
509 object.take_lighting_command(command)
510 });
511 return Ok(());
512 }
513 self.write_property(property, array_index, value, priority)
514 }
515
516 fn write_property(
517 &mut self,
518 property: PropertyIdentifier,
519 array_index: Option<u32>,
520 value: PropertyValue,
521 _priority: Option<u8>,
522 ) -> Result<(), Error> {
523 if matches!(
525 property,
526 PropertyIdentifier::PRESENT_VALUE
527 | PropertyIdentifier::LIGHTING_COMMAND
528 | PropertyIdentifier::VALUE_SOURCE
529 ) {
530 return Err(common::write_access_denied_error());
531 }
532
533 if property == PropertyIdentifier::LIGHTING_COMMAND_DEFAULT_PRIORITY {
537 if let PropertyValue::Unsigned(v) = value {
538 if !(1..=16).contains(&v) || v == 6 {
539 return Err(common::value_out_of_range_error());
540 }
541 self.lighting_command_default_priority = v as u32;
542 return Ok(());
543 }
544 return Err(common::invalid_data_type_error());
545 }
546
547 if property == PropertyIdentifier::RELINQUISH_DEFAULT {
551 if let PropertyValue::Real(f) = value {
552 return self.set_relinquish_default(f);
553 }
554 return Err(common::invalid_data_type_error());
555 }
556
557 if property == PropertyIdentifier::BLINK_WARN_ENABLE {
559 if let PropertyValue::Boolean(v) = value {
560 self.blink_warn_enable = v;
561 return Ok(());
562 }
563 return Err(common::invalid_data_type_error());
564 }
565
566 if property == PropertyIdentifier::EGRESS_TIME {
568 if let PropertyValue::Unsigned(v) = value {
569 self.egress_time = common::u64_to_u32(v)?;
570 return Ok(());
571 }
572 return Err(common::invalid_data_type_error());
573 }
574
575 if property == PropertyIdentifier::DEFAULT_FADE_TIME {
579 if let PropertyValue::Unsigned(v) = value {
580 return self.set_default_fade_time(common::u64_to_u32(v)?);
581 }
582 return Err(common::invalid_data_type_error());
583 }
584 if property == PropertyIdentifier::DEFAULT_RAMP_RATE {
585 if let PropertyValue::Real(v) = value {
586 return self.set_default_ramp_rate(v);
587 }
588 return Err(common::invalid_data_type_error());
589 }
590 if property == PropertyIdentifier::DEFAULT_STEP_INCREMENT {
591 if let PropertyValue::Real(v) = value {
592 return self.set_default_step_increment(v);
593 }
594 return Err(common::invalid_data_type_error());
595 }
596
597 if let Some(result) = self.write_trim(property, &value) {
600 return result;
601 }
602 if let Some(result) = color_link::write(&mut self.color_link, property, &value) {
603 return result;
604 }
605 if let Some(result) = self.profile.write(property, array_index, &value) {
606 return result;
607 }
608 if let Some(result) = common::write_cov_increment(&mut self.cov_increment, property, &value)
609 {
610 return result;
611 }
612 if let Some(result) =
613 common::write_out_of_service(&mut self.out_of_service, property, &value)
614 {
615 return result;
616 }
617 if let Some(result) = common::write_description(&mut self.description, property, &value) {
618 return result;
619 }
620 Err(crate::common::unhandled_write_error(
621 self.property_metadata().as_ref(),
622 property,
623 array_index,
624 ))
625 }
626
627 fn durable_writes_internal(&mut self) -> Option<&mut dyn crate::durable::DurableWrites> {
628 self.profile.capability()
629 }
630
631 fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
632 crate::property_metadata::property_list_from_metadata(self.property_metadata().as_ref())
633 }
634
635 fn property_metadata(&self) -> Cow<'_, [crate::property_metadata::PropertyMetadata]> {
636 metadata::for_lighting_output_object(self)
637 }
638
639 fn cov_increment(&self) -> Option<f64> {
640 Some(f64::from(self.cov_increment))
641 }
642
643 fn supports_cov(&self) -> bool {
644 true
645 }
646
647 fn advance_time_internal(&mut self, elapsed: Duration) -> bool {
648 self.logical_now = self.logical_now.saturating_add(elapsed);
649 self.advance_sourced(self.logical_now)
650 }
651
652 fn bind_monotonic_clock_internal(&mut self, clock: Option<Arc<MonotonicClock>>) {
653 self.monotonic_clock = clock;
654 }
655
656 fn bind_deadline_waker_internal(&mut self, waker: Option<Arc<DeadlineWaker>>) {
657 self.deadline_waker = waker;
658 }
659
660 fn advance_monotonic_time_internal(&mut self, now: Duration) -> bool {
661 self.profile.expire(now);
662 self.advance_sourced(now)
663 }
664
665 fn next_monotonic_deadline_internal(&self) -> Option<Duration> {
666 self.next_deadline()
667 }
668
669 fn set_tracking_cov_increment_internal(&mut self, finest: Option<f64>) {
670 self.finest_tracking_increment = finest;
671 }
672
673 fn cov_snapshot_internal(&self) -> Option<Box<dyn BACnetObject>> {
677 let mut snapshot = self.clone();
678 snapshot.logical_now = self.now();
679 snapshot.monotonic_clock = None;
680 snapshot.deadline_waker = None;
681 Some(Box::new(snapshot))
682 }
683
684 fn lighting_blink_count_internal(&self) -> u64 {
685 self.blink_request_count
686 }
687}
688
689mod binary;
690mod color_link;
691mod command;
692mod engine;
693mod metadata;
694mod trim;
695pub use binary::BinaryLightingOutputObject;
696pub use color_link::{ColorLink, ColorOverride, LightingColor, OutputColor};
697
698#[cfg(test)]
703mod tests;
704
705#[cfg(test)]
706mod required_rows_tests;
707
708#[cfg(test)]
709mod command_tests;
710
711#[cfg(test)]
712mod present_value_tests;
713
714#[cfg(test)]
715mod engine_tests;
716
717#[cfg(test)]
718mod warn_tests;
719
720#[cfg(test)]
721mod trim_tests;
722
723#[cfg(test)]
724mod color_link_tests;
725
726#[cfg(test)]
727mod value_source_tests;