use super::super::*;
use bacnet_objects::analog::AnalogInputObject;
use bacnet_objects::binary::BinaryInputObject;
use bacnet_objects::event_enrollment::EventEnrollmentObject;
use bacnet_objects::traits::BACnetObject;
use bacnet_types::constructed::{
BACnetDeviceObjectPropertyReference, BACnetEventParameter, ChangeOfValueCriteria,
};
use bacnet_types::enums::EngineeringUnits;
#[test]
fn extended_algorithm_produces_no_transition() {
let mut db = ObjectDatabase::new();
let mut ai = AnalogInputObject::new(93, "AI-93", 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(93, "EE-ext", 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::Extended {
vendor_id: 42,
extended_event_type: 99,
parameters: vec![0x21, 0x07],
});
ee.set_event_enable(EventTransitionBits::all());
db.add(Box::new(ee)).unwrap();
let transitions = evaluate_event_enrollments(&mut db, 1);
assert!(
transitions.is_empty(),
"Extended algorithm is not evaluated"
);
}
#[test]
fn legacy_le_out_of_range_fallback_round_trip() {
let mut db = ObjectDatabase::new();
let mut ai = AnalogInputObject::new(94, "AI-94", 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(94, "EE-leg", EventType::OUT_OF_RANGE).unwrap();
ee.set_object_property_reference(Some(BACnetDeviceObjectPropertyReference::new_local(
ai_oid,
PropertyIdentifier::PRESENT_VALUE.to_raw(),
)))
.unwrap();
ee.write_property(
PropertyIdentifier::EVENT_PARAMETERS,
None,
PropertyValue::OctetString(encode_out_of_range_params(80.0, 20.0, 2.0)),
None,
)
.unwrap();
ee.set_event_enable(EventTransitionBits::all());
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::HIGH_LIMIT);
}
#[test]
fn legacy_le_change_of_state_fallback() {
let mut db = ObjectDatabase::new();
let mut bi = BinaryInputObject::new(95, "BI-95").unwrap();
bi.set_present_value(1); let bi_oid = bi.object_identifier();
db.add(Box::new(bi)).unwrap();
let mut ee = EventEnrollmentObject::new(95, "EE-legcos", 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::Opaque {
tag: 0xFF,
data: encode_change_of_state_params(&[1]),
});
ee.set_event_enable(EventTransitionBits::all());
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::OFFNORMAL);
}
#[test]
fn framed_unevaluated_alternative_is_never_le_evaluated() {
let mut db = ObjectDatabase::new();
let mut ai = AnalogInputObject::new(96, "AI-96", 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(96, "EE-unevaluated", 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_enable(EventTransitionBits::all());
let ee_oid = ee.object_identifier();
db.add(Box::new(ee)).unwrap();
let framed: Vec<u8> = vec![
0xDE, 0x0E, 0x91, 0x01, 0x91, 0x02, 0x91, 0x03, 0x91, 0x04, 0x91, 0x05, 0x91, 0x06,
0x0F, 0x1E, 0x0C, 0x00, 0x00, 0x00, 0x01, 0x19, 0x55, 0x1F, 0xDF, ];
let obj = db.get_mut(&ee_oid).unwrap();
obj.write_property(
PropertyIdentifier::EVENT_PARAMETERS,
None,
PropertyValue::ApplicationData(framed.clone()),
None,
)
.unwrap();
let val = db
.get(&ee_oid)
.unwrap()
.read_property(PropertyIdentifier::EVENT_PARAMETERS, None)
.unwrap();
assert_eq!(
val,
PropertyValue::ApplicationData(framed.clone()),
"the framed alternative must round-trip verbatim"
);
assert!(matches!(
bacnet_encoding::constructed::decode_event_parameter(&framed, 0)
.unwrap()
.0,
BACnetEventParameter::AccessEvent { .. }
));
let transitions = evaluate_event_enrollments(&mut db, 1);
assert!(
transitions.is_empty(),
"an unevaluated alternative must not be fed to eval_legacy_le: {transitions:?}"
);
}
#[test]
fn newly_decoded_algorithms_stay_unevaluated() {
use bacnet_types::enums::{AccessEvent, LifeSafetyState, TimerState};
use bacnet_types::primitives::StatusFlags;
let reference = |instance| {
BACnetDeviceObjectPropertyReference::new_local(
ObjectIdentifier::new(ObjectType::ANALOG_VALUE, instance).unwrap(),
PropertyIdentifier::PRESENT_VALUE.to_raw(),
)
};
use BACnetEventParameter as EP;
let algorithms = [
EP::CommandFailure {
time_delay: 7,
feedback_property_reference: reference(1),
},
EP::ChangeOfLifeSafety {
time_delay: 7,
list_of_life_safety_alarm_values: vec![LifeSafetyState::ALARM],
list_of_alarm_values: vec![LifeSafetyState::PRE_ALARM],
mode_property_reference: reference(2),
},
EP::BufferReady {
notification_threshold: 1,
previous_notification_count: 0,
},
EP::UnsignedRange {
time_delay: 7,
low_limit: 0,
high_limit: 1,
},
EP::AccessEvent {
list_of_access_events: vec![AccessEvent::GRANTED],
access_event_time_reference: reference(3),
},
EP::DoubleOutOfRange {
time_delay: 7,
low_limit: 0.0,
high_limit: 1.0,
deadband: 0.0,
},
EP::SignedOutOfRange {
time_delay: 7,
low_limit: 0,
high_limit: 1,
deadband: 0,
},
EP::UnsignedOutOfRange {
time_delay: 7,
low_limit: 0,
high_limit: 1,
deadband: 0,
},
EP::ChangeOfCharacterstring {
time_delay: 7,
list_of_alarm_values: vec!["85".into()],
},
EP::ChangeOfStatusFlags {
time_delay: 7,
selected_flags: StatusFlags::all(),
},
EP::ChangeOfDiscreteValue { time_delay: 7 },
EP::ChangeOfTimer {
time_delay: 7,
alarm_values: vec![TimerState::EXPIRED],
update_time_reference: reference(4),
},
];
for algorithm in algorithms {
let mut db = ObjectDatabase::new();
let mut ai =
AnalogInputObject::new(97, "AI-97", 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(97, "EE-97", 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(algorithm.clone());
ee.set_event_enable(EventTransitionBits::all());
let ee_oid = ee.object_identifier();
db.add(Box::new(ee)).unwrap();
for tick in 1..=3 {
let transitions = evaluate_event_enrollments(&mut db, tick);
assert!(transitions.is_empty(), "{algorithm:?}: {transitions:?}");
}
let state = db
.get(&ee_oid)
.unwrap()
.read_property(PropertyIdentifier::EVENT_STATE, None)
.unwrap();
assert_eq!(
state,
PropertyValue::Enumerated(EventState::NORMAL.to_raw()),
"{algorithm:?}"
);
let expected = match algorithm {
EP::BufferReady { .. } | EP::AccessEvent { .. } => 0,
_ => 7,
};
let delay = db
.get(&ee_oid)
.unwrap()
.read_property(PropertyIdentifier::TIME_DELAY_NORMAL, None)
.unwrap();
assert_eq!(delay, PropertyValue::Unsigned(expected), "{algorithm:?}");
}
}
#[test]
fn change_of_value_bitmask_criteria() {
let mut db = ObjectDatabase::new();
let mut target = EventEnrollmentObject::new(96, "Tgt", EventType::NONE).unwrap();
target.set_event_enable(EventTransitionBits::all()); let target_oid = target.object_identifier();
target
.set_object_property_reference(Some(BACnetDeviceObjectPropertyReference::new_local(
target_oid,
PropertyIdentifier::EVENT_ENABLE.to_raw(),
)))
.unwrap();
target.set_event_parameters(BACnetEventParameter::Extended {
vendor_id: 42,
extended_event_type: 1,
parameters: Vec::new(),
});
db.add(Box::new(target)).unwrap();
let mut ee = EventEnrollmentObject::new(97, "EE-covbm", EventType::CHANGE_OF_VALUE).unwrap();
ee.set_object_property_reference(Some(BACnetDeviceObjectPropertyReference::new_local(
target_oid,
PropertyIdentifier::EVENT_ENABLE.to_raw(),
)))
.unwrap();
ee.set_event_parameters(BACnetEventParameter::ChangeOfValue {
time_delay: 0,
criteria: ChangeOfValueCriteria::Bitmask {
unused_bits: 5,
data: vec![0x80],
},
});
ee.set_event_enable(EventTransitionBits::all());
db.add(Box::new(ee)).unwrap();
assert!(
evaluate_event_enrollments(&mut db, 1).is_empty(),
"first sample initializes the baseline; it must not indicate"
);
db.get_mut(&target_oid)
.unwrap()
.write_property(
PropertyIdentifier::EVENT_ENABLE,
None,
PropertyValue::BitString {
unused_bits: 5,
data: vec![0xA0],
},
None,
)
.unwrap();
assert!(
evaluate_event_enrollments(&mut db, 1).is_empty(),
"only masked bits are significant (13.3.3: a BIT STRING change counts \
only in bits the bitmask selects)"
);
db.get_mut(&target_oid)
.unwrap()
.write_property(
PropertyIdentifier::EVENT_ENABLE,
None,
PropertyValue::BitString {
unused_bits: 5,
data: vec![0x20],
},
None,
)
.unwrap();
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);
assert!(evaluate_event_enrollments(&mut db, 1).is_empty());
}
#[test]
fn change_of_value_wrong_type_monitored_value_skips() {
let mut db = ObjectDatabase::new();
let mut target = EventEnrollmentObject::new(98, "Tgt2", EventType::NONE).unwrap();
target.set_event_enable(EventTransitionBits::all());
let target_oid = target.object_identifier();
db.add(Box::new(target)).unwrap();
let mut ee = EventEnrollmentObject::new(99, "EE-covwt", EventType::CHANGE_OF_VALUE).unwrap();
ee.set_object_property_reference(Some(BACnetDeviceObjectPropertyReference::new_local(
target_oid,
PropertyIdentifier::EVENT_ENABLE.to_raw(),
)))
.unwrap();
ee.set_event_parameters(BACnetEventParameter::ChangeOfValue {
time_delay: 0,
criteria: ChangeOfValueCriteria::ReferencedPropertyIncrement(5.0),
});
ee.set_event_enable(EventTransitionBits::all());
ee.set_event_state(EventState::OFFNORMAL);
db.add(Box::new(ee)).unwrap();
let transitions = evaluate_event_enrollments(&mut db, 1);
assert!(
transitions.is_empty(),
"wrong-type monitored value must skip"
);
}
#[test]
fn no_reference_is_skipped() {
let mut db = ObjectDatabase::new();
let ee = EventEnrollmentObject::new(91, "EE-noref", EventType::OUT_OF_RANGE).unwrap();
db.add(Box::new(ee)).unwrap();
let transitions = evaluate_event_enrollments(&mut db, 1);
assert!(transitions.is_empty());
}
#[test]
fn empty_parameters_is_skipped() {
let mut db = ObjectDatabase::new();
let mut ai = AnalogInputObject::new(92, "AI-92", EngineeringUnits::DEGREES_CELSIUS).unwrap();
ai.set_present_value(100.0);
let ai_oid = ai.object_identifier();
db.add(Box::new(ai)).unwrap();
let mut ee = EventEnrollmentObject::new(92, "EE-noparam", 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_enable(EventTransitionBits::all());
db.add(Box::new(ee)).unwrap();
let transitions = evaluate_event_enrollments(&mut db, 1);
assert!(transitions.is_empty());
}
#[test]
fn evaluation_does_not_use_network_write_route() {
use bacnet_objects::event_enrollment::EventEnrollmentObject;
use bacnet_types::enums::EngineeringUnits;
let mut db = ObjectDatabase::new();
let mut ai = AnalogInputObject::new(77, "AI-77", 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(77, "EE-77", 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::all());
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::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()),
"evaluation must persist Event_State"
);
let obj = db.get_mut(&ee_oid).unwrap();
let net_result = obj.write_property(
PropertyIdentifier::EVENT_STATE,
None,
PropertyValue::Enumerated(EventState::NORMAL.to_raw()),
None,
);
assert!(
net_result.is_err(),
"network EVENT_STATE write must be rejected (read-only over the network)"
);
let obj = db.get(&ee_oid).unwrap();
assert_eq!(
obj.read_property(PropertyIdentifier::EVENT_STATE, None)
.unwrap(),
PropertyValue::Enumerated(EventState::HIGH_LIMIT.to_raw()),
"rejected network write must not mutate Event_State"
);
}