Skip to main content

bacnet_objects/
event.rs

1//! Intrinsic reporting — OUT_OF_RANGE event state machine.
2//!
3//! Per ASHRAE 135-2020 Clause 13.3.2, the OUT_OF_RANGE algorithm monitors
4//! an analog present_value against HIGH_LIMIT and LOW_LIMIT with a DEADBAND
5//! to prevent oscillation at the boundary.
6
7use bacnet_types::enums::{EventState, EventType};
8
9/// A detected change in event state.
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct EventStateChange {
12    /// The previous event state.
13    pub from: EventState,
14    /// The new event state.
15    pub to: EventState,
16}
17
18impl EventStateChange {
19    /// Derive the BACnet EventType from the state transition.
20    ///
21    /// If either the `from` or `to` state is `HIGH_LIMIT` or `LOW_LIMIT`,
22    /// the event type is `OUT_OF_RANGE`. Otherwise it is `CHANGE_OF_STATE`.
23    pub fn event_type(&self) -> EventType {
24        if self.from == EventState::HIGH_LIMIT
25            || self.from == EventState::LOW_LIMIT
26            || self.to == EventState::HIGH_LIMIT
27            || self.to == EventState::LOW_LIMIT
28        {
29            EventType::OUT_OF_RANGE
30        } else {
31            EventType::CHANGE_OF_STATE
32        }
33    }
34
35    /// Derive the event transition category from the state change.
36    ///
37    /// Per Clause 13.2.5:
38    /// - `to == NORMAL` → `ToNormal`
39    /// - `to == FAULT` → `ToFault`
40    /// - Everything else (OFFNORMAL, HIGH_LIMIT, LOW_LIMIT) → `ToOffnormal`
41    pub fn transition(&self) -> EventTransition {
42        if self.to == EventState::NORMAL {
43            EventTransition::ToNormal
44        } else if self.to == EventState::FAULT {
45            EventTransition::ToFault
46        } else {
47            EventTransition::ToOffnormal
48        }
49    }
50}
51
52/// Event transition category per ASHRAE 135-2020 Clause 13.2.5.
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum EventTransition {
55    /// Transition to an off-normal state (OFFNORMAL, HIGH_LIMIT, LOW_LIMIT, etc.).
56    ToOffnormal,
57    /// Transition to FAULT.
58    ToFault,
59    /// Transition to NORMAL.
60    ToNormal,
61}
62
63impl EventTransition {
64    /// Bit mask for this transition in the `BACnetDestination.transitions` field.
65    ///
66    /// bit 0 = TO_OFFNORMAL, bit 1 = TO_FAULT, bit 2 = TO_NORMAL.
67    pub fn bit_mask(self) -> u8 {
68        match self {
69            EventTransition::ToOffnormal => 0x01,
70            EventTransition::ToFault => 0x02,
71            EventTransition::ToNormal => 0x04,
72        }
73    }
74}
75
76/// Which limits are enabled (Clause 12.1.14).
77///
78/// Encoded as a BACnet BIT STRING: bit 0 = low_limit_enable, bit 1 = high_limit_enable.
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub struct LimitEnable {
81    pub low_limit_enable: bool,
82    pub high_limit_enable: bool,
83}
84
85impl LimitEnable {
86    pub const NONE: Self = Self {
87        low_limit_enable: false,
88        high_limit_enable: false,
89    };
90
91    pub const BOTH: Self = Self {
92        low_limit_enable: true,
93        high_limit_enable: true,
94    };
95
96    /// Encode as a BACnet bitstring byte (2 bits used, 6 unused).
97    pub fn to_bits(self) -> u8 {
98        let mut bits = 0u8;
99        if self.low_limit_enable {
100            bits |= 0x80; // bit 0 (MSB first)
101        }
102        if self.high_limit_enable {
103            bits |= 0x40; // bit 1
104        }
105        bits
106    }
107
108    /// Decode from a BACnet bitstring byte.
109    pub fn from_bits(byte: u8) -> Self {
110        Self {
111            low_limit_enable: byte & 0x80 != 0,
112            high_limit_enable: byte & 0x40 != 0,
113        }
114    }
115}
116
117/// OUT_OF_RANGE event detector for analog objects.
118///
119/// Implements the event state machine per Clause 13.3.2:
120/// - NORMAL → HIGH_LIMIT when `present_value > high_limit` (if high_limit enabled)
121/// - NORMAL → LOW_LIMIT when `present_value < low_limit` (if low_limit enabled)
122/// - HIGH_LIMIT → NORMAL when `present_value < high_limit - deadband`
123/// - LOW_LIMIT → NORMAL when `present_value > low_limit + deadband`
124/// - HIGH_LIMIT → LOW_LIMIT when `present_value < low_limit`
125/// - LOW_LIMIT → HIGH_LIMIT when `present_value > high_limit`
126#[derive(Debug, Clone)]
127pub struct OutOfRangeDetector {
128    pub high_limit: f32,
129    pub low_limit: f32,
130    pub deadband: f32,
131    pub limit_enable: LimitEnable,
132    pub notification_class: u32,
133    pub notify_type: u32,
134    pub event_enable: u8,
135    pub time_delay: u32,
136    pub event_state: EventState,
137    /// Acknowledged-transitions bitfield (3 bits: TO_OFFNORMAL, TO_FAULT, TO_NORMAL).
138    /// A set bit means the corresponding transition has been acknowledged.
139    pub acked_transitions: u8,
140}
141
142impl Default for OutOfRangeDetector {
143    fn default() -> Self {
144        Self {
145            high_limit: 100.0,
146            low_limit: 0.0,
147            deadband: 1.0,
148            limit_enable: LimitEnable::NONE,
149            notification_class: 0,
150            notify_type: 0, // ALARM
151            event_enable: 0,
152            time_delay: 0,
153            event_state: EventState::NORMAL,
154            acked_transitions: 0b111, // all acknowledged by default
155        }
156    }
157}
158
159impl OutOfRangeDetector {
160    /// Event_Enable bit masks per Clause 13.1.4.
161    const TO_OFFNORMAL: u8 = 0x01;
162    const TO_FAULT: u8 = 0x02;
163    const TO_NORMAL: u8 = 0x04;
164
165    /// Evaluate the present value against configured limits.
166    ///
167    /// Returns `Some(EventStateChange)` if the event state changed **and**
168    /// the corresponding `event_enable` bit is set (Clause 13.1.4).
169    /// Internal state always updates regardless of event_enable.
170    ///
171    /// Note: This implementation uses instant transitions (ignores time_delay).
172    pub fn evaluate(&mut self, present_value: f32) -> Option<EventStateChange> {
173        let new_state = self.compute_new_state(present_value);
174        if new_state != self.event_state {
175            let change = EventStateChange {
176                from: self.event_state,
177                to: new_state,
178            };
179            self.event_state = new_state;
180
181            // Check event_enable bitmask per Clause 13.1.4
182            let enabled = match new_state {
183                s if s == EventState::NORMAL => self.event_enable & Self::TO_NORMAL != 0,
184                s if s == EventState::HIGH_LIMIT || s == EventState::LOW_LIMIT => {
185                    self.event_enable & Self::TO_OFFNORMAL != 0
186                }
187                _ => self.event_enable & Self::TO_FAULT != 0,
188            };
189
190            if enabled {
191                Some(change)
192            } else {
193                None
194            }
195        } else {
196            None
197        }
198    }
199
200    fn compute_new_state(&self, pv: f32) -> EventState {
201        let high_enabled = self.limit_enable.high_limit_enable;
202        let low_enabled = self.limit_enable.low_limit_enable;
203
204        match self.event_state {
205            s if s == EventState::NORMAL => {
206                // Check for HIGH_LIMIT violation first (higher priority)
207                if high_enabled && pv > self.high_limit {
208                    return EventState::HIGH_LIMIT;
209                }
210                if low_enabled && pv < self.low_limit {
211                    return EventState::LOW_LIMIT;
212                }
213                EventState::NORMAL
214            }
215            s if s == EventState::HIGH_LIMIT => {
216                // Can transition to LOW_LIMIT directly
217                if low_enabled && pv < self.low_limit {
218                    return EventState::LOW_LIMIT;
219                }
220                // Return to NORMAL with deadband
221                if pv < self.high_limit - self.deadband {
222                    return EventState::NORMAL;
223                }
224                EventState::HIGH_LIMIT
225            }
226            s if s == EventState::LOW_LIMIT => {
227                // Can transition to HIGH_LIMIT directly
228                if high_enabled && pv > self.high_limit {
229                    return EventState::HIGH_LIMIT;
230                }
231                // Return to NORMAL with deadband
232                if pv > self.low_limit + self.deadband {
233                    return EventState::NORMAL;
234                }
235                EventState::LOW_LIMIT
236            }
237            _ => self.event_state, // No change for unknown states
238        }
239    }
240}
241
242#[cfg(test)]
243mod tests {
244    use super::*;
245
246    fn make_detector() -> OutOfRangeDetector {
247        OutOfRangeDetector {
248            high_limit: 80.0,
249            low_limit: 20.0,
250            deadband: 2.0,
251            limit_enable: LimitEnable::BOTH,
252            notification_class: 1,
253            notify_type: 0,
254            event_enable: 0x07, // all transitions
255            time_delay: 0,
256            event_state: EventState::NORMAL,
257            acked_transitions: 0b111,
258        }
259    }
260
261    #[test]
262    fn normal_stays_normal_within_limits() {
263        let mut det = make_detector();
264        assert!(det.evaluate(50.0).is_none());
265        assert_eq!(det.event_state, EventState::NORMAL);
266    }
267
268    #[test]
269    fn normal_to_high_limit() {
270        let mut det = make_detector();
271        let change = det.evaluate(81.0).unwrap();
272        assert_eq!(change.from, EventState::NORMAL);
273        assert_eq!(change.to, EventState::HIGH_LIMIT);
274        assert_eq!(det.event_state, EventState::HIGH_LIMIT);
275    }
276
277    #[test]
278    fn normal_to_low_limit() {
279        let mut det = make_detector();
280        let change = det.evaluate(19.0).unwrap();
281        assert_eq!(change.from, EventState::NORMAL);
282        assert_eq!(change.to, EventState::LOW_LIMIT);
283        assert_eq!(det.event_state, EventState::LOW_LIMIT);
284    }
285
286    #[test]
287    fn at_boundary_no_transition() {
288        let mut det = make_detector();
289        // At exactly high_limit — not exceeded, stays NORMAL
290        assert!(det.evaluate(80.0).is_none());
291        // At exactly low_limit — not below, stays NORMAL
292        assert!(det.evaluate(20.0).is_none());
293    }
294
295    #[test]
296    fn high_limit_to_normal_with_deadband() {
297        let mut det = make_detector();
298        det.evaluate(81.0); // → HIGH_LIMIT
299
300        // Still above (high_limit - deadband) = 78.0 — stay HIGH_LIMIT
301        assert!(det.evaluate(79.0).is_none());
302
303        // Drop below deadband threshold
304        let change = det.evaluate(77.0).unwrap();
305        assert_eq!(change.from, EventState::HIGH_LIMIT);
306        assert_eq!(change.to, EventState::NORMAL);
307    }
308
309    #[test]
310    fn low_limit_to_normal_with_deadband() {
311        let mut det = make_detector();
312        det.evaluate(19.0); // → LOW_LIMIT
313
314        // Still below (low_limit + deadband) = 22.0 — stay LOW_LIMIT
315        assert!(det.evaluate(21.0).is_none());
316
317        // Rise above deadband threshold
318        let change = det.evaluate(23.0).unwrap();
319        assert_eq!(change.from, EventState::LOW_LIMIT);
320        assert_eq!(change.to, EventState::NORMAL);
321    }
322
323    #[test]
324    fn high_limit_to_low_limit_direct() {
325        let mut det = make_detector();
326        det.evaluate(81.0); // → HIGH_LIMIT
327
328        // Drop directly below low_limit
329        let change = det.evaluate(19.0).unwrap();
330        assert_eq!(change.from, EventState::HIGH_LIMIT);
331        assert_eq!(change.to, EventState::LOW_LIMIT);
332    }
333
334    #[test]
335    fn low_limit_to_high_limit_direct() {
336        let mut det = make_detector();
337        det.evaluate(19.0); // → LOW_LIMIT
338
339        // Jump directly above high_limit
340        let change = det.evaluate(81.0).unwrap();
341        assert_eq!(change.from, EventState::LOW_LIMIT);
342        assert_eq!(change.to, EventState::HIGH_LIMIT);
343    }
344
345    #[test]
346    fn high_limit_disabled_no_transition() {
347        let mut det = make_detector();
348        det.limit_enable.high_limit_enable = false;
349
350        // Above high_limit but disabled — stays NORMAL
351        assert!(det.evaluate(100.0).is_none());
352    }
353
354    #[test]
355    fn low_limit_disabled_no_transition() {
356        let mut det = make_detector();
357        det.limit_enable.low_limit_enable = false;
358
359        // Below low_limit but disabled — stays NORMAL
360        assert!(det.evaluate(0.0).is_none());
361    }
362
363    #[test]
364    fn both_limits_disabled() {
365        let mut det = make_detector();
366        det.limit_enable = LimitEnable::NONE;
367        assert!(det.evaluate(100.0).is_none());
368        assert!(det.evaluate(0.0).is_none());
369    }
370
371    #[test]
372    fn limit_enable_bits_round_trip() {
373        let le = LimitEnable::BOTH;
374        let bits = le.to_bits();
375        let decoded = LimitEnable::from_bits(bits);
376        assert_eq!(decoded, le);
377
378        let le = LimitEnable {
379            low_limit_enable: true,
380            high_limit_enable: false,
381        };
382        let bits = le.to_bits();
383        let decoded = LimitEnable::from_bits(bits);
384        assert_eq!(decoded, le);
385    }
386
387    #[test]
388    fn deadband_at_exact_boundary() {
389        let mut det = make_detector();
390        det.evaluate(81.0); // → HIGH_LIMIT
391
392        // At exactly (high_limit - deadband) = 78.0 — still HIGH_LIMIT (need to be below)
393        assert!(det.evaluate(78.0).is_none());
394
395        // Just below
396        let change = det.evaluate(77.99).unwrap();
397        assert_eq!(change.to, EventState::NORMAL);
398    }
399
400    #[test]
401    fn event_state_change_derives_event_type() {
402        use bacnet_types::enums::EventType;
403
404        let change = EventStateChange {
405            from: EventState::NORMAL,
406            to: EventState::HIGH_LIMIT,
407        };
408        assert_eq!(change.event_type(), EventType::OUT_OF_RANGE);
409    }
410
411    #[test]
412    fn event_state_change_to_normal_from_high() {
413        use bacnet_types::enums::EventType;
414
415        let change = EventStateChange {
416            from: EventState::HIGH_LIMIT,
417            to: EventState::NORMAL,
418        };
419        assert_eq!(change.event_type(), EventType::OUT_OF_RANGE);
420    }
421
422    #[test]
423    fn event_enable_zero_suppresses_all_notifications() {
424        let mut det = make_detector();
425        det.event_enable = 0x00; // all disabled
426
427        // Should transition internally but return None
428        assert!(det.evaluate(81.0).is_none());
429        assert_eq!(det.event_state, EventState::HIGH_LIMIT); // state still updated
430
431        assert!(det.evaluate(50.0).is_none());
432        assert_eq!(det.event_state, EventState::NORMAL); // state still updated
433
434        assert!(det.evaluate(19.0).is_none());
435        assert_eq!(det.event_state, EventState::LOW_LIMIT); // state still updated
436    }
437
438    #[test]
439    fn event_enable_to_normal_only() {
440        let mut det = make_detector();
441        det.event_enable = 0x04; // only TO_NORMAL
442
443        // NORMAL → HIGH_LIMIT: TO_OFFNORMAL not enabled, suppressed
444        assert!(det.evaluate(81.0).is_none());
445        assert_eq!(det.event_state, EventState::HIGH_LIMIT);
446
447        // HIGH_LIMIT → NORMAL: TO_NORMAL enabled, fires
448        let change = det.evaluate(50.0).unwrap();
449        assert_eq!(change.from, EventState::HIGH_LIMIT);
450        assert_eq!(change.to, EventState::NORMAL);
451
452        // NORMAL → LOW_LIMIT: TO_OFFNORMAL not enabled, suppressed
453        assert!(det.evaluate(19.0).is_none());
454        assert_eq!(det.event_state, EventState::LOW_LIMIT);
455
456        // LOW_LIMIT → NORMAL: TO_NORMAL enabled, fires
457        let change = det.evaluate(50.0).unwrap();
458        assert_eq!(change.from, EventState::LOW_LIMIT);
459        assert_eq!(change.to, EventState::NORMAL);
460    }
461
462    #[test]
463    fn event_enable_to_offnormal_only() {
464        let mut det = make_detector();
465        det.event_enable = 0x01; // only TO_OFFNORMAL
466
467        // NORMAL → HIGH_LIMIT: TO_OFFNORMAL enabled, fires
468        let change = det.evaluate(81.0).unwrap();
469        assert_eq!(change.to, EventState::HIGH_LIMIT);
470
471        // HIGH_LIMIT → NORMAL: TO_NORMAL not enabled, suppressed
472        assert!(det.evaluate(50.0).is_none());
473        assert_eq!(det.event_state, EventState::NORMAL);
474    }
475
476    #[test]
477    fn event_state_change_generic() {
478        use bacnet_types::enums::EventType;
479
480        let change = EventStateChange {
481            from: EventState::NORMAL,
482            to: EventState::NORMAL,
483        };
484        assert_eq!(change.event_type(), EventType::CHANGE_OF_STATE);
485    }
486}