bacnet-objects 0.13.0

BACnet object model: traits, database, and standard object types
Documentation
use super::*;
use crate::command_source::CommandOrigin;
use bacnet_encoding::constructed::decode_value_source;
use bacnet_encoding::primitives::decode_timestamp_choice;
use bacnet_types::constructed::{BACnetDeviceObjectReference, BACnetValueSource};
use bacnet_types::enums::{EventState, EventType, Reliability};
use bacnet_types::primitives::{BACnetTimeStamp, ObjectIdentifier};

fn origin(device: u32) -> CommandOrigin {
    CommandOrigin::Local {
        owner_device: ObjectIdentifier::new(ObjectType::DEVICE, device).unwrap(),
        initiating_object: None,
    }
}

pub(super) fn values(kind: ObjectType) -> [PropertyValue; 3] {
    match kind {
        ObjectType::ANALOG_OUTPUT => [
            PropertyValue::Real(2.5),
            PropertyValue::Real(3.5),
            PropertyValue::Real(0.0),
        ],
        ObjectType::BINARY_OUTPUT => [
            PropertyValue::Enumerated(1),
            PropertyValue::Enumerated(0),
            PropertyValue::Enumerated(0),
        ],
        ObjectType::MULTI_STATE_OUTPUT => [
            PropertyValue::Unsigned(2),
            PropertyValue::Unsigned(3),
            PropertyValue::Unsigned(1),
        ],
        _ => unreachable!(),
    }
}

pub(super) fn state(object: &dyn ProfileOutput) -> Vec<(P, PropertyValue)> {
    [
        P::PRESENT_VALUE,
        P::PRIORITY_ARRAY,
        P::CURRENT_COMMAND_PRIORITY,
        P::VALUE_SOURCE,
        P::VALUE_SOURCE_ARRAY,
        P::LAST_COMMAND_TIME,
        P::RELINQUISH_DEFAULT,
        P::FEEDBACK_VALUE,
        P::STATUS_FLAGS,
        P::RELIABILITY,
        P::EVENT_STATE,
        P::EVENT_TIME_STAMPS,
        P::EVENT_MESSAGE_TEXTS,
        P::ACKED_TRANSITIONS,
    ]
    .into_iter()
    .filter(|p| object.property_list().contains(p))
    .map(|p| (p, object.read_property(p, None).unwrap()))
    .collect()
}

fn projected(object: &dyn ProfileOutput, priority: Option<u8>, device: Option<u32>, time: u16) {
    assert_eq!(
        object
            .read_property(P::CURRENT_COMMAND_PRIORITY, None)
            .unwrap(),
        priority.map_or(PropertyValue::Null, |p| PropertyValue::Unsigned(p.into()))
    );
    let PropertyValue::ApplicationData(bytes) =
        object.read_property(P::VALUE_SOURCE, None).unwrap()
    else {
        panic!("encoded source")
    };
    let (source, used) = decode_value_source(&bytes, 0).unwrap();
    assert_eq!(used, bytes.len());
    assert_eq!(
        source,
        device.map_or(BACnetValueSource::None, |n| {
            BACnetValueSource::Object(BACnetDeviceObjectReference {
                device_identifier: None,
                object_identifier: ObjectIdentifier::new(ObjectType::DEVICE, n).unwrap(),
            })
        })
    );
    let PropertyValue::ApplicationData(bytes) =
        object.read_property(P::LAST_COMMAND_TIME, None).unwrap()
    else {
        panic!("encoded timestamp")
    };
    let (timestamp, used) = decode_timestamp_choice(&bytes, 0).unwrap();
    assert_eq!(used, bytes.len());
    assert_eq!(timestamp, BACnetTimeStamp::SequenceNumber(time));
}

fn tags_leave_state(object: &mut dyn ProfileOutput) {
    let before = state(object);
    object
        .write_property_from(P::TAGS, None, framed(&tags("written")), Some(4), &origin(2))
        .unwrap();
    assert_eq!(state(object), before);
    assert_eq!(
        object.read_property(P::TAGS, Some(1)).unwrap(),
        framed(&tags("written"))
    );
}

