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