use bacnet_types::bitstring::status_flags_from_bacnet;
use bacnet_types::constructed::{BACnetLogRecord, LogDatum, LogValue};
use bacnet_types::enums::PropertyIdentifier as P;
use bacnet_types::primitives::{Date, ObjectIdentifier, PropertyValue, StatusFlags, Time};
use super::{LocalDevice, Member, ObjectDatabase, RECONCILE, property_value_to_log_value};
use crate::cov_change::{effective_increment, is_numeric, reports_change};
use crate::traits::{BACnetObject, MonotonicClock};
#[derive(Debug, Clone)]
pub(super) struct CovBaseline {
value: Result<PropertyValue, LogValue>,
numeric: bool,
increment: Option<f64>,
flags: Option<StatusFlags>,
}
impl CovBaseline {
fn reports(&self, previous: &Self) -> bool {
let value = match (&self.value, &previous.value) {
(Ok(current), Ok(previous)) => {
reports_change(current, previous, self.increment, self.numeric)
}
(Err(current), Err(previous)) => current != previous,
_ => true,
};
value || (self.flags.is_some() && self.flags != previous.flags)
}
fn record(&self, date: Date, time: Time) -> BACnetLogRecord {
let datum = match &self.value {
Ok(value) => property_value_to_log_value(value),
Err(failure) => failure.clone(),
};
BACnetLogRecord {
date,
time,
log_datum: LogDatum::from(datum),
status_flags: self.flags,
}
}
}
fn status_flags(object: &dyn BACnetObject) -> Option<StatusFlags> {
if !object.property_list().contains(&P::STATUS_FLAGS) {
return None;
}
match object.read_property(P::STATUS_FLAGS, None) {
Ok(PropertyValue::BitString {
unused_bits: 4,
data,
}) if data.len() == 1 && data[0] & 0x0f == 0 => Some(status_flags_from_bacnet(&data)),
_ => None,
}
}
impl ObjectDatabase {
pub(super) fn poll_cov(
&mut self,
oid: ObjectIdentifier,
local: LocalDevice,
increment: Option<f32>,
monotonic: &MonotonicClock,
) {
let now = monotonic();
let entry = &self.trend_poll.0[&oid];
if entry
.retry_completed
.is_some_and(|failed| now.saturating_sub(failed) < RECONCILE)
{
return;
}
if !self
.get(&oid)
.is_some_and(|log| log.log_collecting_internal())
{
let entry = self.trend_poll.0.get_mut(&oid).unwrap();
entry.cov_baseline = None;
entry.retry_completed = None;
return;
}
let member: Member = entry.configuration.members[0];
let look = match self.read_member(local, &member) {
Ok((value, target)) => {
let Member::Reference {
property, index, ..
} = member
else {
unreachable!("a read member names a property");
};
let numeric = is_numeric(target, property, index, &value);
CovBaseline {
increment: effective_increment(target, property, numeric, increment),
numeric,
flags: status_flags(target),
value: Ok(value),
}
}
Err(failure) => CovBaseline {
value: Err(failure),
numeric: false,
increment: None,
flags: None,
},
};
if entry
.cov_baseline
.as_ref()
.is_some_and(|previous| !look.reports(previous))
{
return;
}
let frame = self
.clock_frame()
.filter(|frame| frame.is_valid_actual_datetime());
let accepted = frame.is_some_and(|frame| {
let record = look.record(frame.local_date, frame.local_time);
self.get_mut(&oid)
.unwrap()
.add_trend_record(record)
.inspect_err(|error| {
log_warn!(object = %oid, %error, "trend-log COV record insertion failed");
})
.is_ok()
});
let completed = monotonic();
let entry = self.trend_poll.0.get_mut(&oid).unwrap();
if accepted {
entry.cov_baseline = Some(look);
entry.last_success = Some(completed);
entry.retry_completed = None;
} else {
entry.retry_completed = Some(completed);
}
}
}