use std::collections::HashMap;
use std::sync::Arc;
use crate::committed_cov::{BackgroundCommit, CommittedCov};
use crate::cov::CovSubscriptionTable;
use bacnet_objects::clock::ClockFrame;
use bacnet_objects::database::ObjectDatabase;
use bacnet_objects::schedule::{ScheduleTargetOutcome, ScheduleWrite};
use bacnet_types::calendar::SpecificDate;
use bacnet_types::enums::{ObjectType, PropertyIdentifier};
use bacnet_types::primitives::{ObjectIdentifier, PropertyValue, Time};
use tokio::sync::RwLock;
use tracing::{debug, warn};
pub(crate) fn schedule_instant(frame: ClockFrame) -> Option<(SpecificDate, Time)> {
let today = SpecificDate::from_date(&frame.local_date)?;
frame
.local_time
.is_specific()
.then_some((today, frame.local_time))
}
pub async fn tick_schedules(db: &Arc<RwLock<ObjectDatabase>>) {
let runs = {
let mut db_w = db.write().await;
let schedules = db_w.find_by_type(ObjectType::SCHEDULE);
let commit = evaluate(&mut db_w, schedules);
commit.take_command_runs(db, &mut db_w)
};
crate::command_lists::run_unattached(db, runs).await;
}
pub(crate) async fn tick_schedules_committed(
db: &Arc<RwLock<ObjectDatabase>>,
cov_table: &RwLock<CovSubscriptionTable>,
) -> CommittedCov {
let mut db_w = db.write().await;
let schedules = db_w.find_by_type(ObjectType::SCHEDULE);
let commit = evaluate(&mut db_w, schedules);
commit.finish(db, &mut db_w, cov_table).await
}
pub(crate) async fn reevaluate_written(
db: &Arc<RwLock<ObjectDatabase>>,
db_w: &mut ObjectDatabase,
written: &[ObjectIdentifier],
cov_table: &RwLock<CovSubscriptionTable>,
) -> CommittedCov {
let schedules: Vec<_> = written
.iter()
.copied()
.filter(|oid| oid.object_type() == ObjectType::SCHEDULE)
.collect();
if schedules.is_empty() {
return CommittedCov::default();
}
let commit = evaluate(db_w, schedules);
commit.finish(db, db_w, cov_table).await
}
pub(crate) fn retry_for_created(
db_w: &mut ObjectDatabase,
commit: &mut BackgroundCommit,
retries: &[(ObjectIdentifier, ObjectIdentifier)],
) {
for &(schedule, created) in retries {
let Some(write) = db_w
.get(&schedule)
.and_then(|object| object.retry_refusals_naming(created))
else {
continue;
};
debug!(
schedule = %schedule,
created = %created,
"Schedule retrying the references a created object makes writable"
);
deliver(db_w, commit, schedule, &write);
}
}
fn calendar_states(db: &ObjectDatabase, today: SpecificDate) -> HashMap<ObjectIdentifier, bool> {
db.find_by_type(ObjectType::CALENDAR)
.into_iter()
.filter_map(|oid| {
let calendar = db.get(&oid)?;
let active = calendar.calendar_state_internal(today).unwrap_or_else(|| {
matches!(
calendar.read_property(PropertyIdentifier::PRESENT_VALUE, None),
Ok(PropertyValue::Boolean(true))
)
});
Some((oid, active))
})
.collect()
}
fn evaluate(db_w: &mut ObjectDatabase, schedules: Vec<ObjectIdentifier>) -> BackgroundCommit {
let mut commit = BackgroundCommit::new();
let instant = db_w.clock_frame().and_then(schedule_instant);
if instant.is_none() {
debug!("Skipping the Schedule calculation without a valid Device clock");
}
let calendars = instant
.map(|(today, _)| calendar_states(db_w, today))
.unwrap_or_default();
let calendar_active = |oid: ObjectIdentifier| calendars.get(&oid).copied().unwrap_or(false);
let mut writes = Vec::new();
for oid in schedules {
let Some(obj) = db_w.get_mut(&oid) else {
continue;
};
for write in obj.take_owed_schedule_writes() {
debug!(
schedule = %oid,
refs = write.references.len(),
"Schedule owes a write outside its calculation, writing to controlled properties"
);
writes.push((oid, write));
}
let Some((today, now)) = instant else {
continue;
};
if let Some(write) = obj.tick_schedule(today, now, &calendar_active) {
if write.retry {
debug!(
schedule = %oid,
refs = write.references.len(),
"Schedule retrying the controlled properties that refused its value"
);
} else {
debug!(
schedule = %oid,
refs = write.references.len(),
"Schedule value changed, writing to controlled properties"
);
}
writes.push((oid, write));
}
}
for (initiator, write) in writes {
deliver(db_w, &mut commit, initiator, &write);
}
commit
}
fn deliver(
db_w: &mut ObjectDatabase,
commit: &mut BackgroundCommit,
initiator: ObjectIdentifier,
write: &ScheduleWrite,
) {
let origin =
crate::command_source::resolve_local(db_w, crate::LocalCommandSource::Object(initiator))
.ok();
let null = crate::handlers::relinquish::is_null_value(&write.value);
let mut outcomes = Vec::with_capacity(write.references.len());
for reference in &write.references {
let target_oid = reference.object_identifier;
let prop_id = reference.property_identifier;
let property = PropertyIdentifier::from_raw(prop_id);
commit.before_change(db_w, target_oid);
let Some(target_obj) = db_w.get_mut(&target_oid) else {
outcomes.push(ScheduleTargetOutcome::ReferenceRefused);
continue;
};
let result = crate::handlers::check_write_array_index(
&*target_obj,
property,
reference.property_array_index,
)
.and_then(|()| {
crate::command_source::write_target(
target_obj,
property,
reference.property_array_index,
write.value.clone(),
Some(write.priority),
origin.as_ref(),
)
});
let outcome = ScheduleTargetOutcome::of(&result);
match &result {
Ok(()) => commit.changed(target_oid),
Err(error)
if null
&& crate::handlers::relinquish::leaves_unchanged(
&*target_obj,
property,
reference.property_array_index,
error,
) =>
{
outcomes.push(ScheduleTargetOutcome::Accepted);
continue;
}
Err(e)
if write.retry
&& matches!(
outcome,
ScheduleTargetOutcome::DatatypeRefused
| ScheduleTargetOutcome::ReferenceRefused
) =>
{
debug!(
target = %target_oid,
property = prop_id,
error = %e,
"Schedule retry still refused by controlled property"
)
}
Err(e) => warn!(
target = %target_oid,
property = prop_id,
error = %e,
"Schedule failed to write to controlled property"
),
}
outcomes.push(outcome);
}
let reliability_changed = db_w
.get_mut(&initiator)
.is_some_and(|schedule| schedule.complete_schedule_write(write, &outcomes));
if reliability_changed {
commit.changed(initiator);
}
}
#[cfg(test)]
#[path = "schedule_tests.rs"]
pub(crate) mod tests;
#[cfg(test)]
#[path = "schedule_command_tests.rs"]
mod command_tests;
#[cfg(test)]
#[path = "schedule_channel_tests.rs"]
mod channel_tests;
#[cfg(test)]
#[path = "schedule_relinquish_tests.rs"]
mod relinquish_tests;
#[cfg(test)]
#[path = "schedule_reference_reliability_tests.rs"]
mod reference_reliability_tests;
#[cfg(test)]
#[path = "schedule_reference_retry_tests.rs"]
mod reference_retry_tests;