use super::*;
use bacnet_objects::binary::BinaryInputObject;
use bacnet_objects::event_enrollment::EventEnrollmentObject;
use bacnet_objects::notification_class::NotificationClass;
use bacnet_services::alarm_event::{AcknowledgeAlarmRequest, GetEventInformationRequest};
use bacnet_types::bitstring::EventTransitionBits;
use bacnet_types::constructed::{
BACnetDeviceObjectPropertyReference, BACnetEventParameter, BACnetPropertyStates,
};
use bacnet_types::enums::EventType;
use bacnet_types::error::RemoteError;
use bacnet_types::primitives::BACnetTimeStamp;
fn make_db_with_ack_required_ee() -> (ObjectDatabase, ObjectIdentifier) {
let mut db = ObjectDatabase::new();
let mut bi = BinaryInputObject::new(1, "BI-1").unwrap();
bi.set_present_value(1); let bi_oid = bi.object_identifier();
db.add(Box::new(bi)).unwrap();
let mut ee = EventEnrollmentObject::new(7, "EE-COS", EventType::CHANGE_OF_STATE).unwrap();
ee.set_object_property_reference(Some(BACnetDeviceObjectPropertyReference::new_local(
bi_oid,
PropertyIdentifier::PRESENT_VALUE.to_raw(),
)))
.unwrap();
ee.set_event_parameters(BACnetEventParameter::ChangeOfState {
time_delay: 0,
list_of_values: vec![BACnetPropertyStates::BinaryValue(1)],
});
ee.set_event_enable(EventTransitionBits::all());
ee.set_notification_class(7);
let ee_oid = ee.object_identifier();
db.add(Box::new(ee)).unwrap();
let mut nc = NotificationClass::new(7, "NC-7").unwrap();
nc.ack_required = EventTransitionBits::TO_OFFNORMAL; db.add(Box::new(nc)).unwrap();
db.add(Box::new(NotificationClass::new(0, "NC-0").unwrap()))
.unwrap();
(db, ee_oid)
}
fn ack_request(
ee_oid: ObjectIdentifier,
event_state: EventState,
timestamp: BACnetTimeStamp,
) -> bytes::BytesMut {
let request = AcknowledgeAlarmRequest {
acknowledging_process_identifier: 1,
event_object_identifier: ee_oid,
event_state_acknowledged: event_state,
timestamp,
acknowledgment_source: "operator".into(),
time_of_acknowledgment: BACnetTimeStamp::SequenceNumber(2),
};
let mut buf = BytesMut::new();
request.encode(&mut buf).unwrap();
buf
}
fn gei_summary_acked(db: &ObjectDatabase, ee_oid: ObjectIdentifier) -> EventTransitionBits {
let request = GetEventInformationRequest {
last_received_object_identifier: None,
};
let mut buf = BytesMut::new();
request.encode(&mut buf);
let mut ack_buf = BytesMut::new();
handle_get_event_information(db, &buf, &mut ack_buf).unwrap();
let ack = bacnet_services::alarm_event::GetEventInformationAck::decode(&ack_buf).unwrap();
let summary = ack
.list_of_event_summaries
.iter()
.find(|s| s.object_identifier == ee_oid)
.expect("EE must appear in GetEventInformation while non-NORMAL");
summary.acknowledged_transitions
}
#[test]
fn ee_acknowledge_alarm_round_trip_over_services() {
let (mut db, ee_oid) = make_db_with_ack_required_ee();
let transitions = crate::event_enrollment::evaluate_event_enrollments(&mut db, 1);
assert_eq!(transitions.len(), 1);
assert_eq!(transitions[0].change.to, EventState::OFFNORMAL);
assert_eq!(
gei_summary_acked(&db, ee_oid) & EventTransitionBits::TO_OFFNORMAL,
EventTransitionBits::empty(),
"TO_OFFNORMAL ack owed: GEI shows the bit cleared"
);
handle_acknowledge_alarm(
&mut db,
&ack_request(
ee_oid,
EventState::OFFNORMAL,
BACnetTimeStamp::SequenceNumber(0),
),
)
.unwrap();
assert_eq!(
gei_summary_acked(&db, ee_oid) & EventTransitionBits::TO_OFFNORMAL,
EventTransitionBits::TO_OFFNORMAL,
"after the ack the bit is set"
);
handle_acknowledge_alarm(
&mut db,
&ack_request(
ee_oid,
EventState::OFFNORMAL,
BACnetTimeStamp::SequenceNumber(0),
),
)
.unwrap();
assert_eq!(gei_summary_acked(&db, ee_oid), EventTransitionBits::all());
}
#[test]
fn ee_acknowledge_to_normal_bit() {
let (mut db, ee_oid) = make_db_with_ack_required_ee();
db.get_mut(&ee_oid)
.unwrap()
.commit_event_transition_internal(bacnet_objects::event::EventTransitionCommit {
change: bacnet_objects::event::EventStateChange {
from: EventState::NORMAL,
to: EventState::HIGH_LIMIT,
},
coordinate: bacnet_objects::event::EventTransition::ToOffnormal,
ack_required: false,
timestamp: BACnetTimeStamp::SequenceNumber(1),
message_text: None,
})
.unwrap();
db.get_mut(&ee_oid)
.unwrap()
.commit_event_transition_internal(bacnet_objects::event::EventTransitionCommit {
change: bacnet_objects::event::EventStateChange {
from: EventState::HIGH_LIMIT,
to: EventState::NORMAL,
},
coordinate: bacnet_objects::event::EventTransition::ToNormal,
ack_required: true,
timestamp: BACnetTimeStamp::SequenceNumber(2),
message_text: None,
})
.unwrap();
handle_acknowledge_alarm(
&mut db,
&ack_request(
ee_oid,
EventState::NORMAL,
BACnetTimeStamp::SequenceNumber(2),
),
)
.unwrap();
match db
.get(&ee_oid)
.unwrap()
.read_property(PropertyIdentifier::ACKED_TRANSITIONS, None)
.unwrap()
{
PropertyValue::BitString { data, .. } => {
assert_eq!(
EventTransitionBits::from_bacnet(&data),
EventTransitionBits::all()
)
}
other => panic!("expected BitString, got {other:?}"),
}
}
#[test]
fn ee_acknowledge_alarm_detection_disabled_refused() {
let (mut db, ee_oid) = make_db_with_ack_required_ee();
db.get_mut(&ee_oid)
.unwrap()
.write_property(
PropertyIdentifier::EVENT_DETECTION_ENABLE,
None,
PropertyValue::Boolean(false),
None,
)
.unwrap();
let err = handle_acknowledge_alarm(
&mut db,
&ack_request(
ee_oid,
EventState::OFFNORMAL,
BACnetTimeStamp::SequenceNumber(1),
),
)
.unwrap_err();
match err {
Error::Remote(RemoteError::Error { class, code }) => {
assert_eq!(class, ErrorClass::OBJECT);
assert_eq!(code, ErrorCode::NO_ALARM_CONFIGURED);
}
other => panic!("expected OBJECT/NO_ALARM_CONFIGURED, got {other:?}"),
}
match db
.get(&ee_oid)
.unwrap()
.read_property(PropertyIdentifier::ACKED_TRANSITIONS, None)
.unwrap()
{
PropertyValue::BitString { data, .. } => assert_eq!(
EventTransitionBits::from_bacnet(&data),
EventTransitionBits::all(),
"the refused ack must not disturb the initial condition"
),
other => panic!("expected BitString, got {other:?}"),
}
}