use bacnet_objects::analog::{AnalogInputObject, AnalogOutputObject, AnalogValueObject};
use bacnet_objects::binary::{BinaryInputObject, BinaryOutputObject, BinaryValueObject};
use bacnet_objects::multistate::{
MultiStateInputObject, MultiStateOutputObject, MultiStateValueObject,
};
use bacnet_objects::traits::BACnetObject;
use bacnet_types::enums::{ObjectType, PropertyIdentifier};
use bacnet_types::primitives::PropertyValue;
use super::{PicsConfig, PicsGenerator};
use crate::server::ServerConfig;
#[test]
fn is_writable_property_matches_write_property_on_all_core_types() {
use bacnet_objects::event::LimitEnable;
const READ_ONLY: PropertyIdentifier = PropertyIdentifier::STATUS_FLAGS;
fn assert_accepts(
obj: &mut dyn BACnetObject,
pid: PropertyIdentifier,
value: PropertyValue,
label: &str,
) {
assert!(
obj.is_writable_property(pid),
"{label}: is_writable_property must report {pid:?} writable"
);
let result = obj.write_property(pid, None, value, None);
assert!(
result.is_ok(),
"{label}: write_property must accept {pid:?}, got: {result:?}"
);
}
fn assert_rejects(obj: &mut dyn BACnetObject, pid: PropertyIdentifier, label: &str) {
assert!(
!obj.is_writable_property(pid),
"{label}: is_writable_property must report {pid:?} NOT writable"
);
let result = obj.write_property(pid, None, PropertyValue::Null, None);
assert!(
result.is_err(),
"{label}: write_property must reject {pid:?}, got: {result:?}"
);
}
fn assert_accepts_indexed(
obj: &mut dyn BACnetObject,
pid: PropertyIdentifier,
index: u32,
value: PropertyValue,
label: &str,
) {
assert!(
obj.is_writable_property(pid),
"{label}: is_writable_property must report {pid:?} writable"
);
let result = obj.write_property(pid, Some(index), value, None);
assert!(
result.is_ok(),
"{label}: write_property must accept {pid:?}[{index}], got: {result:?}"
);
}
let mut ai = AnalogInputObject::new(1, "ai", 95).unwrap();
ai.write_property(
PropertyIdentifier::OUT_OF_SERVICE,
None,
PropertyValue::Boolean(true),
None,
)
.unwrap();
assert_accepts(
&mut ai,
PropertyIdentifier::PRESENT_VALUE,
PropertyValue::Real(50.0),
"AI",
);
assert_accepts(
&mut ai,
PropertyIdentifier::COV_INCREMENT,
PropertyValue::Real(1.0),
"AI",
);
assert_accepts(
&mut ai,
PropertyIdentifier::RELIABILITY,
PropertyValue::Enumerated(0),
"AI",
);
assert_accepts(
&mut ai,
PropertyIdentifier::LIMIT_ENABLE,
PropertyValue::BitString {
unused_bits: 6,
data: vec![LimitEnable::BOTH.to_bits()],
},
"AI",
);
assert_rejects(&mut ai, READ_ONLY, "AI");
let mut ao = AnalogOutputObject::new(1, "ao", 95).unwrap();
ao.write_property(
PropertyIdentifier::OUT_OF_SERVICE,
None,
PropertyValue::Boolean(true),
None,
)
.unwrap();
assert_accepts(
&mut ao,
PropertyIdentifier::PRESENT_VALUE,
PropertyValue::Real(50.0),
"AO",
);
assert_accepts_indexed(
&mut ao,
PropertyIdentifier::PRIORITY_ARRAY,
1,
PropertyValue::Null,
"AO",
);
assert_accepts(
&mut ao,
PropertyIdentifier::COV_INCREMENT,
PropertyValue::Real(1.0),
"AO",
);
assert_accepts(
&mut ao,
PropertyIdentifier::RELIABILITY,
PropertyValue::Enumerated(0),
"AO",
);
assert_accepts(
&mut ao,
PropertyIdentifier::NOTIFY_TYPE,
PropertyValue::Enumerated(0),
"AO",
);
assert_rejects(&mut ao, READ_ONLY, "AO");
let mut av = AnalogValueObject::new(1, "av", 95).unwrap();
av.write_property(
PropertyIdentifier::OUT_OF_SERVICE,
None,
PropertyValue::Boolean(true),
None,
)
.unwrap();
assert_accepts(
&mut av,
PropertyIdentifier::PRESENT_VALUE,
PropertyValue::Real(50.0),
"AV",
);
assert_accepts_indexed(
&mut av,
PropertyIdentifier::PRIORITY_ARRAY,
1,
PropertyValue::Null,
"AV",
);
assert_accepts(
&mut av,
PropertyIdentifier::COV_INCREMENT,
PropertyValue::Real(1.0),
"AV",
);
assert_accepts(
&mut av,
PropertyIdentifier::RELIABILITY,
PropertyValue::Enumerated(0),
"AV",
);
assert_accepts(
&mut av,
PropertyIdentifier::EVENT_ENABLE,
PropertyValue::BitString {
unused_bits: 5,
data: vec![0xE0], },
"AV",
);
assert_rejects(&mut av, READ_ONLY, "AV");
let mut bi = BinaryInputObject::new(1, "bi").unwrap();
bi.write_property(
PropertyIdentifier::OUT_OF_SERVICE,
None,
PropertyValue::Boolean(true),
None,
)
.unwrap();
assert_accepts(
&mut bi,
PropertyIdentifier::PRESENT_VALUE,
PropertyValue::Enumerated(1),
"BI",
);
assert_accepts(
&mut bi,
PropertyIdentifier::ACTIVE_TEXT,
PropertyValue::CharacterString("on".into()),
"BI",
);
assert_accepts(
&mut bi,
PropertyIdentifier::INACTIVE_TEXT,
PropertyValue::CharacterString("off".into()),
"BI",
);
assert_accepts(
&mut bi,
PropertyIdentifier::EVENT_ENABLE,
PropertyValue::BitString {
unused_bits: 5,
data: vec![0xE0], },
"BI",
);
assert_rejects(&mut bi, READ_ONLY, "BI");
let mut bo = BinaryOutputObject::new(1, "bo").unwrap();
assert_accepts(
&mut bo,
PropertyIdentifier::PRESENT_VALUE,
PropertyValue::Enumerated(1),
"BO",
);
assert_accepts_indexed(
&mut bo,
PropertyIdentifier::PRIORITY_ARRAY,
1,
PropertyValue::Null,
"BO",
);
assert_accepts(
&mut bo,
PropertyIdentifier::ACTIVE_TEXT,
PropertyValue::CharacterString("on".into()),
"BO",
);
assert_accepts(
&mut bo,
PropertyIdentifier::INACTIVE_TEXT,
PropertyValue::CharacterString("off".into()),
"BO",
);
assert_rejects(&mut bo, READ_ONLY, "BO");
let mut bv = BinaryValueObject::new(1, "bv").unwrap();
assert_accepts(
&mut bv,
PropertyIdentifier::PRESENT_VALUE,
PropertyValue::Enumerated(1),
"BV",
);
assert_accepts_indexed(
&mut bv,
PropertyIdentifier::PRIORITY_ARRAY,
1,
PropertyValue::Null,
"BV",
);
assert_accepts(
&mut bv,
PropertyIdentifier::ACTIVE_TEXT,
PropertyValue::CharacterString("on".into()),
"BV",
);
assert_accepts(
&mut bv,
PropertyIdentifier::INACTIVE_TEXT,
PropertyValue::CharacterString("off".into()),
"BV",
);
assert_accepts(
&mut bv,
PropertyIdentifier::NOTIFY_TYPE,
PropertyValue::Enumerated(0),
"BV",
);
assert_rejects(&mut bv, READ_ONLY, "BV");
let mut msi = MultiStateInputObject::new(1, "msi", 2).unwrap();
msi.write_property(
PropertyIdentifier::OUT_OF_SERVICE,
None,
PropertyValue::Boolean(true),
None,
)
.unwrap();
assert_accepts(
&mut msi,
PropertyIdentifier::PRESENT_VALUE,
PropertyValue::Unsigned(1),
"MSI",
);
assert_accepts_indexed(
&mut msi,
PropertyIdentifier::STATE_TEXT,
1,
PropertyValue::CharacterString("s1".into()),
"MSI",
);
assert_accepts(
&mut msi,
PropertyIdentifier::TIME_DELAY,
PropertyValue::Unsigned(5),
"MSI",
);
assert_rejects(&mut msi, READ_ONLY, "MSI");
let mut mso = MultiStateOutputObject::new(1, "mso", 2).unwrap();
assert_accepts(
&mut mso,
PropertyIdentifier::PRESENT_VALUE,
PropertyValue::Unsigned(1),
"MSO",
);
assert_accepts_indexed(
&mut mso,
PropertyIdentifier::PRIORITY_ARRAY,
1,
PropertyValue::Null,
"MSO",
);
assert_accepts_indexed(
&mut mso,
PropertyIdentifier::STATE_TEXT,
1,
PropertyValue::CharacterString("s1".into()),
"MSO",
);
assert_rejects(&mut mso, READ_ONLY, "MSO");
let mut msv = MultiStateValueObject::new(1, "msv", 2).unwrap();
assert_accepts(
&mut msv,
PropertyIdentifier::PRESENT_VALUE,
PropertyValue::Unsigned(1),
"MSV",
);
assert_accepts_indexed(
&mut msv,
PropertyIdentifier::PRIORITY_ARRAY,
1,
PropertyValue::Null,
"MSV",
);
assert_accepts_indexed(
&mut msv,
PropertyIdentifier::STATE_TEXT,
1,
PropertyValue::CharacterString("s1".into()),
"MSV",
);
assert_accepts(
&mut msv,
PropertyIdentifier::NOTIFICATION_CLASS,
PropertyValue::Unsigned(3),
"MSV",
);
assert_rejects(&mut msv, READ_ONLY, "MSV");
}
#[test]
fn is_writable_property_matches_write_property_on_pulse_converter_and_averaging() {
use bacnet_objects::accumulator::PulseConverterObject;
use bacnet_objects::averaging::AveragingObject;
use bacnet_objects::database::ObjectDatabase;
use bacnet_types::enums::ObjectType;
use bacnet_types::primitives::ObjectIdentifier;
fn assert_exactly(
obj: &mut dyn BACnetObject,
label: &str,
accepted: &[(PropertyIdentifier, PropertyValue)],
rejected: &[(PropertyIdentifier, PropertyValue)],
) {
for (pid, value) in accepted.iter().cloned() {
assert!(
obj.is_writable_property(pid),
"{label}: {pid:?} must be advertised writable"
);
assert!(
obj.write_property(pid, None, value, None).is_ok(),
"{label}: {pid:?} advertised writable but the arm rejected a good value"
);
}
for (pid, value) in rejected.iter().cloned() {
assert!(
!obj.is_writable_property(pid),
"{label}: {pid:?} must NOT be advertised writable"
);
assert!(
obj.write_property(pid, None, value, None).is_err(),
"{label}: {pid:?} not advertised but the arm accepted a write"
);
}
}
let reference_value = PropertyValue::List(vec![
PropertyValue::ObjectIdentifier(
ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 5).unwrap(),
),
PropertyValue::Enumerated(PropertyIdentifier::PRESENT_VALUE.to_raw()),
]);
let mut pc = PulseConverterObject::new(1, "PC-1", 62).unwrap();
assert_exactly(
&mut pc,
"PC",
&[
(
PropertyIdentifier::OUT_OF_SERVICE,
PropertyValue::Boolean(true),
),
(PropertyIdentifier::PRESENT_VALUE, PropertyValue::Real(10.0)),
(PropertyIdentifier::SCALE_FACTOR, PropertyValue::Real(1.5)),
(PropertyIdentifier::ADJUST_VALUE, PropertyValue::Real(2.0)),
(PropertyIdentifier::COV_INCREMENT, PropertyValue::Real(0.5)),
(PropertyIdentifier::INPUT_REFERENCE, reference_value.clone()),
(
PropertyIdentifier::DESCRIPTION,
PropertyValue::CharacterString("d".into()),
),
],
&[
(
PropertyIdentifier::OBJECT_NAME,
PropertyValue::CharacterString("renamed".into()),
),
(PropertyIdentifier::UNITS, PropertyValue::Enumerated(95)),
(
PropertyIdentifier::RELIABILITY,
PropertyValue::Enumerated(0),
),
(
PropertyIdentifier::EVENT_STATE,
PropertyValue::Enumerated(0),
),
(
PropertyIdentifier::STATUS_FLAGS,
PropertyValue::BitString {
unused_bits: 4,
data: vec![0],
},
),
],
);
let mut db = ObjectDatabase::new();
db.add(Box::new(pc)).unwrap();
let pics = PicsGenerator::new(&db, &ServerConfig::default(), &PicsConfig::default()).generate();
let pulse_support = pics
.supported_object_types
.iter()
.find(|support| support.object_type == ObjectType::PULSE_CONVERTER)
.unwrap();
assert!(pulse_support.supported_properties.iter().any(|property| {
property.property_id == PropertyIdentifier::PRESENT_VALUE && property.access.writable
}));
let mut avg = AveragingObject::new(1, "AVG-1").unwrap();
assert_exactly(
&mut avg,
"AVG",
&[
(
PropertyIdentifier::OBJECT_PROPERTY_REFERENCE,
reference_value,
),
(
PropertyIdentifier::DESCRIPTION,
PropertyValue::CharacterString("d".into()),
),
(
PropertyIdentifier::OUT_OF_SERVICE,
PropertyValue::Boolean(true),
),
],
&[
(
PropertyIdentifier::OBJECT_NAME,
PropertyValue::CharacterString("renamed".into()),
),
(PropertyIdentifier::PRESENT_VALUE, PropertyValue::Real(1.0)),
(PropertyIdentifier::MINIMUM_VALUE, PropertyValue::Real(1.0)),
(
PropertyIdentifier::WINDOW_INTERVAL,
PropertyValue::Unsigned(60),
),
(
PropertyIdentifier::RELIABILITY,
PropertyValue::Enumerated(0),
),
],
);
}
#[test]
fn pics_log_family_writability_comes_from_runtime_routes() {
use bacnet_objects::database::ObjectDatabase;
use bacnet_objects::event_log::EventLogObject;
use bacnet_objects::trend::{TrendLogMultipleObject, TrendLogObject};
let mut db = ObjectDatabase::new();
db.add(Box::new(EventLogObject::new(1, "EL-1", 3).unwrap()))
.unwrap();
db.add(Box::new(TrendLogObject::new(1, "TL-1", 3).unwrap()))
.unwrap();
db.add(Box::new(
TrendLogMultipleObject::new(1, "TLM-1", 3).unwrap(),
))
.unwrap();
let server_config = ServerConfig::default();
let pics_config = PicsConfig::default();
let pics = PicsGenerator::new(&db, &server_config, &pics_config).generate();
let cases = [
(
ObjectType::EVENT_LOG,
&[
PropertyIdentifier::LOG_ENABLE,
PropertyIdentifier::LOG_INTERVAL,
PropertyIdentifier::STOP_WHEN_FULL,
PropertyIdentifier::RECORD_COUNT,
PropertyIdentifier::OUT_OF_SERVICE,
PropertyIdentifier::DESCRIPTION,
][..],
),
(
ObjectType::TREND_LOG,
&[
PropertyIdentifier::LOG_ENABLE,
PropertyIdentifier::LOG_INTERVAL,
PropertyIdentifier::STOP_WHEN_FULL,
PropertyIdentifier::RECORD_COUNT,
PropertyIdentifier::OUT_OF_SERVICE,
PropertyIdentifier::DESCRIPTION,
][..],
),
(
ObjectType::TREND_LOG_MULTIPLE,
&[
PropertyIdentifier::LOG_ENABLE,
PropertyIdentifier::LOG_INTERVAL,
PropertyIdentifier::STOP_WHEN_FULL,
PropertyIdentifier::RECORD_COUNT,
PropertyIdentifier::DESCRIPTION,
][..],
),
];
for (object_type, expected) in cases {
let support = pics
.supported_object_types
.iter()
.find(|support| support.object_type == object_type)
.unwrap();
let writable = support
.supported_properties
.iter()
.filter(|property| property.access.writable)
.map(|property| property.property_id)
.collect::<Vec<_>>();
assert_eq!(writable.len(), expected.len(), "{object_type:?}");
for property in expected {
assert!(writable.contains(property), "{object_type:?} {property:?}");
}
}
}
#[test]
fn pics_file_resize_writability_uses_the_isolated_representative() {
use bacnet_objects::database::ObjectDatabase;
use bacnet_objects::file::FileObject;
use bacnet_types::enums::FileAccessMethod;
fn property_access(
file: FileObject,
property: PropertyIdentifier,
) -> Option<super::PropertyAccess> {
let mut db = ObjectDatabase::new();
db.add(Box::new(file)).unwrap();
PicsGenerator::new(&db, &ServerConfig::default(), &PicsConfig::default())
.generate()
.supported_object_types
.into_iter()
.find(|support| support.object_type == ObjectType::FILE)
.unwrap()
.supported_properties
.into_iter()
.find(|support| support.property_id == property)
.map(|support| support.access)
}
let stream = FileObject::new(1, "STREAM", "raw").unwrap();
assert!(
property_access(stream, PropertyIdentifier::FILE_SIZE)
.unwrap()
.writable
);
let stream = FileObject::new(2, "STREAM", "raw").unwrap();
assert!(property_access(stream, PropertyIdentifier::RECORD_COUNT).is_none());
let mut record = FileObject::new(3, "RECORD", "raw").unwrap();
record.set_file_access_method(FileAccessMethod::RECORD_ACCESS.to_raw());
assert!(
!property_access(record, PropertyIdentifier::FILE_SIZE)
.unwrap()
.writable
);
let mut record = FileObject::new(4, "RECORD", "raw").unwrap();
record.set_file_access_method(FileAccessMethod::RECORD_ACCESS.to_raw());
assert!(
property_access(record, PropertyIdentifier::RECORD_COUNT)
.unwrap()
.writable
);
let mut read_only_stream = FileObject::new(5, "RO-STREAM", "raw").unwrap();
read_only_stream.set_read_only(true);
assert!(
!property_access(read_only_stream, PropertyIdentifier::FILE_SIZE)
.unwrap()
.writable
);
let mut read_only_record = FileObject::new(6, "RO-RECORD", "raw").unwrap();
read_only_record.set_file_access_method(FileAccessMethod::RECORD_ACCESS.to_raw());
read_only_record.set_read_only(true);
assert!(
!property_access(read_only_record, PropertyIdentifier::RECORD_COUNT)
.unwrap()
.writable
);
}