Skip to main content

bacnet_ops/format/
notification.rs

1//! An event notification's values (its `event_values`, the
2//! BACnetNotificationParameters choice) for every event type the stack
3//! decodes (#2073): as text, `exceeding value 101.5, flags in-alarm,
4//! deadband 1, limit 100`, and as typed data with each field by name. The
5//! CLI's Event Log records, the terminal UI and the MCP server share them.
6
7use bacnet_encoding::constructed::{decode_authentication_factor, decode_bacnet_property_value};
8use bacnet_types::constructed::{
9    BACnetDeviceObjectReference, BACnetPropertyValue, ChangeOfValueChoice, NotificationParameters,
10};
11use bacnet_types::enums::{EventType, ObjectType};
12use bacnet_types::primitives::{Date, PropertyValue, Time};
13
14use super::commands::factor_text;
15use super::constructed::object_reference;
16use super::fields::{
17    FieldList, bits_value, encoded_value, named, property_state_value, reference_value,
18};
19use super::primitive::{date_time, hex};
20use super::{Datum, Fields, Floats, StateTexts, ValueFormat};
21use crate::label::kebab;
22
23/// The element kind the JSON names the values by.
24const ELEMENT: &str = "notification-parameters";
25
26/// Property values read from a change-of-reliability notification before
27/// the rest is left as hex.
28const MAX_PROPERTY_VALUES: usize = 256;
29
30/// An event notification's values, decoded.
31#[derive(Debug, Clone, PartialEq)]
32pub struct EventValues {
33    /// The event type they belong to, kebab case: `out-of-range`.
34    pub event_type: String,
35    /// The fields as text, `exceeding value 101.5, flags in-alarm, ...`;
36    /// empty for a type with none.
37    pub details: String,
38    /// The values as typed data: a `notification-parameters`
39    /// [`Datum::Constructed`] whose text is `event_type: details` and whose
40    /// fields start with `event_type` (an enumerated), then each value by
41    /// its snake-case name.
42    pub data: Datum,
43}
44
45impl EventValues {
46    /// The values of one notification, every digit kept.
47    pub fn new(parameters: &NotificationParameters) -> Self {
48        Self::with_floats(parameters, Floats::Exact)
49    }
50
51    /// [`Self::new`] with floats written `floats`.
52    pub fn with_floats(parameters: &NotificationParameters, floats: Floats) -> Self {
53        let texts = StateTexts::default();
54        let format = ValueFormat::new(ObjectType::DEVICE, &texts, floats).whole_lists();
55        let (event_type, details, fields) = render(&format, parameters);
56        let text = if details.is_empty() {
57            event_type.clone()
58        } else {
59            format!("{event_type}: {details}")
60        };
61        Self {
62            event_type,
63            details,
64            data: Datum::Constructed {
65                element: ELEMENT.into(),
66                text,
67                fields: Some(fields),
68            },
69        }
70    }
71
72    /// The text of [`Self::data`], `out-of-range: exceeding value 101.5, ...`.
73    pub fn text(&self) -> &str {
74        match &self.data {
75            Datum::Constructed { text, .. } => text,
76            _ => &self.details,
77        }
78    }
79}
80
81/// The event type a choice of the notification parameters reports.
82fn event_type(parameters: &NotificationParameters) -> u32 {
83    use NotificationParameters as N;
84    match parameters {
85        N::ChangeOfBitstring { .. } => 0,
86        N::ChangeOfState { .. } => 1,
87        N::ChangeOfValue { .. } => 2,
88        N::CommandFailure { .. } => 3,
89        N::FloatingLimit { .. } => 4,
90        N::OutOfRange { .. } => 5,
91        N::ComplexEventType { .. } => 6,
92        N::ChangeOfLifeSafety { .. } => 8,
93        N::Extended { .. } => 9,
94        N::BufferReady { .. } => 10,
95        N::UnsignedRange { .. } => 11,
96        N::AccessEvent { .. } => 13,
97        N::DoubleOutOfRange { .. } => 14,
98        N::SignedOutOfRange { .. } => 15,
99        N::UnsignedOutOfRange { .. } => 16,
100        N::ChangeOfCharacterstring { .. } => 17,
101        N::ChangeOfStatusFlags { .. } => 18,
102        N::ChangeOfReliability { .. } => 19,
103        N::ChangeOfDiscreteValue { .. } => 21,
104        N::ChangeOfTimer { .. } => 22,
105    }
106}
107
108/// An enumerated value by name.
109fn enumerated(value: u32, name: impl std::fmt::Display) -> (String, Datum) {
110    let name = kebab(name);
111    (name.clone(), named(value, name))
112}
113
114/// A date and time, as text and as a `date-time` element.
115fn date_and_time(date: &Date, time: &Time) -> (String, Datum) {
116    let text = date_time(date, time);
117    let datum = Datum::Constructed {
118        element: "date-time".into(),
119        text: text.clone(),
120        fields: None,
121    };
122    (text, datum)
123}
124
125/// A device object reference, as text and as its element.
126fn object(reference: &BACnetDeviceObjectReference) -> (String, Datum) {
127    let text = object_reference(reference);
128    let datum = Datum::Constructed {
129        element: "device-object-reference".into(),
130        text: text.clone(),
131        fields: None,
132    };
133    (text, datum)
134}
135
136/// One reported property value: `present-value[2] = 72.5 @8`, and the
137/// same by field.
138fn property_value(format: &ValueFormat<'_>, value: &BACnetPropertyValue) -> (String, Datum) {
139    let (shown, datum) = encoded_value(format, &value.value);
140    let mut property = kebab(value.property_identifier);
141    if let Some(index) = value.property_array_index {
142        property.push_str(&format!("[{index}]"));
143    }
144    let mut text = format!("{property} = {shown}");
145    if let Some(priority) = value.priority {
146        text.push_str(&format!(" @{priority}"));
147    }
148    let fields = Fields::new(vec![
149        (
150            "property_identifier".into(),
151            named(
152                value.property_identifier.to_raw(),
153                kebab(value.property_identifier),
154            ),
155        ),
156        (
157            "property_array_index".into(),
158            value
159                .property_array_index
160                .map_or(Datum::Null, |index| Datum::Unsigned(u64::from(index))),
161        ),
162        ("value".into(), datum),
163        (
164            "priority".into(),
165            value
166                .priority
167                .map_or(Datum::Null, |priority| Datum::Unsigned(u64::from(priority))),
168        ),
169    ]);
170    let datum = Datum::Constructed {
171        element: "property-value".into(),
172        text: text.clone(),
173        fields: Some(fields),
174    };
175    (text, datum)
176}
177
178/// The property values of a change-of-reliability notification, each
179/// decoded, or the octets by tag when they don't decode.
180fn property_values(format: &ValueFormat<'_>, octets: &[u8]) -> Option<Vec<(String, Datum)>> {
181    let mut values = Vec::new();
182    let mut offset = 0;
183    while offset < octets.len() {
184        if values.len() >= MAX_PROPERTY_VALUES {
185            return None;
186        }
187        let (value, next) = decode_bacnet_property_value(octets, offset).ok()?;
188        values.push(property_value(format, &value));
189        offset = next;
190    }
191    Some(values)
192}
193
194/// The event type's name, the fields' text, and the fields.
195fn render(
196    format: &ValueFormat<'_>,
197    parameters: &NotificationParameters,
198) -> (String, String, Fields) {
199    use NotificationParameters as N;
200    let raw = event_type(parameters);
201    // Event type 6 has no name of its own: the standard leaves the number
202    // to proprietary algorithms, which report through this choice.
203    let name = match parameters {
204        N::ComplexEventType { .. } => "complex-event-type".to_string(),
205        _ => kebab(EventType::from_raw(raw)),
206    };
207    let mut f = FieldList::new(format);
208    f.quiet("event_type", named(raw, name.clone()));
209    match parameters {
210        N::ChangeOfBitstring {
211            referenced_bitstring,
212            status_flags,
213        } => {
214            let (unused, data) = referenced_bitstring;
215            f.labelled("referenced_bitstring", "bits", bits_value(*unused, data));
216            f.status_flags("status_flags", "flags", *status_flags);
217        }
218        N::ChangeOfState {
219            new_state,
220            status_flags,
221        } => {
222            f.labelled("new_state", "state", property_state_value(new_state));
223            f.status_flags("status_flags", "flags", *status_flags);
224        }
225        N::ChangeOfValue {
226            new_value,
227            status_flags,
228        } => {
229            match new_value {
230                ChangeOfValueChoice::ChangedBits { unused_bits, data } => {
231                    f.labelled(
232                        "changed_bits",
233                        "changed bits",
234                        bits_value(*unused_bits, data),
235                    );
236                }
237                ChangeOfValueChoice::ChangedValue(value) => {
238                    f.real("changed_value", "value", *value);
239                }
240            }
241            f.status_flags("status_flags", "flags", *status_flags);
242        }
243        N::CommandFailure {
244            command_value,
245            status_flags,
246            feedback_value,
247        } => {
248            f.labelled(
249                "command_value",
250                "commanded",
251                encoded_value(format, command_value),
252            );
253            f.status_flags("status_flags", "flags", *status_flags);
254            f.labelled(
255                "feedback_value",
256                "feedback",
257                encoded_value(format, feedback_value),
258            );
259        }
260        N::FloatingLimit {
261            reference_value,
262            status_flags,
263            setpoint_value,
264            error_limit,
265        } => {
266            f.real("reference_value", "value", *reference_value);
267            f.status_flags("status_flags", "flags", *status_flags);
268            f.real("setpoint_value", "setpoint", *setpoint_value);
269            f.real("error_limit", "error limit", *error_limit);
270        }
271        N::OutOfRange {
272            exceeding_value,
273            status_flags,
274            deadband,
275            exceeded_limit,
276        } => {
277            f.real("exceeding_value", "exceeding value", *exceeding_value);
278            f.status_flags("status_flags", "flags", *status_flags);
279            f.real("deadband", "deadband", *deadband);
280            f.real("exceeded_limit", "limit", *exceeded_limit);
281        }
282        N::ComplexEventType { property_values } => {
283            let values = property_values
284                .iter()
285                .map(|value| property_value(format, value))
286                .collect();
287            f.list("property_values", "values", values);
288        }
289        N::ChangeOfLifeSafety {
290            new_state,
291            new_mode,
292            status_flags,
293            operation_expected,
294        } => {
295            f.labelled(
296                "new_state",
297                "state",
298                enumerated(new_state.to_raw(), new_state),
299            );
300            f.labelled("new_mode", "mode", enumerated(new_mode.to_raw(), new_mode));
301            f.status_flags("status_flags", "flags", *status_flags);
302            f.labelled(
303                "operation_expected",
304                "operation",
305                enumerated(operation_expected.to_raw(), operation_expected),
306            );
307        }
308        N::Extended {
309            vendor_id,
310            extended_event_type,
311            parameters,
312        } => {
313            f.quiet("vendor_id", Datum::Unsigned(u64::from(*vendor_id)));
314            f.field(
315                "extended_event_type",
316                format!("vendor {vendor_id} type {extended_event_type}"),
317                Datum::Unsigned(u64::from(*extended_event_type)),
318            );
319            f.labelled(
320                "parameters",
321                "parameters",
322                encoded_value(format, parameters),
323            );
324        }
325        N::BufferReady {
326            buffer_property,
327            previous_notification,
328            current_notification,
329        } => {
330            f.labelled(
331                "buffer_property",
332                "buffer",
333                reference_value(buffer_property),
334            );
335            f.unsigned(
336                "previous_notification",
337                "previous",
338                u64::from(*previous_notification),
339            );
340            f.unsigned(
341                "current_notification",
342                "current",
343                u64::from(*current_notification),
344            );
345        }
346        N::UnsignedRange {
347            exceeding_value,
348            status_flags,
349            exceeded_limit,
350        } => {
351            f.unsigned("exceeding_value", "exceeding value", *exceeding_value);
352            f.status_flags("status_flags", "flags", *status_flags);
353            f.unsigned("exceeded_limit", "limit", *exceeded_limit);
354        }
355        N::AccessEvent {
356            access_event,
357            status_flags,
358            access_event_tag,
359            access_event_time,
360            access_credential,
361            authentication_factor,
362        } => {
363            f.labelled(
364                "access_event",
365                "event",
366                enumerated(access_event.to_raw(), access_event),
367            );
368            f.status_flags("status_flags", "flags", *status_flags);
369            f.unsigned("access_event_tag", "tag", u64::from(*access_event_tag));
370            let (date, time) = access_event_time;
371            f.labelled("access_event_time", "time", date_and_time(date, time));
372            f.labelled("access_credential", "credential", object(access_credential));
373            match authentication_factor {
374                Some(octets) => {
375                    let pair = match decode_authentication_factor(octets, 0) {
376                        Ok((factor, end)) if end == octets.len() => {
377                            let text = factor_text(&factor);
378                            let datum = Datum::Constructed {
379                                element: "authentication-factor".into(),
380                                text: text.clone(),
381                                fields: None,
382                            };
383                            (text, datum)
384                        }
385                        _ => encoded_value(format, octets),
386                    };
387                    f.labelled("authentication_factor", "factor", pair);
388                }
389                None => f.quiet("authentication_factor", Datum::Null),
390            }
391        }
392        N::DoubleOutOfRange {
393            exceeding_value,
394            status_flags,
395            deadband,
396            exceeded_limit,
397        } => {
398            f.double("exceeding_value", "exceeding value", *exceeding_value);
399            f.status_flags("status_flags", "flags", *status_flags);
400            f.double("deadband", "deadband", *deadband);
401            f.double("exceeded_limit", "limit", *exceeded_limit);
402        }
403        N::SignedOutOfRange {
404            exceeding_value,
405            status_flags,
406            deadband,
407            exceeded_limit,
408        } => {
409            f.signed("exceeding_value", "exceeding value", *exceeding_value);
410            f.status_flags("status_flags", "flags", *status_flags);
411            f.unsigned("deadband", "deadband", *deadband);
412            f.signed("exceeded_limit", "limit", *exceeded_limit);
413        }
414        N::UnsignedOutOfRange {
415            exceeding_value,
416            status_flags,
417            deadband,
418            exceeded_limit,
419        } => {
420            f.unsigned("exceeding_value", "exceeding value", *exceeding_value);
421            f.status_flags("status_flags", "flags", *status_flags);
422            f.unsigned("deadband", "deadband", *deadband);
423            f.unsigned("exceeded_limit", "limit", *exceeded_limit);
424        }
425        N::ChangeOfCharacterstring {
426            changed_value,
427            status_flags,
428            alarm_value,
429        } => {
430            f.value(
431                "changed_value",
432                "value",
433                &PropertyValue::CharacterString(changed_value.clone()),
434            );
435            f.status_flags("status_flags", "flags", *status_flags);
436            f.value(
437                "alarm_value",
438                "alarm value",
439                &PropertyValue::CharacterString(alarm_value.clone()),
440            );
441        }
442        N::ChangeOfStatusFlags {
443            present_value,
444            referenced_flags,
445        } => {
446            match present_value {
447                Some(octets) => f.labelled("present_value", "value", encoded_value(format, octets)),
448                None => f.quiet("present_value", Datum::Null),
449            }
450            f.status_flags("referenced_flags", "flags", *referenced_flags);
451        }
452        N::ChangeOfReliability {
453            reliability,
454            status_flags,
455            property_values: octets,
456        } => {
457            f.labelled(
458                "reliability",
459                "reliability",
460                enumerated(reliability.to_raw(), reliability),
461            );
462            f.status_flags("status_flags", "flags", *status_flags);
463            match property_values(format, octets) {
464                Some(values) => f.list("property_values", "values", values),
465                None => f.field(
466                    "property_values",
467                    format!("values {}", super::raw_text(octets)),
468                    Datum::Raw {
469                        raw: hex(octets, ""),
470                    },
471                ),
472            }
473        }
474        N::ChangeOfDiscreteValue {
475            new_value,
476            status_flags,
477        } => {
478            f.labelled("new_value", "value", encoded_value(format, new_value));
479            f.status_flags("status_flags", "flags", *status_flags);
480        }
481        N::ChangeOfTimer {
482            new_state,
483            status_flags,
484            update_time,
485            last_state_change,
486            initial_timeout,
487            expiration_time,
488        } => {
489            f.labelled(
490                "new_state",
491                "state",
492                enumerated(new_state.to_raw(), new_state),
493            );
494            f.status_flags("status_flags", "flags", *status_flags);
495            let (date, time) = update_time;
496            f.labelled("update_time", "updated", date_and_time(date, time));
497            match last_state_change {
498                Some(change) => f.labelled(
499                    "last_state_change",
500                    "last change",
501                    enumerated(change.to_raw(), change),
502                ),
503                None => f.quiet("last_state_change", Datum::Null),
504            }
505            match initial_timeout {
506                Some(seconds) => f.field(
507                    "initial_timeout",
508                    format!("timeout {seconds} s"),
509                    Datum::Unsigned(u64::from(*seconds)),
510                ),
511                None => f.quiet("initial_timeout", Datum::Null),
512            }
513            match expiration_time {
514                Some((date, time)) => {
515                    f.labelled("expiration_time", "expires", date_and_time(date, time))
516                }
517                None => f.quiet("expiration_time", Datum::Null),
518            }
519        }
520    }
521    let (details, fields) = f.finish();
522    (name, details, fields)
523}
524
525#[cfg(test)]
526#[path = "notification_tests.rs"]
527mod tests;