use super::super::*;
use bacnet_objects::analog::AnalogInputObject;
use bacnet_objects::event_enrollment::EventEnrollmentObject;
use bacnet_objects::traits::BACnetObject;
use bacnet_types::constructed::{
BACnetDeviceObjectPropertyReference, BACnetEventParameter, ChangeOfValueCriteria,
};
use bacnet_types::enums::EngineeringUnits;
fn setup_cov(
present_value: f32,
increment: f32,
time_delay: u32,
) -> (ObjectDatabase, ObjectIdentifier, ObjectIdentifier) {
let mut db = ObjectDatabase::new();
let mut ai = AnalogInputObject::new(70, "AI-COV", EngineeringUnits::DEGREES_CELSIUS).unwrap();
ai.set_present_value(present_value);
let ai_oid = ai.object_identifier();
db.add(Box::new(ai)).unwrap();
let mut ee = EventEnrollmentObject::new(70, "EE-COV", EventType::CHANGE_OF_VALUE).unwrap();
ee.set_object_property_reference(Some(BACnetDeviceObjectPropertyReference::new_local(
ai_oid,
PropertyIdentifier::PRESENT_VALUE.to_raw(),
)))
.unwrap();
ee.set_event_parameters(BACnetEventParameter::ChangeOfValue {
time_delay,
criteria: ChangeOfValueCriteria::ReferencedPropertyIncrement(increment),
});
ee.set_event_enable(EventTransitionBits::all());
let ee_oid = ee.object_identifier();
db.add(Box::new(ee)).unwrap();
(db, ee_oid, ai_oid)
}
fn set_monitored(db: &mut ObjectDatabase, ai_oid: &ObjectIdentifier, value: f32) {
let ai = db.get_mut(ai_oid).unwrap();
ai.write_property(
PropertyIdentifier::OUT_OF_SERVICE,
None,
PropertyValue::Boolean(true),
None,
)
.unwrap();
ai.write_property(
PropertyIdentifier::PRESENT_VALUE,
None,
PropertyValue::Real(value),
None,
)
.unwrap();
}
#[test]
fn change_of_value_first_sample_establishes_baseline_without_transition() {
let (mut db, ee_oid, _ai_oid) = setup_cov(10.0, 5.0, 0);
let transitions = evaluate_event_enrollments(&mut db, 1);
assert!(
transitions.is_empty(),
"first sample initializes the baseline; it must not indicate: {transitions:?}"
);
let obj = db.get(&ee_oid).unwrap();
assert_eq!(
obj.read_property(PropertyIdentifier::EVENT_STATE, None)
.unwrap(),
PropertyValue::Enumerated(EventState::NORMAL.to_raw())
);
}
#[test]
fn change_of_value_within_increment_indicates_nothing() {
let (mut db, _ee_oid, ai_oid) = setup_cov(3.0, 5.0, 0);
assert!(evaluate_event_enrollments(&mut db, 1).is_empty());
set_monitored(&mut db, &ai_oid, 7.9); assert!(
evaluate_event_enrollments(&mut db, 1).is_empty(),
"a sub-increment change against the baseline indicates no transition"
);
}
#[test]
fn change_of_value_threshold_crossing_indicates_normal_to_normal() {
let (mut db, ee_oid, ai_oid) = setup_cov(3.0, 5.0, 0);
assert!(evaluate_event_enrollments(&mut db, 1).is_empty());
set_monitored(&mut db, &ai_oid, 8.0); let transitions = evaluate_event_enrollments(&mut db, 1);
assert_eq!(transitions.len(), 1);
assert_eq!(transitions[0].event_type, EventType::CHANGE_OF_VALUE);
assert_eq!(transitions[0].change.from, EventState::NORMAL);
assert_eq!(
transitions[0].change.to,
EventState::NORMAL,
"CHANGE_OF_VALUE can only indicate NORMAL (Figure 13-10)"
);
assert!(
evaluate_event_enrollments(&mut db, 1).is_empty(),
"no re-fire while the value holds at the new baseline"
);
set_monitored(&mut db, &ai_oid, 12.9); assert!(
evaluate_event_enrollments(&mut db, 1).is_empty(),
"the baseline advances at each indicated NORMAL transition"
);
set_monitored(&mut db, &ai_oid, 2.9); let transitions = evaluate_event_enrollments(&mut db, 1);
assert_eq!(transitions.len(), 1);
assert_eq!(transitions[0].change.from, EventState::NORMAL);
assert_eq!(transitions[0].change.to, EventState::NORMAL);
let obj = db.get(&ee_oid).unwrap();
assert_eq!(
obj.read_property(PropertyIdentifier::EVENT_STATE, None)
.unwrap(),
PropertyValue::Enumerated(EventState::NORMAL.to_raw())
);
}
#[test]
fn change_of_value_repeated_changes_each_indicate() {
let (mut db, _ee_oid, ai_oid) = setup_cov(0.0, 5.0, 0);
assert!(evaluate_event_enrollments(&mut db, 1).is_empty());
for (from_label, to) in [("first", 6.0), ("second", 11.0), ("third", 16.0)] {
set_monitored(&mut db, &ai_oid, to);
let transitions = evaluate_event_enrollments(&mut db, 1);
assert_eq!(
transitions.len(),
1,
"{from_label} +increment crossing must indicate"
);
assert_eq!(transitions[0].change.to, EventState::NORMAL);
assert!(
evaluate_event_enrollments(&mut db, 1).is_empty(),
"holding at the new baseline must not re-indicate"
);
}
}
#[test]
fn change_of_value_nonpositive_increment_never_indicates() {
let (mut db, _ee_oid, ai_oid) = setup_cov(0.0, 0.0, 0);
assert!(evaluate_event_enrollments(&mut db, 1).is_empty()); set_monitored(&mut db, &ai_oid, 1000.0);
assert!(
evaluate_event_enrollments(&mut db, 1).is_empty(),
"non-positive pIncrement can never indicate (13.3.3)"
);
}
#[test]
fn change_of_value_change_is_gated_by_the_normal_direction_delay() {
let (mut db, _ee_oid, ai_oid) = setup_cov(0.0, 5.0, 2);
assert!(evaluate_event_enrollments(&mut db, 1).is_empty());
set_monitored(&mut db, &ai_oid, 10.0); assert!(
evaluate_event_enrollments(&mut db, 1).is_empty(),
"pass 1 of the changed condition: countdown seeded, not fired"
);
assert!(
evaluate_event_enrollments(&mut db, 1).is_empty(),
"pass 2: still counting down"
);
let transitions = evaluate_event_enrollments(&mut db, 1);
assert_eq!(
transitions.len(),
1,
"pass 3: pTimeDelayNormal (= Time_Delay fallback, 2) elapsed — the NORMAL transition fires"
);
assert_eq!(transitions[0].change.to, EventState::NORMAL);
}