use bacnet_encoding::constructed::{
encode_daily_schedule, encode_date_range, encode_special_event,
};
use bacnet_types::calendar::SpecificDate;
use bacnet_types::constructed::{
BACnetDateRange, BACnetObjectPropertyReference, BACnetSpecialEvent, BACnetTimeValue,
};
use bacnet_types::enums::{
ErrorClass, ErrorCode, EventState, ObjectType, PropertyIdentifier, Reliability,
};
use bacnet_types::error::Error;
use bacnet_types::primitives::{Date, ObjectIdentifier, PropertyValue, StatusFlags, Time};
use bytes::BytesMut;
use std::borrow::Cow;
use crate::common::{self, read_property_list_property};
use crate::traits::BACnetObject;
mod calendar;
mod calendar_metadata;
mod date_list;
mod evaluation;
mod metadata;
mod out_of_service;
mod reliability;
mod targets;
mod writes;
pub use calendar::CalendarObject;
pub use evaluation::{ScheduleTargetOutcome, ScheduleWrite};
pub struct ScheduleObject {
oid: ObjectIdentifier,
name: String,
description: String,
present_value: PropertyValue,
schedule_default: PropertyValue,
out_of_service: bool,
reliability: Reliability,
reliability_before_out_of_service: Option<Reliability>,
owns_configuration_error: bool,
refusing_references: Vec<BACnetObjectPropertyReference>,
status_flags: StatusFlags,
weekly_schedule: [Vec<BACnetTimeValue>; 7],
exception_schedule: Vec<BACnetSpecialEvent>,
effective_period: BACnetDateRange,
list_of_object_property_references: Vec<BACnetObjectPropertyReference>,
priority_for_writing: u8,
in_effective_period: bool,
simulated_write: Option<PropertyValue>,
held: Option<targets::HeldCommand>,
relinquish_owed: Vec<ScheduleWrite>,
rewrite_owed: bool,
}
impl ScheduleObject {
pub fn new(
instance: u32,
name: impl Into<String>,
schedule_default: PropertyValue,
) -> Result<Self, Error> {
let oid = ObjectIdentifier::new(ObjectType::SCHEDULE, instance)?;
if !schedule_default.is_primitive() {
return Err(common::invalid_data_type_error());
}
Ok(Self {
oid,
name: name.into(),
description: String::new(),
present_value: schedule_default.clone(),
schedule_default,
out_of_service: false,
reliability: Reliability::NO_FAULT_DETECTED,
reliability_before_out_of_service: None,
owns_configuration_error: false,
refusing_references: Vec::new(),
status_flags: StatusFlags::empty(),
weekly_schedule: [vec![], vec![], vec![], vec![], vec![], vec![], vec![]],
exception_schedule: Vec::new(),
effective_period: BACnetDateRange {
start_date: unspecified_date(),
end_date: unspecified_date(),
},
list_of_object_property_references: Vec::new(),
priority_for_writing: 16, in_effective_period: false,
simulated_write: None,
held: None,
relinquish_owed: Vec::new(),
rewrite_owed: false,
})
}
pub fn set_description(&mut self, desc: impl Into<String>) {
self.description = desc.into();
}
pub fn set_weekly_schedule(
&mut self,
day_index: usize,
entries: Vec<BACnetTimeValue>,
) -> Result<(), Error> {
let Some(day) = self.weekly_schedule.get_mut(day_index) else {
return Err(common::value_out_of_range_error());
};
evaluation::check_time_values(&entries)?;
*day = entries;
self.contents_changed();
Ok(())
}
pub fn add_exception(&mut self, event: BACnetSpecialEvent) -> Result<(), Error> {
evaluation::check_special_event(&event)?;
if self.exception_schedule.len() >= writes::MAX_EXCEPTIONS {
return Err(writes::no_space_error());
}
self.exception_schedule.push(event);
self.contents_changed();
Ok(())
}
pub fn set_effective_period(&mut self, period: BACnetDateRange) -> Result<(), Error> {
if !period.is_valid() {
return Err(common::value_out_of_range_error());
}
self.effective_period = period;
Ok(())
}
pub fn set_priority_for_writing(&mut self, priority: u8) -> Result<(), Error> {
if !(1..=16).contains(&priority) {
return Err(common::value_out_of_range_error());
}
self.priority_for_writing = priority;
self.targets_changed();
Ok(())
}
pub fn add_object_property_reference(
&mut self,
r: BACnetObjectPropertyReference,
) -> Result<(), Error> {
if self.list_of_object_property_references.len() >= targets::MAX_REFERENCES {
return Err(writes::no_space_error());
}
self.list_of_object_property_references.push(r);
self.targets_changed();
Ok(())
}
pub fn set_object_property_references(
&mut self,
references: Vec<BACnetObjectPropertyReference>,
) -> Result<(), Error> {
if references.len() > targets::MAX_REFERENCES {
return Err(writes::no_space_error());
}
self.list_of_object_property_references = references;
self.targets_changed();
Ok(())
}
pub fn present_value(&self) -> &PropertyValue {
&self.present_value
}
fn contents_changed(&mut self) {
let _ = self.recompute_reliability();
}
}
fn unspecified_date() -> Date {
Date {
year: Date::UNSPECIFIED,
month: Date::UNSPECIFIED,
day: Date::UNSPECIFIED,
day_of_week: Date::UNSPECIFIED,
}
}
fn daily_schedule(time_values: &[BACnetTimeValue]) -> Result<PropertyValue, Error> {
let mut encoded = BytesMut::new();
encode_daily_schedule(&mut encoded, time_values)?;
Ok(PropertyValue::ApplicationData(encoded.to_vec()))
}
fn special_event(event: &BACnetSpecialEvent) -> Result<PropertyValue, Error> {
let mut encoded = BytesMut::new();
encode_special_event(&mut encoded, event)?;
Ok(PropertyValue::ApplicationData(encoded.to_vec()))
}
impl BACnetObject for ScheduleObject {
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> {
match property {
p if p == PropertyIdentifier::OBJECT_IDENTIFIER => {
Ok(PropertyValue::ObjectIdentifier(self.oid))
}
p if p == PropertyIdentifier::OBJECT_NAME => {
Ok(PropertyValue::CharacterString(self.name.clone()))
}
p if p == PropertyIdentifier::DESCRIPTION => {
Ok(PropertyValue::CharacterString(self.description.clone()))
}
p if p == PropertyIdentifier::OBJECT_TYPE => {
Ok(PropertyValue::Enumerated(ObjectType::SCHEDULE.to_raw()))
}
p if p == PropertyIdentifier::PRESENT_VALUE => Ok(self.present_value.clone()),
p if p == PropertyIdentifier::SCHEDULE_DEFAULT => Ok(self.schedule_default.clone()),
p if p == PropertyIdentifier::STATUS_FLAGS => Ok(common::compute_status_flags(
self.status_flags,
self.reliability,
self.out_of_service,
EventState::NORMAL,
)),
p if p == PropertyIdentifier::EVENT_STATE => {
Ok(PropertyValue::Enumerated(EventState::NORMAL.to_raw()))
}
p if p == PropertyIdentifier::RELIABILITY => {
Ok(PropertyValue::Enumerated(self.reliability.to_raw()))
}
p if p == PropertyIdentifier::OUT_OF_SERVICE => {
Ok(PropertyValue::Boolean(self.out_of_service))
}
p if p == PropertyIdentifier::WEEKLY_SCHEDULE => match array_index {
None => Ok(PropertyValue::List(
self.weekly_schedule
.iter()
.map(|day| daily_schedule(day))
.collect::<Result<_, _>>()?,
)),
Some(0) => Ok(PropertyValue::Unsigned(7)),
Some(idx) if (1..=7).contains(&idx) => {
daily_schedule(&self.weekly_schedule[(idx - 1) as usize])
}
_ => Err(common::invalid_array_index_error()),
},
p if p == PropertyIdentifier::EXCEPTION_SCHEDULE => match array_index {
None => Ok(PropertyValue::List(
self.exception_schedule
.iter()
.map(special_event)
.collect::<Result<_, _>>()?,
)),
Some(0) => Ok(PropertyValue::Unsigned(self.exception_schedule.len() as u64)),
Some(i) => (i as usize)
.checked_sub(1)
.and_then(|idx| self.exception_schedule.get(idx))
.ok_or_else(common::invalid_array_index_error)
.and_then(special_event),
},
p if p == PropertyIdentifier::EFFECTIVE_PERIOD => {
let mut encoded = BytesMut::new();
encode_date_range(&mut encoded, &self.effective_period);
Ok(PropertyValue::ApplicationData(encoded.to_vec()))
}
p if p == PropertyIdentifier::LIST_OF_OBJECT_PROPERTY_REFERENCES => {
Ok(crate::device_reference::reference_run(
&self
.list_of_object_property_references
.iter()
.map(crate::device_reference::local_property_reference)
.collect::<Vec<_>>(),
))
}
p if p == PropertyIdentifier::PRIORITY_FOR_WRITING => {
Ok(PropertyValue::Unsigned(self.priority_for_writing as u64))
}
p if p == PropertyIdentifier::PROPERTY_LIST => {
read_property_list_property(&self.property_list(), array_index)
}
_ => Err(Error::Protocol {
class: ErrorClass::PROPERTY.to_raw() as u32,
code: ErrorCode::UNKNOWN_PROPERTY.to_raw() as u32,
}),
}
}
fn write_property(
&mut self,
property: PropertyIdentifier,
array_index: Option<u32>,
value: PropertyValue,
_priority: Option<u8>,
) -> Result<(), Error> {
if property == PropertyIdentifier::PRESENT_VALUE {
return self.write_present_value(array_index, value);
}
if property == PropertyIdentifier::WEEKLY_SCHEDULE {
return self.write_weekly_schedule(array_index, value);
}
if property == PropertyIdentifier::EXCEPTION_SCHEDULE {
return self.write_exception_schedule(array_index, value);
}
if property == PropertyIdentifier::EFFECTIVE_PERIOD {
if array_index.is_some() {
return Err(common::property_is_not_an_array_error());
}
return self.write_effective_period(value);
}
if property == PropertyIdentifier::LIST_OF_OBJECT_PROPERTY_REFERENCES {
if array_index.is_some() {
return Err(common::property_is_not_an_array_error());
}
return self.write_object_property_references(value);
}
if property == PropertyIdentifier::PRIORITY_FOR_WRITING {
if array_index.is_some() {
return Err(common::property_is_not_an_array_error());
}
return self.write_priority_for_writing(value);
}
if property == PropertyIdentifier::SCHEDULE_DEFAULT {
if array_index.is_some() {
return Err(common::property_is_not_an_array_error());
}
if !value.is_primitive() {
return Err(common::invalid_data_type_error());
}
self.schedule_default = value;
self.contents_changed();
return Ok(());
}
if property == PropertyIdentifier::RELIABILITY {
if !self.out_of_service {
return Err(common::write_access_denied_error());
}
if let PropertyValue::Enumerated(raw) = value {
let v = Reliability::from_raw(raw);
if !common::is_reliability_value_valid(v) {
return Err(common::value_out_of_range_error());
}
self.reliability = v;
return Ok(());
}
return Err(Error::Protocol {
class: ErrorClass::PROPERTY.to_raw() as u32,
code: ErrorCode::INVALID_DATA_TYPE.to_raw() as u32,
});
}
if let Some(result) = common::write_out_of_service_with_reliability_restore(
&mut self.out_of_service,
&mut self.reliability,
&mut self.reliability_before_out_of_service,
property,
&value,
) {
result?;
let _ = self.recompute_reliability();
return Ok(());
}
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 property_metadata(&self) -> Cow<'_, [crate::property_metadata::PropertyMetadata]> {
metadata::for_object(self)
}
fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
crate::property_metadata::property_list_from_metadata(self.property_metadata().as_ref())
}
fn set_reliability_internal(&mut self, reliability: Reliability) -> Result<(), Error> {
if self.out_of_service {
return Err(common::write_access_denied_error());
}
if !common::is_reliability_value_valid(reliability) {
return Err(common::value_out_of_range_error());
}
self.reliability = reliability;
self.owns_configuration_error = false;
Ok(())
}
fn evaluate_reliability_internal(
&mut self,
) -> Result<crate::traits::ReliabilityEvaluation, Error> {
Ok(self.recompute_reliability())
}
fn take_owed_schedule_writes(&mut self) -> Vec<ScheduleWrite> {
self.take_owed_writes()
}
fn complete_schedule_write(
&mut self,
write: &ScheduleWrite,
outcomes: &[ScheduleTargetOutcome],
) -> bool {
self.complete_write(write, outcomes)
}
fn retry_refusals_naming(&self, target: ObjectIdentifier) -> Option<ScheduleWrite> {
self.retry_refused_naming(target)
}
fn tick_schedule(
&mut self,
today: SpecificDate,
time: Time,
calendar_active: &dyn Fn(ObjectIdentifier) -> bool,
) -> Option<ScheduleWrite> {
if self.out_of_service {
return None;
}
let Some(value) = self.evaluate(today, time, calendar_active) else {
self.in_effective_period = false;
self.rewrite_owed = false;
self.held = None;
return None;
};
let entered = !std::mem::replace(&mut self.in_effective_period, true);
if !entered && !self.rewrite_owed && value == self.present_value {
return self.retry_refused();
}
self.present_value = value.clone();
self.command(value)
}
}
#[cfg(test)]
mod tests;
#[cfg(test)]
mod calendar_tests;
#[cfg(test)]
mod evaluation_tests;
#[cfg(test)]
mod out_of_service_tests;
#[cfg(test)]
mod reliability_tests;
#[cfg(test)]
mod retry_tests;
#[cfg(test)]
mod targets_tests;
#[cfg(test)]
mod write_tests;