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,
};
#[test]
fn extended_algorithm_produces_no_transition() {
let mut db = ObjectDatabase::new();
let mut ai = AnalogInputObject::new(93, "AI-93", 62).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.to_raw()).unwrap();
ee.set_object_property_reference(Some(BACnetDeviceObjectPropertyReference::new_local(
ai_oid,
PropertyIdentifier::PRESENT_VALUE.to_raw(),
)));
ee.set_event_parameters(BACnetEventParameter::Extended {
vendor_id: 42,
extended_event_type: 99,
parameters: vec![0x21, 0x07],
});
ee.set_event_enable(0x07);
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", 62).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.to_raw()).unwrap();
ee.set_object_property_reference(Some(BACnetDeviceObjectPropertyReference::new_local(
ai_oid,
PropertyIdentifier::PRESENT_VALUE.to_raw(),
)));
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(0x07);
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.to_raw()).unwrap();
ee.set_object_property_reference(Some(BACnetDeviceObjectPropertyReference::new_local(
bi_oid,
PropertyIdentifier::PRESENT_VALUE.to_raw(),
)));
ee.set_event_parameters(BACnetEventParameter::Opaque {
tag: 0xFF,
data: encode_change_of_state_params(&[1]),
});
ee.set_event_enable(0x07);
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_unmodeled_alternative_is_never_le_evaluated() {
let mut db = ObjectDatabase::new();
let mut ai = AnalogInputObject::new(96, "AI-96", 62).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-unmodeled", EventType::OUT_OF_RANGE.to_raw()).unwrap();
ee.set_object_property_reference(Some(BACnetDeviceObjectPropertyReference::new_local(
ai_oid,
PropertyIdentifier::PRESENT_VALUE.to_raw(),
)));
ee.set_event_enable(0x07);
let ee_oid = ee.object_identifier();
db.add(Box::new(ee)).unwrap();
let framed: Vec<u8> = vec![
0xDE, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
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),
"unmodeled framed alternative must round-trip verbatim"
);
let transitions = evaluate_event_enrollments(&mut db, 1);
assert!(
transitions.is_empty(),
"unmodeled spec alternative must not be fed to eval_legacy_le: {transitions:?}"
);
}
#[test]
fn change_of_value_bitmask_criteria() {
let mut db = ObjectDatabase::new();
let mut target = EventEnrollmentObject::new(96, "Tgt", EventType::NONE.to_raw()).unwrap();
target.set_event_enable(0x07); let target_oid = target.object_identifier();
target.set_object_property_reference(Some(BACnetDeviceObjectPropertyReference::new_local(
target_oid,
PropertyIdentifier::EVENT_ENABLE.to_raw(),
)));
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.to_raw()).unwrap();
ee.set_object_property_reference(Some(BACnetDeviceObjectPropertyReference::new_local(
target_oid,
PropertyIdentifier::EVENT_ENABLE.to_raw(),
)));
ee.set_event_parameters(BACnetEventParameter::ChangeOfValue {
time_delay: 0,
criteria: ChangeOfValueCriteria::Bitmask {
unused_bits: 5,
data: vec![0x80],
},
});
ee.set_event_enable(0x07);
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: 'changes in any of the bits \
specified by a bitmask')"
);
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.to_raw()).unwrap();
target.set_event_enable(0x07);
let target_oid = target.object_identifier();
db.add(Box::new(target)).unwrap();
let mut ee =
EventEnrollmentObject::new(99, "EE-covwt", EventType::CHANGE_OF_VALUE.to_raw()).unwrap();
ee.set_object_property_reference(Some(BACnetDeviceObjectPropertyReference::new_local(
target_oid,
PropertyIdentifier::EVENT_ENABLE.to_raw(),
)));
ee.set_event_parameters(BACnetEventParameter::ChangeOfValue {
time_delay: 0,
criteria: ChangeOfValueCriteria::ReferencedPropertyIncrement(5.0),
});
ee.set_event_enable(0x07);
ee.set_event_state(EventState::OFFNORMAL.to_raw());
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.to_raw()).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", 62).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.to_raw()).unwrap();
ee.set_object_property_reference(Some(BACnetDeviceObjectPropertyReference::new_local(
ai_oid,
PropertyIdentifier::PRESENT_VALUE.to_raw(),
)));
ee.set_event_enable(0x07);
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;
let mut db = ObjectDatabase::new();
let mut ai = AnalogInputObject::new(77, "AI-77", 62).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.to_raw()).unwrap();
ee.set_object_property_reference(Some(BACnetDeviceObjectPropertyReference::new_local(
ai_oid,
PropertyIdentifier::PRESENT_VALUE.to_raw(),
)));
ee.set_event_parameters(BACnetEventParameter::OutOfRange {
time_delay: 0,
low_limit: 20.0,
high_limit: 80.0,
deadband: 2.0,
});
ee.set_event_enable(0x07);
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"
);
}