use std::sync::{Arc, Mutex};
use bacnet_types::enums::{
ErrorClass, ErrorCode, LifeSafetyOperation, LifeSafetyState, PropertyIdentifier as P,
Reliability, SilencedState,
};
use bacnet_types::error::Error;
use bacnet_types::primitives::PropertyValue;
use crate::traits::BACnetObject;
use super::*;
fn point() -> LifeSafetyPointObject {
LifeSafetyPointObject::new(1, "LSP-1").unwrap()
}
fn zone() -> LifeSafetyZoneObject {
LifeSafetyZoneObject::new(1, "LSZ-1").unwrap()
}
fn read(object: &dyn BACnetObject, property: P) -> PropertyValue {
object.read_property(property, None).unwrap()
}
fn write(object: &mut dyn BACnetObject, property: P, value: PropertyValue) -> Result<(), Error> {
object.write_property(property, None, value, None)
}
fn set_out_of_service(object: &mut dyn BACnetObject, out_of_service: bool) {
write(
object,
P::OUT_OF_SERVICE,
PropertyValue::Boolean(out_of_service),
)
.unwrap();
}
fn assert_property_error(result: Result<(), Error>, code: ErrorCode) {
assert!(
matches!(result, Err(Error::Protocol { class, code: actual })
if class == ErrorClass::PROPERTY.to_raw() as u32
&& actual == code.to_raw() as u32),
"expected PROPERTY / {code:?}, got {result:?}"
);
}
fn fault(object: &dyn BACnetObject) -> bool {
match read(object, P::STATUS_FLAGS) {
PropertyValue::BitString { data, .. } => data[0] & 0x40 != 0,
other => panic!("expected Status_Flags bits, got {other:?}"),
}
}
fn enumerated(raw: u32) -> PropertyValue {
PropertyValue::Enumerated(raw)
}
fn assert_in_service_writes_are_refused(object: &mut dyn BACnetObject) {
for property in [P::TRACKING_VALUE, P::RELIABILITY] {
let before = read(object, property);
assert_property_error(
write(object, property, enumerated(2)),
ErrorCode::WRITE_ACCESS_DENIED,
);
assert_eq!(read(object, property), before, "{property:?}");
}
assert!(!fault(object));
}
fn assert_out_of_service_writes_are_taken(object: &mut dyn BACnetObject) {
set_out_of_service(object, true);
write(
object,
P::TRACKING_VALUE,
enumerated(LifeSafetyState::ALARM.to_raw()),
)
.unwrap();
assert_eq!(
read(object, P::TRACKING_VALUE),
enumerated(LifeSafetyState::ALARM.to_raw())
);
write(
object,
P::RELIABILITY,
enumerated(Reliability::NO_SENSOR.to_raw()),
)
.unwrap();
assert_eq!(
read(object, P::RELIABILITY),
enumerated(Reliability::NO_SENSOR.to_raw())
);
assert!(fault(object), "a simulated fault sets FAULT");
for (property, expected) in [
(P::PRESENT_VALUE, LifeSafetyState::QUIET.to_raw()),
(P::SILENCED, SilencedState::UNSILENCED.to_raw()),
(P::OPERATION_EXPECTED, LifeSafetyOperation::NONE.to_raw()),
] {
assert_eq!(read(object, property), enumerated(expected), "{property:?}");
}
}
#[test]
fn point_and_zone_refuse_tracking_value_and_reliability_writes_in_service() {
assert_in_service_writes_are_refused(&mut point());
assert_in_service_writes_are_refused(&mut zone());
}
#[test]
fn point_and_zone_take_tracking_value_and_reliability_writes_out_of_service() {
assert_out_of_service_writes_are_taken(&mut point());
assert_out_of_service_writes_are_taken(&mut zone());
}
fn assert_values_are_checked(object: &mut dyn BACnetObject) {
set_out_of_service(object, true);
write(
object,
P::TRACKING_VALUE,
enumerated(LifeSafetyState::PRE_ALARM.to_raw()),
)
.unwrap();
for raw in [35, 255, 65_536] {
assert_property_error(
write(object, P::TRACKING_VALUE, enumerated(raw)),
ErrorCode::VALUE_OUT_OF_RANGE,
);
}
for wrong in [PropertyValue::Unsigned(1), PropertyValue::Null] {
assert_property_error(
write(object, P::TRACKING_VALUE, wrong.clone()),
ErrorCode::INVALID_DATA_TYPE,
);
assert_property_error(
write(object, P::RELIABILITY, wrong),
ErrorCode::INVALID_DATA_TYPE,
);
}
assert_eq!(
read(object, P::TRACKING_VALUE),
enumerated(LifeSafetyState::PRE_ALARM.to_raw())
);
for raw in [11, 26, 63, 65_536] {
assert_property_error(
write(object, P::RELIABILITY, enumerated(raw)),
ErrorCode::VALUE_OUT_OF_RANGE,
);
}
assert_eq!(
read(object, P::RELIABILITY),
enumerated(Reliability::NO_FAULT_DETECTED.to_raw())
);
for raw in [256, 65_535] {
write(object, P::TRACKING_VALUE, enumerated(raw)).unwrap();
assert_eq!(read(object, P::TRACKING_VALUE), enumerated(raw));
}
}
#[test]
fn simulated_values_outside_their_datatypes_are_refused_unchanged() {
assert_values_are_checked(&mut point());
assert_values_are_checked(&mut zone());
}
#[test]
fn return_to_service_serves_the_device_values_again() {
let mut point = point();
point.set_tracking_value(LifeSafetyState::PRE_ALARM);
point
.set_reliability_internal(Reliability::OVER_RANGE)
.unwrap();
set_out_of_service(&mut point, true);
write(
&mut point,
P::TRACKING_VALUE,
enumerated(LifeSafetyState::ALARM.to_raw()),
)
.unwrap();
write(
&mut point,
P::RELIABILITY,
enumerated(Reliability::NO_SENSOR.to_raw()),
)
.unwrap();
point.set_tracking_value(LifeSafetyState::FAULT);
assert_property_error(
point.set_reliability_internal(Reliability::NO_FAULT_DETECTED),
ErrorCode::WRITE_ACCESS_DENIED,
);
assert_eq!(
read(&point, P::TRACKING_VALUE),
enumerated(LifeSafetyState::ALARM.to_raw())
);
set_out_of_service(&mut point, false);
assert_eq!(
read(&point, P::TRACKING_VALUE),
enumerated(LifeSafetyState::FAULT.to_raw())
);
assert_eq!(
read(&point, P::RELIABILITY),
enumerated(Reliability::OVER_RANGE.to_raw())
);
let mut zone = zone();
zone.set_tracking_value(LifeSafetyState::ALARM);
set_out_of_service(&mut zone, true);
assert_eq!(
read(&zone, P::TRACKING_VALUE),
enumerated(LifeSafetyState::ALARM.to_raw())
);
write(
&mut zone,
P::TRACKING_VALUE,
enumerated(LifeSafetyState::QUIET.to_raw()),
)
.unwrap();
write(&mut zone, P::OUT_OF_SERVICE, PropertyValue::Null).unwrap();
set_out_of_service(&mut zone, true);
assert_eq!(
read(&zone, P::TRACKING_VALUE),
enumerated(LifeSafetyState::QUIET.to_raw())
);
set_out_of_service(&mut zone, false);
assert_eq!(
read(&zone, P::TRACKING_VALUE),
enumerated(LifeSafetyState::ALARM.to_raw())
);
zone.set_tracking_value(LifeSafetyState::PRE_ALARM);
assert_eq!(
read(&zone, P::TRACKING_VALUE),
enumerated(LifeSafetyState::PRE_ALARM.to_raw())
);
}
#[test]
fn reset_out_of_service_sees_the_simulation_and_holds_a_committed_tracking_value() {
let seen = Arc::new(Mutex::new(Vec::new()));
let point_seen = Arc::clone(&seen);
let mut point = point().with_reset_executor(Arc::new(move |context| {
point_seen.lock().unwrap().push(context.tracking_value);
Ok(LifeSafetyPointResetCommit {
present_value: Some(LifeSafetyState::QUIET),
tracking_value: Some(LifeSafetyState::QUIET),
silenced: Some(SilencedState::UNSILENCED),
})
}));
let zone_seen = Arc::clone(&seen);
let mut zone = zone().with_reset_executor(Arc::new(move |context| {
zone_seen.lock().unwrap().push(context.tracking_value);
Ok(LifeSafetyZoneResetCommit {
present_value: Some(LifeSafetyState::QUIET),
tracking_value: Some(LifeSafetyState::QUIET),
silenced: Some(SilencedState::UNSILENCED),
})
}));
point.set_present_value(LifeSafetyState::ALARM);
point.set_silenced(SilencedState::ALL_SILENCED);
zone.set_present_value(LifeSafetyState::ALARM);
zone.set_silenced(SilencedState::ALL_SILENCED);
for object in [
&mut point as &mut dyn BACnetObject,
&mut zone as &mut dyn BACnetObject,
] {
set_out_of_service(object, true);
write(
object,
P::TRACKING_VALUE,
enumerated(LifeSafetyState::ALARM.to_raw()),
)
.unwrap();
object
.set_life_safety_operation_expected_internal(LifeSafetyOperation::RESET)
.unwrap();
let outcome = object
.apply_life_safety_operation(LifeSafetyOperation::RESET)
.unwrap();
assert_eq!(
outcome.changed_properties,
vec![P::PRESENT_VALUE, P::SILENCED, P::OPERATION_EXPECTED]
);
assert_eq!(
read(object, P::TRACKING_VALUE),
enumerated(LifeSafetyState::ALARM.to_raw())
);
set_out_of_service(object, false);
assert_eq!(
read(object, P::TRACKING_VALUE),
enumerated(LifeSafetyState::QUIET.to_raw()),
"the committed value is served from the return to service"
);
}
assert_eq!(
*seen.lock().unwrap(),
vec![LifeSafetyState::ALARM, LifeSafetyState::ALARM]
);
}