use super::super::*;
use bacnet_types::bitstring::LimitEnable;
use bacnet_types::enums::EventState;
#[test]
fn ai_read_present_value() {
let mut ai = AnalogInputObject::new(1, "AI-1", 62).unwrap(); ai.set_present_value(72.5);
let val = ai
.read_property(PropertyIdentifier::PRESENT_VALUE, None)
.unwrap();
assert_eq!(val, PropertyValue::Real(72.5));
}
#[test]
fn ai_read_units() {
let ai = AnalogInputObject::new(1, "AI-1", 62).unwrap();
let val = ai.read_property(PropertyIdentifier::UNITS, None).unwrap();
assert_eq!(val, PropertyValue::Enumerated(62));
}
#[test]
fn ai_write_present_value_denied_when_in_service() {
let mut ai = AnalogInputObject::new(1, "AI-1", 62).unwrap();
let result = ai.write_property(
PropertyIdentifier::PRESENT_VALUE,
None,
PropertyValue::Real(99.0),
None,
);
assert!(result.is_err());
}
#[test]
fn ai_write_present_value_allowed_when_out_of_service() {
let mut ai = AnalogInputObject::new(1, "AI-1", 62).unwrap();
ai.write_property(
PropertyIdentifier::OUT_OF_SERVICE,
None,
PropertyValue::Boolean(true),
None,
)
.unwrap();
ai.write_property(
PropertyIdentifier::PRESENT_VALUE,
None,
PropertyValue::Real(99.0),
None,
)
.unwrap();
let val = ai
.read_property(PropertyIdentifier::PRESENT_VALUE, None)
.unwrap();
assert_eq!(val, PropertyValue::Real(99.0));
}
#[test]
fn ai_read_unknown_property() {
let ai = AnalogInputObject::new(1, "AI-1", 62).unwrap();
let result = ai.read_property(PropertyIdentifier::PRIORITY_ARRAY, None);
assert!(result.is_err());
}
#[test]
fn ai_read_event_state_default_normal() {
let ai = AnalogInputObject::new(1, "AI-1", 62).unwrap();
let val = ai
.read_property(PropertyIdentifier::EVENT_STATE, None)
.unwrap();
assert_eq!(val, PropertyValue::Enumerated(EventState::NORMAL.to_raw()));
}
#[test]
fn ai_read_write_high_limit() {
let mut ai = AnalogInputObject::new(1, "AI-1", 62).unwrap();
ai.write_property(
PropertyIdentifier::HIGH_LIMIT,
None,
PropertyValue::Real(85.0),
None,
)
.unwrap();
assert_eq!(
ai.read_property(PropertyIdentifier::HIGH_LIMIT, None)
.unwrap(),
PropertyValue::Real(85.0)
);
}
#[test]
fn ai_read_write_low_limit() {
let mut ai = AnalogInputObject::new(1, "AI-1", 62).unwrap();
ai.write_property(
PropertyIdentifier::LOW_LIMIT,
None,
PropertyValue::Real(15.0),
None,
)
.unwrap();
assert_eq!(
ai.read_property(PropertyIdentifier::LOW_LIMIT, None)
.unwrap(),
PropertyValue::Real(15.0)
);
}
#[test]
fn ai_read_write_deadband() {
let mut ai = AnalogInputObject::new(1, "AI-1", 62).unwrap();
ai.write_property(
PropertyIdentifier::DEADBAND,
None,
PropertyValue::Real(2.5),
None,
)
.unwrap();
assert_eq!(
ai.read_property(PropertyIdentifier::DEADBAND, None)
.unwrap(),
PropertyValue::Real(2.5)
);
}
#[test]
fn ai_deadband_reject_negative() {
let mut ai = AnalogInputObject::new(1, "AI-1", 62).unwrap();
let result = ai.write_property(
PropertyIdentifier::DEADBAND,
None,
PropertyValue::Real(-1.0),
None,
);
assert!(result.is_err());
}
#[test]
fn ai_read_write_limit_enable() {
let mut ai = AnalogInputObject::new(1, "AI-1", 62).unwrap();
let enable_both = LimitEnable::all().to_bacnet();
ai.write_property(
PropertyIdentifier::LIMIT_ENABLE,
None,
PropertyValue::BitString {
unused_bits: 6,
data: vec![enable_both],
},
None,
)
.unwrap();
let val = ai
.read_property(PropertyIdentifier::LIMIT_ENABLE, None)
.unwrap();
if let PropertyValue::BitString { data, .. } = val {
assert_eq!(LimitEnable::from_bacnet(&data), LimitEnable::all());
} else {
panic!("Expected BitString");
}
}
#[test]
fn ai_intrinsic_reporting_triggers_on_present_value_change() {
let mut ai = AnalogInputObject::new(1, "AI-1", 62).unwrap();
ai.write_property(
PropertyIdentifier::HIGH_LIMIT,
None,
PropertyValue::Real(80.0),
None,
)
.unwrap();
ai.write_property(
PropertyIdentifier::LOW_LIMIT,
None,
PropertyValue::Real(20.0),
None,
)
.unwrap();
ai.write_property(
PropertyIdentifier::DEADBAND,
None,
PropertyValue::Real(2.0),
None,
)
.unwrap();
ai.write_property(
PropertyIdentifier::LIMIT_ENABLE,
None,
PropertyValue::BitString {
unused_bits: 6,
data: vec![LimitEnable::all().to_bacnet()],
},
None,
)
.unwrap();
ai.write_property(
PropertyIdentifier::EVENT_ENABLE,
None,
PropertyValue::BitString {
unused_bits: 5,
data: vec![0xE0], },
None,
)
.unwrap();
ai.set_present_value(50.0);
assert!(ai.evaluate_intrinsic_reporting().is_none());
ai.set_present_value(81.0);
let proposal = ai.evaluate_intrinsic_reporting().unwrap();
let outcome = crate::event::commit_test_proposal(&mut ai, proposal);
let change = outcome.change;
assert_eq!(change.from, EventState::NORMAL);
assert_eq!(change.to, EventState::HIGH_LIMIT);
assert_eq!(
ai.read_property(PropertyIdentifier::EVENT_STATE, None)
.unwrap(),
PropertyValue::Enumerated(EventState::HIGH_LIMIT.to_raw())
);
ai.set_present_value(77.0);
let change = ai.evaluate_intrinsic_reporting().unwrap().change;
assert_eq!(change.to, EventState::NORMAL);
}
#[test]
fn ai_read_reliability_default() {
let ai = AnalogInputObject::new(1, "AI-1", 62).unwrap();
let val = ai
.read_property(PropertyIdentifier::RELIABILITY, None)
.unwrap();
assert_eq!(val, PropertyValue::Enumerated(0)); }
#[test]
fn ai_description_read_write() {
let mut ai = AnalogInputObject::new(1, "AI-1", 62).unwrap();
let val = ai
.read_property(PropertyIdentifier::DESCRIPTION, None)
.unwrap();
assert_eq!(val, PropertyValue::CharacterString(String::new()));
ai.write_property(
PropertyIdentifier::DESCRIPTION,
None,
PropertyValue::CharacterString("Zone temperature sensor".into()),
None,
)
.unwrap();
let val = ai
.read_property(PropertyIdentifier::DESCRIPTION, None)
.unwrap();
assert_eq!(
val,
PropertyValue::CharacterString("Zone temperature sensor".into())
);
}
#[test]
fn ai_set_description_convenience() {
let mut ai = AnalogInputObject::new(1, "AI-1", 62).unwrap();
ai.set_description("Supply air temperature");
assert_eq!(
ai.read_property(PropertyIdentifier::DESCRIPTION, None)
.unwrap(),
PropertyValue::CharacterString("Supply air temperature".into())
);
}
#[test]
fn ai_description_in_property_list() {
let ai = AnalogInputObject::new(1, "AI-1", 62).unwrap();
assert!(ai
.property_list()
.contains(&PropertyIdentifier::DESCRIPTION));
}
#[test]
fn ai_is_createable_matches_factory() {
use crate::traits::BACnetObject;
let ai = AnalogInputObject::new(1, "ai-1", 95).unwrap();
assert!(ai.is_createable(), "AnalogInput is factory-constructable");
}
#[test]
fn ai_is_writable_property_mirrors_write_property() {
use crate::traits::BACnetObject;
let ai = AnalogInputObject::new(1, "ai-1", 95).unwrap();
assert!(ai.is_writable_property(PropertyIdentifier::LIMIT_ENABLE));
assert!(ai.is_writable_property(PropertyIdentifier::NOTIFY_TYPE));
assert!(ai.is_writable_property(PropertyIdentifier::TIME_DELAY));
assert!(ai.is_writable_property(PropertyIdentifier::EVENT_ENABLE));
assert!(ai.is_writable_property(PropertyIdentifier::NOTIFICATION_CLASS));
assert!(ai.is_writable_property(PropertyIdentifier::OUT_OF_SERVICE));
assert!(ai.is_writable_property(PropertyIdentifier::OBJECT_NAME));
assert!(ai.is_writable_property(PropertyIdentifier::DESCRIPTION));
assert!(ai.is_writable_property(PropertyIdentifier::RELIABILITY));
assert!(ai.is_writable_property(PropertyIdentifier::COV_INCREMENT));
assert!(ai.is_writable_property(PropertyIdentifier::PRESENT_VALUE));
let mut ai_mut = AnalogInputObject::new(2, "ai-2", 95).unwrap();
assert!(!ai.is_writable_property(PropertyIdentifier::ACKED_TRANSITIONS));
assert!(ai_mut
.write_property(
PropertyIdentifier::ACKED_TRANSITIONS,
None,
bacnet_types::primitives::PropertyValue::BitString {
unused_bits: 5,
data: vec![0x80],
},
None,
)
.is_err());
assert!(!ai.is_writable_property(PropertyIdentifier::OBJECT_IDENTIFIER));
assert!(!ai.is_writable_property(PropertyIdentifier::OBJECT_TYPE));
assert!(!ai.is_writable_property(PropertyIdentifier::PROPERTY_LIST));
assert!(!ai.is_writable_property(PropertyIdentifier::STATUS_FLAGS));
assert!(!ai.is_writable_property(PropertyIdentifier::PRIORITY_ARRAY));
assert!(!ai.is_writable_property(PropertyIdentifier::RELINQUISH_DEFAULT));
}
fn assert_property_error_code(result: Result<(), Error>, code: bacnet_types::enums::ErrorCode) {
match result.unwrap_err() {
Error::Protocol {
class,
code: actual,
} => {
assert_eq!(
class,
bacnet_types::enums::ErrorClass::PROPERTY.to_raw() as u32
);
assert_eq!(actual, code.to_raw() as u32);
}
other => panic!("expected protocol error, got {other:?}"),
}
}
#[test]
fn ai_notify_type_rejects_out_of_production_values() {
let mut ai = AnalogInputObject::new(1, "AI-1", 62).unwrap();
assert_eq!(
ai.read_property(PropertyIdentifier::NOTIFY_TYPE, None)
.unwrap(),
PropertyValue::Enumerated(0)
);
for out_of_production in [3u32, 99, u32::MAX] {
assert_property_error_code(
ai.write_property(
PropertyIdentifier::NOTIFY_TYPE,
None,
PropertyValue::Enumerated(out_of_production),
None,
),
bacnet_types::enums::ErrorCode::VALUE_OUT_OF_RANGE,
);
assert_eq!(
ai.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] {
ai.write_property(
PropertyIdentifier::NOTIFY_TYPE,
None,
PropertyValue::Enumerated(in_production),
None,
)
.expect("named Notify_Type values must be accepted");
}
}
#[test]
fn ai_event_enable_rejects_noncanonical_bit_strings() {
let mut ai = AnalogInputObject::new(1, "AI-1", 62).unwrap();
let canonical = ai
.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_property_error_code(
ai.write_property(
PropertyIdentifier::EVENT_ENABLE,
None,
PropertyValue::BitString { unused_bits, data },
None,
),
bacnet_types::enums::ErrorCode::INVALID_DATA_ENCODING,
);
assert_eq!(
ai.read_property(PropertyIdentifier::EVENT_ENABLE, None)
.unwrap(),
canonical,
"a refused Event_Enable write must leave the value untouched"
);
}
ai.write_property(
PropertyIdentifier::EVENT_ENABLE,
None,
PropertyValue::BitString {
unused_bits: 5,
data: vec![0xA0], },
None,
)
.unwrap();
assert_eq!(
ai.read_property(PropertyIdentifier::EVENT_ENABLE, None)
.unwrap(),
PropertyValue::BitString {
unused_bits: 5,
data: vec![0xA0],
}
);
}
#[test]
fn ai_limit_enable_rejects_noncanonical_bit_strings() {
let mut ai = AnalogInputObject::new(1, "AI-1", 62).unwrap();
let canonical = ai
.read_property(PropertyIdentifier::LIMIT_ENABLE, None)
.unwrap();
for (unused_bits, data) in [
(0u8, vec![0xFFu8]), (5u8, vec![0xE0u8]), (6u8, vec![0xC0, 0x00]), (6u8, vec![]), ] {
assert_property_error_code(
ai.write_property(
PropertyIdentifier::LIMIT_ENABLE,
None,
PropertyValue::BitString { unused_bits, data },
None,
),
bacnet_types::enums::ErrorCode::INVALID_DATA_ENCODING,
);
assert_eq!(
ai.read_property(PropertyIdentifier::LIMIT_ENABLE, None)
.unwrap(),
canonical,
"a refused Limit_Enable write must leave the value untouched"
);
}
ai.write_property(
PropertyIdentifier::LIMIT_ENABLE,
None,
PropertyValue::BitString {
unused_bits: 6,
data: vec![0xC0], },
None,
)
.unwrap();
assert_eq!(
ai.read_property(PropertyIdentifier::LIMIT_ENABLE, None)
.unwrap(),
PropertyValue::BitString {
unused_bits: 6,
data: vec![LimitEnable::all().to_bacnet()],
}
);
}
fn assert_tdn_protocol_error(result: Result<(), Error>, code: bacnet_types::enums::ErrorCode) {
match result.unwrap_err() {
Error::Protocol {
class,
code: actual,
} => {
assert_eq!(
class,
bacnet_types::enums::ErrorClass::PROPERTY.to_raw() as u32
);
assert_eq!(actual, code.to_raw() as u32);
}
other => panic!("expected protocol error, got {other:?}"),
}
}
#[test]
fn analog_time_delay_normal_round_trips_and_matches_pics() {
for (mut object, label) in [
(
Box::new(AnalogInputObject::new(1, "AI-1", 62).unwrap()) as Box<dyn BACnetObject>,
"AI",
),
(
Box::new(AnalogOutputObject::new(1, "AO-1", 62).unwrap()) as Box<dyn BACnetObject>,
"AO",
),
(
Box::new(AnalogValueObject::new(1, "AV-1", 62).unwrap()) as Box<dyn BACnetObject>,
"AV",
),
] {
object
.write_property(
PropertyIdentifier::TIME_DELAY_NORMAL,
None,
PropertyValue::Unsigned(9),
None,
)
.unwrap_or_else(|e| panic!("{label}: Time_Delay_Normal write rejected: {e:?}"));
assert_eq!(
object
.read_property(PropertyIdentifier::TIME_DELAY_NORMAL, None)
.unwrap(),
PropertyValue::Unsigned(9),
"{label}: Time_Delay_Normal must read back what was written"
);
assert!(
object
.property_list()
.contains(&PropertyIdentifier::TIME_DELAY_NORMAL),
"{label}: Time_Delay_Normal must be advertised in Property_List"
);
assert!(
object.is_writable_property(PropertyIdentifier::TIME_DELAY_NORMAL),
"{label}: Time_Delay_Normal must be advertised writable"
);
assert_tdn_protocol_error(
object.write_property(
PropertyIdentifier::TIME_DELAY_NORMAL,
None,
PropertyValue::Enumerated(9),
None,
),
bacnet_types::enums::ErrorCode::INVALID_DATA_TYPE,
);
assert_tdn_protocol_error(
object.write_property(
PropertyIdentifier::TIME_DELAY_NORMAL,
None,
PropertyValue::Unsigned(u32::MAX as u64 + 1),
None,
),
bacnet_types::enums::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"
);
}
}
#[test]
fn analog_time_delay_normal_defaults_to_time_delay_when_unwritten() {
for (mut object, label) in [
(
Box::new(AnalogInputObject::new(1, "AI-1", 62).unwrap()) as Box<dyn BACnetObject>,
"AI",
),
(
Box::new(AnalogOutputObject::new(1, "AO-1", 62).unwrap()) as Box<dyn BACnetObject>,
"AO",
),
(
Box::new(AnalogValueObject::new(1, "AV-1", 62).unwrap()) as Box<dyn BACnetObject>,
"AV",
),
] {
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 ai_time_delay_normal_gates_the_return_to_normal() {
let mut ai = AnalogInputObject::new(1, "AI-1", 62).unwrap();
for (property, value) in [
(PropertyIdentifier::HIGH_LIMIT, 80.0),
(PropertyIdentifier::LOW_LIMIT, 20.0),
(PropertyIdentifier::DEADBAND, 2.0),
] {
ai.write_property(property, None, PropertyValue::Real(value), None)
.unwrap();
}
ai.write_property(
PropertyIdentifier::LIMIT_ENABLE,
None,
PropertyValue::BitString {
unused_bits: 6,
data: vec![LimitEnable::all().to_bacnet()],
},
None,
)
.unwrap();
ai.write_property(
PropertyIdentifier::TIME_DELAY,
None,
PropertyValue::Unsigned(2),
None,
)
.unwrap();
ai.write_property(
PropertyIdentifier::TIME_DELAY_NORMAL,
None,
PropertyValue::Unsigned(4),
None,
)
.unwrap();
ai.set_present_value(81.0);
assert_eq!(ai.evaluate_intrinsic_reporting(), None);
assert_eq!(ai.tick_intrinsic_reporting(), None);
let proposal = ai.tick_intrinsic_reporting().unwrap();
let outcome = crate::event::commit_test_proposal(&mut ai, proposal);
assert_eq!(
outcome.change.to,
EventState::HIGH_LIMIT,
"condition (a): HIGH_LIMIT after Time_Delay = 2"
);
ai.set_present_value(50.0); assert_eq!(
ai.evaluate_intrinsic_reporting(),
None,
"condition (e): return to NORMAL only seeds the countdown"
);
for _ in 0..3 {
assert_eq!(
ai.tick_intrinsic_reporting(),
None,
"Time_Delay_Normal = 4 must hold the NORMAL state off"
);
}
let outcome = ai.tick_intrinsic_reporting().unwrap();
assert_eq!(outcome.change.from, EventState::HIGH_LIMIT);
assert_eq!(outcome.change.to, EventState::NORMAL);
}