Skip to main content

bacnet_ops/format/
log_records.rs

1//! Event Log and Audit Log records' notifications as typed data (#2103):
2//! an event notification (Clause 12.27.13: the notification's parameters,
3//! its event values through [`EventValues`]) and an audit notification
4//! (Clause 12.64.10), every field a [`Datum`] with its enumerations named,
5//! objects as objects and timestamps rendered.
6//!
7//! Device text stays a [`Datum::Text`] (an event's message, an audit's
8//! comments), and the event values' element is not plain, so a front end
9//! that shows device text as data (the MCP server's envelopes) finds every
10//! string a device wrote by the datum's form alone.
11
12use bacnet_types::constructed::{
13    BACnetAuditNotification, BACnetRecipient, EventNotificationRequest,
14};
15use bacnet_types::enums::{NotifyType, ObjectType, PropertyIdentifier};
16use bacnet_types::primitives::{ObjectIdentifier, PropertyValue};
17use serde::Serialize;
18
19use super::alarms::timestamp;
20use super::notification::EventValues;
21use super::primitive::hex;
22use super::{Datum, Floats, StateTexts, ValueFormat};
23use crate::label::kebab;
24
25/// An Event Log record's event notification, decoded.
26#[derive(Debug, Clone, PartialEq, Serialize)]
27pub struct EventNotificationData {
28    /// The notification's process identifier, a number.
29    pub process_identifier: Datum,
30    /// The device that sent it, a [`Datum::Object`].
31    pub initiating_device: Datum,
32    /// The object whose event it reports.
33    pub event_object: Datum,
34    /// When the transition happened, a `time-stamp` element.
35    pub timestamp: Datum,
36    /// Its notification class, a number.
37    pub notification_class: Datum,
38    /// Its priority, a number.
39    pub priority: Datum,
40    /// The event algorithm, named.
41    pub event_type: Datum,
42    /// The message, as the device wrote it; [`Datum::Null`] when absent.
43    pub message_text: Datum,
44    /// Alarm, event or ack-notification.
45    pub notify_type: Datum,
46    /// Whether the recipient was asked to acknowledge it; [`Datum::Null`]
47    /// for an ack-notification, which doesn't carry it.
48    pub ack_required: Datum,
49    /// The state before the transition, named; [`Datum::Null`] for an
50    /// ack-notification, which doesn't carry it either.
51    pub from_state: Datum,
52    /// The state after it, named.
53    pub to_state: Datum,
54    /// The event values, a `notification-parameters` element decoded
55    /// field by field ([`EventValues`]); [`Datum::Null`] when absent.
56    pub event_values: Datum,
57}
58
59/// An Audit Log record's audit notification, decoded.
60#[derive(Debug, Clone, PartialEq, Serialize)]
61pub struct AuditNotificationData {
62    /// The audited operation, named.
63    pub operation: Datum,
64    /// Who asked for it: a device as an object, or a `recipient` address.
65    pub source_device: Datum,
66    /// The object that asked, when given.
67    pub source_object: Datum,
68    /// When the source sent it, a `time-stamp` element, when given.
69    pub source_timestamp: Datum,
70    /// The source's comment, as the device wrote it.
71    pub source_comment: Datum,
72    /// The source's user identifier, a number, when given.
73    pub source_user_id: Datum,
74    /// The source's user role, a number, when given.
75    pub source_user_role: Datum,
76    /// The invoke ID of the audited request, when given.
77    pub invoke_id: Datum,
78    /// The device the operation targeted.
79    pub target_device: Datum,
80    /// The object it targeted, when given.
81    pub target_object: Datum,
82    /// The property it targeted, named, when given.
83    pub target_property: Datum,
84    /// The array index it targeted, when given.
85    pub target_array_index: Datum,
86    /// The priority it wrote at, when given.
87    pub target_priority: Datum,
88    /// When the target logged it, a `time-stamp` element, when given.
89    pub target_timestamp: Datum,
90    /// The target's comment, as the device wrote it.
91    pub target_comment: Datum,
92    /// The value written, decoded as the target property's.
93    pub target_value: Datum,
94    /// The value the target held, decoded the same way.
95    pub current_value: Datum,
96    /// The error the operation ended in, when it failed: `error_class` and
97    /// `error_code`, named.
98    pub result: Option<AuditResult>,
99}
100
101/// An audited operation's error.
102#[derive(Debug, Clone, PartialEq, Serialize)]
103pub struct AuditResult {
104    /// The error class, named.
105    pub error_class: Datum,
106    /// The error code, named.
107    pub error_code: Datum,
108}
109
110/// The formatter the records name values with: no state texts, every
111/// digit, for objects of `object_type`.
112fn named<R>(object_type: ObjectType, f: impl FnOnce(&ValueFormat<'_>) -> R) -> R {
113    let texts = StateTexts::default();
114    f(&ValueFormat::new(object_type, &texts, Floats::Exact).whole_lists())
115}
116
117fn object(format: &ValueFormat<'_>, object: ObjectIdentifier) -> Datum {
118    format.element_data(
119        PropertyIdentifier::OBJECT_IDENTIFIER,
120        &PropertyValue::ObjectIdentifier(object),
121    )
122}
123
124fn enumerated(format: &ValueFormat<'_>, property: PropertyIdentifier, raw: u32) -> Datum {
125    format.element_data(property, &PropertyValue::Enumerated(raw))
126}
127
128/// An enumeration named by its constant, kebab case, or bare when unknown.
129fn by_name(value: u32, name: Option<&str>) -> Datum {
130    Datum::Enumerated {
131        value,
132        name: name.map(kebab),
133    }
134}
135
136fn number(value: Option<impl Into<u64>>) -> Datum {
137    value.map_or(Datum::Null, |n| Datum::Unsigned(n.into()))
138}
139
140fn text(text: Option<&String>) -> Datum {
141    text.map_or(Datum::Null, |text| Datum::Text(text.clone()))
142}
143
144fn recipient(format: &ValueFormat<'_>, recipient: &BACnetRecipient) -> Datum {
145    match recipient {
146        BACnetRecipient::Device(device) => object(format, *device),
147        BACnetRecipient::Address(address) => Datum::Constructed {
148            element: "recipient".into(),
149            text: format!(
150                "network {} mac {}",
151                address.network_number,
152                hex(&address.mac_address, ":")
153            ),
154            fields: None,
155        },
156    }
157}
158
159impl EventNotificationData {
160    /// One event notification, decoded.
161    pub fn new(notification: &EventNotificationRequest) -> Self {
162        // An acknowledgment's record leaves these out on the wire, so the
163        // decoded request holds placeholders (`EventLogDatum::Notification`).
164        let ack = notification.notify_type == NotifyType::ACK_NOTIFICATION;
165        named(ObjectType::DEVICE, |format| Self {
166            process_identifier: Datum::Unsigned(u64::from(notification.process_identifier)),
167            initiating_device: object(format, notification.initiating_device_identifier),
168            event_object: object(format, notification.event_object_identifier),
169            timestamp: timestamp(&notification.timestamp),
170            notification_class: Datum::Unsigned(u64::from(notification.notification_class)),
171            priority: Datum::Unsigned(u64::from(notification.priority)),
172            event_type: enumerated(
173                format,
174                PropertyIdentifier::EVENT_TYPE,
175                notification.event_type.to_raw(),
176            ),
177            message_text: text(notification.message_text.as_ref()),
178            notify_type: enumerated(
179                format,
180                PropertyIdentifier::NOTIFY_TYPE,
181                notification.notify_type.to_raw(),
182            ),
183            ack_required: if ack {
184                Datum::Null
185            } else {
186                Datum::Boolean(notification.ack_required)
187            },
188            from_state: if ack {
189                Datum::Null
190            } else {
191                enumerated(
192                    format,
193                    PropertyIdentifier::EVENT_STATE,
194                    notification.from_state.to_raw(),
195                )
196            },
197            to_state: enumerated(
198                format,
199                PropertyIdentifier::EVENT_STATE,
200                notification.to_state.to_raw(),
201            ),
202            event_values: notification
203                .event_values
204                .as_ref()
205                .map_or(Datum::Null, |values| EventValues::new(values).data),
206        })
207    }
208}
209
210impl AuditNotificationData {
211    /// One audit notification, decoded. The values written and held decode
212    /// as the target property's, of the target object's type.
213    pub fn new(notification: &BACnetAuditNotification) -> Self {
214        let object_type = notification
215            .target_object
216            .map_or(ObjectType::DEVICE, |target| target.object_type());
217        let property = notification.target_property.as_ref();
218        named(object_type, |format| {
219            let value = |data: Option<&Vec<u8>>| match (data, property) {
220                (Some(data), Some(property)) => format.data(property.property_identifier, data),
221                (Some(data), None) => Datum::Octets {
222                    octets: hex(data, ""),
223                },
224                (None, _) => Datum::Null,
225            };
226            let maybe_object = |target: Option<ObjectIdentifier>| {
227                target.map_or(Datum::Null, |target| object(format, target))
228            };
229            Self {
230                operation: by_name(
231                    notification.operation.to_raw(),
232                    notification.operation.name(),
233                ),
234                source_device: recipient(format, &notification.source_device),
235                source_object: maybe_object(notification.source_object),
236                source_timestamp: notification
237                    .source_timestamp
238                    .as_ref()
239                    .map_or(Datum::Null, timestamp),
240                source_comment: text(notification.source_comment.as_ref()),
241                source_user_id: number(notification.source_user_id),
242                source_user_role: number(notification.source_user_role),
243                invoke_id: number(notification.invoke_id),
244                target_device: recipient(format, &notification.target_device),
245                target_object: maybe_object(notification.target_object),
246                target_property: property.map_or(Datum::Null, |property| {
247                    by_name(
248                        property.property_identifier.to_raw(),
249                        property.property_identifier.name(),
250                    )
251                }),
252                target_array_index: number(property.and_then(|p| p.property_array_index)),
253                target_priority: number(notification.target_priority),
254                target_timestamp: notification
255                    .target_timestamp
256                    .as_ref()
257                    .map_or(Datum::Null, timestamp),
258                target_comment: text(notification.target_comment.as_ref()),
259                target_value: value(notification.target_value.as_ref()),
260                current_value: value(notification.current_value.as_ref()),
261                result: notification.result.map(|(class, code)| AuditResult {
262                    error_class: by_name(u32::from(class.to_raw()), class.name()),
263                    error_code: by_name(u32::from(code.to_raw()), code.name()),
264                }),
265            }
266        })
267    }
268}
269
270#[cfg(test)]
271#[path = "log_records_tests.rs"]
272mod tests;