use super::event_notifications_tests::{
decode_broadcast_notification, local_broadcast_destination, recording_transport,
};
use super::*;
use crate::server::test_transport::{SendLog, TestTransport};
use PropertyIdentifier as P;
use bacnet_encoding::constructed::decode_bacnet_property_value;
use bacnet_objects::access_control::AccessZoneObject;
use bacnet_objects::device::{DeviceConfig, DeviceObject};
use bacnet_objects::notification_class::NotificationClass;
use bacnet_objects::traits::BACnetObject;
use bacnet_services::alarm_event::NotificationParameters;
use bacnet_types::bitstring::EventTransitionBits;
use bacnet_types::constructed::BACnetPropertyStates;
use bacnet_types::enums::{
AccessZoneOccupancyState, EventState, EventType, NotifyType, Reliability,
};
use bacnet_types::primitives::StatusFlags;
use bytes::Bytes;
const CLASS: u32 = 5;
fn zone(time_delay: u64, enable: EventTransitionBits) -> AccessZoneObject {
let mut zone = AccessZoneObject::new(1, "ZONE-1").unwrap();
zone.set_occupancy_limits(0, 5).unwrap();
for (property, value) in [
(
P::ALARM_VALUES,
PropertyValue::List(vec![PropertyValue::Enumerated(
AccessZoneOccupancyState::ABOVE_UPPER_LIMIT.to_raw(),
)]),
),
(P::TIME_DELAY, PropertyValue::Unsigned(time_delay)),
(P::NOTIFICATION_CLASS, PropertyValue::Unsigned(CLASS.into())),
(
P::EVENT_ENABLE,
PropertyValue::BitString {
unused_bits: 5,
data: vec![enable.to_bacnet()],
},
),
] {
zone.write_property(property, None, value, None).unwrap();
}
zone
}
async fn start(zone: AccessZoneObject) -> (BACnetServer<TestTransport>, ObjectIdentifier, SendLog) {
let (transport, sent) = recording_transport();
let oid = zone.object_identifier();
let mut class = NotificationClass::new(CLASS, "NC-5").unwrap();
class
.add_destination(local_broadcast_destination())
.unwrap();
let mut db = clocked_test_database();
db.add(Box::new(zone)).unwrap();
db.add(Box::new(class)).unwrap();
db.add(Box::new(
DeviceObject::new(DeviceConfig {
identity: bacnet_objects::DeviceIdentity::default().with_name("Dev"),
..DeviceConfig::default()
})
.unwrap(),
))
.unwrap();
let server = BACnetServer::start(ServerConfig::default(), db, transport)
.await
.unwrap();
(server, oid, sent)
}
async fn write(
server: &BACnetServer<TestTransport>,
oid: ObjectIdentifier,
property: P,
value: PropertyValue,
) {
server
.write_local(
&oid,
property,
None,
value,
None,
crate::LocalCommandSource::ServerDevice,
)
.await
.unwrap();
}
fn take(sent: &SendLog) -> Vec<Bytes> {
sent.take().into_iter().map(|frame| frame.npdu).collect()
}
async fn event_state(server: &BACnetServer<TestTransport>, oid: ObjectIdentifier) -> PropertyValue {
server
.database()
.read()
.await
.get(&oid)
.unwrap()
.read_property(P::EVENT_STATE, None)
.unwrap()
}
fn change_of_state(state: AccessZoneOccupancyState, flags: StatusFlags) -> NotificationParameters {
NotificationParameters::ChangeOfState {
new_state: BACnetPropertyStates::ZoneOccupancyState(state.to_raw()),
status_flags: flags,
}
}
#[tokio::test(start_paused = true)]
async fn access_zone_occupancy_alarm_reaches_notification_class_recipients() {
let (server, oid, sent) = start(zone(2, EventTransitionBits::all())).await;
write(&server, oid, P::ADJUST_VALUE, PropertyValue::Signed(6)).await;
assert!(sent.is_empty(), "Time_Delay holds the transition back");
tokio::time::sleep(Duration::from_secs(5)).await;
let offnormal = decode_broadcast_notification(&take(&sent));
assert_eq!(offnormal.event_object_identifier, oid);
assert_eq!(offnormal.notification_class, CLASS);
assert_eq!(offnormal.event_type, EventType::CHANGE_OF_STATE);
assert_eq!(offnormal.notify_type, NotifyType::ALARM);
assert_eq!(
(offnormal.from_state, offnormal.to_state),
(EventState::NORMAL, EventState::OFFNORMAL)
);
assert_eq!(
offnormal.event_values,
Some(change_of_state(
AccessZoneOccupancyState::ABOVE_UPPER_LIMIT,
StatusFlags::IN_ALARM,
))
);
assert_eq!(
event_state(&server, oid).await,
PropertyValue::Enumerated(EventState::OFFNORMAL.to_raw())
);
write(&server, oid, P::ADJUST_VALUE, PropertyValue::Signed(-3)).await;
assert!(sent.is_empty());
tokio::time::sleep(Duration::from_secs(5)).await;
let normal = decode_broadcast_notification(&take(&sent));
assert_eq!(
(normal.from_state, normal.to_state),
(EventState::OFFNORMAL, EventState::NORMAL)
);
assert_eq!(
normal.event_values,
Some(change_of_state(
AccessZoneOccupancyState::NORMAL,
StatusFlags::empty(),
))
);
}
#[tokio::test(start_paused = true)]
async fn access_zone_event_enable_withholds_only_the_distribution() {
let (server, oid, sent) = start(zone(0, EventTransitionBits::TO_NORMAL)).await;
write(&server, oid, P::ADJUST_VALUE, PropertyValue::Signed(6)).await;
assert!(sent.is_empty(), "TO_OFFNORMAL is not distributed");
assert_eq!(
event_state(&server, oid).await,
PropertyValue::Enumerated(EventState::OFFNORMAL.to_raw()),
"the transition still happened"
);
write(&server, oid, P::ADJUST_VALUE, PropertyValue::Signed(0)).await;
let normal = decode_broadcast_notification(&take(&sent));
assert_eq!(
(normal.from_state, normal.to_state),
(EventState::OFFNORMAL, EventState::NORMAL)
);
}
#[tokio::test(start_paused = true)]
async fn access_zone_fault_notification_reports_occupancy_state() {
let (server, oid, sent) = start(zone(0, EventTransitionBits::all())).await;
write(
&server,
oid,
P::OUT_OF_SERVICE,
PropertyValue::Boolean(true),
)
.await;
assert!(sent.is_empty());
let unreliable = Reliability::UNRELIABLE_OTHER;
write(
&server,
oid,
P::RELIABILITY,
PropertyValue::Enumerated(unreliable.to_raw()),
)
.await;
let fault = decode_broadcast_notification(&take(&sent));
assert_eq!(fault.event_type, EventType::CHANGE_OF_RELIABILITY);
assert_eq!(
(fault.from_state, fault.to_state),
(EventState::NORMAL, EventState::FAULT)
);
let Some(NotificationParameters::ChangeOfReliability {
reliability,
status_flags,
property_values,
}) = fault.event_values
else {
panic!(
"expected CHANGE_OF_RELIABILITY values, got {:?}",
fault.event_values
);
};
assert_eq!(reliability, unreliable);
assert_eq!(
status_flags,
StatusFlags::IN_ALARM | StatusFlags::FAULT | StatusFlags::OUT_OF_SERVICE
);
let (entry, end) = decode_bacnet_property_value(&property_values, 0).unwrap();
assert_eq!(end, property_values.len());
assert_eq!(entry.property_identifier, P::OCCUPANCY_STATE);
assert_eq!(entry.value, [0x91, 0x00]);
}
#[tokio::test(start_paused = true)]
async fn access_zone_in_alarm_is_listed_by_the_summary_services() {
use crate::handlers::{
handle_get_alarm_summary, handle_get_enrollment_summary, handle_get_event_information,
};
use bacnet_services::alarm_event::{GetEventInformationAck, GetEventInformationRequest};
use bacnet_services::alarm_summary::GetAlarmSummaryAck;
use bacnet_services::enrollment_summary::{
GetEnrollmentSummaryAck, GetEnrollmentSummaryRequest,
};
use bacnet_types::enums::AcknowledgmentFilter;
let (server, oid, _sent) = start(zone(0, EventTransitionBits::all())).await;
write(&server, oid, P::ADJUST_VALUE, PropertyValue::Signed(6)).await;
let db = server.database().read().await;
let mut request = BytesMut::new();
GetEventInformationRequest {
last_received_object_identifier: None,
}
.encode(&mut request);
let mut ack = BytesMut::new();
handle_get_event_information(&db, &request, &mut ack).unwrap();
let ack = GetEventInformationAck::decode(&ack).unwrap();
let [summary] = ack.list_of_event_summaries.as_slice() else {
panic!("one event summary, got {:?}", ack.list_of_event_summaries);
};
assert_eq!(summary.object_identifier, oid);
assert_eq!(summary.event_state, EventState::OFFNORMAL);
assert_eq!(summary.notify_type, NotifyType::ALARM);
assert_eq!(summary.event_enable, EventTransitionBits::all());
let mut ack = BytesMut::new();
handle_get_alarm_summary(&db, &mut ack).unwrap();
let alarms = GetAlarmSummaryAck::decode(&ack).unwrap().entries;
assert_eq!(
alarms
.iter()
.map(|entry| (entry.object_identifier, entry.alarm_state))
.collect::<Vec<_>>(),
[(oid, EventState::OFFNORMAL)]
);
let mut request = BytesMut::new();
GetEnrollmentSummaryRequest {
acknowledgment_filter: AcknowledgmentFilter::ALL,
enrollment_filter: None,
event_state_filter: None,
event_type_filter: None,
priority_filter: None,
notification_class_filter: None,
}
.encode(&mut request);
let mut ack = BytesMut::new();
handle_get_enrollment_summary(&db, &request, &mut ack).unwrap();
let enrollments = GetEnrollmentSummaryAck::decode(&ack).unwrap().entries;
let [enrollment] = enrollments.as_slice() else {
panic!("one enrollment, got {enrollments:?}");
};
assert_eq!(enrollment.object_identifier, oid);
assert_eq!(enrollment.event_type, EventType::CHANGE_OF_STATE);
assert_eq!(enrollment.event_state, EventState::OFFNORMAL);
assert_eq!(enrollment.notification_class, Some(CLASS));
}