Skip to main content

bacnet_ops/format/
alarms.rs

1//! Alarm and event summaries as typed data and as table text (#2077): a
2//! GetEventInformation answer, a GetAlarmSummary entry and a
3//! GetEnrollmentSummary entry, with states, event types and notify types
4//! named, transition bits named, timestamps rendered and objects as
5//! objects. The CLI's tables and JSON, the terminal UI and the MCP server
6//! share them.
7//!
8//! Each summary is built from [`Datum`] values, so its JSON uses the value
9//! forms the CLI reference documents (`{"value": 3, "name":
10//! "high-limit"}`, `{"bits": "011", "names": [...]}`), and its table cells
11//! are written from the same data.
12
13use std::collections::HashSet;
14use std::future::Future;
15
16use bacnet_services::alarm_event::{EventSummary, GetEventInformationAck};
17use bacnet_services::alarm_summary::AlarmSummaryEntry;
18use bacnet_services::enrollment_summary::EnrollmentSummaryEntry;
19use bacnet_types::bitstring::EventTransitionBits;
20use bacnet_types::enums::{ObjectType, PropertyIdentifier};
21use bacnet_types::error::Error;
22use bacnet_types::primitives::{BACnetTimeStamp, ObjectIdentifier, PropertyValue};
23use serde::Serialize;
24
25use crate::label::object_label;
26
27use super::{Datum, Floats, StateTexts, ValueFormat, constructed};
28
29/// One value for each of the three event transitions, in their wire
30/// order: to-offnormal, to-fault, to-normal.
31#[derive(Debug, Clone, PartialEq, Serialize)]
32pub struct Transitions<T> {
33    /// The to-offnormal transition's.
34    pub to_offnormal: T,
35    /// The to-fault transition's.
36    pub to_fault: T,
37    /// The to-normal transition's.
38    pub to_normal: T,
39}
40
41impl<T> Transitions<T> {
42    fn from_array([to_offnormal, to_fault, to_normal]: [T; 3]) -> Self {
43        Self {
44            to_offnormal,
45            to_fault,
46            to_normal,
47        }
48    }
49
50    fn as_array(&self) -> [&T; 3] {
51        [&self.to_offnormal, &self.to_fault, &self.to_normal]
52    }
53}
54
55/// One object of a GetEventInformation answer.
56#[derive(Debug, Clone, PartialEq, Serialize)]
57pub struct EventSummaryData {
58    /// The object, a [`Datum::Object`].
59    pub object: Datum,
60    /// Its Event_State, named.
61    pub event_state: Datum,
62    /// Its Acked_Transitions, as named bits.
63    pub acknowledged_transitions: Datum,
64    /// Its Event_Enable, as named bits.
65    pub event_enable: Datum,
66    /// Its Notify_Type, named.
67    pub notify_type: Datum,
68    /// The time of each transition's last event, as `time-stamp`
69    /// elements.
70    pub event_timestamps: Transitions<Datum>,
71    /// Each transition's notification priority, a number.
72    pub event_priorities: Transitions<Datum>,
73}
74
75/// A GetEventInformation answer, or several pages of one joined.
76#[derive(Debug, Clone, PartialEq, Serialize)]
77pub struct EventInformationData {
78    /// The objects, in the device's order.
79    pub summaries: Vec<EventSummaryData>,
80    /// Whether the device holds more than these: its last answer said so,
81    /// or a cap stopped the reading early.
82    pub more_events: bool,
83}
84
85/// One entry of a GetAlarmSummary answer.
86#[derive(Debug, Clone, PartialEq, Serialize)]
87pub struct AlarmSummaryData {
88    /// The object, a [`Datum::Object`].
89    pub object: Datum,
90    /// Its event state, named.
91    pub alarm_state: Datum,
92    /// Its Acked_Transitions, as named bits.
93    pub acknowledged_transitions: Datum,
94}
95
96/// One entry of a GetEnrollmentSummary answer.
97#[derive(Debug, Clone, PartialEq, Serialize)]
98pub struct EnrollmentSummaryData {
99    /// The object, a [`Datum::Object`].
100    pub object: Datum,
101    /// Its event algorithm, named.
102    pub event_type: Datum,
103    /// Its event state, named.
104    pub event_state: Datum,
105    /// The priority of its notifications, a number.
106    pub priority: Datum,
107    /// Its notification class, a number; [`Datum::Null`] when the device
108    /// left it out.
109    pub notification_class: Datum,
110}
111
112/// The formatter the summaries name values with: no object's state texts,
113/// every digit.
114fn named<R>(f: impl FnOnce(&ValueFormat<'_>) -> R) -> R {
115    let texts = StateTexts::default();
116    f(&ValueFormat::new(ObjectType::DEVICE, &texts, Floats::Exact))
117}
118
119fn object(format: &ValueFormat<'_>, object: ObjectIdentifier) -> Datum {
120    format.element_data(
121        PropertyIdentifier::OBJECT_IDENTIFIER,
122        &PropertyValue::ObjectIdentifier(object),
123    )
124}
125
126fn enumerated(format: &ValueFormat<'_>, property: PropertyIdentifier, raw: u32) -> Datum {
127    format.element_data(property, &PropertyValue::Enumerated(raw))
128}
129
130/// Transition bits as the property `property` names them.
131fn transitions(
132    format: &ValueFormat<'_>,
133    property: PropertyIdentifier,
134    bits: EventTransitionBits,
135) -> Datum {
136    let value = PropertyValue::BitString {
137        unused_bits: 5,
138        data: vec![bits.to_bacnet()],
139    };
140    format.element_data(property, &value)
141}
142
143pub(super) fn timestamp(stamp: &BACnetTimeStamp) -> Datum {
144    Datum::Constructed {
145        element: "time-stamp".into(),
146        text: constructed::timestamp_text(stamp),
147        fields: None,
148    }
149}
150
151impl EventSummaryData {
152    /// The column headings of [`Self::cells`].
153    pub const HEADERS: [&'static str; 9] = [
154        "Object",
155        "State",
156        "Acked",
157        "Enabled",
158        "Notify",
159        "To-offnormal",
160        "To-fault",
161        "To-normal",
162        "Priorities",
163    ];
164
165    /// One object of a GetEventInformation answer, decoded.
166    pub fn new(summary: &EventSummary) -> Self {
167        named(|format| Self {
168            object: object(format, summary.object_identifier),
169            event_state: enumerated(
170                format,
171                PropertyIdentifier::EVENT_STATE,
172                summary.event_state.to_raw(),
173            ),
174            acknowledged_transitions: transitions(
175                format,
176                PropertyIdentifier::ACKED_TRANSITIONS,
177                summary.acknowledged_transitions,
178            ),
179            event_enable: transitions(
180                format,
181                PropertyIdentifier::EVENT_ENABLE,
182                summary.event_enable,
183            ),
184            notify_type: enumerated(
185                format,
186                PropertyIdentifier::NOTIFY_TYPE,
187                summary.notify_type.to_raw(),
188            ),
189            event_timestamps: Transitions::from_array(
190                summary.event_timestamps.each_ref().map(timestamp),
191            ),
192            event_priorities: Transitions::from_array(
193                summary
194                    .event_priorities
195                    .map(|priority| Datum::Unsigned(u64::from(priority))),
196            ),
197        })
198    }
199
200    /// The table row, under [`Self::HEADERS`]: the priorities as
201    /// `to-offnormal/to-fault/to-normal`, `3/5/200`.
202    pub fn cells(&self) -> Vec<String> {
203        let [offnormal, fault, normal] = self.event_timestamps.as_array().map(cell);
204        let priorities = self.event_priorities.as_array().map(cell).join("/");
205        vec![
206            cell(&self.object),
207            cell(&self.event_state),
208            cell(&self.acknowledged_transitions),
209            cell(&self.event_enable),
210            cell(&self.notify_type),
211            offnormal,
212            fault,
213            normal,
214            priorities,
215        ]
216    }
217}
218
219impl EventInformationData {
220    /// One GetEventInformation answer, decoded.
221    pub fn new(ack: &GetEventInformationAck) -> Self {
222        Self::from_summaries(&ack.list_of_event_summaries, ack.more_events)
223    }
224
225    /// Summaries read over one or more answers, decoded; `more_events`
226    /// says whether the device holds more.
227    pub fn from_summaries(summaries: &[EventSummary], more_events: bool) -> Self {
228        Self {
229            summaries: summaries.iter().map(EventSummaryData::new).collect(),
230            more_events,
231        }
232    }
233}
234
235impl AlarmSummaryData {
236    /// The column headings of [`Self::cells`].
237    pub const HEADERS: [&'static str; 3] = ["Object", "State", "Acked"];
238
239    /// One GetAlarmSummary entry, decoded.
240    pub fn new(entry: &AlarmSummaryEntry) -> Self {
241        named(|format| Self {
242            object: object(format, entry.object_identifier),
243            alarm_state: enumerated(
244                format,
245                PropertyIdentifier::EVENT_STATE,
246                entry.alarm_state.to_raw(),
247            ),
248            acknowledged_transitions: transitions(
249                format,
250                PropertyIdentifier::ACKED_TRANSITIONS,
251                entry.acknowledged_transitions,
252            ),
253        })
254    }
255
256    /// The table row, under [`Self::HEADERS`].
257    pub fn cells(&self) -> Vec<String> {
258        vec![
259            cell(&self.object),
260            cell(&self.alarm_state),
261            cell(&self.acknowledged_transitions),
262        ]
263    }
264}
265
266impl EnrollmentSummaryData {
267    /// The column headings of [`Self::cells`].
268    pub const HEADERS: [&'static str; 5] = [
269        "Object",
270        "Event type",
271        "State",
272        "Priority",
273        "Notification class",
274    ];
275
276    /// One GetEnrollmentSummary entry, decoded.
277    pub fn new(entry: &EnrollmentSummaryEntry) -> Self {
278        named(|format| Self {
279            object: object(format, entry.object_identifier),
280            event_type: enumerated(
281                format,
282                PropertyIdentifier::EVENT_TYPE,
283                entry.event_type.to_raw(),
284            ),
285            event_state: enumerated(
286                format,
287                PropertyIdentifier::EVENT_STATE,
288                entry.event_state.to_raw(),
289            ),
290            priority: Datum::Unsigned(u64::from(entry.priority)),
291            notification_class: entry
292                .notification_class
293                .map_or(Datum::Null, |class| Datum::Unsigned(u64::from(class))),
294        })
295    }
296
297    /// The table row, under [`Self::HEADERS`]; a missing notification
298    /// class as `-`.
299    pub fn cells(&self) -> Vec<String> {
300        vec![
301            cell(&self.object),
302            cell(&self.event_type),
303            cell(&self.event_state),
304            cell(&self.priority),
305            cell(&self.notification_class),
306        ]
307    }
308}
309
310/// A summary field as a table cell: an object as `analog-input 1`, an
311/// enumeration by name (its number when it has none), bits by their set
312/// names, comma separated to read apart from the table's bars, or `none`,
313/// a timestamp as its text, and an absent value as `-`: the formatter's
314/// text for each (a test pins them together).
315pub(crate) fn cell(datum: &Datum) -> String {
316    match datum {
317        Datum::Null => "-".into(),
318        Datum::Unsigned(n) => n.to_string(),
319        Datum::Object {
320            object_type,
321            instance,
322            ..
323        } => format!("{object_type} {instance}"),
324        Datum::Enumerated { value, name } => name.clone().unwrap_or_else(|| value.to_string()),
325        Datum::Bits {
326            names: Some(names), ..
327        } if names.is_empty() => "none".into(),
328        Datum::Bits {
329            names: Some(names), ..
330        } => names.join(", "),
331        Datum::Bits { bits, .. } => format!("bits({bits})"),
332        Datum::Constructed { text, .. } => text.clone(),
333        Datum::Boolean(b) => b.to_string(),
334        Datum::Signed(n) => n.to_string(),
335        Datum::Real(v) => v.to_string(),
336        Datum::Double(v) => v.to_string(),
337        Datum::NonFinite { float } => float.clone(),
338        Datum::Text(text) => text.clone(),
339        Datum::Date { date } => date.clone(),
340        Datum::Time { time } => time.clone(),
341        Datum::Octets { octets } => format!("0x{octets}"),
342        Datum::Raw { raw } => format!("[raw {raw}]"),
343        Datum::List(items) => items.iter().map(cell).collect::<Vec<_>>().join(", "),
344    }
345}
346
347/// Every summary a device holds, read one GetEventInformation answer at
348/// a time: `fetch` asks for the objects after its argument (`None` for
349/// the first answer), and the reading goes on while the device says more
350/// remain, up to `max` summaries. It returns the summaries and whether
351/// the device holds more than them.
352///
353/// A device that names an object twice, or says more remain but sends
354/// nothing, would show duplicates or page forever, so either is an error.
355pub async fn read_event_information<F, Fut>(
356    max: usize,
357    fetch: F,
358) -> Result<(Vec<EventSummary>, bool), Error>
359where
360    F: FnMut(Option<ObjectIdentifier>) -> Fut,
361    Fut: Future<Output = Result<GetEventInformationAck, Error>>,
362{
363    read_event_information_until(max, || false, fetch).await
364}
365
366/// [`read_event_information`], stopping early when `stop` says so: it is
367/// asked before every answer after the first, and a stop returns the
368/// summaries read so far with "more" set, so the caller can go on after the
369/// last of them later (a call out of time, say).
370pub async fn read_event_information_until<S, F, Fut>(
371    max: usize,
372    mut stop: S,
373    mut fetch: F,
374) -> Result<(Vec<EventSummary>, bool), Error>
375where
376    S: FnMut() -> bool,
377    F: FnMut(Option<ObjectIdentifier>) -> Fut,
378    Fut: Future<Output = Result<GetEventInformationAck, Error>>,
379{
380    let mut summaries: Vec<EventSummary> = Vec::new();
381    let mut seen = HashSet::new();
382    let mut after = None;
383    loop {
384        if after.is_some() && stop() {
385            return Ok((summaries, true));
386        }
387        let ack = fetch(after).await?;
388        for summary in ack.list_of_event_summaries {
389            if !seen.insert(summary.object_identifier.encode()) {
390                return Err(Error::Encoding(format!(
391                    "GetEventInformation named {} again",
392                    object_label(summary.object_identifier)
393                )));
394            }
395            summaries.push(summary);
396        }
397        if summaries.len() >= max {
398            let more = ack.more_events || summaries.len() > max;
399            summaries.truncate(max);
400            return Ok((summaries, more));
401        }
402        if !ack.more_events {
403            return Ok((summaries, false));
404        }
405        // Every object is new, so the cursor moves unless the page was
406        // empty.
407        match summaries.last().map(|summary| summary.object_identifier) {
408            Some(last) if after != Some(last) => after = Some(last),
409            _ => {
410                return Err(Error::decoding(
411                    0,
412                    "GetEventInformation said more events remain but sent no new object",
413                ));
414            }
415        }
416    }
417}
418
419#[cfg(test)]
420#[path = "alarms_tests.rs"]
421mod tests;