use super::{CovObservation, CovSample, flags::PreparedFlags, prepare::PreparedCovValue};
use bacnet_encoding::{constructed::decode_value_source, primitives::decode_timestamp_choice};
use bacnet_objects::traits::BACnetObject;
use bacnet_services::common::BACnetPropertyValue;
use bacnet_types::{
enums::{ObjectType as O, PropertyIdentifier as P},
error::Error,
primitives::PropertyValue as V,
};
fn fields(commandable: bool, flags: bool) -> &'static [P] {
match (commandable, flags) {
(true, _) => &[
P::PRESENT_VALUE,
P::STATUS_FLAGS,
P::VALUE_SOURCE,
P::LAST_COMMAND_TIME,
P::CURRENT_COMMAND_PRIORITY,
],
(false, true) => &[P::PRESENT_VALUE, P::STATUS_FLAGS, P::VALUE_SOURCE],
(false, false) => &[P::PRESENT_VALUE, P::VALUE_SOURCE],
}
}
pub(crate) fn applies(object: &dyn BACnetObject, property: P) -> bool {
if property != P::VALUE_SOURCE {
return false;
}
let properties = object.property_list();
let commandable = properties.contains(&P::PRIORITY_ARRAY);
match object.object_identifier().object_type() {
O::ANALOG_OUTPUT
| O::BINARY_OUTPUT
| O::MULTI_STATE_OUTPUT
| O::LIGHTING_OUTPUT
| O::BINARY_LIGHTING_OUTPUT => commandable,
O::ANALOG_VALUE | O::BINARY_VALUE | O::MULTI_STATE_VALUE => {
commandable || properties.contains(&P::VALUE_SOURCE)
}
O::COLOR | O::COLOR_TEMPERATURE => properties.contains(&P::VALUE_SOURCE),
_ => false,
}
}
pub(crate) struct PreparedValueSource {
properties: &'static [P],
fields: Vec<PreparedCovValue>,
priority: Option<usize>,
pub observation: CovObservation,
}
impl PreparedValueSource {
pub fn read(object: &dyn BACnetObject, flags: &PreparedFlags) -> Result<Self, Error> {
let commandable = object.property_list().contains(&P::PRIORITY_ARRAY);
let properties = fields(commandable, flags.value.is_some());
let mut fields = Vec::with_capacity(properties.len());
for &property in properties {
let value = if property == P::STATUS_FLAGS {
flags.value.clone().ok_or_else(invalid)?
} else {
object.read_property(property, None)?
};
CovSample::new(&value)?;
validate(object.object_identifier().object_type(), property, &value)?;
fields.push(super::prepare::prepare_value(
object, property, None, None, &value,
)?);
}
if !matches!(
object.object_identifier().object_type(),
O::ANALOG_OUTPUT | O::ANALOG_VALUE | O::LIGHTING_OUTPUT
) {
fields[0].increment = None;
}
let source = position(properties, P::VALUE_SOURCE);
let priority = commandable.then(|| position(properties, P::CURRENT_COMMAND_PRIORITY));
let observation = flags
.observation(fields[source].sample.clone())
.with_source_companions(
fields[0].sample.clone(),
priority.map(|i| fields[i].sample.clone()),
);
Ok(Self {
properties,
fields,
priority,
observation,
})
}
pub fn value(&self, property: P) -> Option<&V> {
self.properties
.iter()
.position(|p| *p == property)
.map(|i| self.fields[i].sample.value())
}
pub fn reports(&self, previous: Option<&CovObservation>) -> bool {
let Some(previous) = previous else {
return true;
};
let Some((pv, priority)) = previous.source_companions() else {
return true;
};
self.fields[0].reports(Some(pv))
|| self.observation.flags_changed(Some(previous))
|| self.observation.sample() != previous.sample()
|| self.priority.map(|i| &self.fields[i].sample) != priority.as_ref()
}
pub fn values(&self) -> Vec<BACnetPropertyValue> {
self.properties
.iter()
.copied()
.zip(&self.fields)
.map(|(property_identifier, field)| BACnetPropertyValue {
property_identifier,
property_array_index: None,
value: field.encoded.clone(),
priority: None,
})
.collect()
}
}
fn position(properties: &[P], property: P) -> usize {
properties
.iter()
.position(|p| *p == property)
.expect("every Table 13-1a-2 field list carries this property")
}
fn invalid() -> Error {
Error::Encoding("Invalid Value_Source COV companion".into())
}
fn validate(object: O, property: P, value: &V) -> Result<(), Error> {
let valid = match property {
P::PRESENT_VALUE => match object {
O::ANALOG_OUTPUT | O::ANALOG_VALUE => matches!(value, V::Real(v) if v.is_finite()),
O::LIGHTING_OUTPUT => matches!(value, V::Real(v) if (0.0..=100.0).contains(v)),
O::BINARY_OUTPUT | O::BINARY_VALUE | O::BINARY_LIGHTING_OUTPUT => {
matches!(value, V::Enumerated(0..=1))
}
O::COLOR => matches!(value, V::List(xy) if xy.len() == 2
&& xy.iter().all(|c| matches!(c, V::Real(v) if v.is_finite()))),
O::COLOR_TEMPERATURE => matches!(value, V::Unsigned(1000..=30_000)),
_ => matches!(value, V::Unsigned(1..=0xffff_ffff)),
},
P::VALUE_SOURCE => {
matches!(value, V::ApplicationData(bytes) if decode_value_source(bytes, 0).is_ok_and(|(_, end)| end == bytes.len()))
}
P::LAST_COMMAND_TIME => {
matches!(value, V::ApplicationData(bytes) if decode_timestamp_choice(bytes, 0).is_ok_and(|(_, end)| end == bytes.len()))
}
P::CURRENT_COMMAND_PRIORITY => matches!(value, V::Null | V::Unsigned(1..=16)),
P::STATUS_FLAGS => super::observation::validate_flags(value).is_ok(),
_ => false,
};
if valid { Ok(()) } else { Err(invalid()) }
}