use super::super::*;
use super::*;
use bacnet_objects::binary::BinaryInputObject;
use bacnet_objects::event_enrollment::EventEnrollmentObject;
use bacnet_objects::notification_class::NotificationClass;
use bacnet_objects::traits::BACnetObject;
use bacnet_types::bitstring::EventTransitionBits;
use bacnet_types::constructed::{
BACnetDeviceObjectPropertyReference, BACnetEventParameter, BACnetPropertyStates,
};
fn setup_cos(
present_value: u32,
alarm_values: &[u32],
time_delay: u32,
) -> (ObjectDatabase, ObjectIdentifier, ObjectIdentifier) {
let mut db = ObjectDatabase::new();
let mut bi = BinaryInputObject::new(1, "BI-1").unwrap();
bi.set_present_value(present_value);
let bi_oid = bi.object_identifier();
db.add(Box::new(bi)).unwrap();
let mut ee = EventEnrollmentObject::new(3, "EE-COS", EventType::CHANGE_OF_STATE).unwrap();
ee.set_object_property_reference(Some(BACnetDeviceObjectPropertyReference::new_local(
bi_oid,
PropertyIdentifier::PRESENT_VALUE.to_raw(),
)))
.unwrap();
ee.set_event_parameters(BACnetEventParameter::ChangeOfState {
time_delay,
list_of_values: alarm_values
.iter()
.map(|v| BACnetPropertyStates::BinaryValue(*v))
.collect(),
});
ee.set_event_enable(EventTransitionBits::all());
let ee_oid = ee.object_identifier();
db.add(Box::new(ee)).unwrap();
(db, ee_oid, bi_oid)
}
fn set_monitored(db: &mut ObjectDatabase, bi_oid: &ObjectIdentifier, value: u32) {
let bi = db.get_mut(bi_oid).unwrap();
bi.write_property(
PropertyIdentifier::OUT_OF_SERVICE,
None,
PropertyValue::Boolean(true),
None,
)
.unwrap();
bi.write_property(
PropertyIdentifier::PRESENT_VALUE,
None,
PropertyValue::Enumerated(value),
None,
)
.unwrap();
}
fn event_state(db: &ObjectDatabase, ee_oid: &ObjectIdentifier) -> EventState {
match db
.get(ee_oid)
.unwrap()
.read_property(PropertyIdentifier::EVENT_STATE, None)
.unwrap()
{
PropertyValue::Enumerated(v) => EventState::from_raw(v),
other => panic!("EVENT_STATE must read Enumerated, got {other:?}"),
}
}
fn acked_transitions(db: &ObjectDatabase, ee_oid: &ObjectIdentifier) -> EventTransitionBits {
match db
.get(ee_oid)
.unwrap()
.read_property(PropertyIdentifier::ACKED_TRANSITIONS, None)
.unwrap()
{
PropertyValue::BitString { data, .. } => EventTransitionBits::from_bacnet(&data),
other => panic!("ACKED_TRANSITIONS must read BitString, got {other:?}"),
}
}
#[test]
fn cos_moving_between_alarm_values_reindicates_offnormal() {
let (mut db, ee_oid, _bi_oid) = setup_cos(1, &[1, 0], 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::OFFNORMAL);
assert!(
evaluate_event_enrollments(&mut db, 1).is_empty(),
"value unchanged at the causing alarm value: no condition true, no transition"
);
set_monitored(&mut db, &_bi_oid, 0);
let transitions = evaluate_event_enrollments(&mut db, 1);
assert_eq!(
transitions.len(),
1,
"condition (c): OFFNORMAL -> OFFNORMAL must be emitted"
);
assert_eq!(transitions[0].change.from, EventState::OFFNORMAL);
assert_eq!(transitions[0].change.to, EventState::OFFNORMAL);
assert_eq!(transitions[0].event_type, EventType::CHANGE_OF_STATE);
assert_eq!(
event_state(&db, &ee_oid),
EventState::OFFNORMAL,
"the specific state is stored, unchanged in value"
);
assert!(
evaluate_event_enrollments(&mut db, 1).is_empty(),
"no re-fire while holding the NEW causing value"
);
set_monitored(&mut db, &_bi_oid, 1);
assert_eq!(
evaluate_event_enrollments(&mut db, 1).len(),
1,
"moving back re-indicates again (causing value tracked per fire, not per config)"
);
}
#[test]
fn cos_same_state_reindication_is_delayed_and_value_discriminated() {
let (mut db, _ee_oid, bi_oid) = setup_cos(1, &[1, 0], 3);
for _ in 0..3 {
assert!(evaluate_event_enrollments(&mut db, 1).is_empty());
}
assert_eq!(
evaluate_event_enrollments(&mut db, 1)[0].change.to,
EventState::OFFNORMAL,
"condition (a) fires after Time_Delay"
);
set_monitored(&mut db, &bi_oid, 0);
for pass in 1..=2 {
assert!(
evaluate_event_enrollments(&mut db, 1).is_empty(),
"(c) pass {pass}: pTimeDelay countdown must gate the same-state re-indication"
);
}
set_monitored(&mut db, &bi_oid, 1);
assert!(evaluate_event_enrollments(&mut db, 1).is_empty());
set_monitored(&mut db, &bi_oid, 0);
for pass in 1..=3 {
assert!(
evaluate_event_enrollments(&mut db, 1).is_empty(),
"re-seeded (c) pass {pass}"
);
}
let transitions = evaluate_event_enrollments(&mut db, 1);
assert_eq!(transitions.len(), 1);
assert_eq!(transitions[0].change.from, EventState::OFFNORMAL);
assert_eq!(transitions[0].change.to, EventState::OFFNORMAL);
}
#[test]
fn cos_initial_delay_accepts_any_continuously_matched_alarm_value() {
let (mut db, _ee_oid, bi_oid) = setup_cos(1, &[1, 0], 2);
assert!(evaluate_event_enrollments(&mut db, 1).is_empty());
set_monitored(&mut db, &bi_oid, 0);
assert!(evaluate_event_enrollments(&mut db, 1).is_empty());
assert_eq!(
evaluate_event_enrollments(&mut db, 1)[0].change.to,
EventState::OFFNORMAL
);
}
#[test]
fn oor_persisting_offnormal_emits_nothing() {
let (mut db, ee_oid, _ai_oid) = setup_out_of_range(85.0, 80.0, 20.0, 2.0);
assert_eq!(evaluate_event_enrollments(&mut db, 1).len(), 1);
for pass in 1..=5 {
assert!(
evaluate_event_enrollments(&mut db, 1).is_empty(),
"pass {pass}: persisting HIGH_LIMIT satisfies no condition \
(Clause 13.3 introduction) — nothing may be emitted"
);
}
assert_eq!(event_state(&db, &ee_oid), EventState::HIGH_LIMIT);
}
#[test]
fn oor_across_band_stores_the_specific_state() {
let (mut db, ee_oid, ai_oid) = setup_out_of_range(85.0, 80.0, 20.0, 2.0);
assert_eq!(evaluate_event_enrollments(&mut db, 1).len(), 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(15.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::LOW_LIMIT,
"the specific state LOW_LIMIT is stored and reported, not collapsed"
);
assert_eq!(event_state(&db, &ee_oid), EventState::LOW_LIMIT);
}
#[test]
fn acked_transitions_bit_clears_when_notification_class_requires_ack() {
let (mut db, ee_oid, bi_oid) = setup_cos(1, &[1, 0], 0);
let mut nc = NotificationClass::new(7, "NC-7").unwrap();
nc.ack_required = EventTransitionBits::TO_OFFNORMAL;
db.add(Box::new(nc)).unwrap();
db.get_mut(&ee_oid)
.unwrap()
.write_property(
PropertyIdentifier::NOTIFICATION_CLASS,
None,
PropertyValue::Unsigned(7),
None,
)
.unwrap();
assert_eq!(
acked_transitions(&db, &ee_oid),
EventTransitionBits::all(),
"initial condition: no event of any type has ever occurred (Clause 12.12)"
);
assert_eq!(evaluate_event_enrollments(&mut db, 1).len(), 1);
assert_eq!(
acked_transitions(&db, &ee_oid),
EventTransitionBits::TO_FAULT | EventTransitionBits::TO_NORMAL,
"TO_OFFNORMAL ack owed -> Acked_Transitions bit 0 cleared (13.2.3)"
);
set_monitored(&mut db, &bi_oid, 0);
assert_eq!(evaluate_event_enrollments(&mut db, 1).len(), 1);
assert_eq!(
acked_transitions(&db, &ee_oid),
EventTransitionBits::TO_FAULT | EventTransitionBits::TO_NORMAL
);
}
#[test]
fn acked_transitions_bit_sets_when_no_ack_required() {
let (mut db, ee_oid, _bi_oid) = setup_cos(1, &[1], 0);
assert_eq!(evaluate_event_enrollments(&mut db, 1).len(), 1);
assert_eq!(
acked_transitions(&db, &ee_oid),
EventTransitionBits::all(),
"no Ack_Required available -> the bit is set (13.2.3)"
);
let nc = NotificationClass::new(3, "NC-3").unwrap();
db.add(Box::new(nc)).unwrap();
{
let obj = db.get_mut(&ee_oid).unwrap();
obj.write_property(
PropertyIdentifier::NOTIFICATION_CLASS,
None,
PropertyValue::Unsigned(3),
None,
)
.unwrap();
obj.set_acked_transitions_internal(EventTransitionBits::TO_NORMAL, false)
.unwrap();
}
assert_eq!(
acked_transitions(&db, &ee_oid),
EventTransitionBits::TO_OFFNORMAL | EventTransitionBits::TO_FAULT
);
set_monitored(&mut db, &_bi_oid, 0);
assert_eq!(evaluate_event_enrollments(&mut db, 1).len(), 1);
assert_eq!(
acked_transitions(&db, &ee_oid),
EventTransitionBits::all(),
"TO_NORMAL with ack not required -> bit 2 set by the transition action"
);
}
#[test]
fn event_enable_suppresses_distribution_not_same_state_actions() {
let (mut db, ee_oid, bi_oid) = setup_cos(1, &[1, 0], 0);
db.get_mut(&ee_oid)
.unwrap()
.write_property(
PropertyIdentifier::EVENT_ENABLE,
None,
PropertyValue::BitString {
unused_bits: 5,
data: vec![0x60], },
None,
)
.unwrap();
let transitions = evaluate_event_enrollments(&mut db, 1);
assert_eq!(transitions.len(), 1);
assert!(
!transitions[0].distribute,
"TO_OFFNORMAL cleared: distribution suppressed, transition reported"
);
assert_eq!(event_state(&db, &ee_oid), EventState::OFFNORMAL);
set_monitored(&mut db, &bi_oid, 0);
let transitions = evaluate_event_enrollments(&mut db, 1);
assert_eq!(
transitions.len(),
1,
"condition (c)'s same-state transition is emitted even with distribution off"
);
assert!(!transitions[0].distribute);
assert_eq!(transitions[0].change.from, EventState::OFFNORMAL);
assert_eq!(transitions[0].change.to, EventState::OFFNORMAL);
}
#[test]
fn acked_transitions_to_normal_clear_with_ack_required() {
let (mut db, ee_oid, bi_oid) = setup_cos(1, &[1], 0);
let mut nc = NotificationClass::new(9, "NC-9").unwrap();
nc.ack_required = EventTransitionBits::TO_NORMAL;
db.add(Box::new(nc)).unwrap();
db.get_mut(&ee_oid)
.unwrap()
.write_property(
PropertyIdentifier::NOTIFICATION_CLASS,
None,
PropertyValue::Unsigned(9),
None,
)
.unwrap();
assert_eq!(evaluate_event_enrollments(&mut db, 1).len(), 1);
assert_eq!(acked_transitions(&db, &ee_oid), EventTransitionBits::all());
set_monitored(&mut db, &bi_oid, 0);
assert_eq!(evaluate_event_enrollments(&mut db, 1).len(), 1);
assert_eq!(
acked_transitions(&db, &ee_oid),
EventTransitionBits::TO_OFFNORMAL | EventTransitionBits::TO_FAULT,
"TO_NORMAL ack owed -> bit 2 cleared (13.2.3 is direction-complete)"
);
}
#[test]
fn ack_bit_maintenance_is_independent_of_event_enable() {
let (mut db, ee_oid, _bi_oid) = setup_cos(1, &[1], 0);
let mut nc = NotificationClass::new(11, "NC-11").unwrap();
nc.ack_required = EventTransitionBits::TO_OFFNORMAL;
db.add(Box::new(nc)).unwrap();
{
let obj = db.get_mut(&ee_oid).unwrap();
obj.write_property(
PropertyIdentifier::NOTIFICATION_CLASS,
None,
PropertyValue::Unsigned(11),
None,
)
.unwrap();
obj.write_property(
PropertyIdentifier::EVENT_ENABLE,
None,
PropertyValue::BitString {
unused_bits: 5,
data: vec![0x60], },
None,
)
.unwrap();
}
let transitions = evaluate_event_enrollments(&mut db, 1);
assert_eq!(transitions.len(), 1);
assert!(
!transitions[0].distribute,
"TO_OFFNORMAL distribution is suppressed"
);
assert_eq!(
acked_transitions(&db, &ee_oid),
EventTransitionBits::TO_FAULT | EventTransitionBits::TO_NORMAL,
"...while the ack-owed bit STILL clears — Event_Enable never scopes it"
);
}