use bacnet_types::bitstring::LimitEnable;
use bacnet_types::enums::{EventState, EventType, PropertyIdentifier as P, Reliability};
use bacnet_types::primitives::{BACnetTimeStamp, PropertyValue};
use super::{commit_test_proposal, EventTransitionCommit, TransitionOutcome};
use crate::access_control::AccessZoneObject;
use crate::analog::AnalogInputObject;
use crate::binary::{BinaryOutputObject, BinaryValueObject};
use crate::event_log::EventLogObject;
use crate::traits::BACnetObject;
fn write(object: &mut dyn BACnetObject, property: P, value: PropertyValue) {
object.write_property(property, None, value, None).unwrap();
}
fn inhibit(object: &mut dyn BACnetObject, on: bool) {
write(
object,
P::EVENT_ALGORITHM_INHIBIT,
PropertyValue::Boolean(on),
);
}
fn state(object: &dyn BACnetObject) -> EventState {
match object.read_property(P::EVENT_STATE, None).unwrap() {
PropertyValue::Enumerated(raw) => EventState::from_raw(raw),
other => panic!("Event_State read {other:?}"),
}
}
fn step(object: &mut dyn BACnetObject, outcome: Option<TransitionOutcome>) -> Option<EventState> {
outcome.map(|outcome| commit_test_proposal(object, outcome).change.to)
}
fn high_input(time_delay: u32) -> AnalogInputObject {
let mut ai = AnalogInputObject::new(1, "AI-1", 62).unwrap();
write(&mut ai, P::HIGH_LIMIT, PropertyValue::Real(80.0));
write(
&mut ai,
P::LIMIT_ENABLE,
PropertyValue::BitString {
unused_bits: 6,
data: vec![LimitEnable::all().to_bacnet()],
},
);
write(
&mut ai,
P::TIME_DELAY,
PropertyValue::Unsigned(u64::from(time_delay)),
);
write(&mut ai, P::TIME_DELAY_NORMAL, PropertyValue::Unsigned(9));
ai.set_present_value(90.0);
ai
}
#[test]
fn an_inhibited_algorithm_proposes_no_offnormal_transition_but_still_faults() {
let mut ai = high_input(0);
inhibit(&mut ai, true);
assert_eq!(ai.evaluate_intrinsic_reporting(), None);
assert_eq!(ai.tick_intrinsic_reporting(), None);
assert_eq!(state(&ai), EventState::NORMAL);
write(&mut ai, P::OUT_OF_SERVICE, PropertyValue::Boolean(true));
write(
&mut ai,
P::RELIABILITY,
PropertyValue::Enumerated(Reliability::OVER_RANGE.to_raw()),
);
let entered = ai.evaluate_intrinsic_reporting();
assert_eq!(step(&mut ai, entered), Some(EventState::FAULT));
write(
&mut ai,
P::RELIABILITY,
PropertyValue::Enumerated(Reliability::NO_FAULT_DETECTED.to_raw()),
);
let recovered = ai.evaluate_intrinsic_reporting();
assert_eq!(step(&mut ai, recovered), Some(EventState::NORMAL));
assert_eq!(ai.evaluate_intrinsic_reporting(), None);
inhibit(&mut ai, false);
let offnormal = ai.evaluate_intrinsic_reporting();
assert_eq!(step(&mut ai, offnormal), Some(EventState::HIGH_LIMIT));
}
#[test]
fn turning_the_inhibit_on_returns_an_offnormal_object_to_normal_at_once() {
let mut ai = high_input(0);
let offnormal = ai.evaluate_intrinsic_reporting();
assert_eq!(step(&mut ai, offnormal), Some(EventState::HIGH_LIMIT));
inhibit(&mut ai, true);
let outcome = ai.tick_intrinsic_reporting().expect("back to NORMAL");
assert_eq!(outcome.change.from, EventState::HIGH_LIMIT);
assert_eq!(outcome.change.to, EventState::NORMAL);
assert_eq!(outcome.event_type, EventType::OUT_OF_RANGE);
commit_test_proposal(&mut ai, outcome);
assert_eq!(ai.tick_intrinsic_reporting(), None);
}
#[test]
fn a_condition_has_to_last_its_whole_delay_after_the_inhibit_clears() {
let mut ai = high_input(2);
assert_eq!(ai.evaluate_intrinsic_reporting(), None);
assert_eq!(ai.tick_intrinsic_reporting(), None);
inhibit(&mut ai, true);
assert_eq!(ai.tick_intrinsic_reporting(), None);
inhibit(&mut ai, false);
assert_eq!(ai.tick_intrinsic_reporting(), None);
assert_eq!(ai.tick_intrinsic_reporting(), None);
let outcome = ai.tick_intrinsic_reporting();
assert_eq!(step(&mut ai, outcome), Some(EventState::HIGH_LIMIT));
}
#[test]
fn the_change_of_state_and_command_failure_detectors_take_the_inhibit_too() {
let mut bv = BinaryValueObject::new(1, "BV-1").unwrap();
write(&mut bv, P::ALARM_VALUE, PropertyValue::Enumerated(1));
let mut bo = BinaryOutputObject::new(1, "BO-1").unwrap();
for object in [&mut bv as &mut dyn BACnetObject, &mut bo] {
write(
object,
P::EVENT_DETECTION_ENABLE,
PropertyValue::Boolean(true),
);
object
.write_property_from(
P::PRESENT_VALUE,
None,
PropertyValue::Enumerated(1),
Some(8),
&crate::command_source::test_origin(),
)
.unwrap();
}
for object in [&mut bv as &mut dyn BACnetObject, &mut bo] {
let name = object.object_name().to_owned();
inhibit(object, true);
assert_eq!(object.evaluate_intrinsic_reporting(), None, "{name}");
inhibit(object, false);
let offnormal = object.evaluate_intrinsic_reporting();
assert_eq!(
step(object, offnormal),
Some(EventState::OFFNORMAL),
"{name}"
);
inhibit(object, true);
let normal = object.evaluate_intrinsic_reporting();
assert_eq!(step(object, normal), Some(EventState::NORMAL), "{name}");
}
}
#[test]
fn an_access_zone_takes_the_rows_and_the_inhibit() {
let mut zone = AccessZoneObject::new(1, "ZONE-1").unwrap();
inhibit(&mut zone, true);
assert_eq!(
zone.read_property(P::EVENT_ALGORITHM_INHIBIT, None)
.unwrap(),
PropertyValue::Boolean(true)
);
let texts = PropertyValue::List(
["Full", "Broken", "Clear"]
.map(|text| PropertyValue::CharacterString(text.into()))
.to_vec(),
);
write(&mut zone, P::EVENT_MESSAGE_TEXTS_CONFIG, texts.clone());
assert_eq!(
zone.read_property(P::EVENT_MESSAGE_TEXTS_CONFIG, None)
.unwrap(),
texts
);
write(
&mut zone,
P::EVENT_DETECTION_ENABLE,
PropertyValue::Boolean(false),
);
assert!(zone
.write_property(
P::EVENT_ALGORITHM_INHIBIT,
None,
PropertyValue::Boolean(false),
None
)
.is_err());
assert_eq!(
zone.read_property(P::EVENT_MESSAGE_TEXTS_CONFIG, None)
.unwrap(),
texts
);
}
#[test]
fn a_configured_text_is_the_message_a_transition_stores() {
let mut ai = high_input(0);
write(
&mut ai,
P::EVENT_MESSAGE_TEXTS_CONFIG,
PropertyValue::List(vec![
PropertyValue::CharacterString("Too hot".into()),
PropertyValue::CharacterString(String::new()),
PropertyValue::CharacterString(String::new()),
]),
);
let offnormal = ai.evaluate_intrinsic_reporting().unwrap();
let commit = |outcome: TransitionOutcome| EventTransitionCommit {
coordinate: outcome.change.transition(),
change: outcome.change,
ack_required: false,
timestamp: BACnetTimeStamp::SequenceNumber(1),
message_text: Some("server text".into()),
};
ai.commit_event_transition_internal(commit(offnormal))
.unwrap();
assert_eq!(
ai.read_property(P::EVENT_MESSAGE_TEXTS, Some(1)).unwrap(),
PropertyValue::CharacterString("Too hot".into())
);
write(&mut ai, P::TIME_DELAY_NORMAL, PropertyValue::Unsigned(0));
ai.set_present_value(10.0);
let normal = ai.evaluate_intrinsic_reporting().unwrap();
ai.commit_event_transition_internal(commit(normal)).unwrap();
assert_eq!(
ai.read_property(P::EVENT_MESSAGE_TEXTS, Some(3)).unwrap(),
PropertyValue::CharacterString("server text".into())
);
}
#[test]
fn an_inhibited_log_holds_its_buffer_ready_report() {
let mut log = EventLogObject::new(1, "EL-1", 8).unwrap();
log.bind_clock_internal(Some(std::sync::Arc::new(FixedClock)));
write(
&mut log,
P::NOTIFICATION_THRESHOLD,
PropertyValue::Unsigned(1),
);
inhibit(&mut log, true);
log.add_event_log_record(record()).unwrap();
assert_eq!(log.tick_intrinsic_reporting(), None);
assert_eq!(log.evaluate_intrinsic_reporting(), None);
inhibit(&mut log, false);
let report = log.tick_intrinsic_reporting().expect("due");
assert_eq!(report.change.to, EventState::NORMAL);
}
struct FixedClock;
impl crate::clock::ClockReader for FixedClock {
fn read_clock(&self) -> Option<crate::clock::ClockFrame> {
Some(crate::clock::ClockFrame {
local_date: DATE,
local_time: TIME,
utc_offset: 0,
daylight_savings_status: false,
})
}
}
const DATE: bacnet_types::primitives::Date = bacnet_types::primitives::Date {
year: 126,
month: 10,
day: 5,
day_of_week: 1,
};
const TIME: bacnet_types::primitives::Time = bacnet_types::primitives::Time {
hour: 9,
minute: 0,
second: 0,
hundredths: 0,
};
fn record() -> bacnet_types::constructed::BACnetEventLogRecord {
bacnet_types::constructed::BACnetEventLogRecord {
date: DATE,
time: TIME,
log_datum: bacnet_types::constructed::EventLogDatum::TimeChange(1.0),
}
}