bacnet-server 0.15.0

BACnet server: APDU dispatch and service handlers
Documentation
//! AcknowledgeAlarm over the wire for EventEnrollment (PR-#290 review
//! blocker 3): EE transitions maintain `Acked_Transitions` per Clause 13.2.3
//! (the evaluator clears a bit whose Notification Class requires ack), so
//! AcknowledgeAlarm on an EE must actually succeed — previously the trait
//! default rejected every ack with OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED and
//! a required acknowledgment could never arrive.

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;

/// Database: BI-1 monitored by a COS enrollment (EE-7) referencing
/// NotificationClass 7, which requires acknowledgment of TO_OFFNORMAL.
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); // in the alarm list below
    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; // TO_OFFNORMAL requires ack
    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
}

/// The full loop: ack-required transition fires -> GEI shows the
/// TO_OFFNORMAL bit cleared (ack owed) -> AcknowledgeAlarm succeeds -> the
/// bit is set -> GEI shows it set. A duplicate ack is idempotent per Clause
/// 13.2.3's unconditional assignment of the acknowledged value.
#[test]
fn ee_acknowledge_alarm_round_trip_over_services() {
    let (mut db, ee_oid) = make_db_with_ack_required_ee();

    // Fire the ack-required NORMAL -> OFFNORMAL transition through the
    // evaluated path (#166's actions: 13.2.3 clears the owed bit).
    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"
    );

    // Acknowledge TO_OFFNORMAL over the service (OFFNORMAL matches any
    // offnormal 'To State', Table 13-9).
    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"
    );

    // Duplicate ack: 13.2.3 sets the bit unconditionally — idempotent
    // success, and the other bits are untouched.
    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());
}

/// A TO_NORMAL ack on an EE is equally serviceable (13.9's state matching:
/// NORMAL matches only NORMAL).
#[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:?}"),
    }
}

/// Table 13-10: an EE with `Event_Detection_Enable` FALSE falls under the
/// existing-object case lacking event-generation configuration — the ack fails
/// OBJECT / NO_ALARM_CONFIGURED, and the initial-condition
/// `Acked_Transitions` it must hold (Clause 12.12) is untouched.
#[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:?}"),
    }
}