use std::borrow::Cow;
use std::collections::HashSet;
use std::sync::{Arc, Mutex};
use bacnet_types::enums::{ObjectType, PropertyIdentifier};
use bacnet_types::error::Error;
use bacnet_types::primitives::{ObjectIdentifier, PropertyValue};
use crate::access_control::{
AccessCredentialObject, AccessDoorObject, AccessPointObject, AccessRightsObject,
AccessUserObject, AccessZoneObject, CredentialDataInputObject,
};
use crate::audit::{AuditLogObject, AuditLogPersistence, AuditLogSnapshot};
use crate::binary::{BinaryInputObject, BinaryOutputObject, BinaryValueObject};
use crate::multistate::{MultiStateInputObject, MultiStateOutputObject, MultiStateValueObject};
use crate::property_metadata::{
PropertyConformance, PropertyMetadata, PropertyPresenceCondition, PropertyWriteCapability,
};
use crate::traits::BACnetObject;
use crate::value_types::{
BitStringValueObject, CharacterStringValueObject, DatePatternValueObject,
DateTimePatternValueObject, DateTimeValueObject, DateValueObject, IntegerValueObject,
LargeAnalogValueObject, OctetStringValueObject, PositiveIntegerValueObject,
TimePatternValueObject, TimeValueObject,
};
mod analog;
mod intrinsic;
pub(crate) fn metadata_row_reads(object: &dyn BACnetObject, property: PropertyIdentifier) -> bool {
use bacnet_types::enums::{ErrorClass, ErrorCode};
let result = object.read_property(property, None);
if matches!(
object.object_identifier().object_type(),
ObjectType::AUDIT_LOG
| ObjectType::EVENT_LOG
| ObjectType::TREND_LOG
| ObjectType::TREND_LOG_MULTIPLE
) && property == PropertyIdentifier::LOG_BUFFER
{
return matches!(result, Err(Error::Protocol { class, code })
if class == ErrorClass::PROPERTY.to_raw() as u32
&& code == ErrorCode::READ_ACCESS_DENIED.to_raw() as u32);
}
result.is_ok()
}
struct InstanceMetadataObject {
oid: ObjectIdentifier,
include_description: bool,
}
#[derive(Default)]
struct MemoryAuditLogPersistence(Mutex<Option<AuditLogSnapshot>>);
impl AuditLogPersistence for MemoryAuditLogPersistence {
fn load(&self, _expected_object: ObjectIdentifier) -> Result<Option<AuditLogSnapshot>, Error> {
Ok(self.0.lock().unwrap().clone())
}
fn commit(&self, snapshot: &AuditLogSnapshot) -> Result<(), Error> {
*self.0.lock().unwrap() = Some(snapshot.clone());
Ok(())
}
}
impl InstanceMetadataObject {
fn new(instance: u32, include_description: bool) -> Self {
Self {
oid: ObjectIdentifier::new(ObjectType::ANALOG_INPUT, instance).unwrap(),
include_description,
}
}
}
impl BACnetObject for InstanceMetadataObject {
fn object_identifier(&self) -> ObjectIdentifier {
self.oid
}
fn object_name(&self) -> &str {
"instance-metadata"
}
fn read_property(
&self,
property: PropertyIdentifier,
array_index: Option<u32>,
) -> Result<PropertyValue, Error> {
match property {
PropertyIdentifier::OBJECT_IDENTIFIER => Ok(PropertyValue::ObjectIdentifier(self.oid)),
PropertyIdentifier::OBJECT_NAME => {
Ok(PropertyValue::CharacterString(self.object_name().into()))
}
PropertyIdentifier::OBJECT_TYPE => Ok(PropertyValue::Enumerated(
self.object_identifier().object_type().to_raw(),
)),
PropertyIdentifier::DESCRIPTION if self.include_description => {
Ok(PropertyValue::CharacterString(String::new()))
}
PropertyIdentifier::PROPERTY_LIST => {
crate::common::read_property_list_property(&self.property_list(), array_index)
}
_ => Err(crate::common::unknown_property_error()),
}
}
fn write_property(
&mut self,
_property: PropertyIdentifier,
_array_index: Option<u32>,
_value: PropertyValue,
_priority: Option<u8>,
) -> Result<(), Error> {
Err(crate::common::write_access_denied_error())
}
fn property_metadata(&self) -> Cow<'_, [PropertyMetadata]> {
let mut rows = vec![
PropertyMetadata::new(
PropertyIdentifier::OBJECT_IDENTIFIER,
PropertyConformance::RequiredRead,
None,
PropertyWriteCapability::ReadOnly,
),
PropertyMetadata::new(
PropertyIdentifier::OBJECT_NAME,
PropertyConformance::RequiredRead,
None,
PropertyWriteCapability::ReadOnly,
),
PropertyMetadata::new(
PropertyIdentifier::OBJECT_TYPE,
PropertyConformance::RequiredRead,
None,
PropertyWriteCapability::ReadOnly,
),
];
if self.include_description {
rows.push(PropertyMetadata::new(
PropertyIdentifier::DESCRIPTION,
PropertyConformance::Optional,
None,
PropertyWriteCapability::ReadOnly,
));
}
rows.push(PropertyMetadata::new(
PropertyIdentifier::PROPERTY_LIST,
PropertyConformance::RequiredRead,
None,
PropertyWriteCapability::ReadOnly,
));
Cow::Owned(rows)
}
fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
let metadata = self.property_metadata();
crate::property_metadata::property_list_from_metadata(metadata.as_ref())
}
}
fn metadata_row(
object: &dyn BACnetObject,
property_identifier: PropertyIdentifier,
) -> PropertyMetadata {
*object
.property_metadata()
.iter()
.find(|row| row.property_identifier == property_identifier)
.expect("property should have a metadata row")
}
fn assert_unique_and_canonical(object: &dyn BACnetObject) {
let metadata = object.property_metadata();
assert!(!metadata.is_empty());
assert!(metadata
.iter()
.any(|row| row.property_identifier == PropertyIdentifier::PROPERTY_LIST));
let unique: HashSet<_> = metadata.iter().map(|row| row.property_identifier).collect();
assert_eq!(
unique.len(),
metadata.len(),
"duplicate metadata identifier"
);
}
#[test]
fn property_metadata_contract_time_value() {
let object = TimeValueObject::new(1, "TV-1").unwrap();
assert_unique_and_canonical(&object);
assert_eq!(object.property_metadata().len(), 12);
let present_value = metadata_row(&object, PropertyIdentifier::PRESENT_VALUE);
assert_eq!(present_value.conformance, PropertyConformance::RequiredRead);
assert_eq!(present_value.presence_condition, None);
assert_eq!(
present_value.write_capability,
PropertyWriteCapability::Always
);
let priority_array = metadata_row(&object, PropertyIdentifier::PRIORITY_ARRAY);
assert_eq!(priority_array.conformance, PropertyConformance::Optional);
assert_eq!(
priority_array.presence_condition,
Some(PropertyPresenceCondition::Commandable)
);
assert_eq!(
priority_array.write_capability,
PropertyWriteCapability::ReadOnly
);
let status_flags = metadata_row(&object, PropertyIdentifier::STATUS_FLAGS);
assert_eq!(status_flags.conformance, PropertyConformance::RequiredRead);
assert_eq!(
status_flags.write_capability,
PropertyWriteCapability::ReadOnly
);
}
#[test]
fn property_metadata_contract_binary_input() {
let object = BinaryInputObject::new(1, "BI-1").unwrap();
assert_unique_and_canonical(&object);
assert_eq!(object.property_metadata().len(), 27);
let present_value = metadata_row(&object, PropertyIdentifier::PRESENT_VALUE);
assert_eq!(present_value.conformance, PropertyConformance::RequiredRead);
assert_eq!(
present_value.write_capability,
PropertyWriteCapability::WhenOutOfService
);
let reliability = metadata_row(&object, PropertyIdentifier::RELIABILITY);
assert_eq!(reliability.conformance, PropertyConformance::Optional);
assert_eq!(
reliability.write_capability,
PropertyWriteCapability::WhenOutOfService
);
let inhibit = metadata_row(&object, PropertyIdentifier::RELIABILITY_EVALUATION_INHIBIT);
assert_eq!(inhibit.conformance, PropertyConformance::Optional);
assert_eq!(inhibit.presence_condition, None);
assert_eq!(inhibit.write_capability, PropertyWriteCapability::Always);
let event_enable = metadata_row(&object, PropertyIdentifier::EVENT_ENABLE);
assert_eq!(event_enable.conformance, PropertyConformance::Optional);
assert_eq!(
event_enable.presence_condition,
Some(PropertyPresenceCondition::IntrinsicReportingRequired)
);
assert_eq!(
event_enable.write_capability,
PropertyWriteCapability::Always
);
for property_identifier in [
PropertyIdentifier::ACTIVE_TEXT,
PropertyIdentifier::INACTIVE_TEXT,
] {
assert_eq!(
metadata_row(&object, property_identifier).presence_condition,
Some(PropertyPresenceCondition::PairedText)
);
}
}
#[test]
fn property_metadata_contract_all_migrated_rows_are_readable() {
let objects: [Box<dyn BACnetObject>; 54] = [
Box::new(crate::device::DeviceObject::new(Default::default()).unwrap()),
Box::new(TimeValueObject::new(1, "TV-1").unwrap()),
Box::new(IntegerValueObject::new(1, "IV-1").unwrap()),
Box::new(PositiveIntegerValueObject::new(1, "PIV-1").unwrap()),
Box::new(LargeAnalogValueObject::new(1, "LAV-1").unwrap()),
Box::new(CharacterStringValueObject::new(1, "CSV-1").unwrap()),
Box::new(OctetStringValueObject::new(1, "OSV-1").unwrap()),
Box::new(BitStringValueObject::new(1, "BSV-1").unwrap()),
Box::new(DateValueObject::new(1, "DV-1").unwrap()),
Box::new(DateTimeValueObject::new(1, "DTV-1").unwrap()),
Box::new(DatePatternValueObject::new(1, "DPV-1").unwrap()),
Box::new(TimePatternValueObject::new(1, "TPV-1").unwrap()),
Box::new(DateTimePatternValueObject::new(1, "DTPV-1").unwrap()),
Box::new(BinaryInputObject::new(1, "BI-1").unwrap()),
Box::new(BinaryValueObject::new(1, "BV-1").unwrap()),
Box::new(BinaryOutputObject::new(1, "BO-1").unwrap()),
Box::new(MultiStateInputObject::new(1, "MSI-1", 3).unwrap()),
Box::new(MultiStateValueObject::new(1, "MSV-1", 3).unwrap()),
Box::new(MultiStateOutputObject::new(1, "MSO-1", 3).unwrap()),
Box::new(crate::loop_obj::LoopObject::new(1, "LOOP-1", 62).unwrap()),
Box::new(crate::program::ProgramObject::new(1, "PRG-1").unwrap()),
Box::new(crate::file::FileObject::new(1, "FILE-1", "raw").unwrap()),
Box::new(crate::trend::TrendLogObject::new(1, "TL-1", 3).unwrap()),
Box::new(crate::trend::TrendLogMultipleObject::new(1, "TLM-1", 3).unwrap()),
Box::new(crate::event_log::EventLogObject::new(1, "EL-1", 3).unwrap()),
Box::new(crate::schedule::ScheduleObject::new(1, "SCH-1", PropertyValue::Null).unwrap()),
Box::new(crate::schedule::CalendarObject::new(1, "CAL-1").unwrap()),
Box::new(crate::life_safety::LifeSafetyPointObject::new(1, "LSP-1").unwrap()),
Box::new(crate::life_safety::LifeSafetyZoneObject::new(1, "LSZ-1").unwrap()),
Box::new(
AuditLogObject::new(1, "AL-1", 3, Arc::new(MemoryAuditLogPersistence::default()))
.unwrap(),
),
Box::new(crate::timer::TimerObject::new(1, "TMR-1").unwrap()),
Box::new(crate::averaging::AveragingObject::new(1, "AVG-1").unwrap()),
Box::new(crate::command::CommandObject::new(1, "CMD-1").unwrap()),
Box::new(crate::group::GroupObject::new(1, "GRP-1").unwrap()),
Box::new(crate::group::GlobalGroupObject::new(1, "GG-1").unwrap()),
Box::new(crate::group::StructuredViewObject::new(1, "SV-1").unwrap()),
Box::new(crate::load_control::LoadControlObject::new(1, "LC-1").unwrap()),
Box::new(
crate::network_port::NetworkPortObject::new_non_bip(
1,
"NP-1",
bacnet_types::enums::NetworkType::from_raw(0),
0,
Default::default(),
1476,
)
.unwrap(),
),
Box::new(crate::elevator::ElevatorGroupObject::new(1, "EG-1").unwrap()),
Box::new(crate::elevator::EscalatorObject::new(1, "ESC-1").unwrap()),
Box::new(crate::elevator::LiftObject::new(1, "LIFT-1", 3).unwrap()),
Box::new(crate::lighting::LightingOutputObject::new(1, "LO-1").unwrap()),
Box::new(crate::lighting::BinaryLightingOutputObject::new(1, "BLO-1").unwrap()),
Box::new(crate::accumulator::AccumulatorObject::new(1, "ACC-1", 95).unwrap()),
Box::new(crate::accumulator::PulseConverterObject::new(1, "PC-1", 62).unwrap()),
Box::new(crate::color::ColorObject::new(1, "CLR-1").unwrap()),
Box::new(crate::color::ColorTemperatureObject::new(1, "CT-1").unwrap()),
Box::new(AccessDoorObject::new(1, "DOOR-1").unwrap()),
Box::new(AccessPointObject::new(1, "AP-1").unwrap()),
Box::new(AccessZoneObject::new(1, "ZONE-1").unwrap()),
Box::new(AccessCredentialObject::new(1, "CRED-1").unwrap()),
Box::new(AccessUserObject::new(1, "USER-1").unwrap()),
Box::new(AccessRightsObject::new(1, "AR-1").unwrap()),
Box::new(CredentialDataInputObject::new(1, "CDI-1").unwrap()),
];
for object in objects {
assert_unique_and_canonical(object.as_ref());
let metadata = object.property_metadata();
for row in metadata.iter() {
assert!(
metadata_row_reads(object.as_ref(), row.property_identifier),
"{:?} must read {:?} without an array index",
object.object_identifier().object_type(),
row.property_identifier
);
}
}
}
#[test]
fn property_metadata_contract_property_list_projection_excludes_property_list() {
let cases: [(Box<dyn BACnetObject>, &[PropertyIdentifier]); 2] = [
(
Box::new(TimeValueObject::new(1, "TV-1").unwrap()),
&[
PropertyIdentifier::OBJECT_IDENTIFIER,
PropertyIdentifier::OBJECT_NAME,
PropertyIdentifier::DESCRIPTION,
PropertyIdentifier::OBJECT_TYPE,
PropertyIdentifier::PRESENT_VALUE,
PropertyIdentifier::STATUS_FLAGS,
PropertyIdentifier::OUT_OF_SERVICE,
PropertyIdentifier::RELIABILITY,
PropertyIdentifier::PRIORITY_ARRAY,
PropertyIdentifier::RELINQUISH_DEFAULT,
PropertyIdentifier::CURRENT_COMMAND_PRIORITY,
],
),
(
Box::new(BinaryInputObject::new(1, "BI-1").unwrap()),
&[
PropertyIdentifier::OBJECT_IDENTIFIER,
PropertyIdentifier::OBJECT_NAME,
PropertyIdentifier::DESCRIPTION,
PropertyIdentifier::OBJECT_TYPE,
PropertyIdentifier::PRESENT_VALUE,
PropertyIdentifier::STATUS_FLAGS,
PropertyIdentifier::EVENT_STATE,
PropertyIdentifier::EVENT_DETECTION_ENABLE,
PropertyIdentifier::EVENT_ENABLE,
PropertyIdentifier::TIME_DELAY,
PropertyIdentifier::TIME_DELAY_NORMAL,
PropertyIdentifier::NOTIFY_TYPE,
PropertyIdentifier::NOTIFICATION_CLASS,
PropertyIdentifier::ACKED_TRANSITIONS,
PropertyIdentifier::EVENT_TIME_STAMPS,
PropertyIdentifier::EVENT_MESSAGE_TEXTS,
PropertyIdentifier::EVENT_MESSAGE_TEXTS_CONFIG,
PropertyIdentifier::EVENT_ALGORITHM_INHIBIT_REF,
PropertyIdentifier::EVENT_ALGORITHM_INHIBIT,
PropertyIdentifier::OUT_OF_SERVICE,
PropertyIdentifier::POLARITY,
PropertyIdentifier::RELIABILITY,
PropertyIdentifier::RELIABILITY_EVALUATION_INHIBIT,
PropertyIdentifier::ACTIVE_TEXT,
PropertyIdentifier::INACTIVE_TEXT,
PropertyIdentifier::ALARM_VALUE,
],
),
];
for (object, expected_projection) in cases {
let projected: Vec<_> = object
.property_metadata()
.iter()
.filter_map(|row| {
(row.property_identifier != PropertyIdentifier::PROPERTY_LIST)
.then_some(row.property_identifier)
})
.collect();
assert_eq!(projected, expected_projection);
assert_eq!(object.property_list().as_ref(), expected_projection);
assert!(!object
.property_list()
.contains(&PropertyIdentifier::PROPERTY_LIST));
let wire_list = object
.read_property(PropertyIdentifier::PROPERTY_LIST, None)
.unwrap();
let expected_wire = PropertyValue::List(
expected_projection
.iter()
.filter(|property_identifier| {
!matches!(
**property_identifier,
PropertyIdentifier::OBJECT_IDENTIFIER
| PropertyIdentifier::OBJECT_NAME
| PropertyIdentifier::OBJECT_TYPE
| PropertyIdentifier::PROPERTY_LIST
)
})
.map(|property_identifier| PropertyValue::Enumerated(property_identifier.to_raw()))
.collect(),
);
assert_eq!(wire_list, expected_wire);
}
}
#[test]
fn property_metadata_contract_all_value_types_opt_in_to_metadata() {
let time_value = TimeValueObject::new(1, "TV-1").unwrap();
let date_value = DateValueObject::new(1, "DV-1").unwrap();
assert!(!time_value.property_metadata().is_empty());
assert!(!date_value.property_metadata().is_empty());
}
#[test]
fn property_metadata_contract_write_capabilities_match_dispatch() {
let mut time_value = TimeValueObject::new(1, "TV-1").unwrap();
assert_eq!(
metadata_row(&time_value, PropertyIdentifier::OBJECT_NAME).write_capability,
PropertyWriteCapability::Always
);
assert!(time_value
.write_property(
PropertyIdentifier::OBJECT_NAME,
None,
PropertyValue::CharacterString("TV-2".into()),
None,
)
.is_ok());
assert_eq!(
metadata_row(&time_value, PropertyIdentifier::STATUS_FLAGS).write_capability,
PropertyWriteCapability::ReadOnly
);
assert!(time_value
.write_property(
PropertyIdentifier::STATUS_FLAGS,
None,
PropertyValue::Boolean(true),
None,
)
.is_err());
let mut binary_input = BinaryInputObject::new(1, "BI-1").unwrap();
for property_identifier in [
PropertyIdentifier::PRESENT_VALUE,
PropertyIdentifier::RELIABILITY,
] {
assert_eq!(
metadata_row(&binary_input, property_identifier).write_capability,
PropertyWriteCapability::WhenOutOfService
);
}
assert!(binary_input
.write_property_from(
PropertyIdentifier::PRESENT_VALUE,
None,
PropertyValue::Enumerated(1),
None,
&crate::command_source::test_origin(),
)
.is_err());
assert!(binary_input
.write_property(
PropertyIdentifier::RELIABILITY,
None,
PropertyValue::Enumerated(1),
None,
)
.is_err());
binary_input
.write_property(
PropertyIdentifier::OUT_OF_SERVICE,
None,
PropertyValue::Boolean(true),
None,
)
.unwrap();
binary_input
.write_property_from(
PropertyIdentifier::PRESENT_VALUE,
None,
PropertyValue::Enumerated(1),
None,
&crate::command_source::test_origin(),
)
.unwrap();
binary_input
.write_property(
PropertyIdentifier::RELIABILITY,
None,
PropertyValue::Enumerated(1),
None,
)
.unwrap();
}
#[test]
fn property_metadata_contract_models_required_write_code() {
let row = PropertyMetadata::new(
PropertyIdentifier::PRESENT_VALUE,
PropertyConformance::RequiredWrite,
None,
PropertyWriteCapability::Always,
);
assert!(row.conformance.is_required());
assert!(row.write_capability.is_writable());
}
#[test]
fn property_metadata_contract_dyn_object_can_return_owned_instance_rows() {
let without_description = InstanceMetadataObject::new(1, false);
let with_description = InstanceMetadataObject::new(2, true);
let cases: [(&dyn BACnetObject, bool); 2] =
[(&without_description, false), (&with_description, true)];
for (object, expect_description) in cases {
let metadata = object.property_metadata();
assert!(matches!(&metadata, Cow::Owned(_)));
assert_eq!(
metadata
.iter()
.any(|row| row.property_identifier == PropertyIdentifier::DESCRIPTION),
expect_description
);
}
}
#[test]
fn property_metadata_migrated_date_value_exact_required_set() {
use PropertyIdentifier as P;
let object = DateValueObject::new(1, "DV-1").unwrap();
let metadata = object.property_metadata();
assert!(matches!(metadata, Cow::Borrowed(_)));
assert_eq!(metadata.len(), 12);
let required = object.required_properties();
assert_eq!(
required.as_ref(),
[
P::OBJECT_IDENTIFIER,
P::OBJECT_NAME,
P::OBJECT_TYPE,
P::PRESENT_VALUE,
P::STATUS_FLAGS,
P::PROPERTY_LIST
]
);
}
#[test]
fn property_metadata_binary_commandable_exact_required_sets() {
use PropertyIdentifier as P;
let objects: [Box<dyn BACnetObject>; 2] = [
Box::new(BinaryValueObject::new(1, "BV-1").unwrap()),
Box::new(BinaryOutputObject::new(1, "BO-1").unwrap()),
];
let intrinsic = [
P::EVENT_DETECTION_ENABLE,
P::EVENT_ENABLE,
P::TIME_DELAY,
P::NOTIFY_TYPE,
P::NOTIFICATION_CLASS,
P::ACKED_TRANSITIONS,
P::EVENT_TIME_STAMPS,
];
for object in objects {
let output = object.object_identifier().object_type() == ObjectType::BINARY_OUTPUT;
let mut required = vec![
P::OBJECT_IDENTIFIER,
P::OBJECT_NAME,
P::OBJECT_TYPE,
P::PRESENT_VALUE,
];
if output {
required.push(P::FEEDBACK_VALUE);
}
required.extend([P::STATUS_FLAGS, P::EVENT_STATE]);
required.extend(intrinsic);
required.push(P::OUT_OF_SERVICE);
if output {
required.extend([
P::PRIORITY_ARRAY,
P::RELINQUISH_DEFAULT,
P::CURRENT_COMMAND_PRIORITY,
P::POLARITY,
]);
} else {
required.push(P::ALARM_VALUE);
}
required.extend([P::VALUE_SOURCE, P::VALUE_SOURCE_ARRAY, P::LAST_COMMAND_TIME]);
required.push(P::PROPERTY_LIST);
assert_unique_and_canonical(object.as_ref());
assert!(matches!(object.property_metadata(), Cow::Borrowed(_)));
assert_eq!(object.required_properties().as_ref(), required);
assert!(object.is_createable());
}
}