Skip to main content

bacnet_objects/multistate/
output.rs

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