1use std::collections::{HashMap, HashSet};
2
3use bacnet_objects::database::ObjectDatabase;
4use bacnet_objects::event::{
5 EventStateChange, EventTransition, EventTransitionCommit, EventTransitionCommitError,
6};
7use bacnet_objects::event_enrollment::EventEnrollmentReliabilityCommit;
8use bacnet_objects::event_enrollment::{EventEnrollmentEvalState, EventEnrollmentMonitoredSource};
9use bacnet_types::enums::{EventState, EventType, PropertyIdentifier, Reliability};
10use bacnet_types::primitives::{ObjectIdentifier, PropertyValue};
11
12use super::EventEnrollmentTransition;
13use crate::server::event_notification_payload::{
14 project_event_enrollment_payload, CapturedReferencedValue, CommittedNotificationPayload,
15 EventEnrollmentProjectionSnapshot,
16};
17use crate::server::event_timestamp::{
18 confirm_event_timestamp, stage_event_timestamp, SampledEventClock,
19};
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum EventEnrollmentEvaluationStage {
24 Evaluation,
26 EvaluationSource,
28 EvaluationState,
30 EventTransition,
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum EventEnrollmentEvaluationOutcome {
37 NoTransition,
39 CancellationCommitted,
41 Rejected,
43 LandedAfterError,
49}
50
51impl EventEnrollmentEvaluationOutcome {
52 pub fn is_failure(self) -> bool {
54 matches!(self, Self::Rejected | Self::LandedAfterError)
55 }
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub struct EventEnrollmentEvaluationDiagnostic {
61 pub enrollment_oid: ObjectIdentifier,
63 pub stage: EventEnrollmentEvaluationStage,
65 pub outcome: EventEnrollmentEvaluationOutcome,
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub enum EventEnrollmentDetailedEvaluationStage {
72 Evaluation,
74 EvaluationSource,
76 EvaluationState,
78 Reliability,
80 EventTransition,
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub enum EventEnrollmentDetailedEvaluationOutcome {
87 NoTransition,
89 CancellationCommitted,
91 ObservationUnavailable,
98 Rejected,
100 LandedAfterError,
106}
107
108impl EventEnrollmentDetailedEvaluationOutcome {
109 pub fn is_failure(self) -> bool {
111 matches!(self, Self::Rejected | Self::LandedAfterError)
112 }
113}
114
115#[derive(Debug, Clone, Copy, PartialEq, Eq)]
117pub struct EventEnrollmentDetailedEvaluationDiagnostic {
118 pub enrollment_oid: ObjectIdentifier,
120 pub stage: EventEnrollmentDetailedEvaluationStage,
122 pub outcome: EventEnrollmentDetailedEvaluationOutcome,
124}
125
126#[derive(Debug, Clone, Copy, PartialEq, Eq)]
128pub enum EventEnrollmentReliabilityCause {
129 Configuration,
131 MonitoredObject,
133 FaultAlgorithm,
135}
136
137#[derive(Debug, Clone, PartialEq, Eq)]
139pub struct EventEnrollmentReliabilityResult {
140 pub enrollment_oid: ObjectIdentifier,
142 pub monitored_oid: Option<ObjectIdentifier>,
144 pub previous_reliability: Reliability,
146 pub new_reliability: Reliability,
148 pub state_change: Option<EventStateChange>,
150 pub distribute: bool,
152 pub cause: EventEnrollmentReliabilityCause,
154}
155
156impl EventEnrollmentReliabilityResult {
157 pub fn event_type(&self, configured_algorithm: EventType) -> Option<EventType> {
162 self.state_change
163 .as_ref()
164 .map(|change| change.event_type(configured_algorithm))
165 }
166}
167
168#[derive(Debug, Clone, Default, PartialEq)]
170pub struct EventEnrollmentEvaluationReport {
171 pub transitions: Vec<EventEnrollmentTransition>,
173 pub diagnostics: Vec<EventEnrollmentEvaluationDiagnostic>,
175}
176
177#[derive(Debug, Clone, Default, PartialEq)]
179pub struct EventEnrollmentDetailedEvaluationReport {
180 pub transitions: Vec<EventEnrollmentTransition>,
182 pub reliability_results: Vec<EventEnrollmentReliabilityResult>,
184 pub diagnostics: Vec<EventEnrollmentDetailedEvaluationDiagnostic>,
186}
187
188pub(crate) enum CommittedEventEnrollmentResult {
190 Normal(EventEnrollmentTransition),
191 Reliability(EventEnrollmentReliabilityResult),
192}
193
194pub(crate) struct CommittedEventEnrollmentDelivery {
196 pub(crate) result: CommittedEventEnrollmentResult,
197 pub(crate) ack_required: bool,
198 pub(crate) recipient_clock: SampledEventClock,
199 pub(crate) event_type: EventType,
200 pub(crate) event_values: CommittedNotificationPayload,
201}
202
203pub(crate) struct EventEnrollmentEvaluationBatch {
205 pub(crate) report: EventEnrollmentDetailedEvaluationReport,
206 pub(crate) deliveries: Vec<CommittedEventEnrollmentDelivery>,
207}
208
209impl EventEnrollmentDetailedEvaluationReport {
210 pub(super) fn into_legacy(self) -> EventEnrollmentEvaluationReport {
211 let diagnostics = self
212 .diagnostics
213 .into_iter()
214 .map(|diagnostic| {
215 let stage = match (diagnostic.stage, diagnostic.outcome) {
216 (
217 EventEnrollmentDetailedEvaluationStage::Reliability,
218 EventEnrollmentDetailedEvaluationOutcome::ObservationUnavailable
219 | EventEnrollmentDetailedEvaluationOutcome::NoTransition,
220 ) => EventEnrollmentEvaluationStage::Evaluation,
221 (EventEnrollmentDetailedEvaluationStage::Reliability, _) => {
222 EventEnrollmentEvaluationStage::EventTransition
223 }
224 (EventEnrollmentDetailedEvaluationStage::Evaluation, _) => {
225 EventEnrollmentEvaluationStage::Evaluation
226 }
227 (EventEnrollmentDetailedEvaluationStage::EvaluationSource, _) => {
228 EventEnrollmentEvaluationStage::EvaluationSource
229 }
230 (EventEnrollmentDetailedEvaluationStage::EvaluationState, _) => {
231 EventEnrollmentEvaluationStage::EvaluationState
232 }
233 (EventEnrollmentDetailedEvaluationStage::EventTransition, _) => {
234 EventEnrollmentEvaluationStage::EventTransition
235 }
236 };
237 let outcome = match diagnostic.outcome {
238 EventEnrollmentDetailedEvaluationOutcome::ObservationUnavailable => {
239 EventEnrollmentEvaluationOutcome::NoTransition
240 }
241 EventEnrollmentDetailedEvaluationOutcome::NoTransition => {
242 EventEnrollmentEvaluationOutcome::NoTransition
243 }
244 EventEnrollmentDetailedEvaluationOutcome::CancellationCommitted => {
245 EventEnrollmentEvaluationOutcome::CancellationCommitted
246 }
247 EventEnrollmentDetailedEvaluationOutcome::Rejected => {
248 EventEnrollmentEvaluationOutcome::Rejected
249 }
250 EventEnrollmentDetailedEvaluationOutcome::LandedAfterError => {
251 EventEnrollmentEvaluationOutcome::LandedAfterError
252 }
253 };
254 EventEnrollmentEvaluationDiagnostic {
255 enrollment_oid: diagnostic.enrollment_oid,
256 stage,
257 outcome,
258 }
259 })
260 .collect();
261 EventEnrollmentEvaluationReport {
262 transitions: self.transitions,
263 diagnostics,
264 }
265 }
266}
267
268pub(crate) fn log_evaluation_report(report: &EventEnrollmentDetailedEvaluationReport) {
269 for result in &report.reliability_results {
270 tracing::debug!(
271 enrollment = %result.enrollment_oid,
272 monitored = ?result.monitored_oid,
273 previous = ?result.previous_reliability,
274 new = ?result.new_reliability,
275 state_change = ?result.state_change,
276 distribute = result.distribute,
277 cause = ?result.cause,
278 "Event enrollment: reliability committed"
279 );
280 }
281 for diagnostic in report
282 .diagnostics
283 .iter()
284 .filter(|diagnostic| diagnostic.outcome.is_failure())
285 {
286 tracing::warn!(
287 enrollment = %diagnostic.enrollment_oid,
288 stage = ?diagnostic.stage,
289 outcome = ?diagnostic.outcome,
290 "Event enrollment: internal commit failed"
291 );
292 }
293}
294
295#[derive(Default)]
297pub(super) struct EnrollmentUpdate {
298 eval_state: Option<EventEnrollmentEvalState>,
299 eval_source: Option<Option<EventEnrollmentMonitoredSource>>,
300 clears_invalidation: bool,
301 fired: Option<FiredTransition>,
302 reliability: Option<ReliabilityUpdate>,
303 observation_unavailable: bool,
304 canceled: bool,
305}
306
307pub(super) struct FiredTransition {
308 pub(super) monitored_oid: ObjectIdentifier,
309 pub(super) event_type_raw: u32,
310 pub(super) from: EventState,
311 pub(super) to: EventState,
312 pub(super) distribute: bool,
313 pub(super) ack_required: bool,
314 pub(super) projection: EventEnrollmentProjectionSnapshot,
315}
316
317pub(super) struct ReliabilityUpdate {
318 pub(super) monitored_oid: Option<ObjectIdentifier>,
319 pub(super) previous: Reliability,
320 pub(super) desired: Reliability,
321 pub(super) from: EventState,
322 pub(super) to: EventState,
323 pub(super) distribute: bool,
324 pub(super) ack_required: bool,
325 pub(super) cause: EventEnrollmentReliabilityCause,
326 pub(super) referenced_value: CapturedReferencedValue,
327}
328
329impl EnrollmentUpdate {
330 pub(super) fn set_eval_state(&mut self, state: EventEnrollmentEvalState) {
334 self.eval_state = Some(state);
335 }
336
337 pub(super) fn reset_eval_state(&mut self) {
338 self.eval_state = Some(EventEnrollmentEvalState::default());
339 self.clears_invalidation = true;
340 }
341
342 pub(super) fn cancel_pending(&mut self, state: EventEnrollmentEvalState) {
343 self.eval_state = Some(state);
344 self.canceled = true;
345 }
346
347 pub(super) fn set_eval_source(&mut self, source: Option<EventEnrollmentMonitoredSource>) {
348 self.eval_source = Some(source);
349 }
350
351 pub(super) fn reset_eval_source_for_observation_gap(
354 &mut self,
355 previous: Option<Option<EventEnrollmentMonitoredSource>>,
356 ) {
357 self.eval_source = match previous {
358 Some(Some(_)) => Some(None),
359 Some(None) | None => None,
360 };
361 }
362
363 pub(super) fn fire(&mut self, fired: FiredTransition) {
364 debug_assert!(
365 self.fired.is_none(),
366 "one enrollment fires at most once per pass"
367 );
368 self.fired = Some(fired);
369 }
370
371 pub(super) fn set_reliability(&mut self, reliability: ReliabilityUpdate) {
372 debug_assert!(
373 self.reliability.is_none() && self.fired.is_none(),
374 "one enrollment commits one transition family per pass"
375 );
376 self.reliability = Some(reliability);
377 }
378
379 pub(super) fn observation_unavailable(&mut self) {
380 self.fired = None;
383 self.reliability = None;
384 self.canceled = false;
385 self.observation_unavailable = true;
386 }
387}
388
389fn diagnostic(
390 enrollment_oid: ObjectIdentifier,
391 stage: EventEnrollmentDetailedEvaluationStage,
392 outcome: EventEnrollmentDetailedEvaluationOutcome,
393) -> EventEnrollmentDetailedEvaluationDiagnostic {
394 EventEnrollmentDetailedEvaluationDiagnostic {
395 enrollment_oid,
396 stage,
397 outcome,
398 }
399}
400
401fn event_state_landed(db: &ObjectDatabase, oid: &ObjectIdentifier, state: EventState) -> bool {
402 matches!(
403 db.get(oid)
404 .and_then(|object| object.read_property(PropertyIdentifier::EVENT_STATE, None).ok()),
405 Some(PropertyValue::Enumerated(raw)) if raw == state.to_raw()
406 )
407}
408
409fn reliability_or_state_landed(
410 db: &ObjectDatabase,
411 oid: &ObjectIdentifier,
412 update: &ReliabilityUpdate,
413) -> bool {
414 let Some(object) = db.get(oid) else {
415 return false;
416 };
417 let reliability_landed = update.previous != update.desired
418 && matches!(
419 object.read_property(PropertyIdentifier::RELIABILITY, None),
420 Ok(PropertyValue::Enumerated(raw)) if raw == update.desired.to_raw()
421 );
422 let state_landed = update.from != update.to
423 && matches!(
424 object.read_property(PropertyIdentifier::EVENT_STATE, None),
425 Ok(PropertyValue::Enumerated(raw)) if raw == update.to.to_raw()
426 );
427 reliability_landed || state_landed
428}
429
430fn clear_source_ownership(
431 db: &mut ObjectDatabase,
432 oid: ObjectIdentifier,
433 database_eval_sources: &HashSet<ObjectIdentifier>,
434) {
435 if database_eval_sources.contains(&oid) {
436 db.set_enrollment_eval_source(oid, None);
437 } else if let Some(object) = db.get_mut(&oid) {
438 let _ = object.set_enrollment_eval_source_internal(None);
439 }
440}
441
442#[cfg(test)]
443pub(super) fn apply_updates(
444 db: &mut ObjectDatabase,
445 oids: &[ObjectIdentifier],
446 updates: HashMap<ObjectIdentifier, EnrollmentUpdate>,
447 database_eval_sources: &HashSet<ObjectIdentifier>,
448) -> EventEnrollmentDetailedEvaluationReport {
449 apply_updates_for_delivery(db, oids, updates, database_eval_sources).report
450}
451
452pub(super) fn apply_updates_for_delivery(
453 db: &mut ObjectDatabase,
454 oids: &[ObjectIdentifier],
455 mut updates: HashMap<ObjectIdentifier, EnrollmentUpdate>,
456 database_eval_sources: &HashSet<ObjectIdentifier>,
457) -> EventEnrollmentEvaluationBatch {
458 let mut report = EventEnrollmentDetailedEvaluationReport::default();
459 let mut deliveries = Vec::new();
460
461 for &oid in oids {
462 let Some(update) = updates.remove(&oid) else {
463 report.diagnostics.push(diagnostic(
464 oid,
465 EventEnrollmentDetailedEvaluationStage::Evaluation,
466 EventEnrollmentDetailedEvaluationOutcome::NoTransition,
467 ));
468 continue;
469 };
470
471 let mut source_failed = false;
472 let mut state_failed = false;
473 if let Some(source) = update.eval_source {
474 if database_eval_sources.contains(&oid) {
475 db.set_enrollment_eval_source(oid, source);
476 } else {
477 let failed = db.get_mut(&oid).is_none_or(|object| {
478 object.set_enrollment_eval_source_internal(source).is_err()
479 });
480 if failed {
481 source_failed = true;
482 report.diagnostics.push(diagnostic(
483 oid,
484 EventEnrollmentDetailedEvaluationStage::EvaluationSource,
485 EventEnrollmentDetailedEvaluationOutcome::Rejected,
486 ));
487 }
488 }
489 }
490
491 let mut state_committed = false;
492 if source_failed {
493 state_failed = db.get_mut(&oid).is_none_or(|object| {
496 object
497 .set_enrollment_eval_state_internal(EventEnrollmentEvalState::default())
498 .is_err()
499 });
500 if state_failed {
501 db.set_enrollment_eval_state_invalidated(oid, true);
502 report.diagnostics.push(diagnostic(
503 oid,
504 EventEnrollmentDetailedEvaluationStage::EvaluationState,
505 EventEnrollmentDetailedEvaluationOutcome::Rejected,
506 ));
507 } else {
508 state_committed = true;
509 }
510 } else if let Some(state) = update.eval_state {
511 state_failed = db
512 .get_mut(&oid)
513 .is_none_or(|object| object.set_enrollment_eval_state_internal(state).is_err());
514 if state_failed {
515 clear_source_ownership(db, oid, database_eval_sources);
516 db.set_enrollment_eval_state_invalidated(oid, true);
517 report.diagnostics.push(diagnostic(
518 oid,
519 EventEnrollmentDetailedEvaluationStage::EvaluationState,
520 EventEnrollmentDetailedEvaluationOutcome::Rejected,
521 ));
522 } else {
523 state_committed = true;
524 if update.clears_invalidation {
525 db.set_enrollment_eval_state_invalidated(oid, false);
526 }
527 }
528 }
529
530 if update.observation_unavailable {
531 if source_failed || state_failed {
532 db.set_enrollment_eval_state_invalidated(oid, true);
533 }
534 report.diagnostics.push(diagnostic(
535 oid,
536 EventEnrollmentDetailedEvaluationStage::Reliability,
537 EventEnrollmentDetailedEvaluationOutcome::ObservationUnavailable,
538 ));
539 continue;
540 }
541
542 if state_failed {
543 continue;
544 }
545
546 if let Some(reliability) = update.reliability {
547 let coordinate = EventTransition::for_target_state(reliability.to);
548 let staged_timestamp = stage_event_timestamp(db);
549 let change = EventStateChange {
550 from: reliability.from,
551 to: reliability.to,
552 };
553 let transition = EventTransitionCommit {
554 change: change.clone(),
555 coordinate,
556 ack_required: reliability.ack_required,
557 timestamp: staged_timestamp.sample.timestamp.clone(),
558 message_text: None,
559 };
560 let commit = EventEnrollmentReliabilityCommit {
561 reliability: reliability.desired,
562 transition: Some(transition),
563 };
564 let commit_result = db
565 .get_mut(&oid)
566 .map_or(Err(EventTransitionCommitError::Unsupported), |object| {
567 object.commit_event_enrollment_reliability_internal(commit)
568 });
569
570 if commit_result.is_err() {
571 let outcome = if reliability_or_state_landed(db, &oid, &reliability) {
572 EventEnrollmentDetailedEvaluationOutcome::LandedAfterError
573 } else {
574 EventEnrollmentDetailedEvaluationOutcome::Rejected
575 };
576 clear_source_ownership(db, oid, database_eval_sources);
577 db.set_enrollment_eval_state_invalidated(oid, true);
578 report.diagnostics.push(diagnostic(
579 oid,
580 EventEnrollmentDetailedEvaluationStage::Reliability,
581 outcome,
582 ));
583 continue;
584 }
585
586 let recipient_clock = confirm_event_timestamp(db, staged_timestamp).clock;
587 let result = EventEnrollmentReliabilityResult {
588 enrollment_oid: oid,
589 monitored_oid: reliability.monitored_oid,
590 previous_reliability: reliability.previous,
591 new_reliability: reliability.desired,
592 state_change: Some(change),
593 distribute: reliability.distribute,
594 cause: reliability.cause,
595 };
596 report.reliability_results.push(result.clone());
597 let event_type = EventType::CHANGE_OF_RELIABILITY;
598 if let Some(event_values) = project_event_enrollment_payload(
599 db,
600 oid,
601 reliability.monitored_oid,
602 result
603 .state_change
604 .as_ref()
605 .expect("committed Reliability delivery has a state change"),
606 event_type,
607 None,
608 Some(&reliability.referenced_value),
609 ) {
610 deliveries.push(CommittedEventEnrollmentDelivery {
611 result: CommittedEventEnrollmentResult::Reliability(result),
612 ack_required: reliability.ack_required,
613 recipient_clock,
614 event_type,
615 event_values,
616 });
617 } else {
618 tracing::debug!(
619 enrollment = %oid,
620 "Committed Event Enrollment reliability payload rejected; suppressing distribution"
621 );
622 }
623 continue;
624 }
625
626 let Some(fired) = update.fired else {
627 report.diagnostics.push(diagnostic(
628 oid,
629 EventEnrollmentDetailedEvaluationStage::EvaluationState,
630 if update.canceled && state_committed {
631 EventEnrollmentDetailedEvaluationOutcome::CancellationCommitted
632 } else {
633 EventEnrollmentDetailedEvaluationOutcome::NoTransition
634 },
635 ));
636 continue;
637 };
638
639 if source_failed && fired.from == fired.to {
644 continue;
645 }
646
647 let coordinate = EventTransition::for_target_state(fired.to);
648 let staged_timestamp = stage_event_timestamp(db);
649 let change = EventStateChange {
650 from: fired.from,
651 to: fired.to,
652 };
653 let commit = EventTransitionCommit {
654 change: change.clone(),
655 coordinate,
656 ack_required: fired.ack_required,
657 timestamp: staged_timestamp.sample.timestamp.clone(),
658 message_text: None,
659 };
660 let commit_result = db
661 .get_mut(&oid)
662 .map_or(Err(EventTransitionCommitError::Unsupported), |object| {
663 object.commit_event_transition_internal(commit)
664 });
665
666 if commit_result.is_err() {
667 let outcome = if fired.from != fired.to && event_state_landed(db, &oid, fired.to) {
668 EventEnrollmentDetailedEvaluationOutcome::LandedAfterError
669 } else {
670 EventEnrollmentDetailedEvaluationOutcome::Rejected
671 };
672 clear_source_ownership(db, oid, database_eval_sources);
673 db.set_enrollment_eval_state_invalidated(oid, true);
674 report.diagnostics.push(diagnostic(
675 oid,
676 EventEnrollmentDetailedEvaluationStage::EventTransition,
677 outcome,
678 ));
679 continue;
680 }
681
682 let recipient_clock = confirm_event_timestamp(db, staged_timestamp).clock;
683 let event_type = change.event_type(EventType::from_raw(fired.event_type_raw));
684 let result = EventEnrollmentTransition {
685 enrollment_oid: oid,
686 monitored_oid: fired.monitored_oid,
687 change,
688 event_type,
689 distribute: fired.distribute,
690 };
691 report.transitions.push(result.clone());
692 if let Some(event_values) = project_event_enrollment_payload(
693 db,
694 oid,
695 Some(fired.monitored_oid),
696 &result.change,
697 event_type,
698 Some(&fired.projection),
699 None,
700 ) {
701 deliveries.push(CommittedEventEnrollmentDelivery {
702 result: CommittedEventEnrollmentResult::Normal(result),
703 ack_required: fired.ack_required,
704 recipient_clock,
705 event_type,
706 event_values,
707 });
708 } else {
709 tracing::debug!(
710 enrollment = %oid,
711 ?event_type,
712 "Committed Event Enrollment payload rejected; suppressing distribution"
713 );
714 }
715 }
716
717 EventEnrollmentEvaluationBatch { report, deliveries }
718}