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