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_types::bitstring::LimitEnable;
use bacnet_types::enums::EngineeringUnits;
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_from(
pid,
None,
value,
None,
&crate::command_source::test_origin(),
);
assert!(
result.is_ok(),
"{label}: source-aware write 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_from(
pid,
None,
PropertyValue::Null,
None,
&crate::command_source::test_origin(),
);
assert!(
result.is_err(),
"{label}: source-aware write 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_from(
pid,
Some(index),
value,
None,
&crate::command_source::test_origin(),
);
assert!(
result.is_ok(),
"{label}: source-aware write must accept {pid:?}[{index}], got: {result:?}"
);
}
let mut ai = AnalogInputObject::new(1, "ai", EngineeringUnits::NO_UNITS).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::all().to_bacnet()],
},
"AI",
);
assert_rejects(&mut ai, READ_ONLY, "AI");
let mut ao = AnalogOutputObject::new(1, "ao", EngineeringUnits::NO_UNITS).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_rejects(&mut ao, PropertyIdentifier::PRIORITY_ARRAY, "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", EngineeringUnits::NO_UNITS).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_rejects(&mut av, PropertyIdentifier::PRIORITY_ARRAY, "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_rejects(&mut bo, PropertyIdentifier::PRIORITY_ARRAY, "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_rejects(&mut bv, PropertyIdentifier::PRIORITY_ARRAY, "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_rejects(&mut mso, PropertyIdentifier::PRIORITY_ARRAY, "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_rejects(&mut msv, PropertyIdentifier::PRIORITY_ARRAY, "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::EngineeringUnits;
use bacnet_types::enums::ObjectType;
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_from(
pid,
None,
value,
None,
&crate::command_source::test_origin()
)
.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_from(
pid,
None,
value,
None,
&crate::command_source::test_origin()
)
.is_err(),
"{label}: {pid:?} not advertised but the arm accepted a write"
);
}
}
let reference_value =
PropertyValue::ApplicationData(vec![0x0C, 0x00, 0x00, 0x00, 0x05, 0x19, 0x55]);
let mut pc = PulseConverterObject::new(1, "PC-1", EngineeringUnits::DEGREES_CELSIUS).unwrap();
pc.add_pulses(1).unwrap();
assert_exactly(
&mut pc,
"PC",
&[
(
PropertyIdentifier::OUT_OF_SERVICE,
PropertyValue::Boolean(true),
),
(PropertyIdentifier::PRESENT_VALUE, PropertyValue::Real(10.0)),
(
PropertyIdentifier::RELIABILITY,
PropertyValue::Enumerated(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),
(
PropertyIdentifier::DESCRIPTION,
PropertyValue::CharacterString("d".into()),
),
],
&[
(
PropertyIdentifier::OBJECT_NAME,
PropertyValue::CharacterString("renamed".into()),
),
(PropertyIdentifier::UNITS, PropertyValue::Enumerated(95)),
(
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,
PropertyValue::ApplicationData(vec![0x0C, 0x00, 0x00, 0x00, 0x05, 0x19, 0x55]),
),
(
PropertyIdentifier::DESCRIPTION,
PropertyValue::CharacterString("d".into()),
),
(
PropertyIdentifier::WINDOW_INTERVAL,
PropertyValue::Unsigned(60),
),
(
PropertyIdentifier::WINDOW_SAMPLES,
PropertyValue::Unsigned(30),
),
(
PropertyIdentifier::ATTEMPTED_SAMPLES,
PropertyValue::Unsigned(0),
),
],
&[
(
PropertyIdentifier::OBJECT_NAME,
PropertyValue::CharacterString("renamed".into()),
),
(
PropertyIdentifier::OUT_OF_SERVICE,
PropertyValue::Boolean(true),
),
(PropertyIdentifier::PRESENT_VALUE, PropertyValue::Real(1.0)),
(PropertyIdentifier::MINIMUM_VALUE, PropertyValue::Real(1.0)),
(
PropertyIdentifier::VALID_SAMPLES,
PropertyValue::Unsigned(0),
),
(
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::STOP_WHEN_FULL,
PropertyIdentifier::RECORD_COUNT,
PropertyIdentifier::DESCRIPTION,
PropertyIdentifier::START_TIME,
PropertyIdentifier::STOP_TIME,
][..],
),
(
ObjectType::TREND_LOG,
&[
PropertyIdentifier::LOG_ENABLE,
PropertyIdentifier::LOG_INTERVAL,
PropertyIdentifier::STOP_WHEN_FULL,
PropertyIdentifier::RECORD_COUNT,
PropertyIdentifier::DESCRIPTION,
PropertyIdentifier::LOG_DEVICE_OBJECT_PROPERTY,
PropertyIdentifier::LOGGING_TYPE,
PropertyIdentifier::START_TIME,
PropertyIdentifier::STOP_TIME,
PropertyIdentifier::ALIGN_INTERVALS,
PropertyIdentifier::INTERVAL_OFFSET,
PropertyIdentifier::TRIGGER,
PropertyIdentifier::COV_RESUBSCRIPTION_INTERVAL,
PropertyIdentifier::CLIENT_COV_INCREMENT,
][..],
),
(
ObjectType::TREND_LOG_MULTIPLE,
&[
PropertyIdentifier::LOG_ENABLE,
PropertyIdentifier::LOG_INTERVAL,
PropertyIdentifier::STOP_WHEN_FULL,
PropertyIdentifier::RECORD_COUNT,
PropertyIdentifier::DESCRIPTION,
PropertyIdentifier::LOG_DEVICE_OBJECT_PROPERTY,
PropertyIdentifier::LOGGING_TYPE,
PropertyIdentifier::START_TIME,
PropertyIdentifier::STOP_TIME,
PropertyIdentifier::ALIGN_INTERVALS,
PropertyIdentifier::INTERVAL_OFFSET,
PropertyIdentifier::TRIGGER,
][..],
),
];
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<_>>();
let reporting = [
PropertyIdentifier::NOTIFICATION_THRESHOLD,
PropertyIdentifier::NOTIFICATION_CLASS,
PropertyIdentifier::EVENT_ENABLE,
PropertyIdentifier::NOTIFY_TYPE,
PropertyIdentifier::EVENT_DETECTION_ENABLE,
];
let options = [
PropertyIdentifier::EVENT_MESSAGE_TEXTS_CONFIG,
PropertyIdentifier::EVENT_ALGORITHM_INHIBIT_REF,
PropertyIdentifier::EVENT_ALGORITHM_INHIBIT,
];
assert_eq!(
writable.len(),
expected.len() + reporting.len() + options.len(),
"{object_type:?}"
);
for property in expected.iter().chain(&reporting).chain(&options) {
assert!(writable.contains(property), "{object_type:?} {property:?}");
}
let optional = |property| {
support
.supported_properties
.iter()
.find(|row| row.property_id == property)
.map(|row| row.access.optional)
};
let trend = object_type == ObjectType::TREND_LOG;
for property in [
PropertyIdentifier::START_TIME,
PropertyIdentifier::STOP_TIME,
] {
assert_eq!(
optional(property),
Some(!trend),
"{object_type:?} {property:?}"
);
}
if object_type != ObjectType::EVENT_LOG {
for property in [
PropertyIdentifier::LOG_INTERVAL,
PropertyIdentifier::LOG_DEVICE_OBJECT_PROPERTY,
] {
assert_eq!(
optional(property),
Some(false),
"{object_type:?} {property:?}"
);
}
}
let read_only = [
PropertyIdentifier::RECORDS_SINCE_NOTIFICATION,
PropertyIdentifier::LAST_NOTIFY_RECORD,
PropertyIdentifier::ACKED_TRANSITIONS,
PropertyIdentifier::EVENT_TIME_STAMPS,
];
for property in reporting.iter().chain(&read_only) {
assert_eq!(
optional(*property),
Some(false),
"{object_type:?} {property:?}"
);
}
let texts = PropertyIdentifier::EVENT_MESSAGE_TEXTS;
for property in options.iter().chain([&texts]) {
assert_eq!(optional(*property), Some(true), "{object_type:?}");
}
}
}
#[test]
fn pics_file_resize_writability_matches_single_instance_metadata() {
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
);
}