bacnet-objects 0.13.0

BACnet object model: traits, database, and standard object types
Documentation
use super::*;

use bacnet_types::primitives::{Date, Time};

use crate::event::{EventStateChange, EventTransitionCommit};

const OFFNORMAL: EventTransitionBits = EventTransitionBits::TO_OFFNORMAL;
const FAULT: EventTransitionBits = EventTransitionBits::TO_FAULT;
const NORMAL: EventTransitionBits = EventTransitionBits::TO_NORMAL;
const ALL: EventTransitionBits = EventTransitionBits::all();

fn sequence(value: u16) -> BACnetTimeStamp {
    BACnetTimeStamp::SequenceNumber(value)
}

fn time(hour: u8) -> BACnetTimeStamp {
    BACnetTimeStamp::Time(Time {
        hour,
        minute: 2,
        second: 3,
        hundredths: 4,
    })
}

fn date_time(day: u8) -> BACnetTimeStamp {
    BACnetTimeStamp::DateTime {
        date: Date {
            year: 126,
            month: 9,
            day,
            day_of_week: 3,
        },
        time: Time {
            hour: 5,
            minute: 6,
            second: 7,
            hundredths: 8,
        },
    }
}

fn commit(
    state: &mut EventState,
    acked: &mut EventTransitionBits,
    history: &mut EventHistory,
    to: EventState,
    timestamp: BACnetTimeStamp,
) {
    let from = *state;
    EventTransitionState::new(state, acked, history)
        .commit(EventTransitionCommit {
            change: EventStateChange { from, to },
            coordinate: EventTransition::for_target_state(to),
            ack_required: true,
            timestamp,
            message_text: None,
        })
        .unwrap();
}

fn assert_service_error(result: Result<(), Error>, expected: ErrorCode) {
    assert!(matches!(
        result,
        Err(Error::Protocol { class, code })
            if class == ErrorClass::SERVICES.to_raw() as u32
                && code == expected.to_raw() as u32
    ));
}

#[test]
fn exact_timestamp_choices_acknowledge_prior_coordinates_one_bit_at_a_time() {
    let mut state = EventState::NORMAL;
    let mut acked = ALL;
    let mut history = EventHistory::default();
    let stamps = [time(1), sequence(22), date_time(2)];

    commit(
        &mut state,
        &mut acked,
        &mut history,
        EventState::HIGH_LIMIT,
        stamps[0].clone(),
    );
    commit(
        &mut state,
        &mut acked,
        &mut history,
        EventState::FAULT,
        stamps[1].clone(),
    );
    commit(
        &mut state,
        &mut acked,
        &mut history,
        EventState::NORMAL,
        stamps[2].clone(),
    );
    assert_eq!(state, EventState::NORMAL);
    assert_eq!(acked, EventTransitionBits::empty());

    let history_before = history.clone();
    for (requested_state, stamp, expected) in [
        (EventState::HIGH_LIMIT, &stamps[0], OFFNORMAL),
        (EventState::FAULT, &stamps[1], OFFNORMAL | FAULT),
        (EventState::NORMAL, &stamps[2], ALL),
    ] {
        history
            .acknowledge_correlated(&mut acked, requested_state, stamp)
            .unwrap();
        assert_eq!(acked, expected);
        assert_eq!(history, history_before);
    }
}

#[test]
fn latest_same_coordinate_timestamp_replaces_the_older_timestamp() {
    let mut state = EventState::NORMAL;
    let mut acked = ALL;
    let mut history = EventHistory::default();
    commit(
        &mut state,
        &mut acked,
        &mut history,
        EventState::HIGH_LIMIT,
        sequence(1),
    );
    commit(
        &mut state,
        &mut acked,
        &mut history,
        EventState::LOW_LIMIT,
        sequence(2),
    );
    let before = (acked, history.clone());

    assert_service_error(
        history.acknowledge_correlated(&mut acked, EventState::LOW_LIMIT, &sequence(1)),
        ErrorCode::INVALID_TIME_STAMP,
    );
    assert_eq!((acked, history.clone()), before);
    assert_service_error(
        history.acknowledge_correlated(&mut acked, EventState::HIGH_LIMIT, &sequence(1)),
        ErrorCode::INVALID_EVENT_STATE,
    );
    assert_eq!((acked, history.clone()), before);
    history
        .acknowledge_correlated(&mut acked, EventState::LOW_LIMIT, &sequence(2))
        .unwrap();
    assert_eq!(acked, before.0 | OFFNORMAL);
}

