use super::super::*;
use super::*;
use bacnet_objects::analog::AnalogInputObject;
use bacnet_objects::event_enrollment::EventEnrollmentObject;
use bacnet_objects::traits::BACnetObject;
use bacnet_types::constructed::{BACnetDeviceObjectPropertyReference, BACnetEventParameter};
use bacnet_types::enums::{EngineeringUnits, ErrorCode};
use bacnet_types::error::{Error, RemoteError};
#[test]
fn out_of_range_normal_stays_normal() {
let (mut db, _ee_oid, _ai_oid) = setup_out_of_range(50.0, 80.0, 20.0, 2.0);
let transitions = evaluate_event_enrollments(&mut db, 1);
assert!(transitions.is_empty());
}
#[test]
fn out_of_range_normal_to_high_limit() {
let (mut db, ee_oid, ai_oid) = setup_out_of_range(85.0, 80.0, 20.0, 2.0);
let transitions = evaluate_event_enrollments(&mut db, 1);
assert_eq!(transitions.len(), 1);
assert_eq!(transitions[0].enrollment_oid, ee_oid);
assert_eq!(transitions[0].monitored_oid, ai_oid);
assert_eq!(transitions[0].change.from, EventState::NORMAL);
assert_eq!(transitions[0].change.to, EventState::HIGH_LIMIT);
assert_eq!(transitions[0].event_type, EventType::OUT_OF_RANGE);
let obj = db.get(&ee_oid).unwrap();
assert_eq!(
obj.read_property(PropertyIdentifier::EVENT_STATE, None)
.unwrap(),
PropertyValue::Enumerated(EventState::HIGH_LIMIT.to_raw())
);
}
#[test]
fn out_of_range_normal_to_low_limit() {
let (mut db, ee_oid, _ai_oid) = setup_out_of_range(15.0, 80.0, 20.0, 2.0);
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::LOW_LIMIT);
let obj = db.get(&ee_oid).unwrap();
assert_eq!(
obj.read_property(PropertyIdentifier::EVENT_STATE, None)
.unwrap(),
PropertyValue::Enumerated(EventState::LOW_LIMIT.to_raw())
);
}
#[test]
fn out_of_range_high_to_normal_with_deadband() {
let (mut db, ee_oid, ai_oid) = setup_out_of_range(85.0, 80.0, 20.0, 2.0);
evaluate_event_enrollments(&mut db, 1);
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(79.0),
None,
)
.unwrap();
let transitions = evaluate_event_enrollments(&mut db, 1);
assert!(transitions.is_empty(), "within deadband — no transition");
let ai = db.get_mut(&ai_oid).unwrap();
ai.write_property(
PropertyIdentifier::PRESENT_VALUE,
None,
PropertyValue::Real(77.0),
None,
)
.unwrap();
let transitions = evaluate_event_enrollments(&mut db, 1);
assert_eq!(transitions.len(), 1);
assert_eq!(transitions[0].change.from, EventState::HIGH_LIMIT);
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 out_of_range_no_change_when_already_faulted() {
let (mut db, _ee_oid, _ai_oid) = setup_out_of_range(85.0, 80.0, 20.0, 2.0);
let t1 = evaluate_event_enrollments(&mut db, 1);
assert_eq!(t1.len(), 1);
let t2 = evaluate_event_enrollments(&mut db, 1);
assert!(t2.is_empty());
}
#[test]
fn out_of_range_event_enable_suppresses_distribution_not_the_transition() {
let mut db = ObjectDatabase::new();
let mut ai = AnalogInputObject::new(10, "AI-10", EngineeringUnits::DEGREES_CELSIUS).unwrap();
ai.set_present_value(85.0);
let ai_oid = ai.object_identifier();
db.add(Box::new(ai)).unwrap();
let mut ee = EventEnrollmentObject::new(10, "EE-sup", EventType::OUT_OF_RANGE).unwrap();
ee.set_object_property_reference(Some(BACnetDeviceObjectPropertyReference::new_local(
ai_oid,
PropertyIdentifier::PRESENT_VALUE.to_raw(),
)))
.unwrap();
ee.set_event_parameters(BACnetEventParameter::OutOfRange {
time_delay: 0,
low_limit: 20.0,
high_limit: 80.0,
deadband: 2.0,
});
ee.set_event_enable(EventTransitionBits::TO_NORMAL);
let ee_oid = ee.object_identifier();
db.add(Box::new(ee)).unwrap();
let transitions = evaluate_event_enrollments(&mut db, 1);
assert_eq!(transitions.len(), 1);
assert!(
!transitions[0].distribute,
"TO_OFFNORMAL disabled: the notification must not be externally distributed"
);
assert_eq!(transitions[0].change.to, EventState::HIGH_LIMIT);
let obj = db.get(&ee_oid).unwrap();
assert_eq!(
obj.read_property(PropertyIdentifier::EVENT_STATE, None)
.unwrap(),
PropertyValue::Enumerated(EventState::HIGH_LIMIT.to_raw())
);
}
#[test]
fn out_of_range_suppressed_offnormal_still_yields_enabled_return_to_normal() {
let mut db = ObjectDatabase::new();
let mut ai = AnalogInputObject::new(11, "AI-11", EngineeringUnits::DEGREES_CELSIUS).unwrap();
ai.set_present_value(85.0);
let ai_oid = ai.object_identifier();
db.add(Box::new(ai)).unwrap();
let mut ee = EventEnrollmentObject::new(11, "EE-ret", EventType::OUT_OF_RANGE).unwrap();
ee.set_object_property_reference(Some(BACnetDeviceObjectPropertyReference::new_local(
ai_oid,
PropertyIdentifier::PRESENT_VALUE.to_raw(),
)))
.unwrap();
ee.set_event_parameters(BACnetEventParameter::OutOfRange {
time_delay: 0,
low_limit: 20.0,
high_limit: 80.0,
deadband: 2.0,
});
ee.set_event_enable(EventTransitionBits::TO_NORMAL);
let ee_oid = ee.object_identifier();
db.add(Box::new(ee)).unwrap();
let transitions = evaluate_event_enrollments(&mut db, 1);
assert_eq!(transitions.len(), 1);
assert!(!transitions[0].distribute);
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(70.0),
None,
)
.unwrap();
let transitions = evaluate_event_enrollments(&mut db, 1);
assert_eq!(
transitions.len(),
1,
"TO_NORMAL is enabled and must be reported"
);
assert_eq!(transitions[0].change.from, EventState::HIGH_LIMIT);
assert_eq!(transitions[0].change.to, EventState::NORMAL);
assert!(
transitions[0].distribute,
"TO_NORMAL is enabled — this transition must be marked eligible for \
distribution (the enrollment path does not send it yet, see #127)"
);
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 out_of_range_event_enable_zero_still_tracks_event_state() {
let mut db = ObjectDatabase::new();
let mut ai = AnalogInputObject::new(12, "AI-12", EngineeringUnits::DEGREES_CELSIUS).unwrap();
ai.set_present_value(15.0);
let ai_oid = ai.object_identifier();
db.add(Box::new(ai)).unwrap();
let mut ee = EventEnrollmentObject::new(12, "EE-zero", EventType::OUT_OF_RANGE).unwrap();
ee.set_object_property_reference(Some(BACnetDeviceObjectPropertyReference::new_local(
ai_oid,
PropertyIdentifier::PRESENT_VALUE.to_raw(),
)))
.unwrap();
ee.set_event_parameters(BACnetEventParameter::OutOfRange {
time_delay: 0,
low_limit: 20.0,
high_limit: 80.0,
deadband: 2.0,
});
ee.set_event_enable(EventTransitionBits::empty());
let ee_oid = ee.object_identifier();
db.add(Box::new(ee)).unwrap();
let transitions = evaluate_event_enrollments(&mut db, 1);
assert_eq!(transitions.len(), 1);
assert_eq!(transitions[0].change.to, EventState::LOW_LIMIT);
assert!(!transitions[0].distribute);
let obj = db.get(&ee_oid).unwrap();
assert_eq!(
obj.read_property(PropertyIdentifier::EVENT_STATE, None)
.unwrap(),
PropertyValue::Enumerated(EventState::LOW_LIMIT.to_raw()),
"Event_State is a readable property, not a notification — it tracks \
the condition regardless of Event_Enable"
);
}
#[test]
fn out_of_range_enrollment_has_no_out_of_service_gate() {
let (mut db, ee_oid, _ai_oid) = setup_out_of_range(85.0, 80.0, 20.0, 2.0);
let obj = db.get_mut(&ee_oid).unwrap();
assert!(matches!(
obj.write_property(
PropertyIdentifier::OUT_OF_SERVICE,
None,
PropertyValue::Boolean(true),
None,
),
Err(Error::Remote(RemoteError::Error { code, .. })) if code == ErrorCode::UNKNOWN_PROPERTY
));
let transitions = evaluate_event_enrollments(&mut db, 1);
assert_eq!(transitions.len(), 1);
assert_eq!(transitions[0].change.to, EventState::HIGH_LIMIT);
}