use bacnet_client::client::BACnetClient;
use bacnet_encoding::primitives::encode_property_value;
use bacnet_services::common::BACnetPropertyValue;
use bacnet_services::wpm::WriteAccessSpecification;
use bacnet_transport::port::TransportPort;
use bacnet_types::enums::{ObjectType, PropertyIdentifier};
use bacnet_types::primitives::{ObjectIdentifier, PropertyValue};
use bytes::BytesMut;
use crate::core::destination::Destination;
use crate::output::{self, OutputFormat};
use crate::parse;
fn encode_value(value: &PropertyValue) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let mut buf = BytesMut::new();
encode_property_value(&mut buf, value)?;
Ok(buf.to_vec())
}
pub struct WritePropertyArgs {
pub object_type: ObjectType,
pub instance: u32,
pub property: PropertyIdentifier,
pub index: Option<u32>,
pub value: PropertyValue,
pub priority: Option<u8>,
}
pub type PropertyWrite = (PropertyIdentifier, Option<u32>, PropertyValue, Option<u8>);
pub type ObjectWrites = (ObjectType, u32, Vec<PropertyWrite>);
pub fn parse_write_multiple(specs: &[String]) -> Result<Vec<ObjectWrites>, String> {
let mut words = specs.iter();
let mut objects = Vec::new();
while let Some(object) = words.next() {
let (object_type, instance) = parse::parse_object_specifier(object)?;
let writes = words
.next()
.ok_or_else(|| format!("expected property=value after {object}"))?;
let props = writes
.split(',')
.map(|pair| {
let pair = pair.trim();
let (property, value) = pair
.split_once('=')
.ok_or_else(|| format!("expected 'property=value', got '{pair}'"))?;
let (property, index) = parse::parse_property(property)?;
let (value, priority) = parse::parse_value_with_priority(value)?;
Ok((property, index, value, priority))
})
.collect::<Result<_, String>>()?;
objects.push((object_type, instance, props));
}
Ok(objects)
}
pub async fn write_property_cmd<T: TransportPort + 'static>(
client: &BACnetClient<T>,
to: &Destination,
args: WritePropertyArgs,
format: OutputFormat,
) -> Result<(), Box<dyn std::error::Error>> {
let WritePropertyArgs {
object_type,
instance,
property,
index,
value,
priority,
} = args;
let oid = ObjectIdentifier::new(object_type, instance)?;
let encoded = encode_value(&value)?;
to.write_property(client, oid, property, index, encoded, priority)
.await?;
output::print_success("OK", format);
Ok(())
}
pub async fn write_property_multiple_cmd<T: TransportPort + 'static>(
client: &BACnetClient<T>,
to: &Destination,
specs: Vec<ObjectWrites>,
format: OutputFormat,
) -> Result<(), Box<dyn std::error::Error>> {
let access_specs: Vec<WriteAccessSpecification> = specs
.into_iter()
.map(|(obj_type, instance, props)| {
let oid = ObjectIdentifier::new(obj_type, instance)?;
let prop_values = props
.into_iter()
.map(|(prop, idx, pv, priority)| {
let encoded = encode_value(&pv)
.map_err(|e| bacnet_types::error::Error::Encoding(e.to_string()))?;
Ok(BACnetPropertyValue {
property_identifier: prop,
property_array_index: idx,
value: encoded,
priority,
})
})
.collect::<Result<Vec<_>, bacnet_types::error::Error>>()?;
Ok(WriteAccessSpecification {
object_identifier: oid,
list_of_properties: prop_values,
})
})
.collect::<Result<Vec<_>, bacnet_types::error::Error>>()?;
to.write_property_multiple(client, access_specs).await?;
output::print_success("OK", format);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn words(text: &str) -> Vec<String> {
text.split_whitespace().map(str::to_owned).collect()
}
#[test]
fn writem_specs_are_objects_each_with_its_writes() {
let specs = parse_write_multiple(&words("av:1 pv=72.5@8,ol[2]=3 bv:2 pv=active")).unwrap();
assert_eq!(specs.len(), 2);
let (object_type, instance, writes) = &specs[0];
assert_eq!((*object_type, *instance), (ObjectType::ANALOG_VALUE, 1));
assert_eq!(writes.len(), 2);
assert_eq!(writes[0].0, PropertyIdentifier::PRESENT_VALUE);
assert_eq!(writes[0].3, Some(8));
assert_eq!(writes[1].1, Some(2));
assert_eq!(specs[1].0, ObjectType::BINARY_VALUE);
}
#[test]
fn a_malformed_writem_spec_is_refused() {
for (text, error) in [
("av:1", "expected property=value after av:1"),
("av:1 pv", "expected 'property=value', got 'pv'"),
("nope:1 pv=1", ""),
] {
let refused = parse_write_multiple(&words(text)).unwrap_err();
assert!(refused.contains(error), "{text}: {refused}");
}
}
}