use super::*;
use bacnet_types::error::RemoteError;
mod access_identity;
mod access_topology;
mod accumulator;
mod audit_log;
mod averaging;
mod calendar;
mod channel;
mod color;
mod command;
mod command_source;
mod elevator;
mod file;
mod group;
mod group_present_value;
mod life_safety;
mod lighting;
mod load_control;
mod loop_program;
mod network_port;
mod notification_class;
mod schedule;
mod timer;
#[test]
fn read_property_handler_success() {
let db = make_db_with_ai();
let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
let request = ReadPropertyRequest {
object_identifier: oid,
property_identifier: PropertyIdentifier::PRESENT_VALUE,
property_array_index: None,
};
let mut buf = BytesMut::new();
request.encode(&mut buf);
let mut ack_buf = BytesMut::new();
handle_read_property(&db, &buf, &mut ack_buf).unwrap();
let ack_bytes = ack_buf.to_vec();
let ack = ReadPropertyACK::decode(&ack_bytes).unwrap();
assert_eq!(ack.object_identifier, oid);
assert_eq!(ack.property_identifier, PropertyIdentifier::PRESENT_VALUE);
let (val, _) =
bacnet_encoding::primitives::decode_application_value(&ack.property_value, 0).unwrap();
assert_eq!(val, bacnet_types::primitives::PropertyValue::Real(72.5));
}
#[test]
fn active_cov_subscriptions_low_level_handlers_read_standalone_empty_list() {
use bacnet_objects::device::{DeviceConfig, DeviceObject};
use bacnet_services::rpm::ReadPropertyMultipleRequest;
use bacnet_types::constructed::{PropertyReference, ReadAccessSpecification};
let oid = ObjectIdentifier::new(ObjectType::DEVICE, 1).unwrap();
let mut db = ObjectDatabase::new();
db.add(Box::new(
DeviceObject::new(DeviceConfig {
identity: bacnet_objects::DeviceIdentity::default().with_name("COV Device"),
..Default::default()
})
.unwrap(),
))
.unwrap();
let request = ReadPropertyRequest {
object_identifier: oid,
property_identifier: PropertyIdentifier::ACTIVE_COV_SUBSCRIPTIONS,
property_array_index: None,
};
let mut request_buf = BytesMut::new();
request.encode(&mut request_buf);
let mut response_buf = BytesMut::new();
handle_read_property(&db, &request_buf, &mut response_buf).unwrap();
assert!(
ReadPropertyACK::decode(&response_buf)
.unwrap()
.property_value
.is_empty()
);
let request = ReadPropertyMultipleRequest {
list_of_read_access_specs: vec![ReadAccessSpecification {
object_identifier: oid,
list_of_property_references: vec![PropertyReference {
property_identifier: PropertyIdentifier::ACTIVE_COV_SUBSCRIPTIONS,
property_array_index: None,
}],
}],
};
let mut request_buf = BytesMut::new();
request.encode(&mut request_buf).unwrap();
let mut response_buf = BytesMut::new();
handle_read_property_multiple(&db, &request_buf, &mut response_buf).unwrap();
let ack = ReadPropertyMultipleACK::decode(&response_buf).unwrap();
let result = &ack.list_of_read_access_results[0].list_of_results[0];
assert_eq!(result.property_value.as_deref(), Some(&[][..]));
assert!(result.error.is_none());
}
#[test]
fn read_property_handler_serves_multistate_event_time_stamps_count() {
let db = make_db_with_msi();
let oid = ObjectIdentifier::new(ObjectType::MULTI_STATE_INPUT, 1).unwrap();
let request = ReadPropertyRequest {
object_identifier: oid,
property_identifier: PropertyIdentifier::EVENT_TIME_STAMPS,
property_array_index: Some(0),
};
let mut buf = BytesMut::new();
request.encode(&mut buf);
let mut ack_buf = BytesMut::new();
handle_read_property(&db, &buf, &mut ack_buf).unwrap();
let ack = ReadPropertyACK::decode(&ack_buf).unwrap();
assert_eq!(ack.property_array_index, Some(0));
let (value, end) =
bacnet_encoding::primitives::decode_application_value(&ack.property_value, 0).unwrap();
assert_eq!(value, bacnet_types::primitives::PropertyValue::Unsigned(3));
assert_eq!(end, ack.property_value.len());
}
#[test]
fn read_property_handler_rejects_multistate_event_time_stamps_out_of_bounds_index() {
let db = make_db_with_msi();
let oid = ObjectIdentifier::new(ObjectType::MULTI_STATE_INPUT, 1).unwrap();
let request = ReadPropertyRequest {
object_identifier: oid,
property_identifier: PropertyIdentifier::EVENT_TIME_STAMPS,
property_array_index: Some(4),
};
let mut buf = BytesMut::new();
request.encode(&mut buf);
let mut ack_buf = BytesMut::new();
match handle_read_property(&db, &buf, &mut ack_buf).unwrap_err() {
Error::Remote(RemoteError::Error { class, code }) => {
assert_eq!(class, ErrorClass::PROPERTY);
assert_eq!(code, ErrorCode::INVALID_ARRAY_INDEX);
}
other => panic!("expected Protocol error, got: {other:?}"),
}
}
#[test]
fn read_property_unknown_object() {
let db = make_db_with_ai();
let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 99).unwrap();
let request = ReadPropertyRequest {
object_identifier: oid,
property_identifier: PropertyIdentifier::PRESENT_VALUE,
property_array_index: None,
};
let mut buf = BytesMut::new();
request.encode(&mut buf);
let mut ack_buf = BytesMut::new();
let result = handle_read_property(&db, &buf, &mut ack_buf);
assert!(result.is_err());
}
#[test]
fn read_property_unknown_property() {
let db = make_db_with_ai();
let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
let request = ReadPropertyRequest {
object_identifier: oid,
property_identifier: PropertyIdentifier::PRIORITY_ARRAY,
property_array_index: None,
};
let mut buf = BytesMut::new();
request.encode(&mut buf);
let mut ack_buf = BytesMut::new();
let result = handle_read_property(&db, &buf, &mut ack_buf);
assert!(result.is_err());
}
#[test]
fn write_property_handler_success() {
let mut db = ObjectDatabase::new();
let bv = bacnet_objects::binary::BinaryValueObject::new(1, "BV-1").unwrap();
db.add(Box::new(bv)).unwrap();
let oid = ObjectIdentifier::new(ObjectType::BINARY_VALUE, 1).unwrap();
let mut value_buf = BytesMut::new();
bacnet_encoding::primitives::encode_app_enumerated(&mut value_buf, 1);
let request = WritePropertyRequest {
object_identifier: oid,
property_identifier: PropertyIdentifier::PRESENT_VALUE,
property_array_index: None,
property_value: value_buf.to_vec(),
priority: None,
};
let mut buf = BytesMut::new();
request.encode(&mut buf).unwrap();
sourced_wp(&mut db, &buf).unwrap();
let obj = db.get(&oid).unwrap();
let val = obj
.read_property(PropertyIdentifier::PRESENT_VALUE, None)
.unwrap();
assert_eq!(val, bacnet_types::primitives::PropertyValue::Enumerated(1));
}
#[test]
fn rpm_handler_success() {
let db = make_db_with_ai();
let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
use bacnet_types::constructed::{PropertyReference, ReadAccessSpecification};
let request = bacnet_services::rpm::ReadPropertyMultipleRequest {
list_of_read_access_specs: vec![ReadAccessSpecification {
object_identifier: oid,
list_of_property_references: vec![
PropertyReference {
property_identifier: PropertyIdentifier::PRESENT_VALUE,
property_array_index: None,
},
PropertyReference {
property_identifier: PropertyIdentifier::OBJECT_NAME,
property_array_index: None,
},
],
}],
};
let mut buf = BytesMut::new();
request.encode(&mut buf).unwrap();
let mut ack_buf = BytesMut::new();
handle_read_property_multiple(&db, &buf, &mut ack_buf).unwrap();
let ack_bytes = ack_buf.to_vec();
let ack = bacnet_services::rpm::ReadPropertyMultipleACK::decode(&ack_bytes).unwrap();
assert_eq!(ack.list_of_read_access_results.len(), 1);
let result = &ack.list_of_read_access_results[0];
assert_eq!(result.object_identifier, oid);
assert_eq!(result.list_of_results.len(), 2);
assert!(result.list_of_results[0].property_value.is_some());
assert!(result.list_of_results[1].property_value.is_some());
let (val, _) = bacnet_encoding::primitives::decode_application_value(
result.list_of_results[0].property_value.as_ref().unwrap(),
0,
)
.unwrap();
assert_eq!(val, bacnet_types::primitives::PropertyValue::Real(72.5));
}
#[test]
fn rpm_handler_unknown_property_returns_inline_error() {
let db = make_db_with_ai();
let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
use bacnet_types::constructed::{PropertyReference, ReadAccessSpecification};
let request = bacnet_services::rpm::ReadPropertyMultipleRequest {
list_of_read_access_specs: vec![ReadAccessSpecification {
object_identifier: oid,
list_of_property_references: vec![
PropertyReference {
property_identifier: PropertyIdentifier::PRESENT_VALUE,
property_array_index: None,
},
PropertyReference {
property_identifier: PropertyIdentifier::PRIORITY_ARRAY,
property_array_index: None,
},
],
}],
};
let mut buf = BytesMut::new();
request.encode(&mut buf).unwrap();
let mut ack_buf = BytesMut::new();
handle_read_property_multiple(&db, &buf, &mut ack_buf).unwrap();
let ack_bytes = ack_buf.to_vec();
let ack = bacnet_services::rpm::ReadPropertyMultipleACK::decode(&ack_bytes).unwrap();
let result = &ack.list_of_read_access_results[0];
assert!(result.list_of_results[0].property_value.is_some()); assert!(result.list_of_results[1].error.is_some()); }
#[test]
fn rpm_handler_unknown_object_returns_inline_error() {
let db = make_db_with_ai();
let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 99).unwrap();
use bacnet_types::constructed::{PropertyReference, ReadAccessSpecification};
let request = bacnet_services::rpm::ReadPropertyMultipleRequest {
list_of_read_access_specs: vec![ReadAccessSpecification {
object_identifier: oid,
list_of_property_references: vec![PropertyReference {
property_identifier: PropertyIdentifier::PRESENT_VALUE,
property_array_index: None,
}],
}],
};
let mut buf = BytesMut::new();
request.encode(&mut buf).unwrap();
let mut ack_buf = BytesMut::new();
handle_read_property_multiple(&db, &buf, &mut ack_buf).unwrap();
let ack_bytes = ack_buf.to_vec();
let ack = bacnet_services::rpm::ReadPropertyMultipleACK::decode(&ack_bytes).unwrap();
let result = &ack.list_of_read_access_results[0];
assert!(result.list_of_results[0].error.is_some());
}
#[test]
fn rpm_handler_all_properties_expanded() {
let db = make_db_with_ai();
let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
use bacnet_types::constructed::{PropertyReference, ReadAccessSpecification};
let request = bacnet_services::rpm::ReadPropertyMultipleRequest {
list_of_read_access_specs: vec![ReadAccessSpecification {
object_identifier: oid,
list_of_property_references: vec![PropertyReference {
property_identifier: PropertyIdentifier::ALL,
property_array_index: None,
}],
}],
};
let mut buf = BytesMut::new();
request.encode(&mut buf).unwrap();
let mut ack_buf = BytesMut::new();
handle_read_property_multiple(&db, &buf, &mut ack_buf).unwrap();
let ack_bytes = ack_buf.to_vec();
let ack = bacnet_services::rpm::ReadPropertyMultipleACK::decode(&ack_bytes).unwrap();
assert_eq!(ack.list_of_read_access_results.len(), 1);
let result = &ack.list_of_read_access_results[0];
assert_eq!(result.object_identifier, oid);
let obj = db.get(&oid).unwrap();
let expected_props = obj.property_list();
assert!(
expected_props.len() > 2,
"sanity: AI should have many properties"
);
assert_eq!(result.list_of_results.len(), expected_props.len());
for (elem, &expected_pid) in result.list_of_results.iter().zip(expected_props.iter()) {
assert_eq!(elem.property_identifier, expected_pid);
}
let pv_elem = result
.list_of_results
.iter()
.find(|e| e.property_identifier == PropertyIdentifier::PRESENT_VALUE)
.expect("PRESENT_VALUE should be in ALL results");
assert!(pv_elem.property_value.is_some());
}
#[test]
fn rpm_all_includes_multistate_event_history() {
let db = make_db_with_msi();
let oid = ObjectIdentifier::new(ObjectType::MULTI_STATE_INPUT, 1).unwrap();
use bacnet_types::constructed::{PropertyReference, ReadAccessSpecification};
let request = bacnet_services::rpm::ReadPropertyMultipleRequest {
list_of_read_access_specs: vec![ReadAccessSpecification {
object_identifier: oid,
list_of_property_references: vec![PropertyReference {
property_identifier: PropertyIdentifier::ALL,
property_array_index: None,
}],
}],
};
let mut buf = BytesMut::new();
request.encode(&mut buf).unwrap();
let mut ack_buf = BytesMut::new();
handle_read_property_multiple(&db, &buf, &mut ack_buf).unwrap();
let ack = bacnet_services::rpm::ReadPropertyMultipleACK::decode(&ack_buf).unwrap();
let results = &ack.list_of_read_access_results[0].list_of_results;
let timestamps = results
.iter()
.find(|result| result.property_identifier == PropertyIdentifier::EVENT_TIME_STAMPS)
.expect("EVENT_TIME_STAMPS missing from RPM ALL")
.property_value
.as_ref()
.expect("EVENT_TIME_STAMPS must succeed");
let mut offset = 0;
for _ in 0..3 {
let (decoded, next) =
bacnet_encoding::primitives::decode_timestamp_choice(timestamps, offset).unwrap();
assert_eq!(decoded, BACnetTimeStamp::SequenceNumber(0));
offset = next;
}
assert_eq!(offset, timestamps.len());
let messages = results
.iter()
.find(|result| result.property_identifier == PropertyIdentifier::EVENT_MESSAGE_TEXTS)
.expect("EVENT_MESSAGE_TEXTS missing from RPM ALL")
.property_value
.as_ref()
.expect("EVENT_MESSAGE_TEXTS must succeed");
let mut offset = 0;
for _ in 0..3 {
let (value, next) =
bacnet_encoding::primitives::decode_application_value(messages, offset).unwrap();
assert_eq!(
value,
bacnet_types::primitives::PropertyValue::CharacterString(String::new())
);
offset = next;
}
assert_eq!(offset, messages.len());
}
#[test]
fn rpm_explicit_index_returns_one_multistate_event_message() {
let db = make_db_with_msi();
let oid = ObjectIdentifier::new(ObjectType::MULTI_STATE_INPUT, 1).unwrap();
use bacnet_types::constructed::{PropertyReference, ReadAccessSpecification};
let request = bacnet_services::rpm::ReadPropertyMultipleRequest {
list_of_read_access_specs: vec![ReadAccessSpecification {
object_identifier: oid,
list_of_property_references: vec![PropertyReference {
property_identifier: PropertyIdentifier::EVENT_MESSAGE_TEXTS,
property_array_index: Some(2),
}],
}],
};
let mut buf = BytesMut::new();
request.encode(&mut buf).unwrap();
let mut ack_buf = BytesMut::new();
handle_read_property_multiple(&db, &buf, &mut ack_buf).unwrap();
let ack = bacnet_services::rpm::ReadPropertyMultipleACK::decode(&ack_buf).unwrap();
let result = &ack.list_of_read_access_results[0].list_of_results[0];
assert_eq!(result.property_array_index, Some(2));
let value = result
.property_value
.as_ref()
.expect("indexed EVENT_MESSAGE_TEXTS must succeed");
let (decoded, end) = bacnet_encoding::primitives::decode_application_value(value, 0).unwrap();
assert_eq!(
decoded,
bacnet_types::primitives::PropertyValue::CharacterString(String::new())
);
assert_eq!(end, value.len());
}
#[test]
fn rpm_handler_required_vs_optional() {
use super::property_metadata::assert_rpm_selector_bytes;
use PropertyIdentifier as P;
let mut db = ObjectDatabase::new();
db.add(Box::new(
bacnet_objects::value_types::DateValueObject::new(1, "DV-1").unwrap(),
))
.unwrap();
let oid = ObjectIdentifier::new(ObjectType::DATE_VALUE, 1).unwrap();
assert!(
!db.get(&oid).unwrap().property_metadata().is_empty(),
"Date Value routes RPM selectors through canonical metadata"
);
let req_pids = [
P::OBJECT_IDENTIFIER,
P::OBJECT_NAME,
P::OBJECT_TYPE,
P::PRESENT_VALUE,
P::STATUS_FLAGS,
];
let opt_pids = [
P::DESCRIPTION,
P::OUT_OF_SERVICE,
P::RELIABILITY,
P::PRIORITY_ARRAY,
P::RELINQUISH_DEFAULT,
P::CURRENT_COMMAND_PRIORITY,
];
let all = [
P::OBJECT_IDENTIFIER,
P::OBJECT_NAME,
P::DESCRIPTION,
P::OBJECT_TYPE,
P::PRESENT_VALUE,
P::STATUS_FLAGS,
P::OUT_OF_SERVICE,
P::RELIABILITY,
P::PRIORITY_ARRAY,
P::RELINQUISH_DEFAULT,
P::CURRENT_COMMAND_PRIORITY,
];
for (selector, expected) in [
(P::REQUIRED, req_pids.as_slice()),
(P::OPTIONAL, opt_pids.as_slice()),
(P::ALL, all.as_slice()),
] {
assert_rpm_selector_bytes(&db, oid, selector, expected);
}
}
#[test]
fn read_property_serves_derived_services_supported() {
let db = make_db_with_device_and_ai();
let oid = ObjectIdentifier::new(ObjectType::DEVICE, 1).unwrap();
let request = ReadPropertyRequest {
object_identifier: oid,
property_identifier: PropertyIdentifier::PROTOCOL_SERVICES_SUPPORTED,
property_array_index: None,
};
let mut buf = BytesMut::new();
request.encode(&mut buf);
let mut ack_buf = BytesMut::new();
handle_read_property(&db, &buf, &mut ack_buf).unwrap();
let ack = ReadPropertyACK::decode(&ack_buf).unwrap();
let (val, _) =
bacnet_encoding::primitives::decode_application_value(&ack.property_value, 0).unwrap();
assert_eq!(
val,
bacnet_types::primitives::PropertyValue::BitString {
unused_bits: 7,
data: vec![0xBF, 0xFB, 0xD8, 0x05, 0xFF, 0xCE, 0x00],
}
);
let bacnet_types::primitives::PropertyValue::BitString { data, .. } = val else {
unreachable!()
};
let ss = bacnet_types::bitstring::ServicesSupported::from_bacnet(&data);
assert!(ss.contains(bacnet_types::enums::ServiceSupported::WHO_IS));
assert!(ss.contains(bacnet_types::enums::ServiceSupported::CONFIRMED_AUDIT_NOTIFICATION));
assert!(ss.contains(bacnet_types::enums::ServiceSupported::UNCONFIRMED_AUDIT_NOTIFICATION));
assert!(ss.contains(bacnet_types::enums::ServiceSupported::AUDIT_LOG_QUERY));
assert!(!ss.contains(bacnet_types::enums::ServiceSupported::I_AM));
assert!(ss.contains(bacnet_types::enums::ServiceSupported::CONFIRMED_EVENT_NOTIFICATION));
assert!(ss.contains(bacnet_types::enums::ServiceSupported::UNCONFIRMED_EVENT_NOTIFICATION));
}
#[test]
fn rpm_multistate_indexed_state_text_and_list_gating_preserve_bytes() {
use PropertyIdentifier as P;
use bacnet_services::rpm::ReadPropertyMultipleACK;
use bacnet_types::constructed::{PropertyReference, ReadAccessSpecification};
use bacnet_types::primitives::PropertyValue;
for mut object in super::property_metadata::multistate_objects() {
let oid = object.object_identifier();
let label = "indexed label".repeat(100);
object
.write_property(
P::STATE_TEXT,
Some(2),
PropertyValue::CharacterString(label.clone()),
None,
)
.unwrap();
let mut db = ObjectDatabase::new();
db.add(object).unwrap();
let references = [
(P::STATE_TEXT, 0),
(P::STATE_TEXT, 1),
(P::STATE_TEXT, 2),
(P::STATE_TEXT, 3),
(P::STATE_TEXT, 4),
(P::ALARM_VALUES, 0),
(P::NUMBER_OF_STATES, 1),
(P::FEEDBACK_VALUE, 1),
];
let request = ReadPropertyMultipleRequest {
list_of_read_access_specs: vec![ReadAccessSpecification {
object_identifier: oid,
list_of_property_references: references
.iter()
.map(|&(p, i)| PropertyReference {
property_identifier: p,
property_array_index: Some(i),
})
.collect(),
}],
};
let mut bytes = BytesMut::new();
request.encode(&mut bytes).unwrap();
let mut legacy = BytesMut::new();
handle_read_property_multiple(&db, &bytes, &mut legacy).unwrap();
let ack = ReadPropertyMultipleACK::decode(&legacy).unwrap();
let results = &ack.list_of_read_access_results[0].list_of_results;
assert_eq!(results.len(), references.len());
for (result, (p, i)) in results.iter().zip(references) {
assert_eq!(result.property_identifier, p);
assert_eq!(
result.property_array_index,
(p == P::STATE_TEXT).then_some(i)
);
if p == P::STATE_TEXT && i <= 3 {
assert!(result.error.is_none());
let value_bytes = result.property_value.as_ref().unwrap();
let (value, end) =
bacnet_encoding::primitives::decode_application_value(value_bytes, 0).unwrap();
let expected = match i {
0 => PropertyValue::Unsigned(3),
2 => PropertyValue::CharacterString(label.clone()),
_ => PropertyValue::CharacterString(format!("State {i}")),
};
assert_eq!(value, expected);
assert_eq!(end, value_bytes.len());
} else {
let expected = if p == P::STATE_TEXT {
ErrorCode::INVALID_ARRAY_INDEX
} else {
ErrorCode::PROPERTY_IS_NOT_AN_ARRAY
};
assert_eq!(result.error, Some((ErrorClass::PROPERTY, expected)));
assert!(result.property_value.is_none());
}
}
let budget = crate::server::ReadPropertyMultipleBudget {
max_result_elements: references.len(),
max_service_ack_bytes: legacy.len(),
};
let mut bounded = BytesMut::new();
super::super::rpm_budget::handle_rpm_budgeted(&db, &bytes, &mut bounded, budget).unwrap();
assert_eq!(bounded, legacy);
let mut prefix = BytesMut::from(&b"prefix"[..]);
assert!(matches!(
super::super::rpm_budget::handle_rpm_budgeted(
&db,
&bytes,
&mut prefix,
crate::server::ReadPropertyMultipleBudget {
max_service_ack_bytes: legacy.len() - 1,
..budget
}
),
Err(super::super::ReadFailure::Bytes)
));
assert_eq!(&prefix[..], b"prefix");
}
}