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