use std::borrow::Cow;
use std::sync::Mutex;
use ScheduleTargetOutcome::{Accepted, Failed, ReferenceRefused};
use bacnet_objects::analog::{AnalogInputObject, AnalogOutputObject};
use bacnet_objects::binary::BinaryValueObject;
use bacnet_objects::device::{DeviceConfig, DeviceObject};
use bacnet_objects::multistate::MultiStateValueObject;
use bacnet_objects::traits::BACnetObject;
use bacnet_types::constructed::BACnetObjectPropertyReference;
use bacnet_types::enums::{ErrorClass, ErrorCode};
use bacnet_types::error::Error;
use super::*;
fn oid(object_type: ObjectType, instance: u32) -> ObjectIdentifier {
ObjectIdentifier::new(object_type, instance).unwrap()
}
fn sch() -> ObjectIdentifier {
oid(ObjectType::SCHEDULE, 1)
}
fn ai() -> ObjectIdentifier {
oid(ObjectType::ANALOG_INPUT, 1)
}
fn ao() -> ObjectIdentifier {
oid(ObjectType::ANALOG_OUTPUT, 2)
}
type Outcomes = Arc<Mutex<Vec<Vec<ScheduleTargetOutcome>>>>;
struct OwingSchedule {
owed: Vec<ScheduleWrite>,
outcomes: Outcomes,
}
impl BACnetObject for OwingSchedule {
fn object_identifier(&self) -> ObjectIdentifier {
sch()
}
fn object_name(&self) -> &str {
"SCH-1"
}
fn read_property(
&self,
property: PropertyIdentifier,
_array_index: Option<u32>,
) -> Result<PropertyValue, Error> {
match property {
p if p == PropertyIdentifier::OBJECT_IDENTIFIER => {
Ok(PropertyValue::ObjectIdentifier(sch()))
}
p if p == PropertyIdentifier::OBJECT_NAME => {
Ok(PropertyValue::CharacterString("SCH-1".into()))
}
p if p == PropertyIdentifier::OBJECT_TYPE => {
Ok(PropertyValue::Enumerated(ObjectType::SCHEDULE.to_raw()))
}
_ => Err(Error::protocol(
ErrorClass::PROPERTY,
ErrorCode::UNKNOWN_PROPERTY,
)),
}
}
fn write_property(
&mut self,
_property: PropertyIdentifier,
_array_index: Option<u32>,
_value: PropertyValue,
_priority: Option<u8>,
) -> Result<(), Error> {
Err(Error::protocol(
ErrorClass::PROPERTY,
ErrorCode::WRITE_ACCESS_DENIED,
))
}
fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
Cow::Borrowed(&[])
}
fn take_owed_schedule_writes(&mut self) -> Vec<ScheduleWrite> {
std::mem::take(&mut self.owed)
}
fn complete_schedule_write(
&mut self,
_write: &ScheduleWrite,
outcomes: &[ScheduleTargetOutcome],
) -> bool {
self.outcomes.lock().unwrap().push(outcomes.to_vec());
false
}
}
fn reference(
object: ObjectIdentifier,
property: PropertyIdentifier,
) -> BACnetObjectPropertyReference {
BACnetObjectPropertyReference::new(object, property.to_raw())
}
fn read(
db: &ObjectDatabase,
object: ObjectIdentifier,
property: PropertyIdentifier,
index: Option<u32>,
) -> PropertyValue {
db.get(&object)
.unwrap()
.read_property(property, index)
.unwrap()
}
#[tokio::test]
async fn a_schedule_null_relinquishes_a_commandable_target_and_leaves_the_others_as_they_are() {
use PropertyIdentifier as P;
use bacnet_types::enums::EngineeringUnits;
let outcomes = Outcomes::default();
let write = |value, references| ScheduleWrite {
value,
priority: 9,
references,
retry: false,
};
let schedule = OwingSchedule {
owed: vec![
write(
PropertyValue::Real(5.0),
vec![
reference(ao(), P::PRESENT_VALUE),
reference(ai(), P::COV_INCREMENT),
],
),
write(
PropertyValue::Null,
vec![
reference(ao(), P::PRESENT_VALUE),
reference(ai(), P::COV_INCREMENT),
reference(ai(), P::STATUS_FLAGS),
reference(oid(ObjectType::ANALOG_INPUT, 9), P::PRESENT_VALUE),
],
),
],
outcomes: Arc::clone(&outcomes),
};
let mut db = ObjectDatabase::new();
db.add(Box::new(
DeviceObject::new(DeviceConfig::default()).unwrap(),
))
.unwrap();
db.add(Box::new(
AnalogInputObject::new(1, "AI-1", EngineeringUnits::DEGREES_CELSIUS).unwrap(),
))
.unwrap();
db.add(Box::new(
AnalogOutputObject::new(2, "AO-2", EngineeringUnits::DEGREES_CELSIUS).unwrap(),
))
.unwrap();
db.add(Box::new(schedule)).unwrap();
let db = Arc::new(RwLock::new(db));
tick_schedules(&db).await;
assert_eq!(
*outcomes.lock().unwrap(),
[
vec![Accepted, Accepted],
vec![Accepted, Accepted, Failed, ReferenceRefused]
]
);
let db = db.read().await;
assert_eq!(
read(&db, ao(), P::PRIORITY_ARRAY, Some(9)),
PropertyValue::Null
);
assert_eq!(
read(&db, ao(), P::PRESENT_VALUE, None),
PropertyValue::Real(0.0)
);
assert_eq!(
read(&db, ai(), P::COV_INCREMENT, None),
PropertyValue::Real(5.0)
);
}
fn indexed(
object: ObjectIdentifier,
property: PropertyIdentifier,
index: u32,
) -> BACnetObjectPropertyReference {
BACnetObjectPropertyReference::new_indexed(object, property.to_raw(), index)
}
#[tokio::test]
async fn a_schedule_target_index_is_checked_as_write_property_checks_it() {
use PropertyIdentifier as P;
use bacnet_types::enums::EngineeringUnits;
let outcomes = Outcomes::default();
let msv = oid(ObjectType::MULTI_STATE_VALUE, 1);
let bv = oid(ObjectType::BINARY_VALUE, 1);
let write = |value, references| ScheduleWrite {
value,
priority: 9,
references,
retry: false,
};
let schedule = OwingSchedule {
owed: vec![
write(
PropertyValue::CharacterString("scheduled".into()),
vec![
indexed(ai(), P::DESCRIPTION, 1),
indexed(msv, P::STATE_TEXT, 2),
indexed(msv, P::STATE_TEXT, 9),
indexed(ai(), P::STATE_TEXT, 1),
],
),
write(
PropertyValue::Real(5.0),
vec![indexed(ai(), P::DESCRIPTION, 1)],
),
write(PropertyValue::Null, vec![indexed(ai(), P::DESCRIPTION, 1)]),
write(
PropertyValue::Enumerated(1),
vec![indexed(bv, P::PRESENT_VALUE, 1)],
),
],
outcomes: Arc::clone(&outcomes),
};
let mut db = ObjectDatabase::new();
db.add(Box::new(
DeviceObject::new(DeviceConfig::default()).unwrap(),
))
.unwrap();
db.add(Box::new(
AnalogInputObject::new(1, "AI-1", EngineeringUnits::DEGREES_CELSIUS).unwrap(),
))
.unwrap();
db.add(Box::new(BinaryValueObject::new(1, "BV-1").unwrap()))
.unwrap();
db.add(Box::new(MultiStateValueObject::new(1, "MSV-1", 3).unwrap()))
.unwrap();
db.add(Box::new(schedule)).unwrap();
let left_alone = |db: &ObjectDatabase| {
[
read(db, ai(), P::DESCRIPTION, None),
read(db, bv, P::PRESENT_VALUE, None),
read(db, msv, P::STATE_TEXT, Some(1)),
read(db, msv, P::STATE_TEXT, Some(3)),
]
};
let before = left_alone(&db);
let db = Arc::new(RwLock::new(db));
tick_schedules(&db).await;
assert_eq!(
*outcomes.lock().unwrap(),
[
vec![
ReferenceRefused,
Accepted,
ReferenceRefused,
ReferenceRefused
],
vec![ReferenceRefused],
vec![ReferenceRefused],
vec![ReferenceRefused]
]
);
let db = db.read().await;
assert_eq!(left_alone(&db), before);
assert_eq!(
read(&db, msv, P::STATE_TEXT, Some(2)),
PropertyValue::CharacterString("scheduled".into())
);
}