use std::collections::HashMap;
use bacnet_objects::database::ObjectDatabase;
use bacnet_objects::event::EventStateChange;
use bacnet_types::enums::{EventState, EventType, PropertyIdentifier};
use bacnet_types::primitives::{ObjectIdentifier, PropertyValue};
fn status_flags_byte(in_alarm: bool) -> u8 {
if in_alarm {
0x08
} else {
0x00
}
}
fn read_real(
obj: &dyn bacnet_objects::traits::BACnetObject,
prop: PropertyIdentifier,
) -> Option<f32> {
match obj.read_property(prop, None) {
Ok(PropertyValue::Real(v)) => Some(v),
_ => None,
}
}
fn read_unsigned(
obj: &dyn bacnet_objects::traits::BACnetObject,
prop: PropertyIdentifier,
) -> Option<u32> {
match obj.read_property(prop, None) {
Ok(PropertyValue::Unsigned(v)) => Some(v as u32),
_ => None,
}
}
fn read_event_enable(obj: &dyn bacnet_objects::traits::BACnetObject) -> u8 {
match obj.read_property(PropertyIdentifier::EVENT_ENABLE, None) {
Ok(PropertyValue::BitString { unused_bits, data }) => {
if data.is_empty() {
return 0;
}
data[0] >> unused_bits
}
Ok(PropertyValue::Unsigned(v)) => v as u8,
_ => 0,
}
}
fn read_enum(
obj: &dyn bacnet_objects::traits::BACnetObject,
prop: PropertyIdentifier,
) -> Option<u32> {
match obj.read_property(prop, None) {
Ok(PropertyValue::Enumerated(v)) => Some(v),
_ => None,
}
}
fn read_bitstring(
obj: &dyn bacnet_objects::traits::BACnetObject,
prop: PropertyIdentifier,
) -> Option<(u8, Vec<u8>)> {
match obj.read_property(prop, None) {
Ok(PropertyValue::BitString { unused_bits, data }) => Some((unused_bits, data)),
_ => None,
}
}
fn read_enum_list(
obj: &dyn bacnet_objects::traits::BACnetObject,
prop: PropertyIdentifier,
) -> Vec<u32> {
match obj.read_property(prop, None) {
Ok(PropertyValue::List(items)) => items
.into_iter()
.filter_map(|pv| match pv {
PropertyValue::Enumerated(v) => Some(v),
_ => None,
})
.collect(),
Ok(PropertyValue::Enumerated(v)) => vec![v],
_ => Vec::new(),
}
}
fn read_bitstring_list(
obj: &dyn bacnet_objects::traits::BACnetObject,
prop: PropertyIdentifier,
) -> Vec<(u8, Vec<u8>)> {
match obj.read_property(prop, None) {
Ok(PropertyValue::List(items)) => items
.into_iter()
.filter_map(|pv| match pv {
PropertyValue::BitString { unused_bits, data } => Some((unused_bits, data)),
_ => None,
})
.collect(),
Ok(PropertyValue::BitString { unused_bits, data }) => vec![(unused_bits, data)],
_ => Vec::new(),
}
}
#[derive(Debug, Clone)]
struct ObjectTracker {
event_state: EventState,
time_delay_remaining: Option<u32>,
pending_state: Option<EventState>,
last_cov_real: Option<f32>,
last_cov_binary: Option<u32>,
}
impl ObjectTracker {
fn new() -> Self {
Self {
event_state: EventState::NORMAL,
time_delay_remaining: None,
pending_state: None,
last_cov_real: None,
last_cov_binary: None,
}
}
}
#[derive(Debug, Clone)]
pub struct IntrinsicEvent {
pub object_id: ObjectIdentifier,
pub change: EventStateChange,
pub event_type: EventType,
pub status_flags: u8,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum AlgorithmType {
ChangeOfState,
ChangeOfBitstring,
FloatingLimit,
CommandFailure,
ChangeOfValue,
}
pub struct IntrinsicReportingEngine {
trackers: HashMap<ObjectIdentifier, ObjectTracker>,
}
impl Default for IntrinsicReportingEngine {
fn default() -> Self {
Self::new()
}
}
impl IntrinsicReportingEngine {
pub fn new() -> Self {
Self {
trackers: HashMap::new(),
}
}
pub fn evaluate(&mut self, db: &ObjectDatabase) -> Vec<IntrinsicEvent> {
let mut events = Vec::new();
let oids: Vec<ObjectIdentifier> = db.iter_objects().map(|(oid, _)| oid).collect();
for oid in oids {
if let Some(obj) = db.get(&oid) {
if let Some(event) = self.evaluate_object(oid, obj) {
events.push(event);
}
}
}
events
}
fn evaluate_object(
&mut self,
oid: ObjectIdentifier,
obj: &dyn bacnet_objects::traits::BACnetObject,
) -> Option<IntrinsicEvent> {
let algo = match read_enum(obj, PropertyIdentifier::EVENT_TYPE) {
Some(v) if v == EventType::CHANGE_OF_STATE.to_raw() => AlgorithmType::ChangeOfState,
Some(v) if v == EventType::CHANGE_OF_BITSTRING.to_raw() => {
AlgorithmType::ChangeOfBitstring
}
Some(v) if v == EventType::FLOATING_LIMIT.to_raw() => AlgorithmType::FloatingLimit,
Some(v) if v == EventType::COMMAND_FAILURE.to_raw() => AlgorithmType::CommandFailure,
Some(v) if v == EventType::CHANGE_OF_VALUE.to_raw() => AlgorithmType::ChangeOfValue,
_ => return None,
};
let event_enable = read_event_enable(obj) as u32;
if event_enable == 0 {
return None;
}
let time_delay = read_unsigned(obj, PropertyIdentifier::TIME_DELAY).unwrap_or(0);
let tracker = self.trackers.entry(oid).or_insert_with(|| {
let mut t = ObjectTracker::new();
t.event_state =
EventState::from_raw(read_enum(obj, PropertyIdentifier::EVENT_STATE).unwrap_or(0));
t
});
match algo {
AlgorithmType::ChangeOfState => {
evaluate_change_of_state(oid, obj, tracker, time_delay, event_enable)
}
AlgorithmType::ChangeOfBitstring => {
evaluate_change_of_bitstring(oid, obj, tracker, time_delay, event_enable)
}
AlgorithmType::FloatingLimit => {
evaluate_floating_limit(oid, obj, tracker, time_delay, event_enable)
}
AlgorithmType::CommandFailure => {
evaluate_command_failure(oid, obj, tracker, time_delay, event_enable)
}
AlgorithmType::ChangeOfValue => evaluate_change_of_value(oid, obj, tracker),
}
}
}
fn apply_time_delay(
tracker: &mut ObjectTracker,
desired_state: EventState,
time_delay: u32,
oid: ObjectIdentifier,
event_type: EventType,
event_enable: u32,
) -> Option<IntrinsicEvent> {
if desired_state == tracker.event_state {
tracker.pending_state = None;
tracker.time_delay_remaining = None;
return None;
}
let enabled = if desired_state == EventState::NORMAL {
event_enable & 0x04 != 0 } else {
event_enable & 0x01 != 0 };
if !enabled {
tracker.event_state = desired_state;
tracker.pending_state = None;
tracker.time_delay_remaining = None;
return None;
}
if time_delay == 0 {
let change = EventStateChange {
from: tracker.event_state,
to: desired_state,
};
let in_alarm = desired_state != EventState::NORMAL;
tracker.event_state = desired_state;
tracker.pending_state = None;
tracker.time_delay_remaining = None;
return Some(IntrinsicEvent {
object_id: oid,
change,
event_type,
status_flags: status_flags_byte(in_alarm),
});
}
match tracker.pending_state {
Some(ps) if ps == desired_state => {
let remaining = tracker.time_delay_remaining.unwrap_or(time_delay);
if remaining <= 1 {
let change = EventStateChange {
from: tracker.event_state,
to: desired_state,
};
let in_alarm = desired_state != EventState::NORMAL;
tracker.event_state = desired_state;
tracker.pending_state = None;
tracker.time_delay_remaining = None;
Some(IntrinsicEvent {
object_id: oid,
change,
event_type,
status_flags: status_flags_byte(in_alarm),
})
} else {
tracker.time_delay_remaining = Some(remaining - 1);
None
}
}
_ => {
tracker.pending_state = Some(desired_state);
tracker.time_delay_remaining = Some(time_delay);
None
}
}
}
fn evaluate_change_of_state(
oid: ObjectIdentifier,
obj: &dyn bacnet_objects::traits::BACnetObject,
tracker: &mut ObjectTracker,
time_delay: u32,
event_enable: u32,
) -> Option<IntrinsicEvent> {
let pv = read_enum(obj, PropertyIdentifier::PRESENT_VALUE)?;
let alarm_values = read_enum_list(obj, PropertyIdentifier::ALARM_VALUES);
let desired = if alarm_values.contains(&pv) {
EventState::OFFNORMAL
} else {
EventState::NORMAL
};
apply_time_delay(
tracker,
desired,
time_delay,
oid,
EventType::CHANGE_OF_STATE,
event_enable,
)
}
fn bitstring_and(a: &[u8], b: &[u8]) -> Vec<u8> {
a.iter().zip(b.iter()).map(|(x, y)| x & y).collect()
}
fn evaluate_change_of_bitstring(
oid: ObjectIdentifier,
obj: &dyn bacnet_objects::traits::BACnetObject,
tracker: &mut ObjectTracker,
time_delay: u32,
event_enable: u32,
) -> Option<IntrinsicEvent> {
let (_pv_unused, pv_data) = read_bitstring(obj, PropertyIdentifier::PRESENT_VALUE)?;
let (_bm_unused, bm_data) = read_bitstring(obj, PropertyIdentifier::BIT_MASK)?;
let alarm_values = read_bitstring_list(obj, PropertyIdentifier::ALARM_VALUES);
let masked = bitstring_and(&pv_data, &bm_data);
let in_alarm = alarm_values.iter().any(|(_unused, av_data)| {
let masked_av = bitstring_and(av_data, &bm_data);
masked_av == masked
});
let desired = if in_alarm {
EventState::OFFNORMAL
} else {
EventState::NORMAL
};
apply_time_delay(
tracker,
desired,
time_delay,
oid,
EventType::CHANGE_OF_BITSTRING,
event_enable,
)
}
fn evaluate_floating_limit(
oid: ObjectIdentifier,
obj: &dyn bacnet_objects::traits::BACnetObject,
tracker: &mut ObjectTracker,
time_delay: u32,
event_enable: u32,
) -> Option<IntrinsicEvent> {
let pv = read_real(obj, PropertyIdentifier::PRESENT_VALUE)?;
let setpoint = read_real(obj, PropertyIdentifier::SETPOINT_REFERENCE)?;
let error_limit = read_real(obj, PropertyIdentifier::ERROR_LIMIT)?;
let deadband = read_real(obj, PropertyIdentifier::DEADBAND).unwrap_or(0.0);
let limit_enable = read_bitstring(obj, PropertyIdentifier::LIMIT_ENABLE)
.and_then(|(_, data)| data.first().copied())
.unwrap_or(0);
let low_enabled = limit_enable & 0x80 != 0;
let high_enabled = limit_enable & 0x40 != 0;
let high_limit = setpoint + error_limit;
let low_limit = setpoint - error_limit;
let desired = match tracker.event_state {
s if s == EventState::NORMAL => {
if high_enabled && pv > high_limit {
EventState::HIGH_LIMIT
} else if low_enabled && pv < low_limit {
EventState::LOW_LIMIT
} else {
EventState::NORMAL
}
}
s if s == EventState::HIGH_LIMIT => {
if low_enabled && pv < low_limit {
EventState::LOW_LIMIT
} else if pv < high_limit - deadband {
EventState::NORMAL
} else {
EventState::HIGH_LIMIT
}
}
s if s == EventState::LOW_LIMIT => {
if high_enabled && pv > high_limit {
EventState::HIGH_LIMIT
} else if pv > low_limit + deadband {
EventState::NORMAL
} else {
EventState::LOW_LIMIT
}
}
_ => tracker.event_state,
};
apply_time_delay(
tracker,
desired,
time_delay,
oid,
EventType::FLOATING_LIMIT,
event_enable,
)
}
fn evaluate_command_failure(
oid: ObjectIdentifier,
obj: &dyn bacnet_objects::traits::BACnetObject,
tracker: &mut ObjectTracker,
time_delay: u32,
event_enable: u32,
) -> Option<IntrinsicEvent> {
let pv = read_enum(obj, PropertyIdentifier::PRESENT_VALUE)?;
let feedback = read_enum(obj, PropertyIdentifier::FEEDBACK_VALUE)?;
let desired = if pv != feedback {
EventState::OFFNORMAL
} else {
EventState::NORMAL
};
apply_time_delay(
tracker,
desired,
time_delay,
oid,
EventType::COMMAND_FAILURE,
event_enable,
)
}
fn evaluate_change_of_value(
oid: ObjectIdentifier,
obj: &dyn bacnet_objects::traits::BACnetObject,
tracker: &mut ObjectTracker,
) -> Option<IntrinsicEvent> {
if let Some(pv) = read_real(obj, PropertyIdentifier::PRESENT_VALUE) {
let increment = read_real(obj, PropertyIdentifier::COV_INCREMENT).unwrap_or(0.0);
let should_notify = match tracker.last_cov_real {
Some(last) => {
let delta = (pv - last).abs();
increment <= 0.0 || delta >= increment
}
None => true,
};
if should_notify {
tracker.last_cov_real = Some(pv);
return Some(IntrinsicEvent {
object_id: oid,
change: EventStateChange {
from: EventState::NORMAL,
to: EventState::NORMAL,
},
event_type: EventType::CHANGE_OF_VALUE,
status_flags: status_flags_byte(false),
});
}
return None;
}
if let Some(pv) = read_enum(obj, PropertyIdentifier::PRESENT_VALUE) {
let should_notify = match tracker.last_cov_binary {
Some(last) => pv != last,
None => true,
};
if should_notify {
tracker.last_cov_binary = Some(pv);
return Some(IntrinsicEvent {
object_id: oid,
change: EventStateChange {
from: EventState::NORMAL,
to: EventState::NORMAL,
},
event_type: EventType::CHANGE_OF_VALUE,
status_flags: status_flags_byte(false),
});
}
}
None
}
#[cfg(test)]
mod tests;