use std::collections::HashSet;
use bacnet_types::calendar::SpecificDate;
use bacnet_types::constructed::{
BACnetObjectPropertyReference, BACnetSpecialEvent, BACnetTimeValue, SpecialEventPeriod,
};
use bacnet_types::enums::{ErrorClass, ErrorCode};
use bacnet_types::error::Error;
use bacnet_types::primitives::{ObjectIdentifier, PropertyValue, Time};
use super::ScheduleObject;
use crate::common;
#[derive(Debug, Clone, PartialEq)]
pub struct ScheduleWrite {
pub value: PropertyValue,
pub priority: u8,
pub references: Vec<BACnetObjectPropertyReference>,
pub retry: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScheduleTargetOutcome {
Accepted,
DatatypeRefused,
ReferenceRefused,
Failed,
}
impl ScheduleTargetOutcome {
pub fn of(result: &Result<(), Error>) -> Self {
const DATATYPE: [ErrorCode; 2] = [
ErrorCode::INVALID_DATA_TYPE,
ErrorCode::DATATYPE_NOT_SUPPORTED,
];
const REFERENCE: [ErrorCode; 4] = [
ErrorCode::UNKNOWN_OBJECT,
ErrorCode::UNKNOWN_PROPERTY,
ErrorCode::PROPERTY_IS_NOT_AN_ARRAY,
ErrorCode::INVALID_ARRAY_INDEX,
];
let code = match result {
Ok(()) => return Self::Accepted,
Err(Error::Protocol { code, .. } | Error::Structured { code, .. }) => *code,
Err(_) => return Self::Failed,
};
let names = |codes: &[ErrorCode]| codes.iter().any(|known| known.to_raw() as u32 == code);
if names(&DATATYPE) {
Self::DatatypeRefused
} else if names(&REFERENCE) {
Self::ReferenceRefused
} else {
Self::Failed
}
}
}
impl ScheduleObject {
pub fn evaluate(
&self,
today: SpecificDate,
time: Time,
calendar_active: &dyn Fn(ObjectIdentifier) -> bool,
) -> Option<PropertyValue> {
if !self.effective_period.contains(today) {
return None;
}
let mut exception: Option<(u64, &PropertyValue)> = None;
for event in &self.exception_schedule {
if exception.is_some_and(|(best, _)| best <= event.event_priority) {
continue;
}
let in_effect = match &event.period {
SpecialEventPeriod::CalendarEntry(entry) => entry.matches(today),
SpecialEventPeriod::CalendarReference(calendar) => calendar_active(*calendar),
};
if !in_effect {
continue;
}
if let Some(value) = current_value(&event.list_of_time_values, time) {
exception = Some((event.event_priority, value));
}
}
if let Some((_, value)) = exception {
return Some(value.clone());
}
let weekly = &self.weekly_schedule[usize::from(today.weekday() - 1)];
Some(
current_value(weekly, time)
.unwrap_or(&self.schedule_default)
.clone(),
)
}
}
fn current_value(entries: &[BACnetTimeValue], time: Time) -> Option<&PropertyValue> {
let key = |t: &Time| (t.hour, t.minute, t.second, t.hundredths);
entries
.iter()
.filter(|entry| key(&entry.time) <= key(&time))
.max_by_key(|entry| key(&entry.time))
.map(|entry| &entry.value)
.filter(|value| **value != PropertyValue::Null)
}
pub(super) fn check_time_values(entries: &[BACnetTimeValue]) -> Result<(), Error> {
let mut times = HashSet::with_capacity(entries.len());
for entry in entries {
if !entry.time.is_specific() {
return Err(common::value_out_of_range_error());
}
if !entry.value.is_primitive() {
return Err(common::invalid_data_type_error());
}
if !times.insert(entry.time) {
return Err(common::protocol_error(
ErrorClass::PROPERTY,
ErrorCode::DUPLICATE_ENTRY,
));
}
}
Ok(())
}
pub(super) fn check_special_event(event: &BACnetSpecialEvent) -> Result<(), Error> {
let period_ok = match &event.period {
SpecialEventPeriod::CalendarEntry(entry) => entry.is_valid(),
SpecialEventPeriod::CalendarReference(_) => true,
};
if !(1..=16).contains(&event.event_priority) || !period_ok {
return Err(common::value_out_of_range_error());
}
check_time_values(&event.list_of_time_values)
}