Skip to main content

bacnet_objects/database/
event_log.rs

1//! The event notifications the device builds, and those it receives, into
2//! its Event Log objects (Clause 12.27). The caller owns synchronization.
3
4use bacnet_types::constructed::{
5    BACnetDeviceObjectPropertyReference, BACnetEventLogRecord, EventLogDatum,
6    EventNotificationRequest, NotificationParameters,
7};
8use bacnet_types::enums::{ErrorClass, ErrorCode, ObjectType, PropertyIdentifier};
9use bacnet_types::error::Error;
10use bacnet_types::primitives::ObjectIdentifier;
11use tracing::{debug, warn};
12
13use super::ObjectDatabase;
14use crate::device_reference::decode_reference;
15
16impl ObjectDatabase {
17    /// Record an event notification this device generated in each of its
18    /// Event Log objects, as a notification record stamped with the Device
19    /// clock's local date and time.
20    ///
21    /// The server calls this for each notification it builds once the
22    /// recipient lookup has read the Notification Class. Each log's own
23    /// lifecycle decides what it keeps, as for a record the application adds
24    /// through `add_record`: a disabled log ignores it, and a full log drops
25    /// its oldest record or, with Stop_When_Full, stops and records that
26    /// instead.
27    ///
28    /// Two kinds go in no log, so that a report can't prompt the next without
29    /// end, through the log it watches or crosswise through another log
30    /// watched in turn. One is a BUFFER_READY report on an Event Log's buffer
31    /// (see `reports_an_event_log_buffer`), as for received notifications. The
32    /// other is any report from an Event Enrollment of this device monitoring
33    /// a property of an Event Log here: whatever its algorithm, it watches
34    /// something each record changes, such as Total_Record_Count, so it isn't
35    /// only BUFFER_READY that would answer its own record. A Trend Log's or
36    /// Trend Log Multiple's report is logged like any other notification:
37    /// those logs take no notifications, so it can't count toward the next.
38    ///
39    /// Without a valid Device clock no record can carry its timestamp, so
40    /// nothing is logged. A log that refuses the record keeps its state; the
41    /// refusal goes to the trace log and the other logs still take it. The
42    /// notification is stored as given, Process Identifier included: Clause
43    /// 12.27 leaves that parameter of a locally generated record to the device.
44    pub fn log_event_notification(&mut self, notification: &EventNotificationRequest) {
45        let logs = self.find_by_type(ObjectType::EVENT_LOG);
46        if logs.is_empty() {
47            return;
48        }
49        if reports_an_event_log_buffer(notification)
50            || self.enrollment_watches_an_event_log(notification.event_object_identifier)
51        {
52            debug!(
53                event_object = %notification.event_object_identifier,
54                "Report on an Event Log not logged"
55            );
56            return;
57        }
58        self.log_notification(logs, notification);
59    }
60
61    /// Whether any Event Log has opted in to the notifications the device
62    /// receives. The server asks before it decodes one.
63    pub fn collects_received_event_notifications(&self) -> bool {
64        self.find_by_type(ObjectType::EVENT_LOG).iter().any(|oid| {
65            self.get(oid)
66                .is_some_and(|log| log.logs_received_event_notifications_internal())
67        })
68    }
69
70    /// Whether [`log_received_event_notification`](Self::log_received_event_notification)
71    /// would record `notification`, received from another device, anywhere:
72    /// some Event Log has opted in, and the notification is one a log may
73    /// take. The server asks before it spends any of a source's allowance.
74    pub fn takes_received_event_notification(
75        &self,
76        notification: &EventNotificationRequest,
77    ) -> bool {
78        self.collects_received_event_notifications() && self.may_log_received(notification)
79    }
80
81    /// Record an event notification this device received, as it decoded, in
82    /// each Event Log that has opted in with
83    /// [`EventLogObject::set_log_received_notifications`](crate::event_log::EventLogObject::set_log_received_notifications),
84    /// stamped with the Device clock's local date and time. Each log's
85    /// lifecycle decides what it keeps, as for the device's own
86    /// notifications, and each record counts toward the log's
87    /// Notification_Threshold.
88    ///
89    /// Two kinds are kept out. A BUFFER_READY report on an Event Log's buffer
90    /// in any device isn't logged (see `reports_an_event_log_buffer`), whatever
91    /// object reports it: another vendor's Event Enrollment running the
92    /// algorithm on its Event Log names the log only there. Logging it could
93    /// prompt a report here that, logged there in turn, prompts the next. Any
94    /// other notification about an Event Log is logged. And one whose
95    /// Initiating Device Identifier names this device isn't logged a second
96    /// time: the device logged it when it built it.
97    ///
98    /// This records whatever it is given. The server calls it only for a
99    /// notification that decoded in full, after holding each source to a few
100    /// records a second, so a flood of them can't push the device's own
101    /// records out of a log faster than that bound.
102    pub fn log_received_event_notification(&mut self, notification: &EventNotificationRequest) {
103        let logs = self.received_notification_logs();
104        if !logs.is_empty() && self.may_log_received(notification) {
105            self.log_notification(logs, notification);
106        }
107    }
108
109    /// The Event Logs that take received notifications.
110    fn received_notification_logs(&self) -> Vec<ObjectIdentifier> {
111        let mut logs = self.find_by_type(ObjectType::EVENT_LOG);
112        logs.retain(|oid| {
113            self.get(oid)
114                .is_some_and(|log| log.logs_received_event_notifications_internal())
115        });
116        logs
117    }
118
119    /// Whether a received notification is one a log may take: it isn't a
120    /// BUFFER_READY report on an Event Log's buffer, and doesn't claim to
121    /// come from this device.
122    fn may_log_received(&self, notification: &EventNotificationRequest) -> bool {
123        !reports_an_event_log_buffer(notification)
124            && self.local_device().identifier() != Some(notification.initiating_device_identifier)
125    }
126
127    /// Add `notification` to each of `logs` as a notification record stamped
128    /// with the Device clock, which has to be valid.
129    fn log_notification(
130        &mut self,
131        logs: Vec<ObjectIdentifier>,
132        notification: &EventNotificationRequest,
133    ) {
134        let Some(frame) = self
135            .clock_frame()
136            .filter(|frame| frame.is_valid_actual_datetime())
137        else {
138            debug!(
139                event_object = %notification.event_object_identifier,
140                "No valid Device clock; event notification not logged"
141            );
142            return;
143        };
144        for oid in logs {
145            let Some(log) = self.get_mut(&oid) else {
146                continue;
147            };
148            let record = BACnetEventLogRecord {
149                date: frame.local_date,
150                time: frame.local_time,
151                log_datum: EventLogDatum::Notification(notification.clone()),
152            };
153            match log.add_event_log_record(record) {
154                Ok(()) => {}
155                Err(error) if is_unsupported(&error) => {
156                    debug!(log = %oid, "Event Log object takes no records");
157                }
158                Err(error) => {
159                    warn!(log = %oid, %error, "Event Log refused an event notification record");
160                }
161            }
162        }
163    }
164
165    /// Whether `event_object` is an Event Enrollment of this device whose
166    /// Object_Property_Reference names an Event Log here.
167    fn enrollment_watches_an_event_log(&self, event_object: ObjectIdentifier) -> bool {
168        match event_object.object_type() {
169            ObjectType::EVENT_ENROLLMENT => self
170                .get(&event_object)
171                .and_then(|enrollment| {
172                    enrollment
173                        .read_property(PropertyIdentifier::OBJECT_PROPERTY_REFERENCE, None)
174                        .ok()
175                })
176                .and_then(|value| {
177                    decode_reference::<BACnetDeviceObjectPropertyReference>(&value).ok()
178                })
179                .is_some_and(|reference| {
180                    reference.object_identifier.object_type() == ObjectType::EVENT_LOG
181                        && self.local_device().is_local(reference.device_identifier)
182                }),
183            _ => false,
184        }
185    }
186}
187
188/// Whether `notification` is a BUFFER_READY report whose Buffer_Property
189/// names an Event Log, in this device or any other. No Event Log records one:
190/// each record a log takes moves its Total_Record_Count, so a log taking
191/// reports on a log's buffer could set off the next report, here or in the
192/// other device, and that report the next, without end.
193fn reports_an_event_log_buffer(notification: &EventNotificationRequest) -> bool {
194    matches!(
195        &notification.event_values,
196        Some(NotificationParameters::BufferReady { buffer_property, .. })
197            if buffer_property.object_identifier.object_type() == ObjectType::EVENT_LOG
198    )
199}
200
201/// The trait default's answer: an object of type Event Log that has no
202/// record insertion.
203fn is_unsupported(error: &Error) -> bool {
204    matches!(error, Error::Protocol { class, code }
205        if *class == ErrorClass::OBJECT.to_raw() as u32
206            && *code == ErrorCode::OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED.to_raw() as u32)
207}
208
209#[cfg(test)]
210#[path = "event_log_tests.rs"]
211mod tests;