bacnet_objects/event_enrollment/mod.rs
1//! EventEnrollment (type 9) object per ASHRAE 135-2020 Clause 12.12.
2
3use bacnet_types::bitstring::EventTransitionBits;
4use bacnet_types::constructed::{
5 BACnetDeviceObjectPropertyReference, BACnetEventParameter, FaultParameters,
6};
7use bacnet_types::enums::{
8 ErrorClass, ErrorCode, EventState, EventType, NotifyType, ObjectType, PropertyIdentifier,
9 Reliability,
10};
11use bacnet_types::error::Error;
12use bacnet_types::primitives::{BACnetTimeStamp, ObjectIdentifier, PropertyValue, StatusFlags};
13use std::borrow::Cow;
14
15use crate::common::{self, read_common_properties};
16use crate::event::history::EventHistory;
17use crate::event::{
18 EnrollmentSummaryCapability, EventTransitionCommit, EventTransitionCommitError,
19};
20use crate::property_metadata::PropertyMetadata;
21use crate::traits::BACnetObject;
22
23mod alert;
24mod metadata;
25mod parameters;
26mod state;
27mod transition;
28pub use alert::AlertEnrollmentObject;
29pub use state::{EventEnrollmentEvalState, EventEnrollmentMonitoredSource, EventEnrollmentPending};
30pub use transition::EventEnrollmentReliabilityCommit;
31
32/// BACnet EventEnrollment object.
33///
34/// Provides algorithmic event detection for a referenced object property.
35/// The `event_parameters` are stored as a structured
36/// [`BACnetEventParameter`], preserving algorithm alternatives and unknown
37/// (vendor/reserved) values across a complete property round trip.
38pub struct EventEnrollmentObject {
39 oid: ObjectIdentifier,
40 name: String,
41 description: String,
42 /// The algorithm the legacy octet layout of Event_Parameters runs, which
43 /// names none itself; every other alternative names its own (#1868).
44 legacy_event_type: EventType,
45 notify_type: NotifyType,
46 event_parameters: BACnetEventParameter,
47 object_property_reference: Option<BACnetDeviceObjectPropertyReference>,
48 event_state: EventState,
49 event_enable: EventTransitionBits,
50 acked_transitions: EventTransitionBits,
51 event_history: EventHistory,
52 event_detection_enable: bool,
53 notification_class: u32,
54 fault_parameters: Option<FaultParameters>,
55 status_flags: StatusFlags,
56 reliability: Reliability,
57 /// `Time_Delay_Normal` (property 356, Table 12-14 conformance O): Clause
58 /// 12.12 feeds this value to the enrollment's algorithm as its
59 /// pTimeDelayNormal input. `None` is the not-configured case and takes on
60 /// the `Time_Delay` carried inside `event_parameters` (Table 12-15 maps
61 /// `Time_Delay` to pTimeDelay for every evaluated algorithm), following
62 /// Clause 13.3's fallback from absent pTimeDelayNormal to pTimeDelay.
63 time_delay_normal: Option<u32>,
64 /// Delayed transition counting down, if any. In-memory only.
65 pending: Option<EventEnrollmentPending>,
66 /// Effective source of the private evaluation state. In-memory only.
67 monitored_reference: Option<EventEnrollmentMonitoredSource>,
68 /// CHANGE_OF_VALUE detection baseline (Clause 13.3.3). In-memory only.
69 cov_baseline: Option<PropertyValue>,
70 /// Domain-tagged monitored value that caused the last OFFNORMAL transition
71 /// (Clause 13.3.2 condition (c)). In-memory only.
72 last_offnormal_value: Option<u64>,
73}
74
75impl EventEnrollmentObject {
76 /// Create a new EventEnrollment object.
77 ///
78 /// Event_Type follows Event_Parameters (Clause 12.12.5): it reads NONE
79 /// while the parameters are the unset `none` alternative and the
80 /// matching type once parameters are set. `legacy_event_type` only
81 /// matters for the pre-framing octet layout of Event_Parameters, which
82 /// carries no algorithm of its own: it is the algorithm that layout runs
83 /// and the Event_Type served while it is held.
84 pub fn new(
85 instance: u32,
86 name: impl Into<String>,
87 legacy_event_type: EventType,
88 ) -> Result<Self, Error> {
89 let oid = ObjectIdentifier::new(ObjectType::EVENT_ENROLLMENT, instance)?;
90 Ok(Self {
91 oid,
92 name: name.into(),
93 description: String::new(),
94 legacy_event_type,
95 notify_type: NotifyType::ALARM,
96 event_parameters: Self::UNSET_EVENT_PARAMETERS,
97 object_property_reference: None,
98 event_state: EventState::NORMAL,
99 event_enable: EventTransitionBits::all(),
100 // Clause 12.12 starts each flag TRUE until a transition of its
101 // kind first happens on the object. That all-TRUE initial value
102 // is also the initial condition the detection-disabled reset
103 // restores, so `RESET_ACKED_TRANSITIONS` names it once.
104 acked_transitions: Self::RESET_ACKED_TRANSITIONS,
105 event_history: EventHistory::default(),
106 event_detection_enable: true,
107 notification_class: 0,
108 fault_parameters: None,
109 status_flags: StatusFlags::empty(),
110 reliability: Reliability::NO_FAULT_DETECTED,
111 // Absent so the delay behavior equals the normative pTimeDelay
112 // fallback until a client writes the property — never an error,
113 // never a zero.
114 time_delay_normal: None,
115 pending: None,
116 monitored_reference: None,
117 cov_baseline: None,
118 last_offnormal_value: None,
119 })
120 }
121
122 /// `Acked_Transitions` in its initial condition: every transition flag TRUE,
123 /// the value a flag holds until its transition kind first occurs on the
124 /// object (ASHRAE 135-2020 Clause 12.12).
125 const RESET_ACKED_TRANSITIONS: EventTransitionBits = EventTransitionBits::all();
126
127 /// `Event_Parameters` before anything sets them: the `none` alternative
128 /// of the BACnetEventParameter CHOICE (context tag 20, a NULL; Clause
129 /// 21), so an unset enrollment serves a member of the property's
130 /// datatype (#1868). The evaluator has no algorithm to run for it.
131 const UNSET_EVENT_PARAMETERS: BACnetEventParameter = BACnetEventParameter::Opaque {
132 tag: 20,
133 data: Vec::new(),
134 };
135
136 /// Put the object in the state ASHRAE 135-2020 Clause 13.2.2.1 prescribes
137 /// for disabled detection (`Event_Detection_Enable` FALSE): Event_State
138 /// reads NORMAL, the three per-transition properties (Event_Time_Stamps,
139 /// Event_Message_Texts, Acked_Transitions) go back to their initial
140 /// values, and no transition is generated while detection stays off.
141 ///
142 /// The monitored-source identity, pending countdown, and both baselines
143 /// are cleared too: they are extensions of the same event-state-detection
144 /// state machine whose evaluation the clause suspends, so a stale
145 /// countdown must not survive into the next
146 /// enabled period and fire against a condition the object no longer
147 /// observes. The intrinsic types make the same choice for their detectors
148 /// (`analog/input.rs` clears `detector.pending` on the identical write).
149 /// The COV baseline's initialization on re-enable is the local matter
150 /// Clause 13.3.3 assigns it; clearing is consistent with the first-sample
151 /// policy.
152 fn apply_detection_disabled_reset(&mut self) {
153 self.event_state = EventState::NORMAL;
154 self.acked_transitions = Self::RESET_ACKED_TRANSITIONS;
155 self.event_history.reset();
156 self.pending = None;
157 self.monitored_reference = None;
158 self.cov_baseline = None;
159 self.last_offnormal_value = None;
160 }
161
162 /// Set the description string.
163 pub fn set_description(&mut self, desc: impl Into<String>) {
164 self.description = desc.into();
165 }
166
167 /// Set Object_Property_Reference, the property the enrollment monitors,
168 /// or `None` for none. The property is read-only over the network.
169 /// A reference whose object or Device is at the reserved instance
170 /// 4194303 is the unset form, so it leaves the enrollment without one
171 /// (#1417).
172 ///
173 /// A reference whose device identifier isn't a Device object is refused
174 /// with PROPERTY / VALUE_OUT_OF_RANGE and the reference set before is
175 /// kept (#1308).
176 pub fn set_object_property_reference(
177 &mut self,
178 reference: Option<BACnetDeviceObjectPropertyReference>,
179 ) -> Result<(), Error> {
180 if let Some(reference) = &reference {
181 crate::device_reference::check_device_member(reference.device_identifier)?;
182 }
183 self.object_property_reference = reference.and_then(crate::device_reference::set_or_unset);
184 self.pending = None;
185 Ok(())
186 }
187
188 /// Set the structured event parameters.
189 pub fn set_event_parameters(&mut self, params: BACnetEventParameter) {
190 self.event_parameters = params;
191 self.pending = None;
192 }
193
194 /// Set the fault parameters for this event enrollment.
195 pub fn set_fault_parameters(&mut self, fp: Option<FaultParameters>) {
196 self.fault_parameters = fp;
197 }
198
199 /// Set the event state.
200 ///
201 /// A configuration/seeding helper, not a lifecycle path — the evaluator
202 /// uses [`BACnetObject::set_event_state_internal`]. It honors the same
203 /// Clause 13.2.2.1 rule: while `Event_Detection_Enable` is FALSE the object
204 /// must read NORMAL, so a non-NORMAL seed is ignored rather than silently
205 /// breaking the invariant. Without this the public API would offer a way
206 /// around a guard the rest of the object enforces.
207 pub fn set_event_state(&mut self, state: EventState) {
208 if !self.event_detection_enable && state != EventState::NORMAL {
209 return;
210 }
211 self.event_state = state;
212 }
213
214 /// Set the notification class.
215 pub fn set_notification_class(&mut self, nc: u32) {
216 self.notification_class = nc;
217 }
218
219 /// Set `Event_Enable`: the transitions whose notifications are distributed.
220 ///
221 /// Flags outside the three named transitions are dropped.
222 pub fn set_event_enable(&mut self, enable: EventTransitionBits) {
223 self.event_enable = enable & EventTransitionBits::all();
224 }
225
226 /// Set `Time_Delay_Normal` (the pTimeDelayNormal parameter). `None`
227 /// restores the not-configured case, which takes on the
228 /// `Event_Parameters` `Time_Delay` value (Clause 13.3 fallback).
229 pub fn set_time_delay_normal(&mut self, delay: Option<u32>) {
230 self.time_delay_normal = delay;
231 self.pending = None;
232 }
233
234 /// The pTimeDelay the stored `Event_Parameters` supply: the `time_delay`
235 /// field every evaluated algorithm carries (Table 12-15). Unmodeled
236 /// alternatives — including `none` and the `0xFF` legacy octet layout,
237 /// which have no time-delay slot — contribute zero, so their TDN fallback reads as 0
238 /// and their evaluation fires immediately, exactly as they did before
239 /// delay honoring existed.
240 fn event_parameters_time_delay(&self) -> u32 {
241 use BACnetEventParameter as P;
242 match &self.event_parameters {
243 P::ChangeOfBitstring { time_delay, .. }
244 | P::ChangeOfState { time_delay, .. }
245 | P::ChangeOfValue { time_delay, .. }
246 | P::FloatingLimit { time_delay, .. }
247 | P::OutOfRange { time_delay, .. } => *time_delay,
248 _ => 0,
249 }
250 }
251
252 /// Event_Type as Event_Parameters gives it (Clause 12.12.5): each
253 /// framed alternative's tag is its event type (`none`, tag 20, is NONE).
254 /// The legacy octet layout uses the constructor's type, and so does an
255 /// opaque alternative at tag 64 or above: such a tag names no event type
256 /// the standard reserves (0 to 63), so nothing is derived from it.
257 fn event_type(&self) -> EventType {
258 match &self.event_parameters {
259 BACnetEventParameter::Opaque { tag, .. } if *tag >= 64 => self.legacy_event_type,
260 parameters => EventType::from_raw(u32::from(parameters.tag())),
261 }
262 }
263
264 fn effective_fault_parameters(&self) -> &FaultParameters {
265 self.fault_parameters
266 .as_ref()
267 .unwrap_or(&FaultParameters::FaultNone)
268 }
269}
270
271impl BACnetObject for EventEnrollmentObject {
272 fn object_identifier(&self) -> ObjectIdentifier {
273 self.oid
274 }
275
276 fn object_name(&self) -> &str {
277 &self.name
278 }
279
280 fn enrollment_summary_capability_internal(&self) -> Option<EnrollmentSummaryCapability> {
281 // Any enrollment with a monitored reference can fault: the monitored
282 // object's fault and the configuration checks run before the
283 // parameters are looked at, so whatever Event_Parameters hold (`none`,
284 // an unmodeled alternative, Extended) its TO_FAULT is listed and
285 // acknowledged (#1868). Event_Detection_Enable FALSE is left to the
286 // summary and acknowledgment paths.
287 self.object_property_reference
288 .is_some()
289 .then(|| EnrollmentSummaryCapability {
290 event_type: self.event_type(),
291 last_transition: self.event_history.last_transition(),
292 })
293 }
294
295 fn read_property(
296 &self,
297 property: PropertyIdentifier,
298 array_index: Option<u32>,
299 ) -> Result<PropertyValue, Error> {
300 if property == PropertyIdentifier::STATUS_FLAGS {
301 // Clause 12.12 fixes OVERRIDDEN and OUT_OF_SERVICE at FALSE;
302 // IN_ALARM follows Event_State and FAULT follows Reliability.
303 return Ok(common::compute_status_flags(
304 StatusFlags::empty(),
305 self.reliability,
306 false,
307 self.event_state,
308 ));
309 }
310 // Table 12-14 has no Out_Of_Service (#1064).
311 if let Some(result) =
312 read_common_properties!(self, property, array_index, no_out_of_service)
313 {
314 return result;
315 }
316 // Event_Time_Stamps, Event_Message_Texts and the reporting options
317 // (Event_Message_Texts_Config and the Event_Algorithm_Inhibit pair,
318 // #1507), all optional or required rows of Table 12-14.
319 if let Some(result) = self.event_history.read(property, array_index) {
320 return result;
321 }
322 match property {
323 p if p == PropertyIdentifier::OBJECT_TYPE => Ok(PropertyValue::Enumerated(
324 ObjectType::EVENT_ENROLLMENT.to_raw(),
325 )),
326 p if p == PropertyIdentifier::EVENT_TYPE => {
327 Ok(PropertyValue::Enumerated(self.event_type().to_raw()))
328 }
329 p if p == PropertyIdentifier::NOTIFY_TYPE => {
330 Ok(PropertyValue::Enumerated(self.notify_type.to_raw()))
331 }
332 p if p == PropertyIdentifier::EVENT_PARAMETERS => {
333 let mut buf = bytes::BytesMut::new();
334 bacnet_encoding::constructed::encode_event_parameter(
335 &mut buf,
336 &self.event_parameters,
337 )?;
338 Ok(PropertyValue::ApplicationData(buf.to_vec()))
339 }
340 // Table 12-14's BACnetDeviceObjectPropertyReference, its optional
341 // index and Device members present only when set (#1182); the
342 // unset form while the enrollment has no reference (#1417).
343 p if p == PropertyIdentifier::OBJECT_PROPERTY_REFERENCE => {
344 Ok(crate::device_reference::optional_reference_value(
345 self.object_property_reference.as_ref(),
346 ObjectType::ANALOG_INPUT,
347 ))
348 }
349 p if p == PropertyIdentifier::EVENT_STATE => {
350 Ok(PropertyValue::Enumerated(self.event_state.to_raw()))
351 }
352 p if p == PropertyIdentifier::EVENT_ENABLE => Ok(PropertyValue::BitString {
353 unused_bits: 5,
354 data: vec![self.event_enable.to_bacnet()],
355 }),
356 p if p == PropertyIdentifier::ACKED_TRANSITIONS => Ok(PropertyValue::BitString {
357 unused_bits: 5,
358 data: vec![self.acked_transitions.to_bacnet()],
359 }),
360 p if p == PropertyIdentifier::EVENT_DETECTION_ENABLE => {
361 Ok(PropertyValue::Boolean(self.event_detection_enable))
362 }
363 p if p == PropertyIdentifier::NOTIFICATION_CLASS => {
364 Ok(PropertyValue::Unsigned(self.notification_class as u64))
365 }
366 p if p == PropertyIdentifier::FAULT_TYPE => Ok(PropertyValue::Enumerated(
367 self.effective_fault_parameters().fault_type().to_raw(),
368 )),
369 p if p == PropertyIdentifier::FAULT_PARAMETERS => {
370 let mut buf = bytes::BytesMut::new();
371 bacnet_encoding::constructed::encode_fault_parameters(
372 &mut buf,
373 self.effective_fault_parameters(),
374 )?;
375 Ok(PropertyValue::ApplicationData(buf.to_vec()))
376 }
377 p if p == PropertyIdentifier::TIME_DELAY_NORMAL => {
378 // Clause 13.3 supplies pTimeDelay as the fallback when
379 // pTimeDelayNormal is absent —
380 // the read-back of an unwritten Time_Delay_Normal is the
381 // Event_Parameters Time_Delay, matching the algorithm's
382 // behavior (mirrors the intrinsic types' read arm).
383 Ok(PropertyValue::Unsigned(
384 self.time_delay_normal
385 .unwrap_or_else(|| self.event_parameters_time_delay())
386 as u64,
387 ))
388 }
389 _ => Err(common::unknown_property_error()),
390 }
391 }
392
393 fn write_property(
394 &mut self,
395 property: PropertyIdentifier,
396 array_index: Option<u32>,
397 value: PropertyValue,
398 _priority: Option<u8>,
399 ) -> Result<(), Error> {
400 if property == PropertyIdentifier::NOTIFY_TYPE {
401 // BACnetNotifyType is a closed three-value production {alarm,
402 // event, ack-notification} (Clause 21); out-of-production values
403 // are PROPERTY / VALUE_OUT_OF_RANGE (Clause 15.9.1.3).
404 if let PropertyValue::Enumerated(v) = value {
405 let notify_type = NotifyType::from_raw(v);
406 if !NotifyType::ALL_NAMED.iter().any(|&(_, n)| n == notify_type) {
407 return Err(common::value_out_of_range_error());
408 }
409 self.notify_type = notify_type;
410 return Ok(());
411 }
412 return Err(common::invalid_data_type_error());
413 }
414 if property == PropertyIdentifier::NOTIFICATION_CLASS {
415 if let PropertyValue::Unsigned(v) = value {
416 self.notification_class = common::u64_to_u32(v)?;
417 return Ok(());
418 }
419 return Err(common::invalid_data_type_error());
420 }
421 if property == PropertyIdentifier::EVENT_ENABLE {
422 // BACnetEventTransitionBits is a 3-bit production (Clause 21):
423 // the written BitString must declare its canonical shape.
424 if let PropertyValue::BitString { unused_bits, data } = &value {
425 let byte = common::check_fixed_width_bit_string(*unused_bits, data, 3)?;
426 self.event_enable = EventTransitionBits::from_bacnet(&[byte]);
427 return Ok(());
428 }
429 return Err(common::invalid_data_type_error());
430 }
431 if property == PropertyIdentifier::EVENT_DETECTION_ENABLE {
432 if let PropertyValue::Boolean(v) = value {
433 self.event_detection_enable = v;
434 // Clause 12.12 states the disabled condition as an invariant —
435 // disabled detection requires a persistent NORMAL Event_State —
436 // not as an action taken later. Resetting here rather than
437 // leaving it to the periodic evaluator closes the window in
438 // which a disabled object would still answer ReadProperty and
439 // the event-summarization services with a stale alarm state.
440 if !v {
441 self.apply_detection_disabled_reset();
442 }
443 return Ok(());
444 }
445 return Err(common::invalid_data_type_error());
446 }
447 // EVENT_STATE is algorithmically derived (ASHRAE 135-2020 Clause 12.12)
448 // and read-only over the network: a WriteProperty of EVENT_STATE falls
449 // through to WRITE_ACCESS_DENIED below. The evaluator sets it through
450 // the internal `set_event_state_internal` trait method instead, so the
451 // network route and the internal lifecycle path no longer share an
452 // access path (issue #130).
453 if property == PropertyIdentifier::EVENT_PARAMETERS {
454 self.set_event_parameters(parameters::decode_event_parameters(value)?);
455 return Ok(());
456 }
457 if property == PropertyIdentifier::FAULT_PARAMETERS {
458 // The context-tagged `none` choice is the unset form (#1417).
459 let parameters = parameters::decode_fault_parameters(value)?;
460 self.fault_parameters =
461 (!matches!(parameters, FaultParameters::FaultNone)).then_some(parameters);
462 return Ok(());
463 }
464 if property == PropertyIdentifier::TIME_DELAY_NORMAL {
465 // Table 12-14 codes the property O, not W; accepting the write is
466 // the Clause 12.1.2 implementor's option the intrinsic types
467 // already exercise, and is what makes the Clause 13.3 delay
468 // asymmetry commissionable on an enrollment at all.
469 if let PropertyValue::Unsigned(v) = value {
470 self.set_time_delay_normal(Some(common::u64_to_u32(v)?));
471 return Ok(());
472 }
473 return Err(common::invalid_data_type_error());
474 }
475 if let Some(result) =
476 self.event_history
477 .write(property, array_index, &value, self.event_detection_enable)
478 {
479 return result;
480 }
481 if let Some(result) = common::write_object_name(&mut self.name, property, &value) {
482 return result;
483 }
484 if let Some(result) = common::write_description(&mut self.description, property, &value) {
485 return result;
486 }
487 Err(crate::common::unhandled_write_error(
488 self.property_metadata().as_ref(),
489 property,
490 array_index,
491 ))
492 }
493
494 /// Internal lifecycle path for the algorithmically-derived `Event_State`.
495 ///
496 /// The evaluator calls this — not `write_property(EVENT_STATE, …)` — so the
497 /// network route (which rejects `EVENT_STATE`) and the internal lifecycle
498 /// path are distinct (issue #130). Stores the modeled state verbatim; the
499 /// only caller is the trusted server evaluator.
500 ///
501 /// Refuses any non-NORMAL state while `Event_Detection_Enable` is FALSE.
502 /// Clause 13.2.2.1 prohibits transitions in that case,
503 /// and the server evaluator already skips such objects — this guard makes
504 /// the invariant hold by construction rather than by the caller
505 /// remembering, so a future caller cannot reintroduce the violation.
506 fn set_event_state_internal(&mut self, state: EventState) -> Result<(), Error> {
507 if !self.event_detection_enable && state != EventState::NORMAL {
508 return Err(common::write_access_denied_error());
509 }
510 self.event_state = state;
511 Ok(())
512 }
513
514 /// Snapshot the pending countdown and algorithm baselines for the server
515 /// evaluator.
516 fn enrollment_eval_state_internal(&self) -> Option<EventEnrollmentEvalState> {
517 Some(EventEnrollmentEvalState {
518 pending: self.pending.clone(),
519 cov_baseline: self.cov_baseline.clone(),
520 last_offnormal_value: self.last_offnormal_value,
521 })
522 }
523
524 /// Store the enrollment evaluation state. Refused while
525 /// `Event_Detection_Enable` is FALSE: Clause 13.2.2.1 freezes the state
526 /// machine by suspending evaluation, and the reset in the
527 /// write arm has already returned these fields to their initial
528 /// condition, so a write arriving while disabled can only be stale.
529 fn set_enrollment_eval_state_internal(
530 &mut self,
531 state: EventEnrollmentEvalState,
532 ) -> Result<(), Error> {
533 if !self.event_detection_enable {
534 return Err(common::write_access_denied_error());
535 }
536 self.pending = state.pending;
537 self.cov_baseline = state.cov_baseline;
538 self.last_offnormal_value = state.last_offnormal_value;
539 Ok(())
540 }
541
542 fn enrollment_eval_source_internal(&self) -> Option<Option<EventEnrollmentMonitoredSource>> {
543 Some(self.monitored_reference)
544 }
545
546 fn set_enrollment_eval_source_internal(
547 &mut self,
548 source: Option<EventEnrollmentMonitoredSource>,
549 ) -> Result<(), Error> {
550 if !self.event_detection_enable {
551 return Err(common::write_access_denied_error());
552 }
553 self.monitored_reference = source;
554 Ok(())
555 }
556
557 /// Acknowledge an alarm transition (the AcknowledgeAlarm service route,
558 /// Clause 13.9): Clause 13.2.3 sets the bit on the acknowledgment
559 /// indication — unconditional and idempotent, so a repeated ack succeeds
560 /// again. A detection-DISABLED enrollment instead refuses with
561 /// OBJECT/NO_ALARM_CONFIGURED, which Table 13-10 uses for an existing object
562 /// lacking event-generation support or configuration: it can
563 /// generate nothing, and Clause 12.12 keeps its `Acked_Transitions` at
564 /// the initial condition, which an accepted ack would break.
565 /// Out_Of_Service does not gate the ack: no clause bars acknowledging a
566 /// notification already issued while the object is out of service.
567 fn acknowledge_alarm(
568 &mut self,
569 transition_bit: EventTransitionBits,
570 ) -> Result<(), bacnet_types::error::Error> {
571 if !self.event_detection_enable {
572 return Err(bacnet_types::error::Error::Protocol {
573 class: bacnet_types::enums::ErrorClass::OBJECT,
574 code: bacnet_types::enums::ErrorCode::NO_ALARM_CONFIGURED,
575 });
576 }
577 self.acked_transitions |= transition_bit & EventTransitionBits::all();
578 Ok(())
579 }
580
581 fn acknowledge_alarm_correlated_internal(
582 &mut self,
583 event_state: EventState,
584 timestamp: &BACnetTimeStamp,
585 ) -> Result<(), Error> {
586 if !self.event_detection_enable || self.enrollment_summary_capability_internal().is_none() {
587 return Err(Error::Protocol {
588 class: ErrorClass::OBJECT,
589 code: ErrorCode::NO_ALARM_CONFIGURED,
590 });
591 }
592 self.event_history.acknowledge_correlated(
593 &mut self.acked_transitions,
594 event_state,
595 timestamp,
596 )
597 }
598
599 fn acknowledge_alarm_correlated_detailed_internal(
600 &mut self,
601 event_state: EventState,
602 timestamp: &BACnetTimeStamp,
603 ) -> Result<Option<crate::event::EventStateChange>, Error> {
604 if !self.event_detection_enable || self.enrollment_summary_capability_internal().is_none() {
605 return Err(Error::Protocol {
606 class: ErrorClass::OBJECT,
607 code: ErrorCode::NO_ALARM_CONFIGURED,
608 });
609 }
610 self.event_history.acknowledge_correlated_detailed(
611 &mut self.acked_transitions,
612 event_state,
613 timestamp,
614 )
615 }
616
617 /// Clause 13.2.3's transition-received maintenance of `Acked_Transitions`:
618 /// the evaluator resolves `Ack_Required` from the referenced Notification
619 /// Class object and this call applies the outcome — clear the bit when
620 /// ack is required, set it otherwise. Refused while detection is
621 /// disabled, preserving the same invariant as above: Acked_Transitions
622 /// must retain its initial value throughout the disabled period.
623 fn set_acked_transitions_internal(
624 &mut self,
625 transition_bit: EventTransitionBits,
626 acknowledged: bool,
627 ) -> Result<(), Error> {
628 if !self.event_detection_enable {
629 return Err(common::write_access_denied_error());
630 }
631 self.acked_transitions
632 .set(transition_bit & EventTransitionBits::all(), acknowledged);
633 Ok(())
634 }
635
636 transition::impl_event_enrollment_transition_commit!();
637
638 /// Mirrors the `write_property` arms above, so PICS reports what dispatch
639 /// actually accepts.
640 ///
641 /// Enumerated explicitly: the intrinsic-reporting objects accept
642 /// `HIGH_LIMIT`, `LOW_LIMIT`, `DEADBAND`, `LIMIT_ENABLE` and `TIME_DELAY`,
643 /// none of which an Event Enrollment accepts, because it carries those
644 /// inside `Event_Parameters` instead. `TIME_DELAY_NORMAL` is accepted here
645 /// as a real (O-coded) property, per Table 12-14.
646 ///
647 /// `Event_Detection_Enable` is writable even though Table 12-14 codes it R
648 /// rather than W: Clause 12.1.2 lets implementors accept writes to an R
649 /// property unless that object's property description expressly forbids
650 /// them. Clause 12.12 has no such prohibition; it anticipates setting the
651 /// value during configuration without mandating that timing. Annex K's
652 /// AE-AVM-A BIBB (Table K-17) positively requires a conforming workstation
653 /// to be able to *write* this property, so refusing the write would be
654 /// interoperably hostile.
655 fn is_writable_property(&self, property: PropertyIdentifier) -> bool {
656 // Table 12-14 has no Out_Of_Service (#1064), so of the common
657 // writable rows only the name and description apply.
658 matches!(
659 property,
660 PropertyIdentifier::OBJECT_NAME
661 | PropertyIdentifier::DESCRIPTION
662 | PropertyIdentifier::NOTIFY_TYPE
663 | PropertyIdentifier::NOTIFICATION_CLASS
664 | PropertyIdentifier::EVENT_ENABLE
665 | PropertyIdentifier::EVENT_DETECTION_ENABLE
666 | PropertyIdentifier::EVENT_PARAMETERS
667 | PropertyIdentifier::FAULT_PARAMETERS
668 | PropertyIdentifier::TIME_DELAY_NORMAL
669 | PropertyIdentifier::EVENT_MESSAGE_TEXTS_CONFIG
670 | PropertyIdentifier::EVENT_ALGORITHM_INHIBIT_REF
671 | PropertyIdentifier::EVENT_ALGORITHM_INHIBIT
672 )
673 }
674
675 fn event_algorithm_inhibit_reference_internal(
676 &self,
677 ) -> Option<bacnet_types::constructed::BACnetObjectPropertyReference> {
678 self.event_history.options.inhibit_reference().cloned()
679 }
680
681 fn follow_event_algorithm_inhibit_internal(&mut self, inhibit: bool) -> bool {
682 self.event_history.options.follow(inhibit)
683 }
684
685 fn property_metadata(&self) -> Cow<'_, [PropertyMetadata]> {
686 Cow::Borrowed(metadata::EVENT_ENROLLMENT_PROPERTIES)
687 }
688
689 fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
690 crate::property_metadata::property_list_from_metadata(metadata::EVENT_ENROLLMENT_PROPERTIES)
691 }
692}
693
694#[cfg(test)]
695mod tests;