#[test]
fn tags_do_not_steal_priorities_source_correction_or_command_time() {
    for kind in KINDS {
        for persistent in [false, true] {
            let storage = Arc::new(Memory::default());
            let mut profiled = object(kind, persistent.then(|| storage.clone())).unwrap();
            let mut control = object(kind, None).unwrap();
            profiled.provision(optional(7)).unwrap();
            let [a, b, default] = values(kind);
            let mut priority_array = vec![PropertyValue::Null; 16];
            for (step, priority, value, writer, active, source, expected) in [
                (1, 8, a.clone(), 1, Some(8), Some(1), a.clone()),
                (2, 4, b, 2, Some(4), Some(2), values(kind)[1].clone()),
                (3, 4, PropertyValue::Null, 2, Some(8), Some(1), a.clone()),
                (4, 8, PropertyValue::Null, 1, None, None, default),
            ] {
                for output in [&mut profiled, &mut control] {
                    output
                        .write_property_from(
                            P::PRESENT_VALUE,
                            None,
                            value.clone(),
                            Some(priority),
                            &origin(writer),
                        )
                        .unwrap();
                }
                priority_array[usize::from(priority - 1)] = value;
                assert_eq!(
                    profiled.read_property(P::PRESENT_VALUE, None).unwrap(),
                    expected
                );
                assert_eq!(
                    profiled.read_property(P::PRIORITY_ARRAY, None).unwrap(),
                    PropertyValue::List(priority_array.clone())
                );
                projected(profiled.as_ref(), active, source, step);
                tags_leave_state(profiled.as_mut());
                assert_eq!(state(profiled.as_ref()), state(control.as_ref()));
                if step == 1 {
                    let claim = profiled.read_property(P::VALUE_SOURCE, None).unwrap();
                    assert_code(
                        profiled.write_property_from(
                            P::VALUE_SOURCE,
                            None,
                            claim.clone(),
                            Some(8),
                            &origin(2),
                        ),
                        ErrorCode::WRITE_ACCESS_DENIED,
                    );
                    profiled
                        .write_property_from(P::VALUE_SOURCE, None, claim, Some(8), &origin(1))
                        .unwrap();
                    assert_eq!(state(profiled.as_ref()), state(control.as_ref()));
                }
            }
            for output in [&mut profiled, &mut control] {
                output
                    .write_property(P::RELINQUISH_DEFAULT, None, a.clone(), None)
                    .unwrap();
            }
            assert_eq!(profiled.read_property(P::PRESENT_VALUE, None).unwrap(), a);
            projected(profiled.as_ref(), None, None, 4);
            assert_eq!(state(profiled.as_ref()), state(control.as_ref()));
            let before = state(profiled.as_ref());
            assert_code(
                profiled.write_property(P::PRESENT_VALUE, None, a.clone(), Some(8)),
                ErrorCode::WRITE_ACCESS_DENIED,
            );
            assert_code(
                profiled.write_property(P::VALUE_SOURCE, None, PropertyValue::Null, Some(8)),
                ErrorCode::WRITE_ACCESS_DENIED,
            );
            assert_code(
                profiled.write_property_from(
                    P::PRESENT_VALUE,
                    None,
                    PropertyValue::Boolean(true),
                    Some(8),
                    &origin(1),
                ),
                ErrorCode::INVALID_DATA_TYPE,
            );
            let invalid = match kind {
                ObjectType::ANALOG_OUTPUT => PropertyValue::Real(f32::NAN),
                ObjectType::BINARY_OUTPUT => PropertyValue::Enumerated(2),
                _ => PropertyValue::Unsigned(0),
            };
            assert_code(
                profiled.write_property_from(P::PRESENT_VALUE, None, invalid, Some(8), &origin(1)),
                ErrorCode::VALUE_OUT_OF_RANGE,
            );
            assert_eq!(state(profiled.as_ref()), before);
            assert_eq!(storage.attempts(), if persistent { 4 } else { 0 });
        }
    }
}

