bacnet-objects 0.13.0

BACnet object model: traits, database, and standard object types
Documentation
use crate::analog::{AnalogInputObject, AnalogOutputObject, AnalogValueObject};
use crate::binary::{BinaryInputObject, BinaryOutputObject, BinaryValueObject};
use crate::multistate::{MultiStateInputObject, MultiStateOutputObject, MultiStateValueObject};
use crate::property_metadata::{PropertyConformance, PropertyWriteCapability};
use crate::traits::{BACnetObject, ReliabilityEvaluation};
use bacnet_types::enums::{EventState, PropertyIdentifier, Reliability};
use bacnet_types::primitives::PropertyValue;

const INHIBIT: PropertyIdentifier = PropertyIdentifier::RELIABILITY_EVALUATION_INHIBIT;

fn nine_objects() -> Vec<(&'static str, Box<dyn BACnetObject>)> {
    vec![
        (
            "analog-input",
            Box::new(AnalogInputObject::new(1, "AI-1", 62).unwrap()),
        ),
        (
            "analog-output",
            Box::new(AnalogOutputObject::new(1, "AO-1", 62).unwrap()),
        ),
        (
            "analog-value",
            Box::new(AnalogValueObject::new(1, "AV-1", 62).unwrap()),
        ),
        (
            "binary-input",
            Box::new(BinaryInputObject::new(1, "BI-1").unwrap()),
        ),
        (
            "binary-output",
            Box::new(BinaryOutputObject::new(1, "BO-1").unwrap()),
        ),
        (
            "binary-value",
            Box::new(BinaryValueObject::new(1, "BV-1").unwrap()),
        ),
        (
            "multistate-input",
            Box::new(MultiStateInputObject::new(1, "MSI-1", 3).unwrap()),
        ),
        (
            "multistate-output",
            Box::new(MultiStateOutputObject::new(1, "MSO-1", 3).unwrap()),
        ),
        (
            "multistate-value",
            Box::new(MultiStateValueObject::new(1, "MSV-1", 3).unwrap()),
        ),
    ]
}

fn read(object: &dyn BACnetObject, property: PropertyIdentifier) -> PropertyValue {
    object.read_property(property, None).unwrap()
}

fn read_reliability(object: &dyn BACnetObject) -> u32 {
    match read(object, PropertyIdentifier::RELIABILITY) {
        PropertyValue::Enumerated(value) => value,
        other => panic!("expected Enumerated Reliability, got {other:?}"),
    }
}

fn write_bool(object: &mut dyn BACnetObject, property: PropertyIdentifier, value: bool) {
    object
        .write_property(property, None, PropertyValue::Boolean(value), None)
        .unwrap();
}

fn write_client_reliability(object: &mut dyn BACnetObject, value: u32) {
    object
        .write_property(
            PropertyIdentifier::RELIABILITY,
            None,
            PropertyValue::Enumerated(value),
            None,
        )
        .unwrap();
}

#[test]
fn all_nine_expose_default_false_boolean_writable_property_and_pics_truth() {
    for (name, mut object) in nine_objects() {
        assert_eq!(
            read(&*object, INHIBIT),
            PropertyValue::Boolean(false),
            "{name}"
        );
        assert!(object.property_list().contains(&INHIBIT), "{name}");
        assert!(
            object.is_writable_property(INHIBIT),
            "{name} PICS writability must match dispatch"
        );
        let wire_list = read(&*object, PropertyIdentifier::PROPERTY_LIST);
        assert!(
            matches!(wire_list, PropertyValue::List(ref values)
                if values.contains(&PropertyValue::Enumerated(INHIBIT.to_raw()))),
            "{name} wire Property_List omitted inhibit"
        );

        assert!(object
            .write_property(INHIBIT, None, PropertyValue::Unsigned(1), None)
            .is_err());
        assert_eq!(
            read(&*object, INHIBIT),
            PropertyValue::Boolean(false),
            "{name}"
        );
        write_bool(&mut *object, INHIBIT, true);
        assert_eq!(
            read(&*object, INHIBIT),
            PropertyValue::Boolean(true),
            "{name}"
        );
        write_bool(&mut *object, INHIBIT, false);
        assert_eq!(
            read(&*object, INHIBIT),
            PropertyValue::Boolean(false),
            "{name}"
        );
    }

    let binary_input = BinaryInputObject::new(2, "BI-metadata").unwrap();
    let row = binary_input
        .property_metadata()
        .iter()
        .find(|row| row.property_identifier == INHIBIT)
        .copied()
        .expect("Binary Input canonical metadata must include inhibit");
    assert_eq!(row.conformance, PropertyConformance::Optional);
    assert_eq!(row.write_capability, PropertyWriteCapability::Always);
}

