Skip to main content

bacnet_objects/multistate/
output.rs

1use super::*;
2use crate::event::CommandFailureDetector;
3
4// ---------------------------------------------------------------------------
5// MultiStateOutput (type 14)
6// ---------------------------------------------------------------------------
7
8/// BACnet Multi-State Output object.
9///
10/// Commandable multi-state output with 16-level priority array.
11/// Present_Value is Unsigned, range 1..=number_of_states.
12pub struct MultiStateOutputObject {
13    oid: ObjectIdentifier,
14    name: String,
15    description: String,
16    present_value: u32,
17    feedback_value: u32,
18    number_of_states: u32,
19    out_of_service: bool,
20    status_flags: StatusFlags,
21    priority_array: [Option<u32>; 16],
22    relinquish_default: u32,
23    /// Reliability: 0 = NO_FAULT_DETECTED.
24    reliability: u32,
25    reliability_before_out_of_service: Option<u32>,
26    reliability_inhibit: common::ReliabilityInhibitState,
27    reliability_evaluator: MultiStateReliabilityState,
28    event_detection_enable: bool,
29    state_text: Vec<String>,
30    /// COMMAND_FAILURE event detector.
31    event_detector: CommandFailureDetector,
32    pub(crate) event_history: EventHistory,
33    /// Value source tracking (optional per spec — exposed via VALUE_SOURCE property).
34    value_source: common::ValueSourceTracking,
35}
36
37impl MultiStateOutputObject {
38    pub fn new(
39        instance: u32,
40        name: impl Into<String>,
41        number_of_states: u32,
42    ) -> Result<Self, Error> {
43        let oid = ObjectIdentifier::new(ObjectType::MULTI_STATE_OUTPUT, instance)?;
44        require_nonzero_states(number_of_states)?;
45        Ok(Self {
46            oid,
47            name: name.into(),
48            description: String::new(),
49            present_value: 1,
50            feedback_value: 1,
51            number_of_states,
52            out_of_service: false,
53            status_flags: StatusFlags::empty(),
54            priority_array: [None; 16],
55            relinquish_default: 1,
56            reliability: 0,
57            reliability_before_out_of_service: None,
58            reliability_inhibit: common::ReliabilityInhibitState::default(),
59            reliability_evaluator: MultiStateReliabilityState::default(),
60            event_detection_enable: false,
61            state_text: (1..=number_of_states)
62                .map(|i| format!("State {i}"))
63                .collect(),
64            event_detector: CommandFailureDetector::default(),
65            event_history: EventHistory::default(),
66            value_source: common::ValueSourceTracking::default(),
67        })
68    }
69
70    /// Set the description string.
71    pub fn set_description(&mut self, desc: impl Into<String>) {
72        self.description = desc.into();
73    }
74
75    fn recalculate_present_value(&mut self) {
76        self.present_value =
77            common::recalculate_from_priority_array(&self.priority_array, self.relinquish_default);
78        let _ = self.recompute_reliability();
79    }
80
81    fn configuration_invalid(&self) -> bool {
82        let is_invalid = |value: u32| !(1..=self.number_of_states).contains(&value);
83        self.priority_array
84            .iter()
85            .flatten()
86            .copied()
87            .any(is_invalid)
88            || is_invalid(self.relinquish_default)
89            || is_invalid(self.feedback_value)
90    }
91
92    fn recompute_reliability(&mut self) -> ReliabilityEvaluation {
93        if self.out_of_service || self.reliability_inhibit.enabled() {
94            return ReliabilityEvaluation::Unchanged;
95        }
96        let configuration_invalid = self.configuration_invalid();
97        self.reliability_evaluator.evaluate(
98            configuration_invalid,
99            self.present_value,
100            self.number_of_states,
101            &mut self.reliability,
102        )
103    }
104
105    /// Change the locally configured number of states.
106    ///
107    /// The BACnet `Number_Of_States` property remains read-only. A successful
108    /// local change resizes State_Text and immediately re-evaluates Reliability;
109    /// retained commands, defaults, feedback, and Present_Value are not repaired.
110    pub fn set_number_of_states(&mut self, number_of_states: u32) -> Result<(), Error> {
111        resize_state_text(
112            &mut self.number_of_states,
113            &mut self.state_text,
114            number_of_states,
115        )?;
116        let _ = self.recompute_reliability();
117        Ok(())
118    }
119
120    /// Set the Relinquish_Default (#270).
121    ///
122    /// Validated the same way a commanded Present_Value is (Unsigned
123    /// 1..=Number_Of_States); after the store, Present_Value is resolved anew
124    /// from the priority array so an empty array falls back to the new
125    /// default immediately.
126    ///
127    /// Number_Of_States shrink interplay: if the state count ever shrinks
128    /// below this value, the standard leaves adjustment of Priority_Array,
129    /// Relinquish_Default, Present_Value, and Feedback_Value "a local matter"
130    /// (Clause 12.19 / Table 12-22 Number_Of_States text). This implementation does
131    /// NOT auto-adjust: out-of-range stored values are a configuration
132    /// decision for the application to resolve; the object-owned evaluator
133    /// reports CONFIGURATION_ERROR while that retained condition remains.
134    pub fn set_relinquish_default(&mut self, value: u32) -> Result<(), Error> {
135        if value < 1 || value > self.number_of_states {
136            return Err(common::value_out_of_range_error());
137        }
138        self.relinquish_default = value;
139        self.recalculate_present_value();
140        Ok(())
141    }
142}
143
144impl BACnetObject for MultiStateOutputObject {
145    fn object_identifier(&self) -> ObjectIdentifier {
146        self.oid
147    }
148
149    fn object_name(&self) -> &str {
150        &self.name
151    }
152
153    fn supports_cov(&self) -> bool {
154        true
155    }
156
157    crate::event::impl_builtin_intrinsic_reporting!(
158        event_detector,
159        event_history,
160        [present_value, feedback_value],
161        reliability,
162        event_detection_enable,
163        CommandFailureDetector::ALGORITHM
164    );
165    impl_intrinsic_write_rollback!(
166        event_detector,
167        event_detection_enable,
168        event_history,
169        reliability_inhibit,
170        reliability,
171        out_of_service,
172        reliability_before_out_of_service;
173        reliability_evaluator,
174        priority_array,
175        relinquish_default,
176        present_value
177    );
178
179    fn acknowledge_alarm_correlated_internal(
180        &mut self,
181        event_state: EventState,
182        timestamp: &BACnetTimeStamp,
183    ) -> Result<(), Error> {
184        if !self.event_detection_enable {
185            return Err(Error::Protocol {
186                class: ErrorClass::OBJECT.to_raw() as u32,
187                code: ErrorCode::NO_ALARM_CONFIGURED.to_raw() as u32,
188            });
189        }
190        self.event_history.acknowledge_correlated(
191            &mut self.event_detector.acked_transitions,
192            event_state,
193            timestamp,
194        )
195    }
196
197    fn read_property(
198        &self,
199        property: PropertyIdentifier,
200        array_index: Option<u32>,
201    ) -> Result<PropertyValue, Error> {
202        if property == PropertyIdentifier::STATUS_FLAGS {
203            return Ok(common::compute_status_flags(
204                self.status_flags,
205                self.reliability,
206                self.out_of_service,
207                self.event_detector.event_state.to_raw(),
208            ));
209        }
210        if property == PropertyIdentifier::EVENT_DETECTION_ENABLE {
211            return Ok(PropertyValue::Boolean(self.event_detection_enable));
212        }
213        if let Some(value) = self.reliability_inhibit.read(property) {
214            return Ok(value);
215        }
216        if let Some(result) = read_common_properties!(self, property, array_index) {
217            return result;
218        }
219        if let Some(result) = read_generic_event_properties!(self, property) {
220            return result;
221        }
222        if let Some(result) = self.event_history.read(property, array_index) {
223            return result;
224        }
225        match property {
226            p if p == PropertyIdentifier::OBJECT_TYPE => Ok(PropertyValue::Enumerated(
227                ObjectType::MULTI_STATE_OUTPUT.to_raw(),
228            )),
229            p if p == PropertyIdentifier::PRESENT_VALUE => {
230                Ok(PropertyValue::Unsigned(self.present_value as u64))
231            }
232            p if p == PropertyIdentifier::FEEDBACK_VALUE => {
233                Ok(PropertyValue::Unsigned(self.feedback_value as u64))
234            }
235            p if p == PropertyIdentifier::NUMBER_OF_STATES => {
236                Ok(PropertyValue::Unsigned(self.number_of_states as u64))
237            }
238            p if p == PropertyIdentifier::VALUE_SOURCE => {
239                Ok(self.value_source.value_source.clone())
240            }
241            p if p == PropertyIdentifier::LAST_COMMAND_TIME => Ok(PropertyValue::Unsigned(
242                match self.value_source.last_command_time {
243                    BACnetTimeStamp::SequenceNumber(n) => u64::from(n),
244                    _ => 0,
245                },
246            )),
247            p if p == PropertyIdentifier::PRIORITY_ARRAY => {
248                common::read_priority_array!(self, array_index, |v: u32| PropertyValue::Unsigned(
249                    v as u64
250                ))
251            }
252            p if p == PropertyIdentifier::RELINQUISH_DEFAULT => {
253                Ok(PropertyValue::Unsigned(self.relinquish_default as u64))
254            }
255            p if p == PropertyIdentifier::CURRENT_COMMAND_PRIORITY => {
256                Ok(common::current_command_priority(&self.priority_array))
257            }
258            p if p == PropertyIdentifier::STATE_TEXT => match array_index {
259                None => Ok(PropertyValue::List(
260                    self.state_text
261                        .iter()
262                        .map(|s| PropertyValue::CharacterString(s.clone()))
263                        .collect(),
264                )),
265                Some(0) => Ok(PropertyValue::Unsigned(self.state_text.len() as u64)),
266                Some(idx) if idx >= 1 && (idx as usize) <= self.state_text.len() => Ok(
267                    PropertyValue::CharacterString(self.state_text[(idx - 1) as usize].clone()),
268                ),
269                _ => Err(common::invalid_array_index_error()),
270            },
271            _ => Err(common::unknown_property_error()),
272        }
273    }
274
275    fn write_property(
276        &mut self,
277        property: PropertyIdentifier,
278        array_index: Option<u32>,
279        value: PropertyValue,
280        priority: Option<u8>,
281    ) -> Result<(), Error> {
282        {
283            let num_states = self.number_of_states;
284            common::write_priority_array_direct!(self, property, array_index, value, |v| {
285                if let PropertyValue::Unsigned(u) = v {
286                    if u < 1 || u > num_states as u64 {
287                        Err(common::value_out_of_range_error())
288                    } else {
289                        Ok(u as u32)
290                    }
291                } else {
292                    Err(common::invalid_data_type_error())
293                }
294            });
295        }
296        if property == PropertyIdentifier::PRESENT_VALUE {
297            let num_states = self.number_of_states;
298            return common::write_priority_array!(self, value, priority, |v| {
299                if let PropertyValue::Unsigned(u) = v {
300                    if u < 1 || u > num_states as u64 {
301                        Err(common::value_out_of_range_error())
302                    } else {
303                        Ok(u as u32)
304                    }
305                } else {
306                    Err(common::invalid_data_type_error())
307                }
308            });
309        }
310        if property == PropertyIdentifier::FEEDBACK_VALUE {
311            if let PropertyValue::Unsigned(u) = value {
312                // Checked for representability but deliberately NOT range-checked against
313                // Number_Of_States, unlike Present_Value. Clause 12.19 treats a
314                // Feedback_Value outside the state set as a condition to be *reported* —
315                // "If any of those properties other than Present_Value are out of range,
316                // the value of the Reliability property shall remain CONFIGURATION_ERROR"
317                // — not as a value to refuse. Feedback_Value reflects a sensed quantity
318                // whose determination is "a local matter", so it can legitimately fall
319                // outside the configured range; refusing it would make CONFIGURATION_ERROR
320                // unreachable. The object-owned evaluator applies that reliability.
321                //
322                // The u32 conversion is still checked. A BACnet Unsigned decodes from up
323                // to 8 octets, so a bare `as u32` would wrap a large value back into the
324                // valid state range — silently turning a disagreeing feedback into an
325                // agreeing one and suppressing the COMMAND_FAILURE transition. That is a
326                // representation limit, not a configuration limit, so it is enforced here
327                // while the state-set range is not.
328                self.feedback_value = common::u64_to_u32(u)?;
329                let _ = self.recompute_reliability();
330                return Ok(());
331            }
332            return Err(common::invalid_data_type_error());
333        }
334        if property == PropertyIdentifier::STATE_TEXT {
335            match array_index {
336                Some(idx) if idx >= 1 && (idx as usize) <= self.state_text.len() => {
337                    if let PropertyValue::CharacterString(s) = value {
338                        self.state_text[(idx - 1) as usize] = s;
339                        return Ok(());
340                    }
341                    return Err(common::invalid_data_type_error());
342                }
343                None => return Err(common::write_access_denied_error()),
344                _ => return Err(common::invalid_array_index_error()),
345            }
346        }
347        if property == PropertyIdentifier::EVENT_DETECTION_ENABLE {
348            if let PropertyValue::Boolean(v) = value {
349                self.event_detection_enable = v;
350                if !v {
351                    self.event_detector.event_state = bacnet_types::enums::EventState::NORMAL;
352                    self.event_detector.acked_transitions = 0b111;
353                    self.event_detector.pending = None;
354                    self.event_detector.fault_reliability = None;
355                    self.event_history.reset();
356                }
357                return Ok(());
358            }
359            return Err(common::invalid_data_type_error());
360        }
361        if let Some(result) = write_generic_event_properties!(self, property, value) {
362            return result;
363        }
364        if let Some(result) = self.reliability_inhibit.write_inhibit(
365            &mut self.reliability,
366            self.out_of_service,
367            property,
368            &value,
369        ) {
370            result?;
371            let _ = self.recompute_reliability();
372            return Ok(());
373        }
374        if let Some(result) = self.reliability_inhibit.write_out_of_service(
375            &mut self.out_of_service,
376            &mut self.reliability,
377            &mut self.reliability_before_out_of_service,
378            property,
379            &value,
380        ) {
381            result?;
382            let _ = self.recompute_reliability();
383            return Ok(());
384        }
385        if let Some(result) = common::write_object_name(&mut self.name, property, &value) {
386            return result;
387        }
388        if let Some(result) = common::write_description(&mut self.description, property, &value) {
389            return result;
390        }
391        // Clause 12.19, while Out_Of_Service is TRUE: "the Present_Value property and
392        // the Reliability property, if present and capable of taking on values other
393        // than NO_FAULT_DETECTED, shall be writable to allow simulating specific
394        // conditions or for testing purposes".
395        // `is_writable_property` stays statically true because it describes capability.
396        if let Some(result) = self.reliability_inhibit.write_client_reliability(
397            self.out_of_service,
398            &mut self.reliability,
399            property,
400            &value,
401        ) {
402            return result;
403        }
404        if property == PropertyIdentifier::RELINQUISH_DEFAULT {
405            if let PropertyValue::Unsigned(u) = value {
406                let v = common::u64_to_u32(u)?;
407                return self.set_relinquish_default(v);
408            }
409            return Err(common::invalid_data_type_error());
410        }
411        Err(common::write_access_denied_error())
412    }
413
414    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
415        static PROPS: &[PropertyIdentifier] = &[
416            PropertyIdentifier::OBJECT_IDENTIFIER,
417            PropertyIdentifier::OBJECT_NAME,
418            PropertyIdentifier::DESCRIPTION,
419            PropertyIdentifier::OBJECT_TYPE,
420            PropertyIdentifier::PRESENT_VALUE,
421            PropertyIdentifier::FEEDBACK_VALUE,
422            PropertyIdentifier::STATUS_FLAGS,
423            PropertyIdentifier::EVENT_STATE,
424            PropertyIdentifier::EVENT_DETECTION_ENABLE,
425            PropertyIdentifier::EVENT_ENABLE,
426            PropertyIdentifier::TIME_DELAY,
427            PropertyIdentifier::TIME_DELAY_NORMAL,
428            PropertyIdentifier::NOTIFY_TYPE,
429            PropertyIdentifier::NOTIFICATION_CLASS,
430            PropertyIdentifier::ACKED_TRANSITIONS,
431            PropertyIdentifier::EVENT_TIME_STAMPS,
432            PropertyIdentifier::EVENT_MESSAGE_TEXTS,
433            PropertyIdentifier::OUT_OF_SERVICE,
434            PropertyIdentifier::NUMBER_OF_STATES,
435            PropertyIdentifier::PRIORITY_ARRAY,
436            PropertyIdentifier::RELINQUISH_DEFAULT,
437            PropertyIdentifier::CURRENT_COMMAND_PRIORITY,
438            PropertyIdentifier::RELIABILITY,
439            PropertyIdentifier::RELIABILITY_EVALUATION_INHIBIT,
440            PropertyIdentifier::STATE_TEXT,
441        ];
442        Cow::Borrowed(PROPS)
443    }
444
445    fn is_createable(&self) -> bool {
446        true
447    }
448    fn set_reliability_internal(&mut self, reliability: u32) -> Result<(), Error> {
449        if self.out_of_service || self.reliability_inhibit.enabled() {
450            return Err(common::write_access_denied_error());
451        }
452        if !common::is_reliability_value_valid(reliability) {
453            return Err(common::value_out_of_range_error());
454        }
455        self.reliability = reliability;
456        self.reliability_evaluator.clear_ownership();
457        Ok(())
458    }
459
460    fn evaluate_reliability_internal(&mut self) -> Result<ReliabilityEvaluation, Error> {
461        Ok(self.recompute_reliability())
462    }
463
464    fn reliability_evaluation_inhibited_internal(&self) -> bool {
465        self.reliability_inhibit.enabled()
466    }
467
468    fn is_writable_property(&self, property: PropertyIdentifier) -> bool {
469        // Mirrors the MultiStateOutput `write_property` arms.
470        common::is_multistate_commandable_writable(property)
471            || property == PropertyIdentifier::RELIABILITY
472            || property == PropertyIdentifier::RELIABILITY_EVALUATION_INHIBIT
473            || common::is_generic_event_property_writable(property)
474            || property == PropertyIdentifier::FEEDBACK_VALUE
475            || property == PropertyIdentifier::EVENT_DETECTION_ENABLE
476    }
477}
478
479#[cfg(test)]
480mod command_failure_tests {
481    use super::*;
482    use bacnet_types::enums::{EventState, EventType};
483
484    fn write_unsigned(
485        object: &mut MultiStateOutputObject,
486        property: PropertyIdentifier,
487        value: u64,
488    ) {
489        object
490            .write_property(property, None, PropertyValue::Unsigned(value), None)
491            .unwrap();
492    }
493
494    fn set_detection_enabled(object: &mut MultiStateOutputObject, enabled: bool) {
495        object
496            .write_property(
497                PropertyIdentifier::EVENT_DETECTION_ENABLE,
498                None,
499                PropertyValue::Boolean(enabled),
500                None,
501            )
502            .unwrap();
503    }
504
505    #[test]
506    fn feedback_value_round_trips_and_is_advertised_writable() {
507        let mut mso = MultiStateOutputObject::new(1, "MSO-1", 3).unwrap();
508
509        write_unsigned(&mut mso, PropertyIdentifier::FEEDBACK_VALUE, 2);
510
511        assert_eq!(
512            mso.read_property(PropertyIdentifier::FEEDBACK_VALUE, None)
513                .unwrap(),
514            PropertyValue::Unsigned(2)
515        );
516        assert!(mso
517            .property_list()
518            .contains(&PropertyIdentifier::FEEDBACK_VALUE));
519        assert!(mso.is_writable_property(PropertyIdentifier::FEEDBACK_VALUE));
520        assert!(mso
521            .write_property(
522                PropertyIdentifier::FEEDBACK_VALUE,
523                None,
524                PropertyValue::Enumerated(2),
525                None,
526            )
527            .is_err());
528    }
529
530    /// Clause 12.19 defines an out-of-range Feedback_Value as a reportable condition
531    /// (Reliability CONFIGURATION_ERROR), not a value to refuse. Refusing it
532    /// would make that reliability unreachable, so the write is accepted even though
533    /// Present_Value at the same value would be rejected.
534    #[test]
535    fn feedback_value_outside_the_state_set_is_accepted_unlike_present_value() {
536        let mut mso = MultiStateOutputObject::new(1, "MSO-1", 3).unwrap();
537
538        write_unsigned(&mut mso, PropertyIdentifier::FEEDBACK_VALUE, 7);
539        assert_eq!(
540            mso.read_property(PropertyIdentifier::FEEDBACK_VALUE, None)
541                .unwrap(),
542            PropertyValue::Unsigned(7)
543        );
544
545        assert!(mso
546            .write_property(
547                PropertyIdentifier::PRESENT_VALUE,
548                None,
549                PropertyValue::Unsigned(7),
550                None,
551            )
552            .is_err());
553    }
554
555    /// `feedback_value` initializes to match the initial `Present_Value` so that enabling
556    /// detection on an untouched object does not immediately report a command failure. This
557    /// states that property directly rather than relying on it incidentally: several other
558    /// tests in this module also fail if the initializer changes, but each does so as a side
559    /// effect of asserting something else, which is a fragile thing to depend on.
560    #[test]
561    fn fresh_object_reports_nothing_when_detection_is_enabled() {
562        let mut mso = MultiStateOutputObject::new(1, "MSO-1", 3).unwrap();
563        set_detection_enabled(&mut mso, true);
564
565        assert_eq!(mso.evaluate_intrinsic_reporting(), None);
566        assert_eq!(
567            mso.read_property(PropertyIdentifier::EVENT_STATE, None)
568                .unwrap(),
569            PropertyValue::Enumerated(EventState::NORMAL.to_raw())
570        );
571    }
572
573    /// A BACnet Unsigned decodes from up to 8 octets, so an unchecked `as u32` would wrap
574    /// a large Feedback_Value back into the valid state range and suppress the very
575    /// transition this object type exists to report. 0x1_0000_0002 truncates to 2, which
576    /// would read as agreeing with a Present_Value of 2.
577    #[test]
578    fn oversized_feedback_value_is_rejected_rather_than_wrapped_into_agreement() {
579        let mut mso = MultiStateOutputObject::new(1, "MSO-1", 3).unwrap();
580        set_detection_enabled(&mut mso, true);
581        write_unsigned(&mut mso, PropertyIdentifier::PRESENT_VALUE, 2);
582
583        assert!(mso
584            .write_property(
585                PropertyIdentifier::FEEDBACK_VALUE,
586                None,
587                PropertyValue::Unsigned(0x1_0000_0002),
588                None,
589            )
590            .is_err());
591
592        // The rejected write left the feedback value alone, so Present_Value 2 against the
593        // initial feedback of 1 still disagrees and still reports COMMAND_FAILURE.
594        assert_eq!(
595            mso.read_property(PropertyIdentifier::FEEDBACK_VALUE, None)
596                .unwrap(),
597            PropertyValue::Unsigned(1)
598        );
599        let outcome = mso.evaluate_intrinsic_reporting().unwrap();
600        assert_eq!(outcome.change.to, EventState::OFFNORMAL);
601        assert_eq!(outcome.event_type, EventType::COMMAND_FAILURE);
602    }
603
604    #[test]
605    fn command_failure_uses_present_and_feedback() {
606        let mut mso = MultiStateOutputObject::new(1, "MSO-1", 3).unwrap();
607        set_detection_enabled(&mut mso, true);
608        write_unsigned(&mut mso, PropertyIdentifier::PRESENT_VALUE, 2);
609
610        let proposal = mso.evaluate_intrinsic_reporting().unwrap();
611        let outcome = crate::event::commit_test_proposal(&mut mso, proposal);
612        assert_eq!(outcome.change.to, EventState::OFFNORMAL);
613        assert_eq!(outcome.event_type, EventType::COMMAND_FAILURE);
614
615        write_unsigned(&mut mso, PropertyIdentifier::FEEDBACK_VALUE, 2);
616        let returned = mso.evaluate_intrinsic_reporting().unwrap();
617        assert_eq!(returned.change.from, EventState::OFFNORMAL);
618        assert_eq!(returned.change.to, EventState::NORMAL);
619        assert_eq!(returned.event_type, EventType::COMMAND_FAILURE);
620    }
621
622    #[test]
623    fn time_delay_gates_command_failure() {
624        let mut mso = MultiStateOutputObject::new(1, "MSO-1", 3).unwrap();
625        set_detection_enabled(&mut mso, true);
626        write_unsigned(&mut mso, PropertyIdentifier::TIME_DELAY, 2);
627        write_unsigned(&mut mso, PropertyIdentifier::PRESENT_VALUE, 2);
628
629        assert_eq!(mso.evaluate_intrinsic_reporting(), None);
630        assert_eq!(mso.tick_intrinsic_reporting(), None);
631        let outcome = mso.tick_intrinsic_reporting().unwrap();
632        assert_eq!(outcome.change.to, EventState::OFFNORMAL);
633        assert_eq!(outcome.event_type, EventType::COMMAND_FAILURE);
634    }
635
636    #[test]
637    fn event_enable_to_offnormal_bit_controls_distribution() {
638        for (encoded, expected) in [(0x80, true), (0x00, false)] {
639            let mut mso = MultiStateOutputObject::new(1, "MSO-1", 3).unwrap();
640            set_detection_enabled(&mut mso, true);
641            mso.write_property(
642                PropertyIdentifier::EVENT_ENABLE,
643                None,
644                PropertyValue::BitString {
645                    unused_bits: 5,
646                    data: vec![encoded],
647                },
648                None,
649            )
650            .unwrap();
651            write_unsigned(&mut mso, PropertyIdentifier::PRESENT_VALUE, 2);
652            assert_eq!(
653                mso.evaluate_intrinsic_reporting().unwrap().distribute,
654                expected
655            );
656        }
657    }
658
659    #[test]
660    fn generic_event_properties_round_trip_and_match_pics() {
661        let mut mso = MultiStateOutputObject::new(1, "MSO-1", 3).unwrap();
662        let writes = [
663            (
664                PropertyIdentifier::NOTIFY_TYPE,
665                PropertyValue::Enumerated(1),
666            ),
667            (
668                PropertyIdentifier::NOTIFICATION_CLASS,
669                PropertyValue::Unsigned(42),
670            ),
671        ];
672        for (property, value) in writes {
673            mso.write_property(property, None, value.clone(), None)
674                .unwrap();
675            assert_eq!(mso.read_property(property, None).unwrap(), value);
676        }
677
678        // Acked_Transitions is readable but NOT writable: only the AcknowledgeAlarm service
679        // may change it. A property write would assign where the service ORs, so it could
680        // both fabricate and erase acknowledgments, and it would break the Clause 12.19
681        // requirement that the field sit at its initial condition while
682        // Event_Detection_Enable is FALSE.
683        assert!(mso
684            .write_property(
685                PropertyIdentifier::ACKED_TRANSITIONS,
686                None,
687                PropertyValue::BitString {
688                    unused_bits: 5,
689                    data: vec![0x80],
690                },
691                None,
692            )
693            .is_err());
694        assert!(!mso.is_writable_property(PropertyIdentifier::ACKED_TRANSITIONS));
695
696        for property in [
697            PropertyIdentifier::EVENT_ENABLE,
698            PropertyIdentifier::TIME_DELAY,
699            PropertyIdentifier::TIME_DELAY_NORMAL,
700            PropertyIdentifier::NOTIFY_TYPE,
701            PropertyIdentifier::NOTIFICATION_CLASS,
702        ] {
703            assert!(mso.property_list().contains(&property));
704            assert!(mso.is_writable_property(property));
705        }
706        assert!(mso
707            .write_property(
708                PropertyIdentifier::EVENT_STATE,
709                None,
710                PropertyValue::Enumerated(EventState::NORMAL.to_raw()),
711                None,
712            )
713            .is_err());
714        assert!(!mso.is_writable_property(PropertyIdentifier::EVENT_STATE));
715    }
716
717    #[test]
718    fn detection_enable_is_a_disabled_by_default_invariant() {
719        let mut mso = MultiStateOutputObject::new(1, "MSO-1", 3).unwrap();
720        write_unsigned(&mut mso, PropertyIdentifier::PRESENT_VALUE, 2);
721
722        assert_eq!(mso.evaluate_intrinsic_reporting(), None);
723        assert_eq!(mso.tick_intrinsic_reporting(), None);
724        assert_eq!(
725            mso.read_property(PropertyIdentifier::EVENT_STATE, None)
726                .unwrap(),
727            PropertyValue::Enumerated(EventState::NORMAL.to_raw())
728        );
729        assert_eq!(
730            mso.read_property(PropertyIdentifier::STATUS_FLAGS, None)
731                .unwrap(),
732            PropertyValue::BitString {
733                unused_bits: 4,
734                data: vec![0],
735            }
736        );
737
738        set_detection_enabled(&mut mso, true);
739        assert_eq!(
740            mso.evaluate_intrinsic_reporting().unwrap().change.to,
741            EventState::OFFNORMAL
742        );
743        mso.event_detector.acked_transitions = 0;
744        set_detection_enabled(&mut mso, false);
745
746        assert_eq!(
747            mso.read_property(PropertyIdentifier::EVENT_STATE, None)
748                .unwrap(),
749            PropertyValue::Enumerated(EventState::NORMAL.to_raw())
750        );
751        assert_eq!(
752            mso.read_property(PropertyIdentifier::ACKED_TRANSITIONS, None)
753                .unwrap(),
754            PropertyValue::BitString {
755                unused_bits: 5,
756                data: vec![0xe0],
757            }
758        );
759        assert_eq!(mso.evaluate_intrinsic_reporting(), None);
760        assert_eq!(mso.tick_intrinsic_reporting(), None);
761
762        mso.reliability = 1;
763        assert_eq!(
764            mso.read_property(PropertyIdentifier::STATUS_FLAGS, None)
765                .unwrap(),
766            PropertyValue::BitString {
767                unused_bits: 4,
768                data: vec![0x40],
769            }
770        );
771        assert_eq!(
772            mso.read_property(PropertyIdentifier::EVENT_DETECTION_ENABLE, None)
773                .unwrap(),
774            PropertyValue::Boolean(false)
775        );
776        assert!(mso
777            .property_list()
778            .contains(&PropertyIdentifier::EVENT_DETECTION_ENABLE));
779        assert!(mso.is_writable_property(PropertyIdentifier::EVENT_DETECTION_ENABLE));
780    }
781}
782
783#[cfg(test)]
784mod reliability_evaluator_tests {
785    use super::*;
786    use bacnet_types::enums::Reliability;
787
788    #[test]
789    fn configuration_error_dominates_bypassed_invalid_present_value() {
790        let mut mso = MultiStateOutputObject::new(1, "MSO-dominance", 2).unwrap();
791        mso.present_value = 3;
792        mso.feedback_value = 3;
793
794        mso.evaluate_reliability_internal().unwrap();
795        assert_eq!(
796            mso.reliability,
797            Reliability::CONFIGURATION_ERROR.to_raw(),
798            "invalid configuration must dominate invalid Present_Value"
799        );
800        mso.feedback_value = 1;
801        mso.evaluate_reliability_internal().unwrap();
802        assert_eq!(
803            mso.reliability,
804            Reliability::MULTI_STATE_OUT_OF_RANGE.to_raw()
805        );
806        mso.write_property(
807            PropertyIdentifier::PRESENT_VALUE,
808            None,
809            PropertyValue::Unsigned(1),
810            None,
811        )
812        .unwrap();
813        assert_eq!(
814            mso.reliability,
815            Reliability::NO_FAULT_DETECTED.to_raw(),
816            "the central priority recalculation must recover synchronously"
817        );
818    }
819}