use super::*;
use crate::handlers::{handle_add_list_element, handle_remove_list_element};
use crate::server::test_transport::{SendLog, TestTransport};
use bacnet_encoding::apdu::decode_apdu;
use bacnet_encoding::constructed::decode_event_notification;
use bacnet_encoding::npdu::decode_npdu;
use bacnet_objects::binary::{BinaryInputObject, BinaryValueObject};
use bacnet_objects::device::{DeviceConfig, DeviceObject};
use bacnet_objects::multistate::{MultiStateInputObject, MultiStateValueObject};
use bacnet_objects::traits::BACnetObject;
use bacnet_services::alarm_event::NotificationParameters;
use bacnet_services::list_manipulation::ListElementRequest;
use bacnet_types::bitstring::EventTransitionBits;
use bacnet_types::enums::{EventState, EventType, NotifyType};
use bacnet_types::primitives::StatusFlags;
use bytes::{Bytes, BytesMut};
const ALARM_STATE: u64 = 2;
const NORMAL_STATE: u64 = 1;
#[tokio::test]
async fn analog_event_enable_set_delivers_committed_event_values() {
use super::event_notifications_tests::{
broadcasts_from_per_write_path, db_with_high_limit_transition,
decode_broadcast_notification,
};
let db = db_with_high_limit_transition(0x80);
db.write()
.await
.get_mut(&ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap())
.unwrap()
.write_property(
PropertyIdentifier::NOTIFY_TYPE,
None,
PropertyValue::Enumerated(NotifyType::EVENT.to_raw()),
None,
)
.unwrap();
let sent = broadcasts_from_per_write_path(&db, DccState::Enable).await;
assert_eq!(sent.len(), 1);
let notification = decode_broadcast_notification(&sent);
assert_eq!(notification.notify_type, NotifyType::EVENT);
assert_eq!(notification.event_type, EventType::OUT_OF_RANGE);
assert_eq!(
notification.event_values,
Some(NotificationParameters::OutOfRange {
exceeding_value: 81.0,
status_flags: StatusFlags::IN_ALARM,
deadband: 2.0,
exceeded_limit: 80.0,
})
);
}
struct Fixture {
db: Arc<RwLock<ObjectDatabase>>,
network: Arc<NetworkLayer<TestTransport>>,
comm_state: Arc<CommState>,
learned_routers: Arc<Mutex<LearnedRouterCache>>,
notification_transactions: Arc<NotificationTransactions>,
sent: SendLog,
oid: ObjectIdentifier,
}
impl Fixture {
async fn new(bits: EventTransitionBits) -> Self {
let mut msi = MultiStateInputObject::new(1, "MSI-1", 3).unwrap();
for (property, value) in [
(
PropertyIdentifier::OUT_OF_SERVICE,
PropertyValue::Boolean(true),
),
(
PropertyIdentifier::EVENT_ENABLE,
PropertyValue::BitString {
unused_bits: 5,
data: vec![bits.to_bacnet()],
},
),
(
PropertyIdentifier::NOTIFY_TYPE,
PropertyValue::Enumerated(NotifyType::EVENT.to_raw()),
),
] {
msi.write_property_from(
property,
None,
value,
None,
&crate::command_source::test_origin(),
)
.unwrap_or_else(|e| panic!("{property:?} must be writable since #229: {e:?}"));
}
let fixture = Self::from_object(Box::new(msi)).await;
fixture.add_alarm_value(ALARM_STATE as u8).await;
fixture
}
async fn from_object(object: Box<dyn BACnetObject>) -> Self {
let oid = object.object_identifier();
let mut db = clocked_test_database();
db.add(object).unwrap();
db.add(Box::new(
DeviceObject::new(DeviceConfig {
identity: bacnet_objects::DeviceIdentity::default().with_name("Dev"),
..DeviceConfig::default()
})
.unwrap(),
))
.unwrap();
db.add(Box::new(
super::event_notifications_tests::notification_class_0_broadcasting(),
))
.unwrap();
let (transport, sent) = super::event_notifications_tests::recording_transport();
Self {
db: Arc::new(RwLock::new(db)),
network: Arc::new(NetworkLayer::new(transport)),
comm_state: Arc::new(CommState::default()), learned_routers: Arc::new(Mutex::new(LearnedRouterCache::new())),
notification_transactions: NotificationTransactions::new(),
sent,
oid,
}
}
async fn write_present_value(&self, value: u64) {
self.write_present_value_as(PropertyValue::Unsigned(value))
.await;
}
async fn write_present_value_as(&self, value: PropertyValue) {
self.db
.write()
.await
.get_mut(&self.oid)
.expect("the MSI is in the database")
.write_property_from(
PropertyIdentifier::PRESENT_VALUE,
None,
value,
None,
&crate::command_source::test_origin(),
)
.expect("Out_Of_Service is TRUE, so Present_Value must be writable");
BACnetServer::<TestTransport>::fire_event_notifications_with_bindings(
&crate::server::event_delivery::EventDelivery {
db: &self.db,
network: &self.network,
comm_state: &self.comm_state,
learned_routers: &self.learned_routers,
notification_transactions: &self.notification_transactions,
device_bindings: &Arc::new(RwLock::new(
crate::server::device_bindings::DeviceBindingTable::new(),
)),
suppressions: &Default::default(),
retry_timeout_ms: 1000,
local_apdu_capacity: 1476,
},
&Arc::new(RwLock::new(crate::cov::CovSubscriptionTable::new())),
&self.oid,
)
.await;
}
async fn add_alarm_value(&self, value: u8) {
let request = ListElementRequest {
object_identifier: self.oid,
property_identifier: PropertyIdentifier::ALARM_VALUES,
property_array_index: None,
list_of_elements: vec![0x21, value],
};
let mut encoded = BytesMut::new();
request.encode(&mut encoded).unwrap();
let mut db = self.db.write().await;
handle_add_list_element(&mut db, &encoded)
.expect("AddListElement is the working network Alarm_Values route");
}
async fn remove_alarm_value(&self, value: u8) {
let request = ListElementRequest {
object_identifier: self.oid,
property_identifier: PropertyIdentifier::ALARM_VALUES,
property_array_index: None,
list_of_elements: vec![0x21, value],
};
let mut encoded = BytesMut::new();
request.encode(&mut encoded).unwrap();
let mut db = self.db.write().await;
handle_remove_list_element(&mut db, &encoded)
.expect("RemoveListElement must remove the commissioned alarm value");
}
fn drain(&self) -> Vec<Bytes> {
self.sent
.take()
.into_iter()
.map(|frame| frame.npdu)
.collect()
}
async fn event_state(&self) -> PropertyValue {
self.db
.read()
.await
.get(&self.oid)
.unwrap()
.read_property(PropertyIdentifier::EVENT_STATE, None)
.unwrap()
}
}
fn assert_sole_notification(
sent: Vec<Bytes>,
oid: ObjectIdentifier,
from: EventState,
to: EventState,
context: &str,
) {
assert_eq!(
sent.len(),
1,
"{context}: expected exactly one broadcast EventNotification"
);
let npdu = decode_npdu(sent[0].clone()).expect("decode NPDU");
let notif = match decode_apdu(npdu.payload).expect("decode APDU") {
Apdu::UnconfirmedRequest(req) => {
assert_eq!(
req.service_choice,
UnconfirmedServiceChoice::UNCONFIRMED_EVENT_NOTIFICATION
);
decode_event_notification(&req.service_request).expect("decode EventNotification")
}
other => panic!("{context}: expected UnconfirmedRequest, got {other:?}"),
};
assert_eq!(
notif.event_object_identifier, oid,
"{context}: notification must name the commissioned object"
);
assert_eq!(
notif.event_type,
EventType::CHANGE_OF_STATE,
"{context}: the detector's CHANGE_OF_STATE algorithm must reach the wire"
);
assert_eq!(notif.from_state, from, "{context}: from_state on the wire");
assert_eq!(notif.to_state, to, "{context}: to_state on the wire");
assert_eq!(
notif.notify_type,
NotifyType::EVENT,
"{context}: the commissioned Notify_Type must reach the wire, not the ALARM fallback"
);
}
#[tokio::test]
async fn msi_to_offnormal_bit_alone_distributes_the_offnormal_transition() {
let enabled = Fixture::new(EventTransitionBits::TO_OFFNORMAL).await;
enabled.write_present_value(ALARM_STATE).await;
assert_sole_notification(
enabled.drain(),
enabled.oid,
EventState::NORMAL,
EventState::OFFNORMAL,
"TO_OFFNORMAL set",
);
for bits in [
EventTransitionBits::TO_FAULT,
EventTransitionBits::TO_NORMAL,
EventTransitionBits::empty(),
] {
let suppressed = Fixture::new(bits).await;
suppressed.write_present_value(ALARM_STATE).await;
assert!(
suppressed.drain().is_empty(),
"Event_Enable {bits} must not distribute a TO_OFFNORMAL notification"
);
assert_eq!(
suppressed.event_state().await,
PropertyValue::Enumerated(EventState::OFFNORMAL.to_raw()),
"Event_Enable {bits}: the transition must still occur, only distribution is withheld"
);
}
}
#[tokio::test]
async fn msi_to_normal_bit_alone_distributes_the_return_to_normal() {
let enabled = Fixture::new(EventTransitionBits::TO_NORMAL).await;
enabled.write_present_value(ALARM_STATE).await;
assert!(
enabled.drain().is_empty(),
"TO_NORMAL set alone must not distribute the entry into OFFNORMAL"
);
assert_eq!(
enabled.event_state().await,
PropertyValue::Enumerated(EventState::OFFNORMAL.to_raw()),
"the object must be in OFFNORMAL for the next write to be a return to normal"
);
enabled.write_present_value(NORMAL_STATE).await;
assert_sole_notification(
enabled.drain(),
enabled.oid,
EventState::OFFNORMAL,
EventState::NORMAL,
"TO_NORMAL set",
);
let offnormal_only = Fixture::new(EventTransitionBits::TO_OFFNORMAL).await;
offnormal_only.write_present_value(ALARM_STATE).await;
assert_eq!(
offnormal_only.drain().len(),
1,
"TO_OFFNORMAL set must distribute the entry into OFFNORMAL"
);
offnormal_only.write_present_value(NORMAL_STATE).await;
assert!(
offnormal_only.drain().is_empty(),
"TO_OFFNORMAL set alone must not distribute the return to NORMAL"
);
assert_eq!(
offnormal_only.event_state().await,
PropertyValue::Enumerated(EventState::NORMAL.to_raw()),
"the return to NORMAL must still have happened internally"
);
}
#[tokio::test]
async fn bi_bv_and_msv_alarm_values_commission_and_reach_the_wire() {
let mut bi = BinaryInputObject::new(1, "BI-1").unwrap();
for (property, value) in [
(
PropertyIdentifier::ALARM_VALUE,
PropertyValue::Enumerated(0),
),
(
PropertyIdentifier::OUT_OF_SERVICE,
PropertyValue::Boolean(true),
),
(
PropertyIdentifier::EVENT_ENABLE,
PropertyValue::BitString {
unused_bits: 5,
data: vec![EventTransitionBits::TO_OFFNORMAL.to_bacnet()],
},
),
(
PropertyIdentifier::NOTIFY_TYPE,
PropertyValue::Enumerated(NotifyType::EVENT.to_raw()),
),
(
PropertyIdentifier::PRESENT_VALUE,
PropertyValue::Enumerated(1),
),
] {
bi.write_property_from(
property,
None,
value,
None,
&crate::command_source::test_origin(),
)
.unwrap();
}
let bi_fixture = Fixture::from_object(Box::new(bi)).await;
bi_fixture
.write_present_value_as(PropertyValue::Enumerated(0))
.await;
assert_sole_notification(
bi_fixture.drain(),
bi_fixture.oid,
EventState::NORMAL,
EventState::OFFNORMAL,
"Binary Input non-default Alarm_Value",
);
let mut bv = BinaryValueObject::new(1, "BV-1").unwrap();
for (property, value) in [
(
PropertyIdentifier::ALARM_VALUE,
PropertyValue::Enumerated(0),
),
(
PropertyIdentifier::EVENT_ENABLE,
PropertyValue::BitString {
unused_bits: 5,
data: vec![EventTransitionBits::TO_OFFNORMAL.to_bacnet()],
},
),
(
PropertyIdentifier::NOTIFY_TYPE,
PropertyValue::Enumerated(NotifyType::EVENT.to_raw()),
),
(
PropertyIdentifier::PRESENT_VALUE,
PropertyValue::Enumerated(1),
),
] {
bv.write_property_from(
property,
None,
value,
None,
&crate::command_source::test_origin(),
)
.unwrap();
}
let bv_fixture = Fixture::from_object(Box::new(bv)).await;
bv_fixture
.write_present_value_as(PropertyValue::Enumerated(0))
.await;
assert_sole_notification(
bv_fixture.drain(),
bv_fixture.oid,
EventState::NORMAL,
EventState::OFFNORMAL,
"Binary Value non-default Alarm_Value",
);
let mut msv = MultiStateValueObject::new(1, "MSV-1", 3).unwrap();
for (property, value) in [
(
PropertyIdentifier::EVENT_ENABLE,
PropertyValue::BitString {
unused_bits: 5,
data: vec![EventTransitionBits::TO_OFFNORMAL.to_bacnet()],
},
),
(
PropertyIdentifier::NOTIFY_TYPE,
PropertyValue::Enumerated(NotifyType::EVENT.to_raw()),
),
] {
msv.write_property_from(
property,
None,
value,
None,
&crate::command_source::test_origin(),
)
.unwrap();
}
let msv_fixture = Fixture::from_object(Box::new(msv)).await;
msv_fixture.add_alarm_value(2).await;
msv_fixture.write_present_value(2).await;
assert_sole_notification(
msv_fixture.drain(),
msv_fixture.oid,
EventState::NORMAL,
EventState::OFFNORMAL,
"Multi-state Value Alarm_Values",
);
}
#[tokio::test]
async fn fresh_binary_input_active_is_offnormal_without_distribution() {
let mut bi = BinaryInputObject::new(2, "BI-default").unwrap();
bi.write_property(
PropertyIdentifier::OUT_OF_SERVICE,
None,
PropertyValue::Boolean(true),
None,
)
.unwrap();
let fixture = Fixture::from_object(Box::new(bi)).await;
fixture
.write_present_value_as(PropertyValue::Enumerated(1))
.await;
assert_eq!(
fixture.event_state().await,
PropertyValue::Enumerated(EventState::OFFNORMAL.to_raw())
);
assert!(
fixture.drain().is_empty(),
"default Event_Enable must suppress distribution"
);
}
#[tokio::test]
async fn remove_last_alarm_value_disarms_msi() {
let fixture = Fixture::new(EventTransitionBits::TO_OFFNORMAL).await;
fixture.remove_alarm_value(ALARM_STATE as u8).await;
assert_eq!(
fixture
.db
.read()
.await
.get(&fixture.oid)
.unwrap()
.read_property(PropertyIdentifier::ALARM_VALUES, None)
.unwrap(),
PropertyValue::List(vec![])
);
fixture.write_present_value(ALARM_STATE).await;
assert_eq!(
fixture.event_state().await,
PropertyValue::Enumerated(EventState::NORMAL.to_raw())
);
assert!(
fixture.drain().is_empty(),
"an empty Alarm_Values list must not distribute"
);
}