#[test]
fn profile_writes_preserve_command_failure_and_out_of_range_transitions() {
    for kind in KINDS {
        let mut profiled = object(kind, None).unwrap();
        let mut control = object(kind, None).unwrap();
        profiled.provision(optional(7)).unwrap();
        for output in [&mut profiled, &mut control] {
            output
                .write_property(
                    P::EVENT_DETECTION_ENABLE,
                    None,
                    PropertyValue::Boolean(true),
                    None,
                )
                .unwrap();
            output
                .write_property(P::TIME_DELAY, None, PropertyValue::Unsigned(0), None)
                .unwrap();
            if kind == ObjectType::ANALOG_OUTPUT {
                for (p, v) in [
                    (P::HIGH_LIMIT, 1.0),
                    (P::LOW_LIMIT, -1.0),
                    (P::DEADBAND, 0.0),
                ] {
                    output
                        .write_property(p, None, PropertyValue::Real(v), None)
                        .unwrap();
                }
                output
                    .write_property(
                        P::LIMIT_ENABLE,
                        None,
                        PropertyValue::BitString {
                            unused_bits: 6,
                            data: vec![0xc0],
                        },
                        None,
                    )
                    .unwrap();
            }
            output
                .write_property_from(
                    P::PRESENT_VALUE,
                    None,
                    values(kind)[0].clone(),
                    Some(8),
                    &origin(1),
                )
                .unwrap();
        }
        tags_leave_state(profiled.as_mut());
        let target = if kind == ObjectType::ANALOG_OUTPUT {
            EventState::HIGH_LIMIT
        } else {
            EventState::OFFNORMAL
        };
        let algorithm = if kind == ObjectType::ANALOG_OUTPUT {
            EventType::OUT_OF_RANGE
        } else {
            EventType::COMMAND_FAILURE
        };
        for output in [&mut profiled, &mut control] {
            let proposal = output.evaluate_intrinsic_reporting().unwrap();
            assert_eq!(proposal.change.to, target);
            assert_eq!(proposal.event_type, algorithm);
            crate::event::commit_test_proposal(output.as_mut(), proposal);
        }
        assert_eq!(state(profiled.as_ref()), state(control.as_ref()));
        tags_leave_state(profiled.as_mut());
        for output in [&mut profiled, &mut control] {
            if kind == ObjectType::ANALOG_OUTPUT {
                output
                    .write_property_from(
                        P::PRESENT_VALUE,
                        None,
                        PropertyValue::Real(0.0),
                        Some(8),
                        &origin(1),
                    )
                    .unwrap();
            } else {
                output
                    .write_property(P::FEEDBACK_VALUE, None, values(kind)[0].clone(), None)
                    .unwrap();
            }
            let proposal = output.evaluate_intrinsic_reporting().unwrap();
            assert_eq!(proposal.change.from, target);
            assert_eq!(proposal.change.to, EventState::NORMAL);
            crate::event::commit_test_proposal(output.as_mut(), proposal);
        }
        assert_eq!(state(profiled.as_ref()), state(control.as_ref()));
        if kind == ObjectType::MULTI_STATE_OUTPUT {
            profiled
                .write_property(P::FEEDBACK_VALUE, None, PropertyValue::Unsigned(7), None)
                .unwrap();
            tags_leave_state(profiled.as_mut());
            assert_eq!(
                profiled.read_property(P::RELIABILITY, None).unwrap(),
                PropertyValue::Enumerated(Reliability::CONFIGURATION_ERROR.to_raw())
            );
        }
    }
}

#[test]
fn profile_writes_preserve_output_engineering_polarity_and_state_configuration() {
    let mut analog = AnalogOutputObject::new(1, "AO", 62).unwrap();
    analog.set_min_pres_value(-5.0);
    analog.set_max_pres_value(5.0);
    let mut binary = BinaryOutputObject::new(1, "BO").unwrap();
    binary
        .write_property(
            P::ACTIVE_TEXT,
            None,
            PropertyValue::CharacterString("Run".into()),
            None,
        )
        .unwrap();
    let mut multistate = MultiStateOutputObject::new(1, "MSO", 3).unwrap();
    multistate.set_number_of_states(5).unwrap();
    for (mut output, rows) in [
        (
            Box::new(analog) as Box<dyn ProfileOutput>,
            vec![
                P::MIN_PRES_VALUE,
                P::MAX_PRES_VALUE,
                P::UNITS,
                P::COV_INCREMENT,
            ],
        ),
        (
            Box::new(binary),
            vec![P::POLARITY, P::ACTIVE_TEXT, P::INACTIVE_TEXT],
        ),
        (
            Box::new(multistate),
            vec![P::NUMBER_OF_STATES, P::STATE_TEXT],
        ),
    ] {
        let before: Vec<_> = rows
            .iter()
            .map(|p| output.read_property(*p, None).unwrap())
            .collect();
        output.provision(optional(7)).unwrap();
        tags_leave_state(output.as_mut());
        assert_eq!(
            rows.iter()
                .map(|p| output.read_property(*p, None).unwrap())
                .collect::<Vec<_>>(),
            before
        );
    }
}