Skip to main content

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;