use super::*;
use bacnet_encoding::constructed::{decode_event_log_record, decode_log_record};
use bacnet_objects::analog::AnalogValueObject;
use bacnet_objects::event_log::EventLogObject;
use bacnet_objects::trend::TrendLogObject;
use bacnet_types::bitstring::LogStatus;
use bacnet_types::constructed::{BACnetDeviceObjectPropertyReference, EventLogDatum, LogDatum};
use bacnet_types::error::RemoteError;
use std::sync::Arc;
use PropertyIdentifier as P;
use bacnet_types::enums::EngineeringUnits;
fn tl1() -> ObjectIdentifier {
ObjectIdentifier::new(ObjectType::TREND_LOG, 1).unwrap()
}
fn av1() -> ObjectIdentifier {
ObjectIdentifier::new(ObjectType::ANALOG_VALUE, 1).unwrap()
}
fn el1() -> ObjectIdentifier {
ObjectIdentifier::new(ObjectType::EVENT_LOG, 1).unwrap()
}
fn database() -> ObjectDatabase {
let mut db = crate::server::clocked_test_database();
let mut av = AnalogValueObject::new(1, "AV-1", EngineeringUnits::NO_UNITS).unwrap();
av.set_relinquish_default(21.5).unwrap();
db.add(Box::new(av)).unwrap();
let mut tl = TrendLogObject::new(1, "TL-1", 8).unwrap();
tl.set_log_device_object_property(Some(BACnetDeviceObjectPropertyReference::new_local(
ObjectIdentifier::new(ObjectType::ANALOG_VALUE, 1).unwrap(),
P::PRESENT_VALUE.to_raw(),
)))
.unwrap();
db.add(Box::new(tl)).unwrap();
db.add(Box::new(EventLogObject::new(1, "EL-1", 8).unwrap()))
.unwrap();
db.set_monotonic_clock_internal(Some(Arc::new(|| std::time::Duration::ZERO)));
db
}
fn wp(
db: &mut ObjectDatabase,
oid: ObjectIdentifier,
property: P,
value: &[u8],
) -> Result<(), Error> {
let mut request = BytesMut::new();
WritePropertyRequest {
object_identifier: oid,
property_identifier: property,
property_array_index: None,
property_value: value.to_vec(),
priority: None,
}
.encode(&mut request)
.unwrap();
sourced_wp(db, &request).map(|_| ())
}
fn rp(db: &ObjectDatabase, oid: ObjectIdentifier, property: P) -> Vec<u8> {
let mut request = BytesMut::new();
ReadPropertyRequest {
object_identifier: oid,
property_identifier: property,
property_array_index: None,
}
.encode(&mut request);
let mut ack = BytesMut::new();
handle_read_property(db, &request, &mut ack).unwrap();
ReadPropertyACK::decode(&ack).unwrap().property_value
}
fn assert_refused(result: Result<(), Error>, class: ErrorClass, code: ErrorCode) {
assert!(
matches!(result, Err(Error::Remote(RemoteError::Error { class: c, code: e }))
if c == class && e == code),
"expected {class:?} / {code:?}, got {result:?}"
);
}
fn statuses(db: &ObjectDatabase, oid: ObjectIdentifier) -> Vec<Option<LogStatus>> {
let records = db.get(&oid).unwrap().log_buffer_internal().unwrap();
(0..records.record_count())
.map(|index| {
let mut bytes = BytesMut::new();
records.encode_record(index, &mut bytes);
if oid.object_type() == ObjectType::TREND_LOG {
match decode_log_record(&bytes, 0).unwrap().0.log_datum {
LogDatum::LogStatus(status) => Some(status),
_ => None,
}
} else {
match decode_event_log_record(&bytes, 0).unwrap().0.log_datum {
EventLogDatum::LogStatus(status) => Some(status),
_ => None,
}
}
})
.collect()
}
const POLLED: [u8; 2] = [0x91, 0x00];
const COV: [u8; 2] = [0x91, 0x01];
const TRIGGERED: [u8; 2] = [0x91, 0x02];
const TRUE: [u8; 1] = [0x11];
const FALSE: [u8; 1] = [0x10];
#[test]
fn trend_log_logging_type_takes_cov_and_steers_log_interval() {
let mut db = database();
wp(&mut db, tl1(), P::LOG_INTERVAL, &[0x21, 0x64]).unwrap();
assert_refused(
wp(&mut db, tl1(), P::LOGGING_TYPE, &[0x91, 0x03]),
ErrorClass::PROPERTY,
ErrorCode::OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED,
);
assert_eq!(rp(&db, tl1(), P::LOGGING_TYPE), POLLED);
wp(&mut db, tl1(), P::LOGGING_TYPE, &COV).unwrap();
assert_eq!(rp(&db, tl1(), P::LOG_INTERVAL), [0x21, 0x00]);
wp(&mut db, tl1(), P::LOG_INTERVAL, &[0x21, 0x64]).unwrap();
assert_eq!(rp(&db, tl1(), P::LOGGING_TYPE), POLLED);
wp(&mut db, tl1(), P::LOG_INTERVAL, &[0x21, 0x00]).unwrap();
assert_eq!(rp(&db, tl1(), P::LOGGING_TYPE), COV);
assert_eq!(rp(&db, tl1(), P::CLIENT_COV_INCREMENT), [0x00]);
wp(
&mut db,
tl1(),
P::CLIENT_COV_INCREMENT,
&[0x44, 0x40, 0x00, 0x00, 0x00],
)
.unwrap();
assert_eq!(
rp(&db, tl1(), P::CLIENT_COV_INCREMENT),
[0x44, 0x40, 0x00, 0x00, 0x00]
);
wp(
&mut db,
tl1(),
P::COV_RESUBSCRIPTION_INTERVAL,
&[0x21, 0x3C],
)
.unwrap();
assert_eq!(rp(&db, tl1(), P::COV_RESUBSCRIPTION_INTERVAL), [0x21, 0x3C]);
assert_refused(
wp(
&mut db,
tl1(),
P::COV_RESUBSCRIPTION_INTERVAL,
&[0x21, 0x00],
),
ErrorClass::PROPERTY,
ErrorCode::VALUE_OUT_OF_RANGE,
);
db.poll_trend_logs();
assert_eq!(statuses(&db, tl1()), [None]);
for (value, records) in [
([0x44, 0x41, 0xB0, 0x00, 0x00], 1),
([0x44, 0x41, 0xD0, 0x00, 0x00], 2),
] {
wp(&mut db, av1(), P::PRESENT_VALUE, &value).unwrap();
db.poll_trend_logs();
assert_eq!(statuses(&db, tl1()).len(), records);
}
wp(&mut db, tl1(), P::LOG_INTERVAL, &[0x21, 0x64]).unwrap();
wp(&mut db, tl1(), P::LOGGING_TYPE, &TRIGGERED).unwrap();
assert_eq!(rp(&db, tl1(), P::LOG_INTERVAL), [0x21, 0x00]);
assert_refused(
wp(&mut db, tl1(), P::LOG_INTERVAL, &[0x21, 0x64]),
ErrorClass::PROPERTY,
ErrorCode::WRITE_ACCESS_DENIED,
);
db.poll_trend_logs();
assert_eq!(statuses(&db, tl1()), [None, None]);
wp(&mut db, tl1(), P::TRIGGER, &TRUE).unwrap();
assert_eq!(rp(&db, tl1(), P::TRIGGER), TRUE);
db.poll_trend_logs();
assert_eq!(statuses(&db, tl1()), [None, None, None]);
assert_eq!(rp(&db, tl1(), P::TRIGGER), FALSE);
}
#[test]
fn trend_log_and_event_log_windows_take_a_date_and_time() {
let mut db = database();
for oid in [tl1(), el1()] {
let start = [
0xA4, 250, 1, 1, 4, 0xB4, 8, 30, 0, 0, ];
wp(&mut db, oid, P::START_TIME, &start).unwrap();
assert_eq!(rp(&db, oid, P::START_TIME), start);
assert_eq!(statuses(&db, oid), [Some(LogStatus::LOG_DISABLED)]);
let unspecified = [0xA4, 0xFF, 0xFF, 0xFF, 0xFF, 0xB4, 0xFF, 0xFF, 0xFF, 0xFF];
assert_eq!(rp(&db, oid, P::STOP_TIME), unspecified);
let partly = [0xA4, 0xFF, 1, 1, 4, 0xB4, 8, 30, 0, 0];
assert_refused(
wp(&mut db, oid, P::STOP_TIME, &partly),
ErrorClass::PROPERTY,
ErrorCode::VALUE_OUT_OF_RANGE,
);
assert_refused(
wp(&mut db, oid, P::STOP_TIME, &start[..5]),
ErrorClass::PROPERTY,
ErrorCode::INVALID_DATA_TYPE,
);
wp(&mut db, oid, P::START_TIME, &unspecified).unwrap();
assert_eq!(
statuses(&db, oid),
[Some(LogStatus::LOG_DISABLED), Some(LogStatus::empty())]
);
}
}