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