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