#[test]
fn request_side_offnormal_wildcard_matches_every_residual_offnormal_state() {
    for stored in [
        EventState::HIGH_LIMIT,
        EventState::LOW_LIMIT,
        EventState::LIFE_SAFETY_ALARM,
        EventState::from_raw(65_535),
    ] {
        let mut state = EventState::NORMAL;
        let mut acked = FAULT | NORMAL;
        let mut history = EventHistory::default();
        commit(&mut state, &mut acked, &mut history, stored, sequence(9));

        let detailed = history
            .acknowledge_correlated_detailed(&mut acked, EventState::OFFNORMAL, &sequence(9))
            .unwrap()
            .expect("committed transitions retain their exact From State");
        assert_eq!(acked, ALL, "stored state {stored:?}");
        assert_eq!(
            detailed,
            EventStateChange {
                from: EventState::NORMAL,
                to: stored,
            },
            "the request wildcard must not replace exact object history"
        );
    }
}

#[test]
fn missing_optional_from_state_preserves_a_correlated_acknowledgment() {
    let mut history = EventHistory::default();
    history.time_stamps[0] = sequence(9);
    history.original_to_states[0] = Some(EventState::HIGH_LIMIT);
    let mut acked = FAULT | NORMAL;

    let detailed = history
        .acknowledge_correlated_detailed(&mut acked, EventState::OFFNORMAL, &sequence(9))
        .unwrap();

    assert_eq!(detailed, None);
    assert_eq!(acked, ALL);
}

#[test]
fn concrete_state_matching_is_exact_and_precedes_timestamp_validation() {
    for (stored, requested) in [
        (EventState::LOW_LIMIT, EventState::HIGH_LIMIT),
        (EventState::OFFNORMAL, EventState::HIGH_LIMIT),
        (EventState::from_raw(60_001), EventState::from_raw(60_002)),
    ] {
        let mut state = EventState::NORMAL;
        let mut acked = OFFNORMAL | NORMAL;
        let mut history = EventHistory::default();
        commit(&mut state, &mut acked, &mut history, stored, sequence(12));
        let before = (acked, history.clone());

        assert_service_error(
            history.acknowledge_correlated(&mut acked, requested, &sequence(99)),
            ErrorCode::INVALID_EVENT_STATE,
        );
        assert_eq!((acked, history.clone()), before);
    }
}

#[test]
fn exact_generic_and_proprietary_states_match_their_committed_identity() {
    for stored in [EventState::OFFNORMAL, EventState::from_raw(60_001)] {
        let mut state = EventState::NORMAL;
        let mut acked = FAULT | NORMAL;
        let mut history = EventHistory::default();
        commit(&mut state, &mut acked, &mut history, stored, sequence(14));

        history
            .acknowledge_correlated(&mut acked, stored, &sequence(14))
            .unwrap();
        assert_eq!(acked, ALL);
    }
}

#[test]
fn uninitialized_sequence_zero_slot_is_not_a_committed_transition() {
    let history = EventHistory::default();
    let mut acked = FAULT;
    let before = (acked, history.clone());

    assert_service_error(
        history.acknowledge_correlated(&mut acked, EventState::HIGH_LIMIT, &sequence(0)),
        ErrorCode::INVALID_TIME_STAMP,
    );
    assert_eq!((acked, history), before);
}

#[test]
fn already_set_bit_is_idempotent_and_preserves_all_history() {
    let mut state = EventState::NORMAL;
    let mut acked = ALL;
    let mut history = EventHistory::default();
    commit(
        &mut state,
        &mut acked,
        &mut history,
        EventState::HIGH_LIMIT,
        sequence(7),
    );
    acked |= OFFNORMAL;
    let before = (state, acked, history.clone());

    history
        .acknowledge_correlated(&mut acked, EventState::HIGH_LIMIT, &sequence(7))
        .unwrap();
    assert_eq!((state, acked, history), before);
}