use super::*;
use bacnet_encoding::primitives::{decode_timestamp_choice, encode_timestamp_choice};
use bacnet_types::bitstring::EventTransitionBits;
use bacnet_types::enums::{ErrorClass, ErrorCode, EventState};
use bacnet_types::primitives::{Date, Time};
use bytes::BytesMut;
fn timestamp_property_value(timestamp: &BACnetTimeStamp) -> PropertyValue {
let mut bytes = BytesMut::new();
encode_timestamp_choice(&mut bytes, timestamp).unwrap();
PropertyValue::ApplicationData(bytes.to_vec())
}
fn assert_timestamp_value(actual: PropertyValue, expected: &BACnetTimeStamp, context: &str) {
let PropertyValue::ApplicationData(bytes) = actual else {
panic!("{context}: expected timestamp ApplicationData");
};
let (decoded, end) = decode_timestamp_choice(&bytes, 0).unwrap();
assert_eq!(&decoded, expected, "{context}: timestamp choice");
assert_eq!(end, bytes.len(), "{context}: trailing timestamp bytes");
}
fn assert_timestamp_array(actual: PropertyValue, expected: &[BACnetTimeStamp; 3], context: &str) {
let PropertyValue::List(elements) = actual else {
panic!("{context}: expected timestamp list");
};
assert_eq!(elements.len(), expected.len(), "{context}: array length");
for (slot, (actual, expected)) in elements.into_iter().zip(expected).enumerate() {
assert_timestamp_value(actual, expected, &format!("{context} slot {}", slot + 1));
}
}
fn assert_protocol_error(result: Result<(), Error>, code: ErrorCode) {
match result.unwrap_err() {
Error::Protocol {
class,
code: actual,
} => {
assert_eq!(class, ErrorClass::PROPERTY.to_raw() as u32);
assert_eq!(actual, code.to_raw() as u32);
}
other => panic!("expected protocol error, got {other:?}"),
}
}
fn assert_property_read_error(result: Result<PropertyValue, Error>, code: ErrorCode) {
match result.unwrap_err() {
Error::Protocol {
class,
code: actual,
} => {
assert_eq!(class, ErrorClass::PROPERTY.to_raw() as u32);
assert_eq!(actual, code.to_raw() as u32);
}
other => panic!("expected protocol error, got {other:?}"),
}
}
fn assert_write_access_denied(
result: Result<(), Error>,
property: PropertyIdentifier,
label: &str,
) {
match result.expect_err(&format!("{label}: {property:?} write must be refused")) {
Error::Protocol { class, code } => {
assert_eq!(
class,
ErrorClass::PROPERTY.to_raw() as u32,
"{label}: {property:?} error class"
);
assert_eq!(
code,
ErrorCode::WRITE_ACCESS_DENIED.to_raw() as u32,
"{label}: {property:?} error code"
);
}
other => panic!("{label}: {property:?} expected WRITE_ACCESS_DENIED, got {other:?}"),
}
}
fn assert_event_properties_round_trip(object: &mut dyn BACnetObject, label: &str) {
for (property, value) in [
(
PropertyIdentifier::EVENT_ENABLE,
PropertyValue::BitString {
unused_bits: 5,
data: vec![EventTransitionBits::all().to_bacnet()],
},
),
(
PropertyIdentifier::NOTIFY_TYPE,
PropertyValue::Enumerated(1),
),
(
PropertyIdentifier::NOTIFICATION_CLASS,
PropertyValue::Unsigned(42),
),
(PropertyIdentifier::TIME_DELAY, PropertyValue::Unsigned(7)),
(
PropertyIdentifier::TIME_DELAY_NORMAL,
PropertyValue::Unsigned(9),
),
] {
object
.write_property(property, None, value.clone(), None)
.unwrap_or_else(|e| panic!("{label}: {property:?} write rejected: {e:?}"));
assert_eq!(
object.read_property(property, None).unwrap(),
value,
"{label}: {property:?} must read back what was written"
);
assert!(
object.property_list().contains(&property),
"{label}: {property:?} must be advertised in Property_List"
);
assert!(
object.is_writable_property(property),
"{label}: {property:?} must be advertised writable"
);
}
assert_protocol_error(
object.write_property(
PropertyIdentifier::TIME_DELAY_NORMAL,
None,
PropertyValue::Enumerated(9),
None,
),
ErrorCode::INVALID_DATA_TYPE,
);
assert_protocol_error(
object.write_property(
PropertyIdentifier::TIME_DELAY_NORMAL,
None,
PropertyValue::Unsigned(u32::MAX as u64 + 1),
None,
),
ErrorCode::VALUE_OUT_OF_RANGE,
);
assert_eq!(
object
.read_property(PropertyIdentifier::TIME_DELAY_NORMAL, None)
.unwrap(),
PropertyValue::Unsigned(9),
"{label}: refused Time_Delay_Normal writes must leave the value untouched"
);
object
.write_property(
PropertyIdentifier::EVENT_ENABLE,
None,
PropertyValue::BitString {
unused_bits: 5,
data: vec![EventTransitionBits::TO_NORMAL.to_bacnet()],
},
None,
)
.unwrap();
assert_eq!(
object
.read_property(PropertyIdentifier::EVENT_ENABLE, None)
.unwrap(),
PropertyValue::BitString {
unused_bits: 5,
data: vec![0x20],
},
"{label}: TO_NORMAL alone must read back as wire bit 2"
);
assert_write_access_denied(
object.write_property(
PropertyIdentifier::ACKED_TRANSITIONS,
None,
PropertyValue::BitString {
unused_bits: 5,
data: vec![EventTransitionBits::TO_OFFNORMAL.to_bacnet()],
},
None,
),
PropertyIdentifier::ACKED_TRANSITIONS,
label,
);
assert!(!object.is_writable_property(PropertyIdentifier::ACKED_TRANSITIONS));
assert!(object
.property_list()
.contains(&PropertyIdentifier::ACKED_TRANSITIONS));
assert_write_access_denied(
object.write_property(
PropertyIdentifier::EVENT_STATE,
None,
PropertyValue::Enumerated(EventState::OFFNORMAL.to_raw()),
None,
),
PropertyIdentifier::EVENT_STATE,
label,
);
assert!(!object.is_writable_property(PropertyIdentifier::EVENT_STATE));
}
#[test]
fn bi_event_properties_round_trip_and_match_pics() {
let mut bi = BinaryInputObject::new(1, "BI-1").unwrap();
assert_event_properties_round_trip(&mut bi, "BI");
}
#[test]
fn bv_event_properties_round_trip_and_match_pics() {
let mut bv = BinaryValueObject::new(1, "BV-1").unwrap();
assert_event_properties_round_trip(&mut bv, "BV");
}
#[test]
fn binary_time_delay_normal_defaults_to_time_delay_when_unwritten() {
for (mut object, label) in [
(
Box::new(BinaryInputObject::new(1, "BI-1").unwrap()) as Box<dyn BACnetObject>,
"BI",
),
(
Box::new(BinaryOutputObject::new(1, "BO-1").unwrap()) as Box<dyn BACnetObject>,
"BO",
),
(
Box::new(BinaryValueObject::new(1, "BV-1").unwrap()) as Box<dyn BACnetObject>,
"BV",
),
] {
assert_eq!(
object
.read_property(PropertyIdentifier::TIME_DELAY_NORMAL, None)
.unwrap(),
object
.read_property(PropertyIdentifier::TIME_DELAY, None)
.unwrap(),
"{label}: unwritten Time_Delay_Normal reads back Time_Delay"
);
object
.write_property(
PropertyIdentifier::TIME_DELAY,
None,
PropertyValue::Unsigned(11),
None,
)
.unwrap();
assert_eq!(
object
.read_property(PropertyIdentifier::TIME_DELAY_NORMAL, None)
.unwrap(),
PropertyValue::Unsigned(11),
"{label}: the fallback tracks Time_Delay"
);
}
}
#[test]
fn binary_alarm_value_round_trips_and_matches_pics() {
for object in [
&mut BinaryInputObject::new(1, "BI-1").unwrap() as &mut dyn BACnetObject,
&mut BinaryValueObject::new(1, "BV-1").unwrap() as &mut dyn BACnetObject,
] {
assert_eq!(
object
.read_property(PropertyIdentifier::ALARM_VALUE, None)
.unwrap(),
PropertyValue::Enumerated(1)
);
object
.write_property(
PropertyIdentifier::ALARM_VALUE,
None,
PropertyValue::Enumerated(0),
None,
)
.unwrap();
assert_eq!(
object
.read_property(PropertyIdentifier::ALARM_VALUE, None)
.unwrap(),
PropertyValue::Enumerated(0)
);
assert!(object
.property_list()
.contains(&PropertyIdentifier::ALARM_VALUE));
assert!(object.is_writable_property(PropertyIdentifier::ALARM_VALUE));
assert!(!object
.property_list()
.contains(&PropertyIdentifier::ALARM_VALUES));
assert_protocol_error(
object.write_property(
PropertyIdentifier::ALARM_VALUE,
None,
PropertyValue::Unsigned(1),
None,
),
ErrorCode::INVALID_DATA_TYPE,
);
assert_eq!(
object
.read_property(PropertyIdentifier::ALARM_VALUE, None)
.unwrap(),
PropertyValue::Enumerated(0),
"a rejected wrong-type write must preserve Alarm_Value"
);
assert_protocol_error(
object.write_property(
PropertyIdentifier::ALARM_VALUE,
None,
PropertyValue::Enumerated(2),
None,
),
ErrorCode::VALUE_OUT_OF_RANGE,
);
}
assert_protocol_error(
BinaryInputObject::new(2, "BI-2")
.unwrap()
.read_property(PropertyIdentifier::ALARM_VALUES, None)
.map(|_| ()),
ErrorCode::UNKNOWN_PROPERTY,
);
}
#[test]
fn fresh_binary_input_is_default_armed_for_active() {
let mut bi = BinaryInputObject::new(1, "BI-1").unwrap();
bi.set_present_value(1);
let outcome = bi
.evaluate_intrinsic_reporting()
.expect("ACTIVE is the default Alarm_Value");
assert_eq!(outcome.change.to, EventState::OFFNORMAL);
}
#[test]
fn binary_event_history_is_listed_readable_and_read_only() {
let timestamps = PropertyValue::List(vec![
timestamp_property_value(&BACnetTimeStamp::SequenceNumber(0)),
timestamp_property_value(&BACnetTimeStamp::SequenceNumber(0)),
timestamp_property_value(&BACnetTimeStamp::SequenceNumber(0)),
]);
let messages = PropertyValue::List(vec![
PropertyValue::CharacterString(String::new()),
PropertyValue::CharacterString(String::new()),
PropertyValue::CharacterString(String::new()),
]);
for (mut object, label) in [
(
Box::new(BinaryInputObject::new(1, "BI-1").unwrap()) as Box<dyn BACnetObject>,
"BI",
),
(
Box::new(BinaryOutputObject::new(1, "BO-1").unwrap()) as Box<dyn BACnetObject>,
"BO",
),
(
Box::new(BinaryValueObject::new(1, "BV-1").unwrap()) as Box<dyn BACnetObject>,
"BV",
),
] {
for (property, expected) in [
(PropertyIdentifier::EVENT_TIME_STAMPS, ×tamps),
(PropertyIdentifier::EVENT_MESSAGE_TEXTS, &messages),
] {
assert!(object.property_list().contains(&property), "{label}");
assert_eq!(object.read_property(property, None).unwrap(), *expected);
assert_eq!(
object.read_property(property, Some(0)).unwrap(),
PropertyValue::Unsigned(3)
);
let second = match property {
p if p == PropertyIdentifier::EVENT_TIME_STAMPS => {
timestamp_property_value(&BACnetTimeStamp::SequenceNumber(0))
}
_ => PropertyValue::CharacterString(String::new()),
};
assert_eq!(object.read_property(property, Some(2)).unwrap(), second);
assert_property_read_error(
object.read_property(property, Some(4)),
ErrorCode::INVALID_ARRAY_INDEX,
);
assert_write_access_denied(
object.write_property(property, None, expected.clone(), None),
property,
label,
);
assert!(!object.is_writable_property(property));
}
}
}
macro_rules! assert_seeded_binary_event_history {
($object:expr, $label:literal) => {{
let mut object = $object;
let expected_timestamps = [
BACnetTimeStamp::Time(Time {
hour: 1,
minute: 2,
second: 3,
hundredths: 4,
}),
BACnetTimeStamp::SequenceNumber(22),
BACnetTimeStamp::DateTime {
date: Date {
year: 126,
month: 7,
day: 30,
day_of_week: 4,
},
time: Time {
hour: 12,
minute: 34,
second: 56,
hundredths: 78,
},
},
];
object.event_history.time_stamps = expected_timestamps.clone();
object.event_history.message_texts = ["offnormal".into(), "fault".into(), "normal".into()];
assert_timestamp_array(
object
.read_property(PropertyIdentifier::EVENT_TIME_STAMPS, None)
.unwrap(),
&expected_timestamps,
concat!($label, " timestamps"),
);
assert_eq!(
object
.read_property(PropertyIdentifier::EVENT_MESSAGE_TEXTS, None)
.unwrap(),
PropertyValue::List(vec![
PropertyValue::CharacterString("offnormal".into()),
PropertyValue::CharacterString("fault".into()),
PropertyValue::CharacterString("normal".into()),
]),
"{} messages",
$label
);
for property in [
PropertyIdentifier::EVENT_TIME_STAMPS,
PropertyIdentifier::EVENT_MESSAGE_TEXTS,
] {
assert_eq!(
object.read_property(property, Some(0)).unwrap(),
PropertyValue::Unsigned(3),
"{} {property:?} count",
$label
);
}
for (slot, expected) in expected_timestamps.iter().enumerate() {
assert_timestamp_value(
object
.read_property(PropertyIdentifier::EVENT_TIME_STAMPS, Some(slot as u32 + 1))
.unwrap(),
expected,
&format!("{} timestamp slot {}", $label, slot + 1),
);
}
for (slot, expected) in ["offnormal", "fault", "normal"].into_iter().enumerate() {
assert_eq!(
object
.read_property(
PropertyIdentifier::EVENT_MESSAGE_TEXTS,
Some(slot as u32 + 1),
)
.unwrap(),
PropertyValue::CharacterString(expected.into()),
"{} message slot {}",
$label,
slot + 1
);
}
for property in [
PropertyIdentifier::EVENT_TIME_STAMPS,
PropertyIdentifier::EVENT_MESSAGE_TEXTS,
] {
for index in [4, u32::MAX] {
assert_property_read_error(
object.read_property(property, Some(index)),
ErrorCode::INVALID_ARRAY_INDEX,
);
}
}
}};
}
#[test]
fn binary_seeded_event_history_preserves_array_order_and_timestamp_choices() {
assert_seeded_binary_event_history!(BinaryInputObject::new(1, "BI-1").unwrap(), "BI");
assert_seeded_binary_event_history!(BinaryOutputObject::new(1, "BO-1").unwrap(), "BO");
assert_seeded_binary_event_history!(BinaryValueObject::new(1, "BV-1").unwrap(), "BV");
}
macro_rules! assert_binary_history_reset {
($object:expr, $label:literal) => {{
let mut object = $object;
object.event_history.time_stamps[0] = BACnetTimeStamp::SequenceNumber(91);
object.event_history.message_texts[1] = "seeded".into();
object
.write_property(
PropertyIdentifier::EVENT_DETECTION_ENABLE,
None,
PropertyValue::Boolean(false),
None,
)
.unwrap();
assert_eq!(
object
.read_property(PropertyIdentifier::EVENT_TIME_STAMPS, None)
.unwrap(),
PropertyValue::List(vec![
timestamp_property_value(
&BACnetTimeStamp::SequenceNumber(0)
);
3
]),
"{} timestamp reset",
$label
);
assert_eq!(
object
.read_property(PropertyIdentifier::EVENT_MESSAGE_TEXTS, None)
.unwrap(),
PropertyValue::List(vec![PropertyValue::CharacterString(String::new()); 3]),
"{} message reset",
$label
);
}};
}
#[test]
fn binary_detection_disable_resets_each_event_history() {
assert_binary_history_reset!(BinaryInputObject::new(1, "BI-1").unwrap(), "BI");
assert_binary_history_reset!(BinaryOutputObject::new(1, "BO-1").unwrap(), "BO");
assert_binary_history_reset!(BinaryValueObject::new(1, "BV-1").unwrap(), "BV");
}
#[test]
fn bi_notify_type_rejects_out_of_production_values() {
let mut bi = BinaryInputObject::new(1, "BI-1").unwrap();
assert_eq!(
bi.read_property(PropertyIdentifier::NOTIFY_TYPE, None)
.unwrap(),
PropertyValue::Enumerated(0)
);
for out_of_production in [3u32, 99, u32::MAX] {
assert_protocol_error(
bi.write_property(
PropertyIdentifier::NOTIFY_TYPE,
None,
PropertyValue::Enumerated(out_of_production),
None,
),
ErrorCode::VALUE_OUT_OF_RANGE,
);
assert_eq!(
bi.read_property(PropertyIdentifier::NOTIFY_TYPE, None)
.unwrap(),
PropertyValue::Enumerated(0),
"a refused Notify_Type write must leave the value untouched ({out_of_production})"
);
}
for in_production in [0u32, 1, 2] {
bi.write_property(
PropertyIdentifier::NOTIFY_TYPE,
None,
PropertyValue::Enumerated(in_production),
None,
)
.expect("named Notify_Type values must be accepted");
}
}
#[test]
fn bi_event_enable_rejects_noncanonical_bit_strings() {
let mut bi = BinaryInputObject::new(1, "BI-1").unwrap();
let canonical = bi
.read_property(PropertyIdentifier::EVENT_ENABLE, None)
.unwrap();
for (unused_bits, data) in [
(0u8, vec![0xFFu8]), (5u8, vec![0xFF, 0xFF]), (4u8, vec![0xF0u8]), (5u8, vec![]), ] {
assert_protocol_error(
bi.write_property(
PropertyIdentifier::EVENT_ENABLE,
None,
PropertyValue::BitString { unused_bits, data },
None,
),
ErrorCode::INVALID_DATA_ENCODING,
);
assert_eq!(
bi.read_property(PropertyIdentifier::EVENT_ENABLE, None)
.unwrap(),
canonical,
"a refused Event_Enable write must leave the value untouched"
);
}
bi.write_property(
PropertyIdentifier::EVENT_ENABLE,
None,
PropertyValue::BitString {
unused_bits: 5,
data: vec![0xA0], },
None,
)
.unwrap();
assert_eq!(
bi.read_property(PropertyIdentifier::EVENT_ENABLE, None)
.unwrap(),
PropertyValue::BitString {
unused_bits: 5,
data: vec![0xA0],
}
);
}