Skip to main content

bacnet_objects/analog/
input.rs

1use super::*;
2use crate::common::{
3    read_analog_event_properties, read_generic_event_properties, write_analog_event_properties,
4    write_generic_event_properties,
5};
6use crate::object_profile::{ObjectProfile, ProfileState, TagsPersistence};
7use crate::prelude::*;
8use crate::property_metadata::PropertyMetadata;
9use alloc::sync::Arc;
10
11mod metadata;
12
13// ---------------------------------------------------------------------------
14// AnalogInput (type 0)
15// ---------------------------------------------------------------------------
16
17/// BACnet Analog Input object.
18pub struct AnalogInputObject {
19    profile: ProfileState,
20    oid: ObjectIdentifier,
21    name: String,
22    description: String,
23    present_value: f32,
24    units: EngineeringUnits,
25    out_of_service: bool,
26    status_flags: StatusFlags,
27    /// COV_Increment: minimum change threshold for COV notifications.
28    /// Default 0.0 means notify on any write (including no-change).
29    /// Set to a positive value for delta-based filtering.
30    cov_increment: f32,
31    event_detector: OutOfRangeDetector,
32    /// Event_Detection_Enable (Clause 12.2). A FALSE value suspends
33    /// event-state-machine evaluation under Clause 13.2.2.1.
34    event_detection_enable: bool,
35    /// Reliability; NO_FAULT_DETECTED until a fault is evaluated or simulated.
36    reliability: Reliability,
37    reliability_before_out_of_service: Option<Reliability>,
38    reliability_inhibit: common::ReliabilityInhibitState,
39    fault_out_of_range: FaultOutOfRangeState,
40    /// Optional minimum engineering bound metadata for Present_Value.
41    min_pres_value: Option<f32>,
42    /// Optional maximum engineering bound metadata for Present_Value.
43    max_pres_value: Option<f32>,
44    pub(crate) event_history: EventHistory,
45}
46
47impl AnalogInputObject {
48    /// Create a new Analog Input object.
49    pub fn new(
50        instance: u32,
51        name: impl Into<String>,
52        units: EngineeringUnits,
53    ) -> Result<Self, Error> {
54        let _oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, instance)?;
55        Ok(Self {
56            profile: ProfileState::default(),
57            oid: _oid,
58            name: name.into(),
59            description: String::new(),
60            present_value: 0.0,
61            units,
62            out_of_service: false,
63            status_flags: StatusFlags::empty(),
64            cov_increment: 0.0,
65            event_detector: OutOfRangeDetector::default(),
66            event_detection_enable: true,
67            reliability: Reliability::NO_FAULT_DETECTED,
68            reliability_before_out_of_service: None,
69            reliability_inhibit: common::ReliabilityInhibitState::default(),
70            fault_out_of_range: FaultOutOfRangeState::default(),
71            min_pres_value: None,
72            max_pres_value: None,
73            event_history: EventHistory::default(),
74        })
75    }
76
77    /// Build with application-owned Tags storage. Saved Tags override configured
78    /// Tags only while [`Self::set_profile`] provisions the row. Loaded data must
79    /// satisfy the shared Tags rules and opt-in 1 MiB snapshot limit.
80    pub fn with_tags_persistence(
81        instance: u32,
82        name: impl Into<String>,
83        units: EngineeringUnits,
84        persistence: Arc<dyn TagsPersistence>,
85    ) -> Result<Self, Error> {
86        let mut object = Self::new(instance, name, units)?;
87        object.profile = ProfileState::persistent(object.oid, persistence)?;
88        Ok(object)
89    }
90
91    /// Provision independent optional Tags, Profile_Location and Profile_Name
92    /// rows before registration. Tags is writable in either Out_Of_Service state;
93    /// the text rows are network read-only. Invalid configuration leaves the
94    /// previous profile and its attached storage unchanged.
95    pub fn set_profile(&mut self, profile: ObjectProfile) -> Result<(), Error> {
96        self.profile.provision(profile)
97    }
98
99    /// Wait for queued save attempts to finish; writes report their own outcomes.
100    /// This is not a success receipt. Unstaged synchronous writes block their caller.
101    pub fn wait_for_tag_saves(&self) {
102        self.profile.wait_for_saves();
103    }
104
105    /// Mutate `Present_Value` on an unattached or otherwise application-owned object.
106    ///
107    /// This low-level helper bypasses running-server `Out_Of_Service` ownership,
108    /// validation, intrinsic-event processing, and COV processing. Applications
109    /// updating a live object should use `BACnetServer::set_present_value_local`.
110    pub fn set_present_value(&mut self, value: f32) {
111        debug_assert!(
112            value.is_finite(),
113            "set_present_value called with non-finite value"
114        );
115        self.present_value = value;
116    }
117
118    /// Validate and store a `Present_Value` write, without any access check.
119    ///
120    /// Shared by the network and internal routes, which differ only in the
121    /// `Out_Of_Service` condition each requires.
122    fn apply_present_value(&mut self, value: PropertyValue) -> Result<(), Error> {
123        let PropertyValue::Real(v) = value else {
124            return Err(common::invalid_data_type_error());
125        };
126        if !v.is_finite() {
127            return Err(common::value_out_of_range_error());
128        }
129        self.present_value = v;
130        Ok(())
131    }
132
133    /// Set the description string.
134    pub fn set_description(&mut self, desc: impl Into<String>) {
135        self.description = desc.into();
136    }
137
138    /// Set minimum engineering-bound metadata; this is not a reliability fault limit.
139    pub fn set_min_pres_value(&mut self, value: f32) {
140        self.min_pres_value = Some(value);
141    }
142
143    /// Set maximum engineering-bound metadata; this is not a reliability fault limit.
144    pub fn set_max_pres_value(&mut self, value: f32) {
145        self.max_pres_value = Some(value);
146    }
147
148    /// Configure the optional object-owned FAULT_OUT_OF_RANGE algorithm.
149    ///
150    /// Both limits become readable together. Equal limits are valid; non-finite
151    /// limits and a low limit greater than the high limit are rejected without
152    /// changing the prior configuration.
153    pub fn configure_fault_out_of_range(&mut self, low: f32, high: f32) -> Result<(), Error> {
154        self.fault_out_of_range.configure(low, high)
155    }
156}
157
158impl BACnetObject for AnalogInputObject {
159    fn object_identifier(&self) -> ObjectIdentifier {
160        self.oid
161    }
162
163    fn object_name(&self) -> &str {
164        &self.name
165    }
166
167    fn read_property(
168        &self,
169        property: PropertyIdentifier,
170        array_index: Option<u32>,
171    ) -> Result<PropertyValue, Error> {
172        // IN_ALARM: override STATUS_FLAGS with event_state before common macro
173        if property == PropertyIdentifier::STATUS_FLAGS {
174            return Ok(common::compute_status_flags(
175                self.status_flags,
176                self.reliability,
177                self.out_of_service,
178                self.event_detector.event_state,
179            ));
180        }
181        if let Some(value) = self.reliability_inhibit.read(property) {
182            return Ok(value);
183        }
184        if let Some(result) = read_common_properties!(self, property, array_index) {
185            return result;
186        }
187        if let Some(result) = read_analog_event_properties!(self, property) {
188            return result;
189        }
190        if let Some(result) = self.event_history.read(property, array_index) {
191            return result;
192        }
193        if let Some(result) = read_generic_event_properties!(self, property) {
194            return result;
195        }
196        if property == PropertyIdentifier::EVENT_DETECTION_ENABLE {
197            return Ok(PropertyValue::Boolean(self.event_detection_enable));
198        }
199        if let Some(value) = self.fault_out_of_range.read_limit(property) {
200            return Ok(value);
201        }
202        if let Some(result) = self.profile.read(property, array_index) {
203            return result;
204        }
205        match property {
206            p if p == PropertyIdentifier::OBJECT_TYPE => {
207                Ok(PropertyValue::Enumerated(ObjectType::ANALOG_INPUT.to_raw()))
208            }
209            p if p == PropertyIdentifier::PRESENT_VALUE => {
210                Ok(PropertyValue::Real(self.present_value))
211            }
212            p if p == PropertyIdentifier::UNITS => {
213                Ok(PropertyValue::Enumerated(self.units.to_raw()))
214            }
215            p if p == PropertyIdentifier::COV_INCREMENT => {
216                Ok(PropertyValue::Real(self.cov_increment))
217            }
218            p if p == PropertyIdentifier::MIN_PRES_VALUE => match self.min_pres_value {
219                Some(v) => Ok(PropertyValue::Real(v)),
220                None => Err(common::unknown_property_error()),
221            },
222            p if p == PropertyIdentifier::MAX_PRES_VALUE => match self.max_pres_value {
223                Some(v) => Ok(PropertyValue::Real(v)),
224                None => Err(common::unknown_property_error()),
225            },
226            _ => Err(common::unknown_property_error()),
227        }
228    }
229
230    fn write_property(
231        &mut self,
232        property: PropertyIdentifier,
233        array_index: Option<u32>,
234        value: PropertyValue,
235        _priority: Option<u8>,
236    ) -> Result<(), Error> {
237        // AI present-value is writable only when out-of-service
238        if property == PropertyIdentifier::PRESENT_VALUE {
239            if !self.out_of_service {
240                return Err(common::write_access_denied_error());
241            }
242            return self.apply_present_value(value);
243        }
244        if let Some(result) = self.profile.write(property, array_index, &value) {
245            return result;
246        }
247        if let Some(result) = self.reliability_inhibit.write_inhibit(
248            &mut self.reliability,
249            self.out_of_service,
250            property,
251            &value,
252        ) {
253            return result;
254        }
255        if let Some(result) = self.reliability_inhibit.write_out_of_service(
256            &mut self.out_of_service,
257            &mut self.reliability,
258            &mut self.reliability_before_out_of_service,
259            property,
260            &value,
261        ) {
262            return result.map(|_| ());
263        }
264        if let Some(result) = common::write_object_name(&mut self.name, property, &value) {
265            return result;
266        }
267        if let Some(result) = common::write_description(&mut self.description, property, &value) {
268            return result;
269        }
270        if let Some(result) = self.reliability_inhibit.write_client_reliability(
271            self.out_of_service,
272            &mut self.reliability,
273            property,
274            &value,
275        ) {
276            return result;
277        }
278        if let Some(result) = common::write_cov_increment(&mut self.cov_increment, property, &value)
279        {
280            return result;
281        }
282        if property == PropertyIdentifier::EVENT_DETECTION_ENABLE {
283            if let PropertyValue::Boolean(v) = value {
284                self.event_detection_enable = v;
285                if !v {
286                    self.event_detector.event_state = bacnet_types::enums::EventState::NORMAL;
287                    self.event_detector.acked_transitions =
288                        bacnet_types::bitstring::EventTransitionBits::all();
289                    self.event_detector.pending = None;
290                    self.event_detector.fault_reliability = None;
291                    self.event_history.reset();
292                }
293                return Ok(());
294            }
295            return Err(common::invalid_data_type_error());
296        }
297        if let Some(result) = write_analog_event_properties!(self, property, value) {
298            return result;
299        }
300        // Event_Message_Texts_Config and the Event_Algorithm_Inhibit pair (#1329).
301        if let Some(result) =
302            self.event_history
303                .write(property, array_index, &value, self.event_detection_enable)
304        {
305            return result;
306        }
307        if let Some(result) = write_generic_event_properties!(self, property, value) {
308            return result;
309        }
310        Err(crate::common::unhandled_write_error(
311            self.property_metadata().as_ref(),
312            property,
313            array_index,
314        ))
315    }
316
317    fn durable_writes_internal(&mut self) -> Option<&mut dyn crate::durable::DurableWrites> {
318        self.profile.capability()
319    }
320
321    fn advance_monotonic_time_internal(&mut self, now: core::time::Duration) -> bool {
322        self.profile.expire(now);
323        false
324    }
325
326    fn property_metadata(&self) -> Cow<'_, [PropertyMetadata]> {
327        metadata::for_object(self)
328    }
329
330    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
331        crate::property_metadata::property_list_from_metadata(self.property_metadata().as_ref())
332    }
333
334    fn supports_cov(&self) -> bool {
335        true
336    }
337
338    fn cov_increment(&self) -> Option<f64> {
339        Some(f64::from(self.cov_increment))
340    }
341
342    crate::event::impl_builtin_intrinsic_reporting!(
343        event_detector,
344        event_history,
345        [present_value],
346        reliability,
347        event_detection_enable,
348        OutOfRangeDetector::ALGORITHM
349    );
350
351    fn acknowledge_alarm(
352        &mut self,
353        transition_bit: bacnet_types::bitstring::EventTransitionBits,
354    ) -> Result<(), bacnet_types::error::Error> {
355        self.event_detector.acked_transitions |=
356            transition_bit & bacnet_types::bitstring::EventTransitionBits::all();
357        Ok(())
358    }
359
360    fn acknowledge_alarm_correlated_internal(
361        &mut self,
362        event_state: EventState,
363        timestamp: &BACnetTimeStamp,
364    ) -> Result<(), Error> {
365        if !self.event_detection_enable {
366            return Err(Error::protocol(
367                ErrorClass::OBJECT,
368                ErrorCode::NO_ALARM_CONFIGURED,
369            ));
370        }
371        self.event_history.acknowledge_correlated(
372            &mut self.event_detector.acked_transitions,
373            event_state,
374            timestamp,
375        )
376    }
377
378    fn set_reliability_internal(&mut self, reliability: Reliability) -> Result<(), Error> {
379        if self.out_of_service || self.reliability_inhibit.enabled() {
380            return Err(common::write_access_denied_error());
381        }
382        if !common::is_reliability_value_valid(reliability) {
383            return Err(common::value_out_of_range_error());
384        }
385        self.reliability = reliability;
386        self.fault_out_of_range.clear_ownership();
387        Ok(())
388    }
389
390    fn set_present_value_internal(&mut self, value: PropertyValue) -> Result<(), Error> {
391        // Local safe-ownership policy: preserve the client's OOS simulation.
392        if self.out_of_service {
393            return Err(common::write_access_denied_error());
394        }
395        self.apply_present_value(value)
396    }
397
398    fn evaluate_reliability_internal(&mut self) -> Result<ReliabilityEvaluation, Error> {
399        if self.out_of_service || self.reliability_inhibit.enabled() {
400            return Ok(ReliabilityEvaluation::Unchanged);
401        }
402        self.fault_out_of_range
403            .evaluate(self.present_value, &mut self.reliability)
404    }
405
406    fn reliability_evaluation_inhibited_internal(&self) -> bool {
407        self.reliability_inhibit.enabled()
408    }
409
410    fn is_createable(&self) -> bool {
411        true
412    }
413
414    fn creation_only_properties(&self) -> &'static [PropertyIdentifier] {
415        super::CREATION_ONLY
416    }
417
418    fn initialize_property(
419        &mut self,
420        property: PropertyIdentifier,
421        value: PropertyValue,
422    ) -> Result<(), Error> {
423        super::initialize_units(&mut self.units, property, value)
424    }
425}
426
427#[cfg(test)]
428mod detection_enable_reset_tests {
429    use super::*;
430    use bacnet_types::error::RemoteError;
431
432    #[test]
433    fn built_in_intrinsic_transition_is_a_proposal_until_committed() {
434        use crate::event::EventTransitionCommit;
435
436        let mut ai = AnalogInputObject::new(1, "AI-1", EngineeringUnits::DEGREES_CELSIUS).unwrap();
437        ai.event_detector.high_limit = 80.0;
438        ai.event_detector.limit_enable = bacnet_types::bitstring::LimitEnable::all();
439        ai.event_detector.event_enable = bacnet_types::bitstring::EventTransitionBits::all();
440        ai.set_present_value(81.0);
441
442        let outcome = ai
443            .evaluate_intrinsic_reporting()
444            .expect("out-of-range value should propose TO_OFFNORMAL");
445        assert_eq!(
446            outcome.change.to,
447            bacnet_types::enums::EventState::HIGH_LIMIT
448        );
449        assert_eq!(
450            ai.event_detector.event_state,
451            bacnet_types::enums::EventState::NORMAL,
452            "built-in evaluation must not confirm its own proposal"
453        );
454        assert_eq!(
455            ai.event_detector.acked_transitions,
456            bacnet_types::bitstring::EventTransitionBits::all()
457        );
458        assert!(ai.event_detector.pending.is_none());
459
460        ai.commit_event_transition_internal(EventTransitionCommit {
461            coordinate: outcome.change.transition(),
462            change: outcome.change,
463            ack_required: true,
464            timestamp: BACnetTimeStamp::SequenceNumber(41),
465            message_text: None,
466        })
467        .expect("built-in object should lend all transition state to the kernel");
468
469        assert_eq!(
470            ai.event_detector.event_state,
471            bacnet_types::enums::EventState::HIGH_LIMIT
472        );
473        assert_eq!(
474            ai.event_detector.acked_transitions,
475            bacnet_types::bitstring::EventTransitionBits::TO_FAULT
476                | bacnet_types::bitstring::EventTransitionBits::TO_NORMAL
477        );
478        assert_eq!(
479            ai.event_history.time_stamps[0],
480            BACnetTimeStamp::SequenceNumber(41)
481        );
482    }
483
484    #[test]
485    fn rejected_delayed_and_fault_reindication_proposals_remain_retryable() {
486        use crate::event::{
487            EventStateChange, EventTransition, EventTransitionCommit, EventTransitionCommitError,
488        };
489        use bacnet_types::enums::{EngineeringUnits, EventState, Reliability};
490
491        let mut delayed =
492            AnalogInputObject::new(1, "AI-delayed", EngineeringUnits::DEGREES_CELSIUS).unwrap();
493        delayed.event_detector.high_limit = 80.0;
494        delayed.event_detector.limit_enable = bacnet_types::bitstring::LimitEnable::all();
495        delayed.event_detector.time_delay = 1;
496        delayed.set_present_value(81.0);
497        assert_eq!(delayed.evaluate_intrinsic_reporting(), None);
498        let proposal = delayed.tick_intrinsic_reporting().unwrap();
499        assert_eq!(
500            delayed.event_detector.pending.as_ref().unwrap().remaining,
501            1
502        );
503
504        let stale = EventTransitionCommit {
505            change: EventStateChange {
506                from: EventState::FAULT,
507                to: proposal.change.to,
508            },
509            coordinate: proposal.change.transition(),
510            ack_required: false,
511            timestamp: BACnetTimeStamp::SequenceNumber(9),
512            message_text: None,
513        };
514        assert_eq!(
515            delayed.commit_event_transition_internal(stale),
516            Err(EventTransitionCommitError::CurrentStateMismatch {
517                expected: EventState::FAULT,
518                actual: EventState::NORMAL,
519            })
520        );
521        assert_eq!(
522            delayed.event_detector.pending.as_ref().unwrap().remaining,
523            1
524        );
525        assert_eq!(delayed.tick_intrinsic_reporting(), Some(proposal));
526
527        let mut faulted =
528            AnalogInputObject::new(2, "AI-fault", EngineeringUnits::DEGREES_CELSIUS).unwrap();
529        faulted.reliability = Reliability::OVER_RANGE;
530        let entry = faulted.evaluate_intrinsic_reporting().unwrap();
531        crate::event::commit_test_proposal(&mut faulted, entry);
532        faulted.reliability = Reliability::NO_SENSOR;
533        let reindication = faulted.evaluate_intrinsic_reporting().unwrap();
534        assert_eq!(
535            reindication.change,
536            EventStateChange {
537                from: EventState::FAULT,
538                to: EventState::FAULT,
539            }
540        );
541        assert_eq!(
542            faulted.commit_event_transition_internal(EventTransitionCommit {
543                change: reindication.change.clone(),
544                coordinate: EventTransition::ToNormal,
545                ack_required: false,
546                timestamp: BACnetTimeStamp::SequenceNumber(10),
547                message_text: None,
548            }),
549            Err(EventTransitionCommitError::CoordinateTargetMismatch {
550                coordinate: EventTransition::ToNormal,
551                target: EventState::FAULT,
552            })
553        );
554        assert_eq!(
555            faulted.event_detector.fault_reliability,
556            Some(Reliability::OVER_RANGE)
557        );
558        assert_eq!(faulted.evaluate_intrinsic_reporting(), Some(reindication));
559    }
560
561    /// Regression guard for issue #123: once transitions populate timestamps and messages,
562    /// disabling event detection must still restore their Clause 13.2.2.1 initial conditions.
563    #[test]
564    fn ai_disabling_detection_resets_event_history() {
565        let mut ai = AnalogInputObject::new(1, "AI-1", EngineeringUnits::DEGREES_CELSIUS).unwrap();
566        assert_eq!(
567            ai.read_property(PropertyIdentifier::EVENT_DETECTION_ENABLE, None)
568                .unwrap(),
569            PropertyValue::Boolean(true)
570        );
571        ai.event_detector.event_state = bacnet_types::enums::EventState::HIGH_LIMIT;
572        ai.event_detector.acked_transitions = bacnet_types::bitstring::EventTransitionBits::empty();
573        ai.event_detector.pending = Some(crate::event::PendingTransition {
574            state: bacnet_types::enums::EventState::HIGH_LIMIT,
575            remaining: 2,
576        });
577        ai.event_detector.fault_reliability = Some(bacnet_types::enums::Reliability::NO_SENSOR);
578        ai.event_history.time_stamps = [
579            BACnetTimeStamp::SequenceNumber(1),
580            BACnetTimeStamp::SequenceNumber(2),
581            BACnetTimeStamp::SequenceNumber(3),
582        ];
583        ai.event_history.message_texts = ["offnormal".into(), "fault".into(), "normal".into()];
584
585        ai.write_property(
586            PropertyIdentifier::EVENT_DETECTION_ENABLE,
587            None,
588            PropertyValue::Boolean(false),
589            None,
590        )
591        .unwrap();
592
593        assert_eq!(
594            ai.read_property(PropertyIdentifier::EVENT_DETECTION_ENABLE, None)
595                .unwrap(),
596            PropertyValue::Boolean(false)
597        );
598        assert_eq!(
599            ai.event_detector.event_state,
600            bacnet_types::enums::EventState::NORMAL
601        );
602        assert_eq!(
603            ai.event_detector.acked_transitions,
604            bacnet_types::bitstring::EventTransitionBits::all()
605        );
606        assert!(ai.event_detector.pending.is_none());
607        assert!(ai.event_detector.fault_reliability.is_none());
608        assert_eq!(
609            ai.event_history.time_stamps,
610            [
611                BACnetTimeStamp::SequenceNumber(0),
612                BACnetTimeStamp::SequenceNumber(0),
613                BACnetTimeStamp::SequenceNumber(0),
614            ]
615        );
616        assert_eq!(ai.event_history.original_from_states, [None, None, None]);
617        assert_eq!(ai.event_history.original_to_states, [None, None, None]);
618        assert_eq!(
619            ai.event_history.message_texts,
620            [String::new(), String::new(), String::new()]
621        );
622    }
623
624    #[test]
625    fn ai_rejected_detection_write_preserves_hidden_state() {
626        let mut ai = AnalogInputObject::new(1, "AI-1", EngineeringUnits::DEGREES_CELSIUS).unwrap();
627        ai.event_detector.event_state = bacnet_types::enums::EventState::HIGH_LIMIT;
628        ai.event_detector.acked_transitions =
629            bacnet_types::bitstring::EventTransitionBits::TO_FAULT;
630        ai.event_detector.pending = Some(crate::event::PendingTransition {
631            state: bacnet_types::enums::EventState::NORMAL,
632            remaining: 2,
633        });
634        ai.event_detector.fault_reliability = Some(bacnet_types::enums::Reliability::NO_SENSOR);
635        ai.event_history.time_stamps[0] = BACnetTimeStamp::SequenceNumber(7);
636        ai.event_history.original_from_states[0] = Some(EventState::NORMAL);
637        ai.event_history.original_to_states[0] = Some(EventState::HIGH_LIMIT);
638        ai.event_history.message_texts[0] = "offnormal".into();
639        let error = ai
640            .write_property(
641                PropertyIdentifier::EVENT_DETECTION_ENABLE,
642                None,
643                PropertyValue::Unsigned(0),
644                None,
645            )
646            .unwrap_err();
647        assert!(
648            matches!(error, bacnet_types::error::Error::Remote(RemoteError::Error { class, code })
649            if class == ErrorClass::PROPERTY
650                && code == ErrorCode::INVALID_DATA_TYPE)
651        );
652
653        assert!(ai.event_detection_enable);
654        assert_eq!(
655            ai.event_detector.event_state,
656            bacnet_types::enums::EventState::HIGH_LIMIT
657        );
658        assert_eq!(
659            ai.event_detector.acked_transitions,
660            bacnet_types::bitstring::EventTransitionBits::TO_FAULT
661        );
662        assert_eq!(ai.event_detector.pending.unwrap().remaining, 2);
663        assert_eq!(
664            ai.event_detector.fault_reliability,
665            Some(bacnet_types::enums::Reliability::NO_SENSOR)
666        );
667        assert_eq!(
668            ai.event_history.time_stamps[0],
669            BACnetTimeStamp::SequenceNumber(7)
670        );
671        assert_eq!(
672            ai.event_history.original_from_states[0],
673            Some(EventState::NORMAL)
674        );
675        assert_eq!(
676            ai.event_history.original_to_states[0],
677            Some(EventState::HIGH_LIMIT)
678        );
679        assert_eq!(ai.event_history.message_texts[0], "offnormal");
680    }
681}
682
683#[cfg(test)]
684mod acknowledge_alarm_correlation_tests;
685
686#[cfg(test)]
687mod fault_out_of_range_non_finite_tests {
688    use super::*;
689    use bacnet_types::enums::{ErrorClass, ErrorCode, Reliability};
690    use bacnet_types::error::RemoteError;
691
692    fn assert_value_out_of_range(result: Result<ReliabilityEvaluation, Error>) {
693        assert!(matches!(
694            result,
695            Err(Error::Remote(RemoteError::Error { class, code }))
696                if class == ErrorClass::PROPERTY
697                    && code == ErrorCode::VALUE_OUT_OF_RANGE
698        ));
699    }
700
701    #[test]
702    fn ai_non_finite_monitored_values_preserve_reliability_status_and_ownership() {
703        for non_finite in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
704            let mut normal =
705                AnalogInputObject::new(1, "AI-normal", EngineeringUnits::DEGREES_CELSIUS).unwrap();
706            normal.configure_fault_out_of_range(10.0, 20.0).unwrap();
707            let status_before = normal
708                .read_property(PropertyIdentifier::STATUS_FLAGS, None)
709                .unwrap();
710            normal.present_value = non_finite;
711
712            assert_value_out_of_range(normal.evaluate_reliability_internal());
713            assert_eq!(normal.present_value.to_bits(), non_finite.to_bits());
714            assert_eq!(normal.reliability, Reliability::NO_FAULT_DETECTED);
715            assert_eq!(
716                normal
717                    .read_property(PropertyIdentifier::STATUS_FLAGS, None)
718                    .unwrap(),
719                status_before
720            );
721            assert!(normal.fault_out_of_range.owned_fault.is_none());
722
723            normal.present_value = 9.0;
724            assert_eq!(
725                normal.evaluate_reliability_internal().unwrap(),
726                ReliabilityEvaluation::Changed {
727                    old_reliability: Reliability::NO_FAULT_DETECTED,
728                    new_reliability: Reliability::UNDER_RANGE,
729                }
730            );
731
732            let mut owned =
733                AnalogInputObject::new(2, "AI-owned", EngineeringUnits::DEGREES_CELSIUS).unwrap();
734            owned.configure_fault_out_of_range(10.0, 20.0).unwrap();
735            owned.present_value = 9.0;
736            owned.evaluate_reliability_internal().unwrap();
737            let status_before = owned
738                .read_property(PropertyIdentifier::STATUS_FLAGS, None)
739                .unwrap();
740            owned.present_value = non_finite;
741
742            assert_value_out_of_range(owned.evaluate_reliability_internal());
743            assert_eq!(owned.present_value.to_bits(), non_finite.to_bits());
744            assert_eq!(owned.reliability, Reliability::UNDER_RANGE);
745            assert_eq!(
746                owned
747                    .read_property(PropertyIdentifier::STATUS_FLAGS, None)
748                    .unwrap(),
749                status_before
750            );
751            assert!(matches!(
752                owned.fault_out_of_range.owned_fault,
753                Some(OwnedRangeFault::UnderRange)
754            ));
755
756            owned.present_value = 21.0;
757            assert_eq!(
758                owned.evaluate_reliability_internal().unwrap(),
759                ReliabilityEvaluation::Changed {
760                    old_reliability: Reliability::UNDER_RANGE,
761                    new_reliability: Reliability::OVER_RANGE,
762                }
763            );
764        }
765    }
766}