Skip to main content

bacnet_types/enums/
resolve.rs

1// ===========================================================================
2// Enumerated resolution: raw number -> named enum, keyed by property
3// ===========================================================================
4//
5// On the wire a BACnet `Enumerated` is just a number (see
6// `PropertyValue::Enumerated(u32)`). The number carries no information about
7// *which* enumeration it belongs to; that is determined by the property being
8// read. ASHRAE 135 Clause 12 (object properties) and Clause 21 (application
9// types) fix, per property, the concrete `BACnetXxx` ENUMERATED type. This
10// module encodes that property -> enum-type mapping so a decoded value can be
11// promoted from a bare number to its named variant.
12//
13// Properties whose ENUMERATED type depends on the *object type* rather than the
14// property alone (e.g. `present-value`, plural `alarm-values`, and
15// `fault-values`, whose type varies between Binary/Multi-state/Life-Safety
16// objects) are intentionally
17// left unmapped: the property identifier alone is insufficient to name them
18// correctly, so they resolve to `Unknown`.
19//
20// One deliberate exception: `tracking-value` (164) is object-type-dependent
21// (Lighting Output types it REAL), but `BACnetLifeSafetyState` is its only
22// ENUMERATED form in the standard, so promoting the `Enumerated` case is
23// unambiguous and the arm is kept.
24
25#[cfg(not(feature = "std"))]
26use alloc::{string::String, vec::Vec};
27
28use crate::bitstring::{EventTransitionBits, LimitEnable, ObjectTypesSupported, ServicesSupported};
29use crate::primitives::{Date, ObjectIdentifier, PropertyValue, StatusFlags, Time};
30
31use super::*;
32
33macro_rules! resolved_enum {
34    ($($variant:ident($ty:ty)),+ $(,)?) => {
35        /// A BACnet `Enumerated` value promoted to its named enumeration.
36        ///
37        /// Produced by [`ResolvedEnum::from_property`], which uses the property
38        /// identifier to decide which `BACnetXxx` enum a raw number denotes.
39        /// Values whose enumeration cannot be determined from the property alone
40        /// (vendor-proprietary or object-type-dependent properties) become
41        /// [`ResolvedEnum::Unknown`], preserving the raw number.
42        #[derive(Clone, Copy, PartialEq, Eq, Debug)]
43        pub enum ResolvedEnum {
44            $(
45                #[doc = concat!("A [`", stringify!($ty), "`] value.")]
46                $variant($ty),
47            )+
48            /// No known enumeration for this property; the raw wire value is kept.
49            Unknown(u32),
50        }
51
52        impl ResolvedEnum {
53            /// The named constant this value is (`"ANALOG_INPUT"`, `"HIGH_LIMIT"`),
54            /// or `None` when the property has no known enumeration or the
55            /// number has no name in it (a vendor or reserved value).
56            pub const fn name(&self) -> Option<&'static str> {
57                match self {
58                    $( Self::$variant(v) => v.name(), )+
59                    Self::Unknown(_) => None,
60                }
61            }
62
63            /// The enumeration this value belongs to: its type name
64            /// (`"EventState"`) and every named constant with its wire value,
65            /// for parsing a name typed for the same property. `None` for
66            /// [`ResolvedEnum::Unknown`].
67            pub fn enumeration(&self) -> Option<(&'static str, Vec<(&'static str, u32)>)> {
68                match self {
69                    $( Self::$variant(_) => Some((
70                        stringify!($variant),
71                        <$ty>::ALL_NAMED
72                            .iter()
73                            .map(|&(name, value)| (name, u32::from(value.to_raw())))
74                            .collect(),
75                    )), )+
76                    Self::Unknown(_) => None,
77                }
78            }
79        }
80
81        impl core::fmt::Display for ResolvedEnum {
82            fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
83                match self {
84                    $( Self::$variant(v) => core::fmt::Display::fmt(v, f), )+
85                    Self::Unknown(n) => write!(f, "{n}"),
86                }
87            }
88        }
89    };
90}
91
92resolved_enum! {
93    ObjectType(ObjectType),
94    EventState(EventState),
95    EventType(EventType),
96    FaultType(FaultType),
97    NotifyType(NotifyType),
98    Reliability(Reliability),
99    DeviceStatus(DeviceStatus),
100    Segmentation(Segmentation),
101    EngineeringUnits(EngineeringUnits),
102    Polarity(Polarity),
103    BinaryPV(BinaryPV),
104    ProgramState(ProgramState),
105    ProgramChange(ProgramChange),
106    ProgramError(ProgramError),
107    NodeType(NodeType),
108    LoggingType(LoggingType),
109    FileAccessMethod(FileAccessMethod),
110    BackupAndRestoreState(BackupAndRestoreState),
111    RestartReason(RestartReason),
112    LifeSafetyMode(LifeSafetyMode),
113    LifeSafetyState(LifeSafetyState),
114    LifeSafetyOperation(LifeSafetyOperation),
115    SilencedState(SilencedState),
116    Maintenance(Maintenance),
117    DoorStatus(DoorStatus),
118    DoorAlarmState(DoorAlarmState),
119    DoorSecuredStatus(DoorSecuredStatus),
120    LockStatus(LockStatus),
121    AccessEvent(AccessEvent),
122    AuthorizationMode(AuthorizationMode),
123    AccessPassbackMode(AccessPassbackMode),
124    AccessUserType(AccessUserType),
125    AccessCredentialDisable(AccessCredentialDisable),
126    AccessCredentialDisableReason(AccessCredentialDisableReason),
127    AuthenticationStatus(AuthenticationStatus),
128    AuthorizationExemption(AuthorizationExemption),
129    AccessZoneOccupancyState(AccessZoneOccupancyState),
130    Relationship(Relationship),
131    NetworkType(NetworkType),
132    NetworkNumberQuality(NetworkNumberQuality),
133    IPMode(IPMode),
134    NetworkPortCommand(NetworkPortCommand),
135    ProtocolLevel(ProtocolLevel),
136    LightingInProgress(LightingInProgress),
137    LightingTransition(LightingTransition),
138    TimerState(TimerState),
139    TimerTransition(TimerTransition),
140    WriteStatus(WriteStatus),
141    LiftCarMode(LiftCarMode),
142    LiftGroupMode(LiftGroupMode),
143    EscalatorMode(EscalatorMode),
144    EscalatorOperationDirection(EscalatorOperationDirection),
145    LiftCarDriveStatus(LiftCarDriveStatus),
146    LiftCarDirection(LiftCarDirection),
147    LiftCarDoorCommand(LiftCarDoorCommand),
148    AuditLevel(AuditLevel),
149}
150
151impl ResolvedEnum {
152    /// Promote a raw `Enumerated` wire value to its named enumeration using the
153    /// property it was read from.
154    ///
155    /// The property numbers below are the identifiers defined in
156    /// [`PropertyIdentifier`] (ASHRAE 135 Clause 21). A property with no known
157    /// scalar ENUMERATED type — including vendor-proprietary properties and ones
158    /// whose type depends on the object type — yields [`ResolvedEnum::Unknown`].
159    pub fn from_property(property: PropertyIdentifier, value: u32) -> Self {
160        match property.to_raw() {
161            79 => Self::ObjectType(ObjectType::from_raw(value)), // object-type
162            36 => Self::EventState(EventState::from_raw(value)), // event-state
163            37 => Self::EventType(EventType::from_raw(value)),   // event-type
164            359 => Self::FaultType(FaultType::from_raw(value)),  // fault-type
165            72 => Self::NotifyType(NotifyType::from_raw(value)), // notify-type
166            103 => Self::Reliability(Reliability::from_raw(value)), // reliability
167            112 => Self::DeviceStatus(DeviceStatus::from_raw(value)), // system-status
168            // segmentation-supported: Segmentation is the one u8-backed enum,
169            // so out-of-range wire values fall through to `Unknown` instead of
170            // wrapping into a valid-looking named variant.
171            107 if value <= u8::MAX as u32 => {
172                Self::Segmentation(Segmentation::from_raw(value as u8))
173            }
174
175            // engineering-units: units + the various *-units properties.
176            20 | 27 | 50 | 82 | 94 | 117 | 455 => {
177                Self::EngineeringUnits(EngineeringUnits::from_raw(value))
178            }
179
180            84 => Self::Polarity(Polarity::from_raw(value)), // polarity
181            // alarm-value is BACnetBinaryPV on both object types that define it.
182            6 => Self::BinaryPV(BinaryPV::from_raw(value)),
183            92 => Self::ProgramState(ProgramState::from_raw(value)), // program-state
184            90 => Self::ProgramChange(ProgramChange::from_raw(value)), // program-change
185            100 => Self::ProgramError(ProgramError::from_raw(value)), // reason-for-halt
186            208 => Self::NodeType(NodeType::from_raw(value)),        // node-type
187            197 => Self::LoggingType(LoggingType::from_raw(value)),  // logging-type
188            41 => Self::FileAccessMethod(FileAccessMethod::from_raw(value)), // file-access-method
189            338 => Self::BackupAndRestoreState(BackupAndRestoreState::from_raw(value)), // backup-and-restore-state
190            196 => Self::RestartReason(RestartReason::from_raw(value)), // last-restart-reason
191
192            // Life safety.
193            160 | 175 => Self::LifeSafetyMode(LifeSafetyMode::from_raw(value)), // mode / accepted-modes
194            // tracking-value: BACnetLifeSafetyState is its only ENUMERATED
195            // form; Lighting Output's REAL never reaches this arm.
196            164 => Self::LifeSafetyState(LifeSafetyState::from_raw(value)),
197            161 => Self::LifeSafetyOperation(LifeSafetyOperation::from_raw(value)), // operation-expected
198            163 => Self::SilencedState(SilencedState::from_raw(value)),             // silenced
199            // maintenance-required: BACnetMaintenance on life safety
200            // point/zone (12.15/12.16) and access door (12.26).
201            158 => Self::Maintenance(Maintenance::from_raw(value)),
202
203            // Access door.
204            231 => Self::DoorStatus(DoorStatus::from_raw(value)), // door-status
205            226 => Self::DoorAlarmState(DoorAlarmState::from_raw(value)), // door-alarm-state
206            233 => Self::LockStatus(LockStatus::from_raw(value)), // lock-status
207            235 => Self::DoorSecuredStatus(DoorSecuredStatus::from_raw(value)), // secured-status
208
209            // Access control.
210            247 | 275 => Self::AccessEvent(AccessEvent::from_raw(value)), // access-event / last-access-event
211            261 => Self::AuthorizationMode(AuthorizationMode::from_raw(value)), // authorization-mode
212            300 => Self::AccessPassbackMode(AccessPassbackMode::from_raw(value)), // passback-mode
213            318 => Self::AccessUserType(AccessUserType::from_raw(value)),       // user-type
214            263 => Self::AccessCredentialDisable(AccessCredentialDisable::from_raw(value)), // credential-disable
215            // credential-status is BACnetBinaryPV on the one object type that
216            // defines it, the Access Credential (Table 12-40).
217            264 => Self::BinaryPV(BinaryPV::from_raw(value)),
218            303 => {
219                Self::AccessCredentialDisableReason(AccessCredentialDisableReason::from_raw(value))
220            } // reason-for-disable
221            260 => Self::AuthenticationStatus(AuthenticationStatus::from_raw(value)), // authentication-status
222            296 => Self::AccessZoneOccupancyState(AccessZoneOccupancyState::from_raw(value)), // occupancy-state
223            // authorization-exemptions is BACnetLIST of the exemption type;
224            // resolve_value recurses into list elements with the same arm.
225            364 => Self::AuthorizationExemption(AuthorizationExemption::from_raw(value)),
226
227            // Network port.
228            427 => Self::NetworkType(NetworkType::from_raw(value)), // network-type
229            426 => Self::NetworkNumberQuality(NetworkNumberQuality::from_raw(value)), // network-number-quality
230            408 | 435 => Self::IPMode(IPMode::from_raw(value)), // bacnet-ip-mode / bacnet-ipv6-mode
231            417 => Self::NetworkPortCommand(NetworkPortCommand::from_raw(value)), // command
232            482 => Self::ProtocolLevel(ProtocolLevel::from_raw(value)), // protocol-level
233
234            // Structured view (12.29): subordinate-relationships is
235            // BACnetARRAY[N] of BACnetRelationship (resolve_value recurses
236            // into elements); default-subordinate-relationship is the scalar
237            // companion. Represents (491) is BACnetDeviceObjectReference, so
238            // it has no enumerating arm here.
239            489 | 490 => Self::Relationship(Relationship::from_raw(value)),
240
241            // Lighting / timer / channel.
242            378 => Self::LightingInProgress(LightingInProgress::from_raw(value)), // in-progress
243            385 => Self::LightingTransition(LightingTransition::from_raw(value)), // transition
244            398 => Self::TimerState(TimerState::from_raw(value)),                 // timer-state
245            395 => Self::TimerTransition(TimerTransition::from_raw(value)), // last-state-change
246            370 => Self::WriteStatus(WriteStatus::from_raw(value)),         // write-status
247
248            // Lift / escalator.
249            456 => Self::LiftCarMode(LiftCarMode::from_raw(value)), // car-mode
250            467 => Self::LiftGroupMode(LiftGroupMode::from_raw(value)), // group-mode
251            462 => Self::EscalatorMode(EscalatorMode::from_raw(value)), // escalator-mode
252            477 => Self::EscalatorOperationDirection(EscalatorOperationDirection::from_raw(value)), // operation-direction
253            453 => Self::LiftCarDriveStatus(LiftCarDriveStatus::from_raw(value)), // car-drive-status
254            448 | 457 => Self::LiftCarDirection(LiftCarDirection::from_raw(value)), // car-assigned/moving-direction
255            449 => Self::LiftCarDoorCommand(LiftCarDoorCommand::from_raw(value)), // car-door-command
256            // car-door-status is `BACnetARRAY[N] of BACnetDoorStatus` (Lift
257            // object, Clause 12); the standard defines no lift-specific door
258            // status enumeration.
259            450 => Self::DoorStatus(DoorStatus::from_raw(value)),
260
261            // Audit.
262            498 => Self::AuditLevel(AuditLevel::from_raw(value)), // audit-level
263
264            _ => Self::Unknown(value),
265        }
266    }
267}
268
269/// Finish the job that `decode_application_value` starts: if `value` is an
270/// `Enumerated`, resolve it to its named enumeration for `property`.
271///
272/// Returns `None` for any non-enumerated value (there is nothing to promote).
273/// For a `PropertyValue::List`, resolve each element with
274/// [`ResolvedEnum::from_property`] yourself — the element enumeration is the
275/// same as the scalar case for that property.
276///
277/// ```
278/// use bacnet_types::enums::{resolve_enum, PropertyIdentifier, ResolvedEnum};
279/// use bacnet_types::primitives::PropertyValue;
280///
281/// // Suppose `decode_application_value` returned this for the `object-type`
282/// // property of some object:
283/// let decoded = PropertyValue::Enumerated(19);
284///
285/// match resolve_enum(PropertyIdentifier::OBJECT_TYPE, &decoded) {
286///     Some(ResolvedEnum::ObjectType(t)) => {
287///         assert_eq!(t, bacnet_types::enums::ObjectType::MULTI_STATE_VALUE);
288///         assert_eq!(t.to_string(), "MULTI_STATE_VALUE");
289///     }
290///     _ => unreachable!(),
291/// }
292/// ```
293pub fn resolve_enum(property: PropertyIdentifier, value: &PropertyValue) -> Option<ResolvedEnum> {
294    match value {
295        PropertyValue::Enumerated(n) => Some(ResolvedEnum::from_property(property, *n)),
296        _ => None,
297    }
298}
299
300/// A BACnet `BitString` promoted to its named bit-string type, keyed by property.
301///
302/// The bit-string analogue of [`ResolvedEnum`]: on the wire a `BitString` is
303/// just `(unused_bits, data)` bytes with no type identity, so the property
304/// decides which `BACnetXxx` BIT STRING it is (ASHRAE 135 Clause 21). A property
305/// with no known named bit-string type yields [`ResolvedBits::Unknown`],
306/// preserving the raw bytes.
307#[derive(Clone, PartialEq, Eq, Debug)]
308pub enum ResolvedBits {
309    /// `status-flags` / `member-status-flags` → [`StatusFlags`].
310    StatusFlags(StatusFlags),
311    /// `event-enable` / `acked-transitions` / `ack-required` → [`EventTransitionBits`].
312    EventTransitionBits(EventTransitionBits),
313    /// `limit-enable` → [`LimitEnable`].
314    LimitEnable(LimitEnable),
315    /// `protocol-services-supported` → [`ServicesSupported`].
316    ServicesSupported(ServicesSupported),
317    /// `protocol-object-types-supported` → [`ObjectTypesSupported`].
318    ObjectTypesSupported(ObjectTypesSupported),
319    /// No known named bit string for this property; raw bytes preserved.
320    Unknown {
321        /// Number of unused bits in the last byte.
322        unused_bits: u8,
323        /// The bit data bytes.
324        data: Vec<u8>,
325    },
326}
327
328impl ResolvedBits {
329    /// Promote a raw `BitString` payload to its named bit-string type using the
330    /// property it was read from.
331    ///
332    /// `unused_bits` and `data` are the fields of
333    /// [`PropertyValue::BitString`]. A property with no known named bit-string
334    /// type — vendor-proprietary or object-type-dependent — yields
335    /// [`ResolvedBits::Unknown`] with the bytes intact.
336    pub fn from_property(property: PropertyIdentifier, unused_bits: u8, data: &[u8]) -> Self {
337        match property.to_raw() {
338            // status-flags / member-status-flags.
339            111 | 347 => Self::StatusFlags(crate::bitstring::status_flags_from_bacnet(data)),
340
341            // event-enable / acked-transitions / ack-required.
342            0 | 1 | 35 => Self::EventTransitionBits(EventTransitionBits::from_bacnet(data)),
343
344            // limit-enable.
345            52 => Self::LimitEnable(LimitEnable::from_bacnet(data)),
346
347            // protocol-services-supported.
348            97 => Self::ServicesSupported(ServicesSupported::from_bacnet(data)),
349
350            // protocol-object-types-supported.
351            96 => Self::ObjectTypesSupported(ObjectTypesSupported::from_bacnet(data)),
352
353            _ => Self::Unknown {
354                unused_bits,
355                data: data.to_vec(),
356            },
357        }
358    }
359}
360
361impl core::fmt::Display for ResolvedBits {
362    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
363        match self {
364            Self::StatusFlags(v) => core::fmt::Display::fmt(v, f),
365            Self::EventTransitionBits(v) => core::fmt::Display::fmt(v, f),
366            Self::LimitEnable(v) => core::fmt::Display::fmt(v, f),
367            Self::ServicesSupported(v) => core::fmt::Display::fmt(v, f),
368            Self::ObjectTypesSupported(v) => core::fmt::Display::fmt(v, f),
369            Self::Unknown { data, .. } => {
370                f.write_str("0x")?;
371                for byte in data {
372                    write!(f, "{byte:02X}")?;
373                }
374                Ok(())
375            }
376        }
377    }
378}
379
380/// Bit-string analogue of [`resolve_enum`]: if `value` is a `BitString`, resolve
381/// it to its named bit-string type for `property`; otherwise return `None`.
382pub fn resolve_bits(property: PropertyIdentifier, value: &PropertyValue) -> Option<ResolvedBits> {
383    match value {
384        PropertyValue::BitString { unused_bits, data } => {
385            Some(ResolvedBits::from_property(property, *unused_bits, data))
386        }
387        _ => None,
388    }
389}
390
391/// A decoded property value with every `Enumerated` promoted to its named enum.
392///
393/// This mirrors [`PropertyValue`] one-to-one, except that
394/// [`PropertyValue::Enumerated`]'s bare `u32` is replaced by a resolved
395/// [`ResolvedEnum`], and [`PropertyValue::BitString`]'s raw bytes by a resolved
396/// [`ResolvedBits`]. Every other variant carries exactly the same data as its
397/// `PropertyValue` counterpart. Build one with [`resolve_value`].
398#[derive(Debug, Clone, PartialEq)]
399pub enum ResolvedValue {
400    /// Null value.
401    Null,
402    /// Boolean value.
403    Boolean(bool),
404    /// Unsigned integer (up to 64-bit for BACnet Unsigned64).
405    Unsigned(u64),
406    /// Signed integer.
407    Signed(i32),
408    /// IEEE 754 single-precision float.
409    Real(f32),
410    /// IEEE 754 double-precision float.
411    Double(f64),
412    /// Octet string (raw bytes).
413    OctetString(Vec<u8>),
414    /// Character string (UTF-8).
415    CharacterString(String),
416    /// Bit string, resolved to its named bit-string type for the property.
417    BitString(ResolvedBits),
418    /// Enumerated value, resolved to its named enumeration for the property.
419    Enumerated(ResolvedEnum),
420    /// Date value.
421    Date(Date),
422    /// Time value.
423    Time(Time),
424    /// Object identifier.
425    ObjectIdentifier(ObjectIdentifier),
426    /// A sequence (array) of resolved values.
427    List(Vec<ResolvedValue>),
428    /// Raw, already-encoded application-layer bytes (see
429    /// [`crate::primitives::PropertyValue::ApplicationData`]) — there is no
430    /// enum/bit-string naming for opaque context-tagged content, so it
431    /// carries through unchanged.
432    ApplicationData(Vec<u8>),
433}
434
435/// Finish the job `decode_application_value` starts: take its decoded
436/// [`PropertyValue`] and return the same value with every `Enumerated` promoted
437/// to its named [`ResolvedEnum`] for `property`. All non-enumerated data is
438/// carried through unchanged. `List` elements are resolved recursively against
439/// the same property.
440///
441/// ```
442/// use bacnet_types::enums::{resolve_value, PropertyIdentifier, ResolvedEnum, ResolvedValue, ObjectType};
443/// use bacnet_types::primitives::PropertyValue;
444///
445/// // What decode_application_value() gave you for the `object-type` property:
446/// let decoded = PropertyValue::Enumerated(19);
447///
448/// let resolved = resolve_value(PropertyIdentifier::OBJECT_TYPE, decoded);
449/// assert_eq!(
450///     resolved,
451///     ResolvedValue::Enumerated(ResolvedEnum::ObjectType(ObjectType::MULTI_STATE_VALUE)),
452/// );
453///
454/// // Non-enum data is untouched:
455/// let n = resolve_value(PropertyIdentifier::PRESENT_VALUE, PropertyValue::Real(21.5));
456/// assert_eq!(n, ResolvedValue::Real(21.5));
457/// ```
458pub fn resolve_value(property: PropertyIdentifier, value: PropertyValue) -> ResolvedValue {
459    match value {
460        PropertyValue::Null => ResolvedValue::Null,
461        PropertyValue::Boolean(v) => ResolvedValue::Boolean(v),
462        PropertyValue::Unsigned(v) => ResolvedValue::Unsigned(v),
463        PropertyValue::Signed(v) => ResolvedValue::Signed(v),
464        PropertyValue::Real(v) => ResolvedValue::Real(v),
465        PropertyValue::Double(v) => ResolvedValue::Double(v),
466        PropertyValue::OctetString(v) => ResolvedValue::OctetString(v),
467        PropertyValue::CharacterString(v) => ResolvedValue::CharacterString(v),
468        PropertyValue::BitString { unused_bits, data } => {
469            ResolvedValue::BitString(ResolvedBits::from_property(property, unused_bits, &data))
470        }
471        PropertyValue::Enumerated(n) => {
472            ResolvedValue::Enumerated(ResolvedEnum::from_property(property, n))
473        }
474        PropertyValue::Date(v) => ResolvedValue::Date(v),
475        PropertyValue::Time(v) => ResolvedValue::Time(v),
476        PropertyValue::ObjectIdentifier(v) => ResolvedValue::ObjectIdentifier(v),
477        PropertyValue::List(values) => ResolvedValue::List(
478            values
479                .into_iter()
480                .map(|v| resolve_value(property, v))
481                .collect(),
482        ),
483        PropertyValue::ApplicationData(bytes) => ResolvedValue::ApplicationData(bytes),
484    }
485}
486
487#[cfg(test)]
488mod tests {
489    use super::*;
490
491    #[test]
492    fn resolves_object_type() {
493        let r = ResolvedEnum::from_property(PropertyIdentifier::OBJECT_TYPE, 19);
494        assert_eq!(r, ResolvedEnum::ObjectType(ObjectType::MULTI_STATE_VALUE));
495        assert_eq!(r.to_string(), "MULTI_STATE_VALUE");
496    }
497
498    #[test]
499    fn resolves_units_across_aliases() {
500        for prop in [
501            PropertyIdentifier::UNITS,
502            PropertyIdentifier::OUTPUT_UNITS,
503            PropertyIdentifier::CAR_LOAD_UNITS,
504        ] {
505            assert!(matches!(
506                ResolvedEnum::from_property(prop, 0),
507                ResolvedEnum::EngineeringUnits(_)
508            ));
509        }
510    }
511
512    #[test]
513    fn unmapped_property_is_unknown() {
514        // present-value is object-type-dependent, so it stays Unknown.
515        let r = ResolvedEnum::from_property(PropertyIdentifier::PRESENT_VALUE, 7);
516        assert_eq!(r, ResolvedEnum::Unknown(7));
517        assert_eq!(r.to_string(), "7");
518    }
519
520    #[test]
521    fn alarm_value_resolves_to_binary_pv() {
522        assert_eq!(
523            ResolvedEnum::from_property(PropertyIdentifier::ALARM_VALUE, 1),
524            ResolvedEnum::BinaryPV(BinaryPV::ACTIVE),
525        );
526    }
527
528    #[test]
529    fn credential_status_resolves_to_binary_pv() {
530        assert_eq!(
531            ResolvedEnum::from_property(PropertyIdentifier::CREDENTIAL_STATUS, 1),
532            ResolvedEnum::BinaryPV(BinaryPV::ACTIVE),
533        );
534        assert_eq!(
535            ResolvedEnum::from_property(PropertyIdentifier::CREDENTIAL_STATUS, 0).to_string(),
536            "INACTIVE"
537        );
538    }
539
540    #[test]
541    fn plural_alarm_and_fault_values_remain_unknown() {
542        assert_eq!(
543            ResolvedEnum::from_property(PropertyIdentifier::ALARM_VALUES, 1),
544            ResolvedEnum::Unknown(1)
545        );
546        assert_eq!(
547            ResolvedEnum::from_property(PropertyIdentifier::FAULT_VALUES, 1),
548            ResolvedEnum::Unknown(1)
549        );
550    }
551
552    #[test]
553    fn vendor_object_type_passes_through() {
554        // 128 is a vendor-proprietary object type: named enum keeps the number.
555        let r = ResolvedEnum::from_property(PropertyIdentifier::OBJECT_TYPE, 128);
556        assert_eq!(r, ResolvedEnum::ObjectType(ObjectType::from_raw(128)));
557        assert_eq!(r.to_string(), "128");
558    }
559
560    #[test]
561    fn resolve_value_promotes_enum_and_keeps_rest() {
562        // Enum gets a named variant.
563        assert_eq!(
564            resolve_value(
565                PropertyIdentifier::EVENT_STATE,
566                PropertyValue::Enumerated(0)
567            ),
568            ResolvedValue::Enumerated(ResolvedEnum::EventState(EventState::from_raw(0))),
569        );
570        // Non-enum data passes through byte-for-byte.
571        assert_eq!(
572            resolve_value(
573                PropertyIdentifier::OBJECT_NAME,
574                PropertyValue::CharacterString("AI-1".into())
575            ),
576            ResolvedValue::CharacterString("AI-1".into()),
577        );
578    }
579
580    #[test]
581    fn resolve_value_recurses_into_lists() {
582        let list = PropertyValue::List(vec![
583            PropertyValue::Enumerated(0),
584            PropertyValue::Enumerated(1),
585        ]);
586        let ResolvedValue::List(items) = resolve_value(PropertyIdentifier::ACCEPTED_MODES, list)
587        else {
588            panic!("expected list");
589        };
590        assert!(items.iter().all(|i| matches!(
591            i,
592            ResolvedValue::Enumerated(ResolvedEnum::LifeSafetyMode(_))
593        )));
594    }
595
596    #[test]
597    fn resolves_status_flags_bitstring() {
598        // 4-bit status-flags, 4 unused: wire byte 0x80 (in-alarm set), unused=4.
599        let bits = ResolvedBits::from_property(PropertyIdentifier::STATUS_FLAGS, 4, &[0x80]);
600        assert_eq!(bits, ResolvedBits::StatusFlags(StatusFlags::IN_ALARM));
601        assert_eq!(bits.to_string(), "IN_ALARM");
602    }
603
604    #[test]
605    fn status_flags_decode_ignores_declared_length() {
606        // Clause 20.2.10 fixes bit 0 at the MSB of the first octet, so the
607        // flags survive a peer that declares the wrong unused-bit count.
608        for unused in [0, 4, 5] {
609            assert_eq!(
610                ResolvedBits::from_property(PropertyIdentifier::STATUS_FLAGS, unused, &[0xC0]),
611                ResolvedBits::StatusFlags(StatusFlags::IN_ALARM | StatusFlags::FAULT),
612            );
613        }
614    }
615
616    #[test]
617    fn segmentation_out_of_range_stays_unknown() {
618        // Segmentation is u8-backed; a wire value past u8 must not wrap into a
619        // valid-looking named variant (259 & 0xFF would be NONE).
620        assert_eq!(
621            ResolvedEnum::from_property(PropertyIdentifier::SEGMENTATION_SUPPORTED, 259),
622            ResolvedEnum::Unknown(259),
623        );
624        assert_eq!(
625            ResolvedEnum::from_property(PropertyIdentifier::SEGMENTATION_SUPPORTED, 3),
626            ResolvedEnum::Segmentation(Segmentation::NONE),
627        );
628    }
629
630    #[test]
631    fn car_door_status_resolves_to_door_status() {
632        // Car_Door_Status is BACnetARRAY[N] of BACnetDoorStatus (Lift object);
633        // 135-2020 has no lift-specific door status enumeration.
634        let r = ResolvedEnum::from_property(PropertyIdentifier::CAR_DOOR_STATUS, 3);
635        assert_eq!(r, ResolvedEnum::DoorStatus(DoorStatus::DOOR_FAULT));
636        assert_eq!(r.to_string(), "DOOR_FAULT");
637        assert_eq!(
638            ResolvedEnum::from_property(PropertyIdentifier::CAR_DOOR_STATUS, 0),
639            ResolvedEnum::DoorStatus(DoorStatus::CLOSED),
640        );
641    }
642
643    #[test]
644    fn backup_and_restore_state_failure_states_resolve_by_name() {
645        // Backup_And_Restore_State (338) legitimately reports both failure
646        // states during Clause 19.1 procedures; they must not surface as
647        // bare numbers.
648        let r = ResolvedEnum::from_property(PropertyIdentifier::BACKUP_AND_RESTORE_STATE, 5);
649        assert_eq!(
650            r,
651            ResolvedEnum::BackupAndRestoreState(BackupAndRestoreState::BACKUP_FAILURE)
652        );
653        assert_eq!(r.to_string(), "BACKUP_FAILURE");
654        assert_eq!(
655            ResolvedEnum::from_property(PropertyIdentifier::BACKUP_AND_RESTORE_STATE, 6)
656                .to_string(),
657            "RESTORE_FAILURE"
658        );
659    }
660
661    #[test]
662    fn resolves_limit_enable_and_event_enable() {
663        assert!(matches!(
664            ResolvedBits::from_property(PropertyIdentifier::LIMIT_ENABLE, 6, &[0xC0]),
665            ResolvedBits::LimitEnable(_)
666        ));
667        assert!(matches!(
668            ResolvedBits::from_property(PropertyIdentifier::EVENT_ENABLE, 5, &[0xE0]),
669            ResolvedBits::EventTransitionBits(_)
670        ));
671    }
672
673    #[test]
674    fn resolves_object_types_supported() {
675        let mut data = vec![0u8; 9];
676        data[8] = 0x80; // bit 64 => ColorTemperature
677        let bits = ResolvedBits::from_property(
678            PropertyIdentifier::PROTOCOL_OBJECT_TYPES_SUPPORTED,
679            7,
680            &data,
681        );
682        match bits {
683            ResolvedBits::ObjectTypesSupported(ots) => {
684                assert!(ots.contains(ObjectType::COLOR_TEMPERATURE));
685            }
686            _ => panic!("expected ObjectTypesSupported"),
687        }
688    }
689
690    #[test]
691    fn unmapped_bitstring_is_unknown() {
692        let bits = ResolvedBits::from_property(PropertyIdentifier::PROPERTY_LIST, 0, &[0xAB]);
693        assert_eq!(
694            bits,
695            ResolvedBits::Unknown {
696                unused_bits: 0,
697                data: vec![0xAB],
698            }
699        );
700        assert_eq!(bits.to_string(), "0xAB");
701    }
702
703    #[test]
704    fn resolve_value_promotes_bitstring() {
705        let v = resolve_value(
706            PropertyIdentifier::STATUS_FLAGS,
707            PropertyValue::BitString {
708                unused_bits: 4,
709                data: vec![0x40], // fault set
710            },
711        );
712        assert_eq!(
713            v,
714            ResolvedValue::BitString(ResolvedBits::StatusFlags(StatusFlags::FAULT)),
715        );
716    }
717
718    #[test]
719    fn resolve_bits_ignores_non_bitstring() {
720        assert!(
721            resolve_bits(
722                PropertyIdentifier::STATUS_FLAGS,
723                &PropertyValue::Unsigned(1)
724            )
725            .is_none()
726        );
727    }
728
729    #[test]
730    fn resolve_enum_ignores_non_enumerated() {
731        assert!(
732            resolve_enum(
733                PropertyIdentifier::OBJECT_TYPE,
734                &PropertyValue::Unsigned(19)
735            )
736            .is_none()
737        );
738        assert!(matches!(
739            resolve_enum(
740                PropertyIdentifier::EVENT_STATE,
741                &PropertyValue::Enumerated(0)
742            ),
743            Some(ResolvedEnum::EventState(_))
744        ));
745    }
746}