#[test]
fn true_normalizes_existing_fault_and_false_does_not_restore_it_on_all_nine() {
    for (name, mut object) in nine_objects() {
        object
            .set_reliability_internal(Reliability::NO_SENSOR)
            .unwrap();
        write_bool(&mut *object, INHIBIT, true);
        assert_eq!(
            read_reliability(&*object),
            Reliability::NO_FAULT_DETECTED.to_raw(),
            "{name} froze the prior fault"
        );
        assert!(object
            .set_reliability_internal(Reliability::OVER_RANGE)
            .is_err());
        assert_eq!(
            read_reliability(&*object),
            Reliability::NO_FAULT_DETECTED.to_raw(),
            "{name} internal setter violated inhibit"
        );
        write_bool(&mut *object, INHIBIT, false);
        assert_eq!(
            read_reliability(&*object),
            Reliability::NO_FAULT_DETECTED.to_raw(),
            "{name} restored a pre-inhibit fault"
        );
    }
}

#[test]
fn inhibited_ai_and_av_range_faults_skip_and_reenable_current_evaluation() {
    let mut ai = AnalogInputObject::new(1, "AI-range", 62).unwrap();
    ai.configure_fault_out_of_range(10.0, 20.0).unwrap();
    ai.set_present_value(21.0);
    assert!(matches!(
        ai.evaluate_reliability_internal().unwrap(),
        ReliabilityEvaluation::Changed { new_reliability, .. }
            if new_reliability == Reliability::OVER_RANGE
    ));
    write_bool(&mut ai, INHIBIT, true);
    assert_eq!(
        read_reliability(&ai),
        Reliability::NO_FAULT_DETECTED.to_raw()
    );
    assert_eq!(
        ai.evaluate_reliability_internal().unwrap(),
        ReliabilityEvaluation::Unchanged
    );
    write_bool(&mut ai, INHIBIT, false);
    assert_eq!(
        ai.evaluate_reliability_internal().unwrap(),
        ReliabilityEvaluation::Changed {
            old_reliability: Reliability::NO_FAULT_DETECTED,
            new_reliability: Reliability::OVER_RANGE,
        },
        "inhibit must not clear the range detector's ownership"
    );

    let mut av = AnalogValueObject::new(1, "AV-range", 62).unwrap();
    av.configure_fault_out_of_range(10.0, 20.0).unwrap();
    av.set_relinquish_default(9.0).unwrap();
    write_bool(&mut av, INHIBIT, true);
    assert_eq!(
        av.evaluate_reliability_internal().unwrap(),
        ReliabilityEvaluation::Unchanged
    );
    assert_eq!(
        read_reliability(&av),
        Reliability::NO_FAULT_DETECTED.to_raw()
    );
    write_bool(&mut av, INHIBIT, false);
    assert_eq!(
        av.evaluate_reliability_internal().unwrap(),
        ReliabilityEvaluation::Changed {
            old_reliability: Reliability::NO_FAULT_DETECTED,
            new_reliability: Reliability::UNDER_RANGE,
        }
    );
}

#[test]
fn out_of_service_ordering_without_client_override_always_exposes_zero() {
    for inhibit_first in [false, true] {
        let mut object = BinaryInputObject::new(1, "BI-order").unwrap();
        object
            .set_reliability_internal(Reliability::OVER_RANGE)
            .unwrap();
        if inhibit_first {
            write_bool(&mut object, INHIBIT, true);
        }
        write_bool(&mut object, PropertyIdentifier::OUT_OF_SERVICE, true);
        if !inhibit_first {
            write_bool(&mut object, INHIBIT, true);
        }
        assert_eq!(
            read_reliability(&object),
            Reliability::NO_FAULT_DETECTED.to_raw()
        );
        write_bool(&mut object, PropertyIdentifier::OUT_OF_SERVICE, false);
        assert_eq!(
            read_reliability(&object),
            Reliability::NO_FAULT_DETECTED.to_raw()
        );
        write_bool(&mut object, INHIBIT, false);
        assert_eq!(
            read_reliability(&object),
            Reliability::NO_FAULT_DETECTED.to_raw()
        );
    }
}

