use bacnet_encoding::constructed::{
decode_event_notification_subscription, decode_port_permission,
};
use bacnet_types::bitstring::DaysOfWeek;
use bacnet_types::constructed::BACnetRecipient;
use bacnet_types::enums::{EventState, ObjectType, PropertyIdentifier};
use bacnet_types::primitives::{ObjectIdentifier, PropertyValue, Time};
use crate::database::ObjectDatabase;
use crate::event::EventTransition;
use crate::notification_class::filter_recipient_list;
use crate::subscribed_recipients::MAX_SUBSCRIBED_RECIPIENTS;
use crate::traits::BACnetObject;
#[derive(Debug, Clone, Copy)]
pub struct ForwardingInput<'a> {
pub process_identifier: u32,
pub to_state: EventState,
pub locally_initiated: bool,
pub receiving_port: Option<u8>,
pub today: DaysOfWeek,
pub current_time: &'a Time,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ForwardingTargets {
pub forwarders: Vec<ObjectIdentifier>,
pub recipients: Vec<(BACnetRecipient, u32, bool)>,
}
pub fn forwarding_targets(
db: &ObjectDatabase,
input: &ForwardingInput<'_>,
skip: &[ObjectIdentifier],
) -> ForwardingTargets {
let mut selected = ForwardingTargets::default();
for oid in db.find_by_type(ObjectType::NOTIFICATION_FORWARDER) {
if skip.contains(&oid) {
continue;
}
let Some(forwarder) = db.get(&oid) else {
continue;
};
if !takes(forwarder, input) {
continue;
}
selected.forwarders.push(oid);
for destination in destinations(forwarder, input) {
if !selected.recipients.contains(&destination) {
selected.recipients.push(destination);
}
}
}
selected
}
fn takes(forwarder: &dyn BACnetObject, input: &ForwardingInput<'_>) -> bool {
let read = |property| forwarder.read_property(property, None);
if matches!(
read(PropertyIdentifier::OUT_OF_SERVICE),
Ok(PropertyValue::Boolean(true))
) {
return false;
}
let process_matches = match read(PropertyIdentifier::PROCESS_IDENTIFIER_FILTER) {
Ok(PropertyValue::Null) => true,
Ok(PropertyValue::Unsigned(filter)) => filter == u64::from(input.process_identifier),
_ => false,
};
let origin_allowed = match read(PropertyIdentifier::LOCAL_FORWARDING_ONLY) {
Ok(PropertyValue::Boolean(local_only)) => !local_only || input.locally_initiated,
_ => false,
};
let port_enabled = match input.receiving_port {
None => true,
Some(port) => match read(PropertyIdentifier::PORT_FILTER) {
Ok(value) => port_enabled(&value, port),
Err(_) => true,
},
};
process_matches && origin_allowed && port_enabled
}
fn port_enabled(value: &PropertyValue, port: u8) -> bool {
let elements: &[PropertyValue] = match value {
PropertyValue::List(elements) => elements,
single => std::slice::from_ref(single),
};
elements.iter().any(|element| {
let PropertyValue::ApplicationData(bytes) = element else {
return false;
};
matches!(
decode_port_permission(bytes, 0),
Ok((permission, end)) if end == bytes.len()
&& permission.port_id == port
&& permission.enabled
)
})
}
fn destinations(
forwarder: &dyn BACnetObject,
input: &ForwardingInput<'_>,
) -> Vec<(BACnetRecipient, u32, bool)> {
let mut destinations = match forwarder.read_property(PropertyIdentifier::RECIPIENT_LIST, None) {
Ok(value) => filter_recipient_list(
&value,
EventTransition::for_target_state(input.to_state),
input.today,
input.current_time,
),
Err(_) => Vec::new(),
};
if let Ok(PropertyValue::ApplicationData(bytes)) =
forwarder.read_property(PropertyIdentifier::SUBSCRIBED_RECIPIENTS, None)
{
destinations.extend(subscribed(&bytes));
}
destinations
}
fn subscribed(bytes: &[u8]) -> Vec<(BACnetRecipient, u32, bool)> {
let mut entries = Vec::new();
let mut offset = 0;
while offset < bytes.len() {
if entries.len() == MAX_SUBSCRIBED_RECIPIENTS {
return Vec::new();
}
let Ok((entry, next)) = decode_event_notification_subscription(bytes, offset) else {
return Vec::new();
};
offset = next;
if entry.time_remaining > 0 {
entries.push((
entry.recipient,
entry.process_identifier,
entry.issue_confirmed_notifications,
));
}
}
entries
}