use super::super::*;
use bacnet_types::enums::{EventState, EventType};
fn write_enumerated(object: &mut BinaryOutputObject, property: PropertyIdentifier, value: u32) {
object
.write_property_from(
property,
None,
PropertyValue::Enumerated(value),
None,
&crate::command_source::test_origin(),
)
.unwrap();
}
fn write_event_enable(object: &mut BinaryOutputObject, byte: u8) {
object
.write_property(
PropertyIdentifier::EVENT_ENABLE,
None,
PropertyValue::BitString {
unused_bits: 5,
data: vec![byte],
},
None,
)
.unwrap();
}
fn set_detection_enabled(object: &mut BinaryOutputObject, enabled: bool) {
object
.write_property(
PropertyIdentifier::EVENT_DETECTION_ENABLE,
None,
PropertyValue::Boolean(enabled),
None,
)
.unwrap();
}
#[test]
fn bo_feedback_value_round_trips_and_is_advertised_writable() {
let mut bo = BinaryOutputObject::new(1, "BO-1").unwrap();
write_enumerated(&mut bo, PropertyIdentifier::FEEDBACK_VALUE, 1);
assert_eq!(
bo.read_property(PropertyIdentifier::FEEDBACK_VALUE, None)
.unwrap(),
PropertyValue::Enumerated(1)
);
assert!(bo
.property_list()
.contains(&PropertyIdentifier::FEEDBACK_VALUE));
assert!(bo.is_writable_property(PropertyIdentifier::FEEDBACK_VALUE));
assert!(bo
.write_property(
PropertyIdentifier::FEEDBACK_VALUE,
None,
PropertyValue::Unsigned(1),
None,
)
.is_err());
}
#[test]
fn bo_feedback_value_rejects_a_value_outside_the_binary_enumeration() {
let mut bo = BinaryOutputObject::new(1, "BO-1").unwrap();
assert!(bo
.write_property(
PropertyIdentifier::FEEDBACK_VALUE,
None,
PropertyValue::Enumerated(2),
None,
)
.is_err());
assert_eq!(
bo.read_property(PropertyIdentifier::FEEDBACK_VALUE, None)
.unwrap(),
PropertyValue::Enumerated(0),
"a rejected write must not have stored anything"
);
}
#[test]
fn bo_fresh_object_reports_nothing_when_detection_is_enabled() {
let mut bo = BinaryOutputObject::new(1, "BO-1").unwrap();
set_detection_enabled(&mut bo, true);
assert_eq!(bo.evaluate_intrinsic_reporting(), None);
assert_eq!(
bo.read_property(PropertyIdentifier::EVENT_STATE, None)
.unwrap(),
PropertyValue::Enumerated(EventState::NORMAL.to_raw())
);
}
#[test]
fn bo_disabling_detection_discards_an_in_flight_time_delay_countdown() {
fn ticks_until_offnormal(bo: &mut BinaryOutputObject) -> usize {
for n in 1..20 {
if let Some(outcome) = bo.tick_intrinsic_reporting() {
assert_eq!(outcome.change.to, EventState::OFFNORMAL);
return n;
}
}
panic!("never fired");
}
let mut baseline = BinaryOutputObject::new(1, "BO-baseline").unwrap();
set_detection_enabled(&mut baseline, true);
baseline.event_detector.time_delay = 3;
write_enumerated(&mut baseline, PropertyIdentifier::PRESENT_VALUE, 1);
let fresh = ticks_until_offnormal(&mut baseline);
let mut cycled = BinaryOutputObject::new(2, "BO-cycled").unwrap();
set_detection_enabled(&mut cycled, true);
cycled.event_detector.time_delay = 3;
write_enumerated(&mut cycled, PropertyIdentifier::PRESENT_VALUE, 1);
assert_eq!(cycled.evaluate_intrinsic_reporting(), None);
assert_eq!(cycled.tick_intrinsic_reporting(), None);
set_detection_enabled(&mut cycled, false);
set_detection_enabled(&mut cycled, true);
assert_eq!(
ticks_until_offnormal(&mut cycled),
fresh,
"a disable/enable cycle must restart Time_Delay, not resume it"
);
}
#[test]
fn bo_command_failure_uses_present_and_feedback_values() {
let mut disagreeing = BinaryOutputObject::new(1, "BO-disagree").unwrap();
set_detection_enabled(&mut disagreeing, true);
write_enumerated(&mut disagreeing, PropertyIdentifier::PRESENT_VALUE, 1);
let outcome = disagreeing.evaluate_intrinsic_reporting().unwrap();
assert_eq!(outcome.change.to, EventState::OFFNORMAL);
assert_eq!(outcome.event_type, EventType::COMMAND_FAILURE);
let mut agreeing = BinaryOutputObject::new(2, "BO-agree").unwrap();
set_detection_enabled(&mut agreeing, true);
write_enumerated(&mut agreeing, PropertyIdentifier::FEEDBACK_VALUE, 1);
write_enumerated(&mut agreeing, PropertyIdentifier::PRESENT_VALUE, 1);
assert_eq!(agreeing.evaluate_intrinsic_reporting(), None);
assert_eq!(
agreeing
.read_property(PropertyIdentifier::EVENT_STATE, None)
.unwrap(),
PropertyValue::Enumerated(EventState::NORMAL.to_raw())
);
}
#[test]
fn bo_time_delay_gates_command_failure() {
let mut bo = BinaryOutputObject::new(1, "BO-1").unwrap();
set_detection_enabled(&mut bo, true);
bo.write_property(
PropertyIdentifier::TIME_DELAY,
None,
PropertyValue::Unsigned(2),
None,
)
.unwrap();
write_enumerated(&mut bo, PropertyIdentifier::PRESENT_VALUE, 1);
assert_eq!(bo.evaluate_intrinsic_reporting(), None);
assert_eq!(bo.tick_intrinsic_reporting(), None);
let proposal = bo.tick_intrinsic_reporting().unwrap();
let outcome = crate::event::commit_test_proposal(&mut bo, proposal);
assert_eq!(outcome.change.to, EventState::OFFNORMAL);
assert_eq!(outcome.event_type, EventType::COMMAND_FAILURE);
}
#[test]
fn bo_time_delay_normal_selects_the_return_to_normal_delay() {
let mut bo = BinaryOutputObject::new(1, "BO-1").unwrap();
set_detection_enabled(&mut bo, true);
bo.write_property(
PropertyIdentifier::TIME_DELAY,
None,
PropertyValue::Unsigned(2),
None,
)
.unwrap();
bo.write_property(
PropertyIdentifier::TIME_DELAY_NORMAL,
None,
PropertyValue::Unsigned(4),
None,
)
.unwrap();
write_enumerated(&mut bo, PropertyIdentifier::PRESENT_VALUE, 1);
assert_eq!(bo.evaluate_intrinsic_reporting(), None);
assert_eq!(bo.tick_intrinsic_reporting(), None);
let proposal = bo.tick_intrinsic_reporting().unwrap();
let outcome = crate::event::commit_test_proposal(&mut bo, proposal);
assert_eq!(
outcome.change.to,
EventState::OFFNORMAL,
"condition (a): OFFNORMAL after Time_Delay = 2"
);
assert_eq!(outcome.event_type, EventType::COMMAND_FAILURE);
write_enumerated(&mut bo, PropertyIdentifier::FEEDBACK_VALUE, 1);
assert_eq!(
bo.evaluate_intrinsic_reporting(),
None,
"condition (b): agreement only seeds the NORMAL countdown"
);
for _ in 0..3 {
assert_eq!(
bo.tick_intrinsic_reporting(),
None,
"Time_Delay_Normal = 4 must hold the NORMAL state off"
);
}
let outcome = bo.tick_intrinsic_reporting().unwrap();
assert_eq!(outcome.change.from, EventState::OFFNORMAL);
assert_eq!(outcome.change.to, EventState::NORMAL);
assert_eq!(outcome.event_type, EventType::COMMAND_FAILURE);
}
#[test]
fn bo_event_enable_to_offnormal_bit_controls_distribution() {
for (encoded, expected) in [(0x80, true), (0x00, false)] {
let mut bo = BinaryOutputObject::new(1, "BO-1").unwrap();
set_detection_enabled(&mut bo, true);
write_event_enable(&mut bo, encoded);
write_enumerated(&mut bo, PropertyIdentifier::PRESENT_VALUE, 1);
assert_eq!(
bo.evaluate_intrinsic_reporting().unwrap().distribute,
expected
);
}
}
#[test]
fn bo_detection_enable_is_a_disabled_by_default_invariant() {
let mut bo = BinaryOutputObject::new(1, "BO-1").unwrap();
write_enumerated(&mut bo, PropertyIdentifier::PRESENT_VALUE, 1);
assert_eq!(bo.evaluate_intrinsic_reporting(), None);
assert_eq!(bo.tick_intrinsic_reporting(), None);
assert_eq!(
bo.read_property(PropertyIdentifier::EVENT_STATE, None)
.unwrap(),
PropertyValue::Enumerated(EventState::NORMAL.to_raw())
);
assert_eq!(
bo.read_property(PropertyIdentifier::STATUS_FLAGS, None)
.unwrap(),
PropertyValue::BitString {
unused_bits: 4,
data: vec![0],
}
);
bo.event_detector.time_delay = 2;
set_detection_enabled(&mut bo, true);
assert_eq!(bo.evaluate_intrinsic_reporting(), None);
bo.event_detection_enable = false;
let pending = bo.event_detector.pending.clone();
assert_eq!(bo.tick_intrinsic_reporting(), None);
assert_eq!(bo.event_detector.pending, pending);
set_detection_enabled(&mut bo, true);
bo.event_detector.time_delay = 0;
assert_eq!(
bo.evaluate_intrinsic_reporting().unwrap().change.to,
EventState::OFFNORMAL
);
bo.event_detector.acked_transitions = bacnet_types::bitstring::EventTransitionBits::empty();
set_detection_enabled(&mut bo, false);
assert_eq!(
bo.read_property(PropertyIdentifier::EVENT_STATE, None)
.unwrap(),
PropertyValue::Enumerated(EventState::NORMAL.to_raw())
);
assert_eq!(
bo.read_property(PropertyIdentifier::ACKED_TRANSITIONS, None)
.unwrap(),
PropertyValue::BitString {
unused_bits: 5,
data: vec![0xe0],
}
);
assert_eq!(bo.evaluate_intrinsic_reporting(), None);
assert_eq!(bo.tick_intrinsic_reporting(), None);
bo.reliability = Reliability::NO_SENSOR;
assert_eq!(
bo.read_property(PropertyIdentifier::STATUS_FLAGS, None)
.unwrap(),
PropertyValue::BitString {
unused_bits: 4,
data: vec![0x40],
}
);
assert_eq!(
bo.read_property(PropertyIdentifier::EVENT_DETECTION_ENABLE, None)
.unwrap(),
PropertyValue::Boolean(false)
);
assert!(bo
.property_list()
.contains(&PropertyIdentifier::EVENT_DETECTION_ENABLE));
assert!(bo.is_writable_property(PropertyIdentifier::EVENT_DETECTION_ENABLE));
}
#[test]
fn bo_generic_event_properties_round_trip_and_match_pics() {
let mut bo = BinaryOutputObject::new(1, "BO-1").unwrap();
let writes = [
(
PropertyIdentifier::NOTIFY_TYPE,
PropertyValue::Enumerated(1),
),
(
PropertyIdentifier::NOTIFICATION_CLASS,
PropertyValue::Unsigned(42),
),
];
for (property, value) in writes {
bo.write_property_from(
property,
None,
value.clone(),
None,
&crate::command_source::test_origin(),
)
.unwrap();
assert_eq!(bo.read_property(property, None).unwrap(), value);
}
assert!(bo
.write_property(
PropertyIdentifier::ACKED_TRANSITIONS,
None,
PropertyValue::BitString {
unused_bits: 5,
data: vec![0x80],
},
None,
)
.is_err());
assert!(!bo.is_writable_property(PropertyIdentifier::ACKED_TRANSITIONS));
for property in [
PropertyIdentifier::EVENT_ENABLE,
PropertyIdentifier::TIME_DELAY,
PropertyIdentifier::NOTIFY_TYPE,
PropertyIdentifier::NOTIFICATION_CLASS,
] {
assert!(bo.property_list().contains(&property));
assert!(bo.is_writable_property(property));
}
assert!(bo
.write_property(
PropertyIdentifier::EVENT_STATE,
None,
PropertyValue::Enumerated(EventState::NORMAL.to_raw()),
None,
)
.is_err());
assert!(!bo.is_writable_property(PropertyIdentifier::EVENT_STATE));
}