#[test]
fn accepted_oos_alternate_is_owned_even_when_same_value_and_restores_by_exit_mode() {
    let mut same_value = BinaryInputObject::new(1, "BI-same").unwrap();
    same_value
        .set_reliability_internal(Reliability::NO_SENSOR)
        .unwrap();
    write_bool(&mut same_value, PropertyIdentifier::OUT_OF_SERVICE, true);
    write_client_reliability(&mut same_value, Reliability::NO_SENSOR.to_raw());
    write_bool(&mut same_value, INHIBIT, true);
    write_bool(&mut same_value, PropertyIdentifier::OUT_OF_SERVICE, true);
    assert_eq!(
        read_reliability(&same_value),
        Reliability::NO_SENSOR.to_raw()
    );
    write_bool(&mut same_value, INHIBIT, false);
    write_bool(&mut same_value, PropertyIdentifier::OUT_OF_SERVICE, false);
    assert_eq!(
        read_reliability(&same_value),
        Reliability::NO_SENSOR.to_raw(),
        "non-inhibited exit must restore the evaluated value saved on entry"
    );

    let mut inhibited_exit = BinaryInputObject::new(2, "BI-inhibited-exit").unwrap();
    inhibited_exit
        .set_reliability_internal(Reliability::OVER_RANGE)
        .unwrap();
    write_bool(
        &mut inhibited_exit,
        PropertyIdentifier::OUT_OF_SERVICE,
        true,
    );
    write_client_reliability(&mut inhibited_exit, Reliability::NO_SENSOR.to_raw());
    write_bool(&mut inhibited_exit, INHIBIT, true);
    assert_eq!(
        read_reliability(&inhibited_exit),
        Reliability::NO_SENSOR.to_raw()
    );
    write_bool(
        &mut inhibited_exit,
        PropertyIdentifier::OUT_OF_SERVICE,
        false,
    );
    assert_eq!(
        read_reliability(&inhibited_exit),
        Reliability::NO_FAULT_DETECTED.to_raw()
    );
    write_bool(&mut inhibited_exit, INHIBIT, false);
    assert_eq!(
        read_reliability(&inhibited_exit),
        Reliability::NO_FAULT_DETECTED.to_raw()
    );
}

#[test]
fn rejected_oos_reliability_writes_do_not_create_an_override() {
    for rejected in [PropertyValue::Boolean(true), PropertyValue::Enumerated(11)] {
        let mut object = BinaryInputObject::new(1, "BI-invalid").unwrap();
        object
            .set_reliability_internal(Reliability::OVER_RANGE)
            .unwrap();
        write_bool(&mut object, PropertyIdentifier::OUT_OF_SERVICE, true);
        assert!(object
            .write_property(PropertyIdentifier::RELIABILITY, None, rejected, None)
            .is_err());
        assert_eq!(read_reliability(&object), Reliability::OVER_RANGE.to_raw());
        write_bool(&mut object, INHIBIT, true);
        assert_eq!(
            read_reliability(&object),
            Reliability::NO_FAULT_DETECTED.to_raw(),
            "rejected Reliability write incorrectly established ownership"
        );
    }
}

#[test]
fn repeated_oos_cycles_clear_old_client_ownership() {
    let mut object = BinaryInputObject::new(1, "BI-cycles").unwrap();
    object
        .set_reliability_internal(Reliability::OVER_RANGE)
        .unwrap();
    write_bool(&mut object, PropertyIdentifier::OUT_OF_SERVICE, true);
    write_client_reliability(&mut object, Reliability::NO_SENSOR.to_raw());
    write_bool(&mut object, PropertyIdentifier::OUT_OF_SERVICE, false);
    assert_eq!(read_reliability(&object), Reliability::OVER_RANGE.to_raw());

    write_bool(&mut object, INHIBIT, true);
    for _ in 0..2 {
        write_bool(&mut object, PropertyIdentifier::OUT_OF_SERVICE, true);
        assert_eq!(
            read_reliability(&object),
            Reliability::NO_FAULT_DETECTED.to_raw()
        );
        write_bool(&mut object, PropertyIdentifier::OUT_OF_SERVICE, false);
        assert_eq!(
            read_reliability(&object),
            Reliability::NO_FAULT_DETECTED.to_raw()
        );
    }
}

#[test]
fn inhibit_does_not_change_detection_enable_and_recovery_is_reportable() {
    let mut object = AnalogInputObject::new(1, "AI-report", 62).unwrap();
    object
        .set_reliability_internal(Reliability::NO_SENSOR)
        .unwrap();
    let fault = object
        .evaluate_intrinsic_reporting()
        .expect("nonzero Reliability must propose FAULT");
    crate::event::commit_test_proposal(&mut object, fault);
    assert_eq!(
        read(&object, PropertyIdentifier::EVENT_STATE),
        PropertyValue::Enumerated(EventState::FAULT.to_raw())
    );

    let detection_enable = read(&object, PropertyIdentifier::EVENT_DETECTION_ENABLE);
    write_bool(&mut object, INHIBIT, true);
    assert_eq!(
        read(&object, PropertyIdentifier::EVENT_DETECTION_ENABLE),
        detection_enable
    );
    let recovery = object
        .evaluate_intrinsic_reporting()
        .expect("Reliability normalization must remain visible to intrinsic reporting");
    assert_eq!(recovery.change.from, EventState::FAULT);
    assert_eq!(recovery.change.to, EventState::NORMAL);
}