use std::borrow::Cow;
use std::sync::Arc;
use std::time::Duration;
use bacnet_types::constructed::BACnetColorCommand;
use bacnet_types::enums::{ColorOperation, ColorTransition, ObjectType, PropertyIdentifier};
use bacnet_types::error::Error;
use bacnet_types::primitives::{ObjectIdentifier, PropertyValue};
use super::engine::Engine;
use super::{
command, metadata, milliseconds, noncommandable_audit_policy, written_transition,
written_unsigned,
};
use crate::audit::{AuditPolicyAuthority, ObjectAuditPolicy};
use crate::command_source::{CommandOrigin, SingleValueSource};
use crate::common::{self, read_identity_properties};
use crate::object_profile::{ObjectProfile, ProfileState, TagsPersistence};
use crate::traits::{BACnetObject, DeadlineWaker, MonotonicClock};
use crate::transition::Transition;
const KELVIN_SAMPLE_STEP: f64 = 10.0;
#[derive(Clone)]
pub struct ColorTemperatureObject {
oid: ObjectIdentifier,
name: String,
description: String,
present_value: u32,
color_command: BACnetColorCommand,
default_color_temperature: u32,
default_fade_time: u32,
default_ramp_rate: u32,
default_step_increment: u32,
transition: ColorTransition,
min_pres_value: u32,
max_pres_value: u32,
audit_policy: ObjectAuditPolicy,
value_source: SingleValueSource,
profile: ProfileState,
engine: Engine<u32>,
}
impl ColorTemperatureObject {
pub fn new(instance: u32, name: impl Into<String>) -> Result<Self, Error> {
let oid = ObjectIdentifier::new(ObjectType::COLOR_TEMPERATURE, instance)?;
Ok(Self {
oid,
name: name.into(),
description: String::new(),
present_value: 4000,
color_command: BACnetColorCommand::new(ColorOperation::NONE),
default_color_temperature: 4000,
default_fade_time: *command::FADE_TIME_MS.start(),
default_ramp_rate: 100,
default_step_increment: 50,
transition: ColorTransition::NONE,
min_pres_value: *command::KELVIN.start(),
max_pres_value: *command::KELVIN.end(),
audit_policy: ObjectAuditPolicy::default(),
value_source: SingleValueSource::default(),
profile: ProfileState::default(),
engine: Engine::new(KELVIN_SAMPLE_STEP),
})
}
pub fn set_value_source_tracking(&mut self, enabled: bool) {
self.value_source.set_enabled(enabled);
}
pub(super) fn value_source(&self) -> &SingleValueSource {
&self.value_source
}
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(super) fn profile(&self) -> &ObjectProfile {
self.profile.profile()
}
pub fn set_audit_policy(&mut self, policy: ObjectAuditPolicy) {
self.audit_policy = noncommandable_audit_policy(policy);
}
pub(super) fn audit_policy(&self) -> &ObjectAuditPolicy {
&self.audit_policy
}
pub fn set_present_value(&mut self, kelvin: u32) -> Result<(), Error> {
let kelvin = self.written_kelvin(u64::from(kelvin))?;
self.write_present_value(kelvin);
self.value_source.forget();
Ok(())
}
pub fn set_min_max(&mut self, min: u32, max: u32) -> Result<(), Error> {
if !command::KELVIN.contains(&min) || !command::KELVIN.contains(&max) || min > max {
return Err(common::value_out_of_range_error());
}
self.min_pres_value = min;
self.max_pres_value = max;
let clamped = self.present_value.clamp(min, max);
if clamped != self.present_value {
self.engine.halt(self.engine.now());
self.present_value = clamped;
self.value_source.forget();
}
if self.default_color_temperature != 0 {
self.default_color_temperature = self.default_color_temperature.clamp(min, max);
}
Ok(())
}
pub fn color_command(&self) -> BACnetColorCommand {
self.color_command
}
pub fn set_color_command(&mut self, command: BACnetColorCommand) -> Result<(), Error> {
command::check_color_temperature(&command)?;
if self.take_color_command(command) {
self.value_source.forget();
}
Ok(())
}
fn written_kelvin(&self, kelvin: u64) -> Result<u32, Error> {
u32::try_from(kelvin)
.ok()
.filter(|kelvin| command::KELVIN.contains(kelvin))
.map(|kelvin| self.clamped(kelvin))
.ok_or_else(common::value_out_of_range_error)
}
fn clamped(&self, kelvin: u32) -> u32 {
kelvin.clamp(self.min_pres_value, self.max_pres_value)
}
fn take_color_command(&mut self, command: BACnetColorCommand) -> bool {
self.color_command = command;
let now = self.engine.now();
let tracking = self.engine.tracking(self.present_value, now);
let target = command
.target_color_temperature
.map(|kelvin| self.clamped(kelvin));
match (command.operation, target) {
(ColorOperation::FADE_TO_CCT, Some(target)) => {
let fade_time = command.fade_time.unwrap_or(self.default_fade_time);
self.present_value = target;
self.engine.start(Transition::fade(
tracking,
target,
now,
milliseconds(fade_time),
));
true
}
(ColorOperation::RAMP_TO_CCT, Some(target)) => {
let rate = command.ramp_rate.unwrap_or(self.default_ramp_rate);
self.present_value = target;
self.engine
.start(Transition::ramp(tracking, target, now, f64::from(rate)));
true
}
(operation @ (ColorOperation::STEP_UP_CCT | ColorOperation::STEP_DOWN_CCT), _) => {
let increment = command
.step_increment
.unwrap_or(self.default_step_increment);
let stepped = if operation == ColorOperation::STEP_UP_CCT {
tracking.saturating_add(increment)
} else {
tracking.saturating_sub(increment)
};
self.present_value = self.clamped(stepped);
self.engine.start(None);
true
}
(ColorOperation::STOP, _) => match self.engine.halt(now) {
Some(reached) => {
self.present_value = reached;
true
}
None => false,
},
_ => false,
}
}
fn write_output(
&mut self,
property: PropertyIdentifier,
value: PropertyValue,
) -> Result<bool, Error> {
if property == PropertyIdentifier::PRESENT_VALUE {
let PropertyValue::Unsigned(kelvin) = value else {
return Err(common::invalid_data_type_error());
};
let kelvin = self.written_kelvin(kelvin)?;
self.write_present_value(kelvin);
return Ok(true);
}
let command = command::decode_write(value, command::check_color_temperature)?;
Ok(self.take_color_command(command))
}
fn write_present_value(&mut self, kelvin: u32) {
let now = self.engine.now();
let tracking = self.engine.tracking(self.present_value, now);
self.present_value = kelvin;
let transition = match self.transition {
ColorTransition::FADE => {
Transition::fade(tracking, kelvin, now, milliseconds(self.default_fade_time))
}
ColorTransition::RAMP => {
Transition::ramp(tracking, kelvin, now, f64::from(self.default_ramp_rate))
}
_ => None,
};
self.engine.start(transition);
}
}
impl BACnetObject for ColorTemperatureObject {
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_identity_properties!(self, property, array_index) {
return result;
}
if let Some(result) = self.audit_policy.read(property, array_index) {
return result;
}
if let Some(result) = self.value_source.read(property, array_index) {
return result;
}
if let Some(result) = self.profile.read(property, array_index) {
return result;
}
let unsigned = |value: u32| Ok(PropertyValue::Unsigned(u64::from(value)));
match property {
p if p == PropertyIdentifier::OBJECT_TYPE => Ok(PropertyValue::Enumerated(
ObjectType::COLOR_TEMPERATURE.to_raw(),
)),
p if p == PropertyIdentifier::PRESENT_VALUE => unsigned(self.present_value),
p if p == PropertyIdentifier::TRACKING_VALUE => {
unsigned(self.engine.tracking(self.present_value, self.engine.now()))
}
p if p == PropertyIdentifier::COLOR_COMMAND => Ok(command::encode(&self.color_command)),
p if p == PropertyIdentifier::IN_PROGRESS => Ok(PropertyValue::Enumerated(
self.engine.in_progress(self.engine.now()).to_raw(),
)),
p if p == PropertyIdentifier::DEFAULT_COLOR_TEMPERATURE => {
unsigned(self.default_color_temperature)
}
p if p == PropertyIdentifier::DEFAULT_FADE_TIME => unsigned(self.default_fade_time),
p if p == PropertyIdentifier::DEFAULT_RAMP_RATE => unsigned(self.default_ramp_rate),
p if p == PropertyIdentifier::DEFAULT_STEP_INCREMENT => {
unsigned(self.default_step_increment)
}
p if p == PropertyIdentifier::MIN_PRES_VALUE => unsigned(self.min_pres_value),
p if p == PropertyIdentifier::MAX_PRES_VALUE => unsigned(self.max_pres_value),
p if p == PropertyIdentifier::TRANSITION => {
Ok(PropertyValue::Enumerated(self.transition.to_raw()))
}
_ => Err(common::unknown_property_error()),
}
}
fn write_property(
&mut self,
property: PropertyIdentifier,
array_index: Option<u32>,
value: PropertyValue,
priority: Option<u8>,
) -> Result<(), Error> {
if let Some(result) = common::write_description(&mut self.description, property, &value) {
return result;
}
if let Some(result) = self
.audit_policy
.write(property, array_index, &value, priority)
{
return result;
}
if let Some(result) = self.profile.write(property, array_index, &value) {
return result;
}
match property {
p if p == PropertyIdentifier::PRESENT_VALUE
|| p == PropertyIdentifier::COLOR_COMMAND =>
{
self.value_source.unsourced()?;
self.write_output(property, value)?;
}
p if p == PropertyIdentifier::DEFAULT_COLOR_TEMPERATURE => {
self.default_color_temperature = match value {
PropertyValue::Unsigned(0) => 0,
PropertyValue::Unsigned(kelvin) => self.written_kelvin(kelvin)?,
_ => return Err(common::invalid_data_type_error()),
};
}
p if p == PropertyIdentifier::DEFAULT_FADE_TIME => {
self.default_fade_time = written_unsigned(value, command::FADE_TIME_MS)?;
}
p if p == PropertyIdentifier::DEFAULT_RAMP_RATE => {
self.default_ramp_rate = written_unsigned(value, command::KELVIN_STEP)?;
}
p if p == PropertyIdentifier::DEFAULT_STEP_INCREMENT => {
self.default_step_increment = written_unsigned(value, command::KELVIN_STEP)?;
}
p if p == PropertyIdentifier::TRANSITION => {
self.transition = written_transition(value, ColorTransition::RAMP)?;
}
_ => {
return Err(common::unhandled_write_error(
self.property_metadata().as_ref(),
property,
array_index,
))
}
}
Ok(())
}
fn write_property_from(
&mut self,
property: PropertyIdentifier,
array_index: Option<u32>,
value: PropertyValue,
priority: Option<u8>,
origin: &CommandOrigin,
) -> Result<(), Error> {
match property {
PropertyIdentifier::VALUE_SOURCE => {
self.value_source.correct(array_index, value, origin)
}
PropertyIdentifier::PRESENT_VALUE | PropertyIdentifier::COLOR_COMMAND => {
self.value_source.admit(origin)?;
if self.write_output(property, value)? {
self.value_source.record(origin);
}
Ok(())
}
_ => self.write_property(property, array_index, value, priority),
}
}
fn property_metadata(&self) -> Cow<'_, [crate::property_metadata::PropertyMetadata]> {
metadata::for_color_temperature_object(self)
}
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 supports_cov(&self) -> bool {
true
}
fn audit_object_policy_internal(&self) -> ObjectAuditPolicy {
self.audit_policy
}
fn audit_policy_authority_internal(&mut self) -> Option<AuditPolicyAuthority<'_>> {
Some(AuditPolicyAuthority::new(&mut self.audit_policy))
}
fn advance_time_internal(&mut self, elapsed: Duration) -> bool {
self.engine.advance_by(elapsed)
}
fn bind_monotonic_clock_internal(&mut self, clock: Option<Arc<MonotonicClock>>) {
self.engine.bind_clock(clock);
}
fn bind_deadline_waker_internal(&mut self, waker: Option<Arc<DeadlineWaker>>) {
self.engine.bind_waker(waker);
}
fn advance_monotonic_time_internal(&mut self, now: Duration) -> bool {
self.profile.expire(now);
self.engine.advance_to(now)
}
fn next_monotonic_deadline_internal(&self) -> Option<Duration> {
self.engine.deadline()
}
fn set_tracking_cov_increment_internal(&mut self, finest: Option<f64>) {
self.engine.set_finest_increment(finest);
}
fn cov_snapshot_internal(&self) -> Option<Box<dyn BACnetObject>> {
Some(Box::new(Self {
engine: self.engine.frozen(),
..self.clone()
}))
}
}