use bacnet_types::constructed::BACnetLightingCommand;
use bacnet_types::enums::{LightingOperation, ObjectType, PropertyIdentifier, Reliability};
use bacnet_types::error::Error;
use bacnet_types::primitives::{ObjectIdentifier, PropertyValue, StatusFlags};
use std::borrow::Cow;
use std::sync::Arc;
use std::time::Duration;
use crate::common::{self, read_common_properties, read_priority_array};
use crate::object_profile::{ObjectProfile, ProfileState, TagsPersistence};
use crate::traits::{BACnetObject, DeadlineWaker, MonotonicClock};
#[derive(Clone)]
pub struct LightingOutputObject {
oid: ObjectIdentifier,
name: String,
description: String,
present_value: f32,
lighting_command: BACnetLightingCommand,
lighting_command_default_priority: u32,
operation: Option<engine::Operation>,
blink_request_count: u64,
blink_warn_enable: bool,
egress_time: u32,
default_fade_time: u32,
default_ramp_rate: f32,
default_step_increment: f32,
cov_increment: f32,
finest_tracking_increment: Option<f64>,
out_of_service: bool,
status_flags: StatusFlags,
reliability: Reliability,
priority_array: [Option<f32>; 16],
relinquish_default: f32,
trims: trim::Trims,
color_link: Option<ColorLink>,
profile: ProfileState,
monotonic_clock: Option<Arc<MonotonicClock>>,
deadline_waker: Option<Arc<DeadlineWaker>>,
logical_now: Duration,
}
impl LightingOutputObject {
pub fn new(instance: u32, name: impl Into<String>) -> Result<Self, Error> {
let oid = ObjectIdentifier::new(ObjectType::LIGHTING_OUTPUT, instance)?;
Ok(Self {
oid,
name: name.into(),
description: String::new(),
present_value: 0.0,
lighting_command: BACnetLightingCommand::new(LightingOperation::NONE),
lighting_command_default_priority: 16,
operation: None,
blink_request_count: 0,
blink_warn_enable: false,
egress_time: 0,
default_fade_time: *DEFAULT_FADE_TIME_MS.start(),
default_ramp_rate: 100.0,
default_step_increment: 1.0,
cov_increment: 0.0,
finest_tracking_increment: None,
out_of_service: false,
status_flags: StatusFlags::empty(),
reliability: Reliability::NO_FAULT_DETECTED,
priority_array: [None; 16],
relinquish_default: 0.0,
trims: trim::Trims::NONE,
color_link: None,
profile: ProfileState::default(),
monotonic_clock: None,
deadline_waker: None,
logical_now: Duration::ZERO,
})
}
pub fn set_description(&mut self, desc: impl Into<String>) {
self.description = desc.into();
}
fn recalculate_present_value(&mut self) {
self.present_value =
common::recalculate_from_priority_array(&self.priority_array, self.relinquish_default);
}
pub fn set_relinquish_default(&mut self, value: f32) -> Result<(), Error> {
self.relinquish_default = normalized_level(value)?;
self.recalculate_present_value();
Ok(())
}
pub fn lighting_command(&self) -> BACnetLightingCommand {
self.lighting_command
}
pub fn set_lighting_command(&mut self, command: BACnetLightingCommand) -> Result<(), Error> {
command::check(&command)?;
self.take_lighting_command(command);
Ok(())
}
fn take_lighting_command(&mut self, command: BACnetLightingCommand) {
self.lighting_command = command;
let now = self.now();
self.execute(&command, now);
}
pub fn set_color_link(&mut self, link: Option<ColorLink>) -> Result<(), Error> {
self.color_link = link.map(ColorLink::checked).transpose()?;
Ok(())
}
pub fn color_link(&self) -> Option<&ColorLink> {
self.color_link.as_ref()
}
pub fn with_tags_persistence(
instance: u32,
name: impl Into<String>,
persistence: Arc<dyn TagsPersistence>,
) -> Result<Self, Error> {
let mut object = Self::new(instance, name)?;
object.profile = ProfileState::persistent(object.oid, persistence)?;
Ok(object)
}
pub fn wait_for_tag_saves(&self) {
self.profile.wait_for_saves();
}
pub fn set_profile(&mut self, profile: ObjectProfile) -> Result<(), Error> {
self.profile.provision(profile)
}
pub fn set_default_fade_time(&mut self, milliseconds: u32) -> Result<(), Error> {
if !DEFAULT_FADE_TIME_MS.contains(&milliseconds) {
return Err(common::value_out_of_range_error());
}
self.default_fade_time = milliseconds;
Ok(())
}
pub fn set_default_ramp_rate(&mut self, value: f32) -> Result<(), Error> {
self.default_ramp_rate = lighting_percent(value)?;
Ok(())
}
pub fn set_default_step_increment(&mut self, value: f32) -> Result<(), Error> {
self.default_step_increment = lighting_percent(value)?;
Ok(())
}
}
fn normalized_level(value: f32) -> Result<f32, Error> {
if !(0.0..=100.0).contains(&value) {
return Err(common::value_out_of_range_error());
}
Ok(if value == 0.0 {
0.0
} else if value < 1.0 {
1.0
} else {
value
})
}
const DEFAULT_FADE_TIME_MS: std::ops::RangeInclusive<u32> = 100..=86_400_000;
fn lighting_percent(value: f32) -> Result<f32, Error> {
if (0.1..=100.0).contains(&value) {
Ok(value)
} else {
Err(common::value_out_of_range_error())
}
}
impl BACnetObject for LightingOutputObject {
fn object_identifier(&self) -> ObjectIdentifier {
self.oid
}
fn object_name(&self) -> &str {
&self.name
}
fn read_property(
&self,
property: PropertyIdentifier,
array_index: Option<u32>,
) -> Result<PropertyValue, Error> {
if let Some(result) = read_common_properties!(self, property, array_index) {
return result;
}
match property {
p if p == PropertyIdentifier::OBJECT_TYPE => Ok(PropertyValue::Enumerated(
ObjectType::LIGHTING_OUTPUT.to_raw(),
)),
p if p == PropertyIdentifier::PRESENT_VALUE => {
Ok(PropertyValue::Real(self.present_value))
}
p if p == PropertyIdentifier::TRACKING_VALUE => {
Ok(PropertyValue::Real(self.tracking_value_at(self.now())))
}
p if p == PropertyIdentifier::LIGHTING_COMMAND => {
Ok(command::encode(&self.lighting_command))
}
p if p == PropertyIdentifier::LIGHTING_COMMAND_DEFAULT_PRIORITY => Ok(
PropertyValue::Unsigned(self.lighting_command_default_priority as u64),
),
p if p == PropertyIdentifier::IN_PROGRESS => Ok(PropertyValue::Enumerated(
self.in_progress_at(self.now()).to_raw(),
)),
p if p == PropertyIdentifier::BLINK_WARN_ENABLE => {
Ok(PropertyValue::Boolean(self.blink_warn_enable))
}
p if p == PropertyIdentifier::EGRESS_TIME => {
Ok(PropertyValue::Unsigned(self.egress_time as u64))
}
p if p == PropertyIdentifier::EGRESS_ACTIVE => {
Ok(PropertyValue::Boolean(self.egress_active()))
}
p if p == PropertyIdentifier::PRIORITY_ARRAY => {
read_priority_array!(self, array_index, PropertyValue::Real)
}
p if p == PropertyIdentifier::RELINQUISH_DEFAULT => {
Ok(PropertyValue::Real(self.relinquish_default))
}
p if p == PropertyIdentifier::DEFAULT_FADE_TIME => {
Ok(PropertyValue::Unsigned(u64::from(self.default_fade_time)))
}
p if p == PropertyIdentifier::DEFAULT_RAMP_RATE => {
Ok(PropertyValue::Real(self.default_ramp_rate))
}
p if p == PropertyIdentifier::DEFAULT_STEP_INCREMENT => {
Ok(PropertyValue::Real(self.default_step_increment))
}
p if p == PropertyIdentifier::COV_INCREMENT => {
Ok(PropertyValue::Real(self.cov_increment))
}
p if p == PropertyIdentifier::CURRENT_COMMAND_PRIORITY => {
Ok(common::current_command_priority(&self.priority_array))
}
p => color_link::read(self.color_link.as_ref(), p)
.or_else(|| self.read_trim(p))
.or_else(|| self.profile.read(p, array_index))
.unwrap_or_else(|| Err(common::unknown_property_error())),
}
}
fn write_property(
&mut self,
property: PropertyIdentifier,
array_index: Option<u32>,
value: PropertyValue,
priority: Option<u8>,
) -> Result<(), Error> {
if property == PropertyIdentifier::PRESENT_VALUE {
let priority = priority.unwrap_or(16);
if !(1..=16).contains(&priority) {
return Err(common::value_out_of_range_error());
}
let write = engine::PresentValueWrite::decode(value)?;
let now = self.now();
self.write_present_value(priority, write, now);
return Ok(());
}
if property == PropertyIdentifier::LIGHTING_COMMAND {
let command = command::decode_write(value)?;
self.take_lighting_command(command);
return Ok(());
}
if property == PropertyIdentifier::LIGHTING_COMMAND_DEFAULT_PRIORITY {
if let PropertyValue::Unsigned(v) = value {
if !(1..=16).contains(&v) || v == 6 {
return Err(common::value_out_of_range_error());
}
self.lighting_command_default_priority = v as u32;
return Ok(());
}
return Err(common::invalid_data_type_error());
}
if property == PropertyIdentifier::RELINQUISH_DEFAULT {
if let PropertyValue::Real(f) = value {
return self.set_relinquish_default(f);
}
return Err(common::invalid_data_type_error());
}
if property == PropertyIdentifier::BLINK_WARN_ENABLE {
if let PropertyValue::Boolean(v) = value {
self.blink_warn_enable = v;
return Ok(());
}
return Err(common::invalid_data_type_error());
}
if property == PropertyIdentifier::EGRESS_TIME {
if let PropertyValue::Unsigned(v) = value {
self.egress_time = common::u64_to_u32(v)?;
return Ok(());
}
return Err(common::invalid_data_type_error());
}
if property == PropertyIdentifier::DEFAULT_FADE_TIME {
if let PropertyValue::Unsigned(v) = value {
return self.set_default_fade_time(common::u64_to_u32(v)?);
}
return Err(common::invalid_data_type_error());
}
if property == PropertyIdentifier::DEFAULT_RAMP_RATE {
if let PropertyValue::Real(v) = value {
return self.set_default_ramp_rate(v);
}
return Err(common::invalid_data_type_error());
}
if property == PropertyIdentifier::DEFAULT_STEP_INCREMENT {
if let PropertyValue::Real(v) = value {
return self.set_default_step_increment(v);
}
return Err(common::invalid_data_type_error());
}
if let Some(result) = self.write_trim(property, &value) {
return result;
}
if let Some(result) = color_link::write(&mut self.color_link, property, &value) {
return result;
}
if let Some(result) = self.profile.write(property, array_index, &value) {
return result;
}
if let Some(result) = common::write_cov_increment(&mut self.cov_increment, property, &value)
{
return result;
}
if let Some(result) =
common::write_out_of_service(&mut self.out_of_service, property, &value)
{
return result;
}
if let Some(result) = common::write_description(&mut self.description, property, &value) {
return result;
}
Err(crate::common::unhandled_write_error(
self.property_metadata().as_ref(),
property,
array_index,
))
}
fn durable_writes_internal(&mut self) -> Option<&mut dyn crate::durable::DurableWrites> {
self.profile.capability()
}
fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
crate::property_metadata::property_list_from_metadata(self.property_metadata().as_ref())
}
fn property_metadata(&self) -> Cow<'_, [crate::property_metadata::PropertyMetadata]> {
metadata::for_lighting_output_object(self)
}
fn cov_increment(&self) -> Option<f64> {
Some(f64::from(self.cov_increment))
}
fn supports_cov(&self) -> bool {
true
}
fn advance_time_internal(&mut self, elapsed: Duration) -> bool {
self.logical_now = self.logical_now.saturating_add(elapsed);
self.advance_to(self.logical_now)
}
fn bind_monotonic_clock_internal(&mut self, clock: Option<Arc<MonotonicClock>>) {
self.monotonic_clock = clock;
}
fn bind_deadline_waker_internal(&mut self, waker: Option<Arc<DeadlineWaker>>) {
self.deadline_waker = waker;
}
fn advance_monotonic_time_internal(&mut self, now: Duration) -> bool {
self.profile.expire(now);
self.advance_to(now)
}
fn next_monotonic_deadline_internal(&self) -> Option<Duration> {
self.next_deadline()
}
fn set_tracking_cov_increment_internal(&mut self, finest: Option<f64>) {
self.finest_tracking_increment = finest;
}
fn cov_snapshot_internal(&self) -> Option<Box<dyn BACnetObject>> {
let mut snapshot = self.clone();
snapshot.logical_now = self.now();
snapshot.monotonic_clock = None;
snapshot.deadline_waker = None;
Some(Box::new(snapshot))
}
fn lighting_blink_count_internal(&self) -> u64 {
self.blink_request_count
}
}
mod binary;
mod color_link;
mod command;
mod engine;
mod metadata;
mod trim;
pub use binary::BinaryLightingOutputObject;
pub use color_link::{ColorLink, ColorOverride, LightingColor, OutputColor};
#[cfg(test)]
mod tests;
#[cfg(test)]
mod required_rows_tests;
#[cfg(test)]
mod command_tests;
#[cfg(test)]
mod present_value_tests;
#[cfg(test)]
mod engine_tests;
#[cfg(test)]
mod warn_tests;
#[cfg(test)]
mod trim_tests;
#[cfg(test)]
mod color_link_tests;