use bacnet_objects::database::ObjectDatabase;
use bacnet_types::enums::{ObjectType, PropertyIdentifier, Reliability};
use bacnet_types::primitives::{ObjectIdentifier, PropertyValue};
#[derive(Debug, Clone, PartialEq)]
pub struct ReliabilityChange {
pub object_id: ObjectIdentifier,
pub old_reliability: u32,
pub new_reliability: u32,
}
pub struct FaultDetector {
pub comm_timeout: Option<std::time::Duration>,
}
impl Default for FaultDetector {
fn default() -> Self {
Self {
comm_timeout: Some(std::time::Duration::from_secs(60)),
}
}
}
impl FaultDetector {
pub fn new(comm_timeout: Option<std::time::Duration>) -> Self {
Self { comm_timeout }
}
pub fn evaluate(&self, db: &mut ObjectDatabase) -> Vec<ReliabilityChange> {
let analog_types = [
ObjectType::ANALOG_INPUT,
ObjectType::ANALOG_OUTPUT,
ObjectType::ANALOG_VALUE,
];
let mut updates: Vec<(ObjectIdentifier, u32, u32)> = Vec::new();
for &obj_type in &analog_types {
let oids = db.find_by_type(obj_type);
for oid in oids {
if let Some(obj) = db.get(&oid) {
let current_reliability =
match obj.read_property(PropertyIdentifier::RELIABILITY, None) {
Ok(PropertyValue::Enumerated(v)) => v,
_ => 0,
};
let present_value =
match obj.read_property(PropertyIdentifier::PRESENT_VALUE, None) {
Ok(PropertyValue::Real(v)) => v,
_ => continue,
};
let min_pres = obj
.read_property(PropertyIdentifier::MIN_PRES_VALUE, None)
.ok()
.and_then(|v| match v {
PropertyValue::Real(f) => Some(f),
_ => None,
});
let max_pres = obj
.read_property(PropertyIdentifier::MAX_PRES_VALUE, None)
.ok()
.and_then(|v| match v {
PropertyValue::Real(f) => Some(f),
_ => None,
});
let new_reliability = if let Some(max) = max_pres {
if present_value > max {
Reliability::OVER_RANGE.to_raw()
} else if let Some(min) = min_pres {
if present_value < min {
Reliability::UNDER_RANGE.to_raw()
} else {
Reliability::NO_FAULT_DETECTED.to_raw()
}
} else {
Reliability::NO_FAULT_DETECTED.to_raw()
}
} else if let Some(min) = min_pres {
if present_value < min {
Reliability::UNDER_RANGE.to_raw()
} else {
Reliability::NO_FAULT_DETECTED.to_raw()
}
} else {
continue;
};
if new_reliability != current_reliability {
updates.push((oid, current_reliability, new_reliability));
}
}
}
}
let mut changes = Vec::new();
for (oid, old_rel, new_rel) in updates {
if let Some(obj) = db.get_mut(&oid) {
if obj
.write_property(
PropertyIdentifier::RELIABILITY,
None,
PropertyValue::Enumerated(new_rel),
None,
)
.is_ok()
{
changes.push(ReliabilityChange {
object_id: oid,
old_reliability: old_rel,
new_reliability: new_rel,
});
}
}
}
changes
}
}
#[cfg(test)]
mod tests {
use super::*;
use bacnet_objects::analog::{AnalogInputObject, AnalogOutputObject, AnalogValueObject};
fn db_with_analog_input(
present_value: f32,
min_pres: Option<f32>,
max_pres: Option<f32>,
) -> ObjectDatabase {
let mut ai = AnalogInputObject::new(1, "AI-1", 62).unwrap();
ai.set_present_value(present_value);
if let Some(min) = min_pres {
ai.set_min_pres_value(min);
}
if let Some(max) = max_pres {
ai.set_max_pres_value(max);
}
let mut db = ObjectDatabase::new();
db.add(Box::new(ai)).unwrap();
db
}
#[test]
fn no_fault_when_in_range() {
let mut db = db_with_analog_input(50.0, Some(0.0), Some(100.0));
let detector = FaultDetector::default();
let changes = detector.evaluate(&mut db);
assert!(changes.is_empty(), "no change expected for in-range value");
}
#[test]
fn over_range_detected() {
let mut db = db_with_analog_input(150.0, Some(0.0), Some(100.0));
let detector = FaultDetector::default();
let changes = detector.evaluate(&mut db);
assert_eq!(changes.len(), 1);
assert_eq!(changes[0].new_reliability, Reliability::OVER_RANGE.to_raw());
assert_eq!(
changes[0].old_reliability,
Reliability::NO_FAULT_DETECTED.to_raw()
);
}
#[test]
fn under_range_detected() {
let mut db = db_with_analog_input(-10.0, Some(0.0), Some(100.0));
let detector = FaultDetector::default();
let changes = detector.evaluate(&mut db);
assert_eq!(changes.len(), 1);
assert_eq!(
changes[0].new_reliability,
Reliability::UNDER_RANGE.to_raw()
);
}
#[test]
fn returns_to_no_fault_after_correction() {
let mut db = db_with_analog_input(150.0, Some(0.0), Some(100.0));
let detector = FaultDetector::default();
let changes = detector.evaluate(&mut db);
assert_eq!(changes.len(), 1);
assert_eq!(changes[0].new_reliability, Reliability::OVER_RANGE.to_raw());
let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
let obj = db.get_mut(&oid).unwrap();
obj.write_property(
PropertyIdentifier::PRESENT_VALUE,
None,
PropertyValue::Real(50.0),
None,
)
.unwrap_or_else(|_| {
obj.write_property(
PropertyIdentifier::OUT_OF_SERVICE,
None,
PropertyValue::Boolean(true),
None,
)
.unwrap();
obj.write_property(
PropertyIdentifier::PRESENT_VALUE,
None,
PropertyValue::Real(50.0),
None,
)
.unwrap();
});
let changes = detector.evaluate(&mut db);
assert_eq!(changes.len(), 1);
assert_eq!(
changes[0].new_reliability,
Reliability::NO_FAULT_DETECTED.to_raw()
);
}
#[test]
fn no_limits_means_no_evaluation() {
let mut db = db_with_analog_input(999.0, None, None);
let detector = FaultDetector::default();
let changes = detector.evaluate(&mut db);
assert!(changes.is_empty());
}
#[test]
fn max_only_over_range() {
let mut db = db_with_analog_input(200.0, None, Some(100.0));
let detector = FaultDetector::default();
let changes = detector.evaluate(&mut db);
assert_eq!(changes.len(), 1);
assert_eq!(changes[0].new_reliability, Reliability::OVER_RANGE.to_raw());
}
#[test]
fn min_only_under_range() {
let mut db = db_with_analog_input(-5.0, Some(0.0), None);
let detector = FaultDetector::default();
let changes = detector.evaluate(&mut db);
assert_eq!(changes.len(), 1);
assert_eq!(
changes[0].new_reliability,
Reliability::UNDER_RANGE.to_raw()
);
}
#[test]
fn no_change_emitted_when_already_faulted() {
let mut db = db_with_analog_input(150.0, Some(0.0), Some(100.0));
let detector = FaultDetector::default();
let changes = detector.evaluate(&mut db);
assert_eq!(changes.len(), 1);
let changes = detector.evaluate(&mut db);
assert!(changes.is_empty());
}
#[test]
fn evaluates_multiple_analog_types() {
let mut db = ObjectDatabase::new();
let mut ai = AnalogInputObject::new(1, "AI-1", 62).unwrap();
ai.set_present_value(200.0);
ai.set_max_pres_value(100.0);
db.add(Box::new(ai)).unwrap();
let ao = AnalogOutputObject::new(1, "AO-1", 62).unwrap();
db.add(Box::new(ao)).unwrap();
let mut av = AnalogValueObject::new(1, "AV-1", 62).unwrap();
av.set_present_value(-10.0);
av.set_min_pres_value(0.0);
db.add(Box::new(av)).unwrap();
let detector = FaultDetector::default();
let changes = detector.evaluate(&mut db);
assert_eq!(changes.len(), 2);
}
}