use super::*;
use bacnet_types::enums::{ErrorClass, ErrorCode};
use PropertyIdentifier as P;
fn read(avg: &AveragingObject, property: P) -> PropertyValue {
avg.read_property(property, None).unwrap()
}
fn write(avg: &mut AveragingObject, property: P, value: PropertyValue) -> Result<(), Error> {
avg.write_property(property, None, value, None)
}
fn statistics(avg: &AveragingObject) -> (u32, u32, u32, PropertyValue, PropertyValue) {
let real = |property| match read(avg, property) {
PropertyValue::Real(v) => v.to_bits(),
other => panic!("{property:?} is a REAL: {other:?}"),
};
(
real(P::MINIMUM_VALUE),
real(P::MAXIMUM_VALUE),
real(P::AVERAGE_VALUE),
read(avg, P::ATTEMPTED_SAMPLES),
read(avg, P::VALID_SAMPLES),
)
}
fn window_of(count: u64, minimum: f32, maximum: f32, average: f32) -> [PropertyValue; 5] {
let count = PropertyValue::Unsigned(count);
[
PropertyValue::Real(minimum),
PropertyValue::Real(maximum),
PropertyValue::Real(average),
count.clone(),
count,
]
}
fn read_all(avg: &AveragingObject) -> [PropertyValue; 5] {
[
read(avg, P::MINIMUM_VALUE),
read(avg, P::MAXIMUM_VALUE),
read(avg, P::AVERAGE_VALUE),
read(avg, P::ATTEMPTED_SAMPLES),
read(avg, P::VALID_SAMPLES),
]
}
fn empty() -> (u32, u32, u32, PropertyValue, PropertyValue) {
(
f32::INFINITY.to_bits(),
f32::NEG_INFINITY.to_bits(),
f32::NAN.to_bits(),
PropertyValue::Unsigned(0),
PropertyValue::Unsigned(0),
)
}
fn assert_error(error: Error, code: ErrorCode) {
assert!(
matches!(error, Error::Protocol { class, code: c }
if class == ErrorClass::PROPERTY.to_raw() as u32 && c == code.to_raw() as u32),
"expected PROPERTY/{code:?}, got {error:?}"
);
}
#[test]
fn averaging_statistics_start_at_infinity_nan_and_negative_infinity() {
let avg = AveragingObject::new(1, "AVG-1").unwrap();
assert_eq!(statistics(&avg), empty());
assert_eq!(
read(&avg, P::MINIMUM_VALUE),
PropertyValue::Real(f32::INFINITY)
);
assert_eq!(
read(&avg, P::MAXIMUM_VALUE),
PropertyValue::Real(f32::NEG_INFINITY)
);
assert_eq!(read(&avg, P::WINDOW_SAMPLES), PropertyValue::Unsigned(15));
assert_eq!(read(&avg, P::WINDOW_INTERVAL), PropertyValue::Unsigned(900));
}
#[test]
fn averaging_window_keeps_only_the_most_recent_samples() {
let mut avg = AveragingObject::new(1, "AVG-1").unwrap();
write(&mut avg, P::WINDOW_SAMPLES, PropertyValue::Unsigned(4)).unwrap();
for value in [10.0, 50.0, 20.0, 30.0] {
avg.add_sample(value).unwrap();
}
assert_eq!(read_all(&avg), window_of(4, 10.0, 50.0, 27.5));
avg.add_sample(40.0).unwrap();
assert_eq!(read_all(&avg), window_of(4, 20.0, 50.0, 35.0));
avg.add_sample(5.0).unwrap();
assert_eq!(read_all(&avg), window_of(4, 5.0, 40.0, 23.75));
}
#[test]
fn averaging_window_average_has_no_drift_over_many_samples() {
let mut avg = AveragingObject::new(1, "AVG-1").unwrap();
for value in 0..1000u16 {
avg.add_sample(f32::from(value)).unwrap();
}
assert_eq!(
read_all(&avg),
window_of(15, 985.0, 999.0, 992.0),
"Attempted_Samples and Valid_Samples stop at Window_Samples"
);
}
#[test]
fn averaging_each_reset_route_discards_the_samples() {
let reference = PropertyValue::ApplicationData(vec![0x0C, 0x00, 0x00, 0x00, 0x03, 0x19, 0x55]);
for (property, value) in [
(P::ATTEMPTED_SAMPLES, PropertyValue::Unsigned(0)),
(P::OBJECT_PROPERTY_REFERENCE, reference),
(
P::OBJECT_PROPERTY_REFERENCE,
PropertyValue::ApplicationData(vec![0x0C, 0x00, 0x3F, 0xFF, 0xFF, 0x19, 0x55]),
),
(P::WINDOW_INTERVAL, PropertyValue::Unsigned(900)),
(P::WINDOW_INTERVAL, PropertyValue::Unsigned(60)),
(P::WINDOW_SAMPLES, PropertyValue::Unsigned(15)),
(P::WINDOW_SAMPLES, PropertyValue::Unsigned(30)),
] {
let mut avg = AveragingObject::new(1, "AVG-1").unwrap();
for sample in [4.0, 8.0, 6.0] {
avg.add_sample(sample).unwrap();
}
assert_ne!(statistics(&avg), empty());
write(&mut avg, property, value.clone())
.unwrap_or_else(|e| panic!("{property:?} = {value:?}: {e:?}"));
assert_eq!(statistics(&avg), empty(), "{property:?} = {value:?}");
assert_eq!(read(&avg, property), value, "the write itself lands");
avg.add_sample(2.0).unwrap();
assert_eq!(read_all(&avg), window_of(1, 2.0, 2.0, 2.0));
}
}
#[test]
fn averaging_description_write_keeps_the_samples() {
let mut avg = AveragingObject::new(1, "AVG-1").unwrap();
avg.add_sample(7.0).unwrap();
let before = statistics(&avg);
write(
&mut avg,
P::DESCRIPTION,
PropertyValue::CharacterString("zone".into()),
)
.unwrap();
assert_eq!(statistics(&avg), before);
}
#[test]
fn averaging_window_writes_refuse_bad_values_and_keep_the_samples() {
let mut avg = AveragingObject::new(1, "AVG-1").unwrap();
write(&mut avg, P::WINDOW_SAMPLES, PropertyValue::Unsigned(20)).unwrap();
write(&mut avg, P::WINDOW_INTERVAL, PropertyValue::Unsigned(600)).unwrap();
avg.add_sample(3.0).unwrap();
avg.add_sample(9.0).unwrap();
let before = statistics(&avg);
for (property, value, code) in [
(
P::WINDOW_SAMPLES,
PropertyValue::Unsigned(0),
ErrorCode::VALUE_OUT_OF_RANGE,
),
(
P::WINDOW_SAMPLES,
PropertyValue::Unsigned(1_441),
ErrorCode::VALUE_OUT_OF_RANGE,
),
(
P::WINDOW_SAMPLES,
PropertyValue::Unsigned(u64::MAX),
ErrorCode::VALUE_OUT_OF_RANGE,
),
(
P::WINDOW_SAMPLES,
PropertyValue::Signed(4),
ErrorCode::INVALID_DATA_TYPE,
),
(
P::WINDOW_INTERVAL,
PropertyValue::Unsigned(0),
ErrorCode::VALUE_OUT_OF_RANGE,
),
(
P::WINDOW_INTERVAL,
PropertyValue::Unsigned(u64::from(u32::MAX) + 1),
ErrorCode::VALUE_OUT_OF_RANGE,
),
(
P::WINDOW_INTERVAL,
PropertyValue::Real(60.0),
ErrorCode::INVALID_DATA_TYPE,
),
(
P::ATTEMPTED_SAMPLES,
PropertyValue::Unsigned(2),
ErrorCode::VALUE_OUT_OF_RANGE,
),
(
P::ATTEMPTED_SAMPLES,
PropertyValue::Null,
ErrorCode::INVALID_DATA_TYPE,
),
] {
let error = write(&mut avg, property, value.clone()).unwrap_err();
assert_error(error, code);
assert_eq!(statistics(&avg), before, "{property:?} = {value:?}");
assert_eq!(read(&avg, P::WINDOW_SAMPLES), PropertyValue::Unsigned(20));
assert_eq!(read(&avg, P::WINDOW_INTERVAL), PropertyValue::Unsigned(600));
}
write(&mut avg, P::WINDOW_SAMPLES, PropertyValue::Unsigned(1_440)).unwrap();
write(
&mut avg,
P::WINDOW_INTERVAL,
PropertyValue::Unsigned(u32::MAX.into()),
)
.unwrap();
assert_eq!(
read(&avg, P::WINDOW_SAMPLES),
PropertyValue::Unsigned(1_440)
);
}