Skip to main content

bacnet_types/constructed/
mod.rs

1//! BACnet constructed data types per ASHRAE 135-2020.
2//!
3//! This module provides compound/structured types that are used by higher-level
4//! BACnet objects (Calendar, Schedule, TrendLog, NotificationClass, Loop, etc.).
5//! All types follow the same `no_std`-compatible pattern used in `primitives.rs`.
6
7#[cfg(not(feature = "std"))]
8use alloc::{string::String, vec::Vec};
9
10use crate::bitstring::{DaysOfWeek, EventTransitionBits};
11use crate::enums::LifeSafetyState;
12use crate::error::Error;
13use crate::primitives::{Date, ObjectIdentifier, PropertyValue, Time};
14use crate::MacAddr;
15
16mod action;
17pub use action::{BACnetActionCommand, BACnetActionList};
18mod access;
19pub use access::{
20    BACnetAccessRule, BACnetAssignedAccessRights, BACnetAuthenticationFactor,
21    BACnetAuthenticationFactorFormat, BACnetAuthenticationPolicy, BACnetAuthenticationPolicyEntry,
22    BACnetCredentialAuthenticationFactor,
23};
24mod audit;
25pub use audit::{
26    AuditPropertyReference, BACnetAuditLogDatum, BACnetAuditLogQueryParameters,
27    BACnetAuditLogRecord, BACnetAuditLogRecordResult, BACnetAuditNotification,
28    BACnetObjectSelector,
29};
30mod color;
31pub use color::{BACnetColorCommand, BACnetXyColor};
32mod event_notification;
33pub use event_notification::{
34    ChangeOfValueChoice, EventNotificationRequest, NotificationParameters,
35};
36mod lift;
37pub use lift::{
38    AssignedLandingCall, BACnetAssignedLandingCalls, BACnetLandingCallStatus,
39    BACnetLandingDoorStatus, BACnetLiftCarCallList, LandingCallCommand, LandingDoor,
40};
41mod lighting;
42pub use lighting::BACnetLightingCommand;
43mod log;
44pub use log::{
45    BACnetEventLogRecord, BACnetLogMultipleRecord, BACnetLogRecord, EventLogDatum, LogData,
46    LogDatum, LogValue,
47};
48mod name_value;
49pub use name_value::BACnetNameValue;
50mod network_port;
51pub use network_port::{BACnetBDTEntry, BACnetFDTEntry, BACnetHostAddress, BACnetHostNPort};
52mod property_access;
53pub use property_access::{AccessResult, BACnetPropertyAccessResult};
54mod property_value;
55pub use property_value::BACnetPropertyValue;
56mod read_access;
57pub use read_access::{PropertyReference, ReadAccessSpecification};
58mod staging;
59pub use staging::BACnetStageLimitValue;
60
61// ---------------------------------------------------------------------------
62// BACnetDateRange (Clause 21 -- used by CalendarEntry and BACnetSpecialEvent)
63// ---------------------------------------------------------------------------
64
65/// BACnet date range: a SEQUENCE of start and end Date values.
66///
67/// On the wire each Date is application-tagged; the codec is
68/// `bacnet_encoding::constructed::{encode_date_range, decode_date_range}`.
69/// An unspecified start or end date leaves that side of the range open.
70#[derive(Debug, Clone, PartialEq, Eq)]
71pub struct BACnetDateRange {
72    /// The start of the date range (inclusive).
73    pub start_date: Date,
74    /// The end of the date range (inclusive).
75    pub end_date: Date,
76}
77
78// ---------------------------------------------------------------------------
79// BACnetWeekNDay (Clause 21 -- used by CalendarEntry)
80// ---------------------------------------------------------------------------
81
82/// BACnet Week-And-Day: OCTET STRING(3) encoding month, week_of_month,
83/// and day_of_week.
84///
85/// Each field may be `0xFF` to mean "any" (wildcard).
86///
87/// - `month`: 1-12, 13=odd months, 14=even months, 0xFF=any
88/// - `week_of_month`: 1-5 = the days numbered 1-7, 8-14, 15-21, 22-28 and
89///   29-31; 6 = the last 7 days of the month; 7, 8 and 9 = the 7 days before
90///   the last 7, 14 and 21 days; 0xFF=any
91/// - `day_of_week`: 1=Monday..7=Sunday, 0xFF=any
92#[derive(Debug, Clone, PartialEq, Eq)]
93pub struct BACnetWeekNDay {
94    /// Month (1-14, or 0xFF for any).
95    pub month: u8,
96    /// Week of month (1-9, or 0xFF for any).
97    pub week_of_month: u8,
98    /// Day of week (1-7, or 0xFF for any).
99    pub day_of_week: u8,
100}
101
102impl BACnetWeekNDay {
103    /// Wildcard value indicating "any" for any field.
104    pub const ANY: u8 = 0xFF;
105
106    /// Encode to 3 bytes.
107    pub fn encode(&self) -> [u8; 3] {
108        [self.month, self.week_of_month, self.day_of_week]
109    }
110
111    /// Decode from at least 3 bytes.
112    pub fn decode(data: &[u8]) -> Result<Self, Error> {
113        if data.len() < 3 {
114            return Err(Error::buffer_too_short(3, data.len()));
115        }
116        Ok(Self {
117            month: data[0],
118            week_of_month: data[1],
119            day_of_week: data[2],
120        })
121    }
122}
123
124// ---------------------------------------------------------------------------
125// BACnetCalendarEntry (Clause 21.6 -- Calendar Date_List; Calendar object Clause 12.9)
126// ---------------------------------------------------------------------------
127
128/// BACnet calendar entry: a CHOICE between a specific date, a date range,
129/// or a week-and-day pattern.
130///
131/// Context tags per spec:
132/// - `[0]` Date
133/// - `[1]` DateRange
134/// - `[2]` WeekNDay
135///
136/// The wire codec is `bacnet_encoding::constructed::{encode_calendar_entry,
137/// decode_calendar_entry}`.
138#[derive(Debug, Clone, PartialEq, Eq)]
139pub enum BACnetCalendarEntry {
140    /// A single specific date (context tag 0).
141    Date(Date),
142    /// A contiguous date range (context tag 1).
143    DateRange(BACnetDateRange),
144    /// A recurring week-and-day pattern (context tag 2).
145    WeekNDay(BACnetWeekNDay),
146}
147
148// ---------------------------------------------------------------------------
149// BACnetTimeValue (Clause 12.24 -- Schedule Weekly_Schedule; Clause 21.6)
150// ---------------------------------------------------------------------------
151
152/// BACnet time-value pair: a Time followed by a value of any primitive
153/// datatype.
154///
155/// The value is typed, so a Schedule writes it with its own datatype. Only
156/// primitive values ([`PropertyValue::is_primitive`]) belong here; the codec
157/// refuses to encode a `List` or `ApplicationData`.
158#[derive(Debug, Clone, PartialEq)]
159pub struct BACnetTimeValue {
160    /// The time at which the value applies.
161    pub time: Time,
162    /// The value: NULL, BOOLEAN, Unsigned, INTEGER, REAL, Double, OCTET
163    /// STRING, CharacterString, BIT STRING, ENUMERATED, Date, Time or
164    /// BACnetObjectIdentifier.
165    pub value: PropertyValue,
166}
167
168// ---------------------------------------------------------------------------
169// SpecialEventPeriod (Clause 12.24 -- Schedule Exception_Schedule; Clause 21.6)
170// ---------------------------------------------------------------------------
171
172/// The period portion of a BACnetSpecialEvent: either an inline
173/// CalendarEntry or a reference to an existing Calendar object.
174///
175/// Context tags per spec:
176/// - `[0]` CalendarEntry (constructed)
177/// - `[1]` CalendarReference (ObjectIdentifier)
178#[derive(Debug, Clone, PartialEq, Eq)]
179pub enum SpecialEventPeriod {
180    /// An inline calendar entry (context tag 0).
181    CalendarEntry(BACnetCalendarEntry),
182    /// A reference to a Calendar object (context tag 1).
183    CalendarReference(ObjectIdentifier),
184}
185
186// ---------------------------------------------------------------------------
187// BACnetSpecialEvent (Clause 12.24 -- Schedule Exception_Schedule; Clause 21.6)
188// ---------------------------------------------------------------------------
189
190/// BACnet special event: an exception schedule entry combining a period
191/// definition, a list of time-value pairs, and a priority.
192#[derive(Debug, Clone, PartialEq)]
193pub struct BACnetSpecialEvent {
194    /// The period this special event applies to.
195    pub period: SpecialEventPeriod,
196    /// Ordered list of time-value pairs to apply during this period.
197    pub list_of_time_values: Vec<BACnetTimeValue>,
198    /// Priority for conflict resolution, 1 (highest) to 16 (lowest).
199    ///
200    /// Held as the full Unsigned the wire can carry, so a decoded event keeps
201    /// a priority outside that range intact for its consumer to refuse; the
202    /// Schedule object answers such a value with VALUE_OUT_OF_RANGE.
203    pub event_priority: u64,
204}
205
206// ---------------------------------------------------------------------------
207// BACnetObjectPropertyReference (Clause 21 -- used by Loop and others)
208// ---------------------------------------------------------------------------
209
210/// A reference to a specific property (and optionally an array index) on
211/// a specific object within the same device.
212#[derive(Debug, Clone, PartialEq, Eq)]
213pub struct BACnetObjectPropertyReference {
214    /// The object being referenced.
215    pub object_identifier: ObjectIdentifier,
216    /// The property being referenced (PropertyIdentifier raw value).
217    pub property_identifier: u32,
218    /// Optional array index within the property.
219    pub property_array_index: Option<u32>,
220}
221
222impl BACnetObjectPropertyReference {
223    /// Create a reference without an array index.
224    pub fn new(object_identifier: ObjectIdentifier, property_identifier: u32) -> Self {
225        Self {
226            object_identifier,
227            property_identifier,
228            property_array_index: None,
229        }
230    }
231
232    /// Create a reference with an array index.
233    pub fn new_indexed(
234        object_identifier: ObjectIdentifier,
235        property_identifier: u32,
236        array_index: u32,
237    ) -> Self {
238        Self {
239            object_identifier,
240            property_identifier,
241            property_array_index: Some(array_index),
242        }
243    }
244
245    /// Whether the reference names nothing: its object is at the reserved
246    /// instance 4194303, which Clause 12.1 lets an identifier hold to mean
247    /// uninitialized or unused. A property of this type has no empty
248    /// encoding, so the stack serves an unset one in this form (#1417).
249    pub fn is_unset(&self) -> bool {
250        self.object_identifier.instance_number() == ObjectIdentifier::WILDCARD_INSTANCE
251    }
252}
253
254// ---------------------------------------------------------------------------
255// BACnetDeviceObjectPropertyReference (Clause 21 -- used by several objects)
256// ---------------------------------------------------------------------------
257
258/// Like `BACnetObjectPropertyReference` but may also specify a remote device.
259///
260/// When `device_identifier` is `None`, the reference is to an object in the
261/// local device.
262#[derive(Debug, Clone, PartialEq, Eq)]
263pub struct BACnetDeviceObjectPropertyReference {
264    /// The object being referenced.
265    pub object_identifier: ObjectIdentifier,
266    /// The property being referenced (PropertyIdentifier raw value).
267    pub property_identifier: u32,
268    /// Optional array index within the property.
269    pub property_array_index: Option<u32>,
270    /// Optional device identifier (None = local device).
271    pub device_identifier: Option<ObjectIdentifier>,
272}
273
274impl BACnetDeviceObjectPropertyReference {
275    /// Create a local-device reference without an array index.
276    pub fn new_local(object_identifier: ObjectIdentifier, property_identifier: u32) -> Self {
277        Self {
278            object_identifier,
279            property_identifier,
280            property_array_index: None,
281            device_identifier: None,
282        }
283    }
284
285    /// Create a remote-device reference without an array index.
286    pub fn new_remote(
287        object_identifier: ObjectIdentifier,
288        property_identifier: u32,
289        device_identifier: ObjectIdentifier,
290    ) -> Self {
291        Self {
292            object_identifier,
293            property_identifier,
294            property_array_index: None,
295            device_identifier: Some(device_identifier),
296        }
297    }
298
299    /// Create a reference with an array index (may be local or remote).
300    pub fn with_index(mut self, array_index: u32) -> Self {
301        self.property_array_index = Some(array_index);
302        self
303    }
304
305    /// Whether the device identifier is absent or names a Device object,
306    /// the rule [`BACnetDeviceObjectReference::device_identifier_is_device`]
307    /// applies to that type's device member.
308    pub fn device_identifier_is_device(&self) -> bool {
309        device_identifier_is_device(self.device_identifier)
310    }
311
312    /// Whether the reference names nothing: its object, or its Device when
313    /// it has one, is at the reserved instance 4194303 (Clause 12.1), the
314    /// rule Clause 12.30.11 gives an empty Trend Log Multiple element. A
315    /// property of this type has no empty encoding, so the stack serves an
316    /// unset one in this form (#1417).
317    pub fn is_unset(&self) -> bool {
318        let reserved =
319            |oid: ObjectIdentifier| oid.instance_number() == ObjectIdentifier::WILDCARD_INSTANCE;
320        reserved(self.object_identifier) || self.device_identifier.is_some_and(reserved)
321    }
322}
323
324/// Whether an optional device member is absent or a Device object
325/// identifier, the only object type the member of a device-qualified
326/// reference can hold (Clause 21). The `device_identifier_is_device` methods
327/// of both reference types apply it, and so does every check the objects
328/// and the Python bindings run on a device member.
329pub fn device_identifier_is_device(device: Option<ObjectIdentifier>) -> bool {
330    device.is_none_or(|device| device.object_type() == crate::enums::ObjectType::DEVICE)
331}
332
333// ---------------------------------------------------------------------------
334// BACnetAddress (Clause 21 -- network address used by BACnetRecipient)
335// ---------------------------------------------------------------------------
336
337/// A BACnet network address: network number (0 = local) plus MAC address.
338#[derive(Debug, Clone, PartialEq, Eq)]
339pub struct BACnetAddress {
340    /// Network number (0 = local network, 1-65534 = remote, 65535 = broadcast).
341    pub network_number: u16,
342    /// MAC-layer address (variable length, at most [`Self::MAX_MAC_LEN`] on the
343    /// wire; empty = broadcast).
344    pub mac_address: MacAddr,
345}
346
347impl BACnetAddress {
348    /// The longest `mac_address`, in octets, that this stack encodes or decodes
349    /// in any `BACnetAddress` (#1124, #1156), and of any DADR or SADR the
350    /// network layer encodes or decodes (#1141).
351    ///
352    /// Clause 21 puts no length on the OCTET STRING, but a MAC is only useful
353    /// if it names a node on some data link. Table 6-2 gives the network-layer
354    /// address length of each standard data link, and the longest there is 7
355    /// octets (a LonTalk Neuron_ID destination). The data links this stack
356    /// serves address a node in at most 6 octets: 1 for MS/TP, 3 for B/IPv6
357    /// (its VMAC, U.1.3) and 6 for B/IP, Ethernet and B/SC. The bound stays at
358    /// 18, the length of a B/IPv6 address (16-octet IPv6 address and 2-octet
359    /// UDP port), which the stack once used as the B/IPv6 MAC: it is a
360    /// defensive ceiling, and lowering it is a decision of its own. A longer
361    /// MAC names no node on any data link the stack serves, nor on a standard
362    /// one behind a router.
363    ///
364    /// Every codec for this type in bacnet-encoding holds to the bound: the
365    /// recipient, ValueSource and AuditLogQuery decoders refuse a longer MAC
366    /// and their encoders refuse to write one. The local setters that store a
367    /// configured recipient (a Recipient_List destination or the
368    /// Audit_Notification_Recipient) refuse one too, so a stored recipient
369    /// always decodes again. The NPDU codec refuses a longer DLEN or SLEN
370    /// (`NpduAddress::MAX_MAC_LEN` in bacnet-encoding), and bacnet-network's
371    /// `NetworkLayer` and `BACnetRouter` drop a frame whose link-layer source
372    /// MAC is longer (#1198), so the source addresses the stack learns off the
373    /// network, which COV subscription lists and audit records report, fit the
374    /// bound as well. So does the device MAC a You-Are request assigns (#1200).
375    pub const MAX_MAC_LEN: usize = 18;
376
377    /// Create a local-broadcast address.
378    pub fn local_broadcast() -> Self {
379        Self {
380            network_number: 0,
381            mac_address: MacAddr::new(),
382        }
383    }
384
385    /// Create a BACnet/IP address from a 6-byte octet-string (4-byte IPv4 + 2-byte port).
386    pub fn from_ip(ip_port_bytes: [u8; 6]) -> Self {
387        Self {
388            network_number: 0,
389            mac_address: MacAddr::from_slice(&ip_port_bytes),
390        }
391    }
392}
393
394// ---------------------------------------------------------------------------
395// BACnetRecipient (Clause 21 -- used by BACnetDestination / NotificationClass)
396// ---------------------------------------------------------------------------
397
398/// A BACnet notification recipient: either a Device object reference or a
399/// network address.
400///
401/// Context tags per spec:
402/// - `[0]` Device (ObjectIdentifier)
403/// - `[1]` Address (BACnetAddress)
404#[derive(Debug, Clone, PartialEq, Eq)]
405pub enum BACnetRecipient {
406    /// A device identified by its Object Identifier (context tag 0).
407    Device(ObjectIdentifier),
408    /// A device identified by its network address (context tag 1).
409    Address(BACnetAddress),
410}
411
412// ---------------------------------------------------------------------------
413// BACnetDestination -- notification recipient, schedule, and delivery options
414// ---------------------------------------------------------------------------
415
416/// A single entry in a NotificationClass recipient list.
417///
418/// Specifies *who* receives the notification, *when* (days/times), and *how*
419/// (confirmed vs. unconfirmed, which transition types).
420#[derive(Debug, Clone, PartialEq, Eq)]
421pub struct BACnetDestination {
422    /// Weekdays when this destination accepts notifications.
423    pub valid_days: DaysOfWeek,
424    /// Start of the daily time window during which this destination is active.
425    pub from_time: Time,
426    /// End of the daily time window.
427    pub to_time: Time,
428    /// The notification recipient.
429    pub recipient: BACnetRecipient,
430    /// Process identifier on the receiving device.
431    pub process_identifier: u32,
432    /// If true, use ConfirmedEventNotification; otherwise unconfirmed.
433    pub issue_confirmed_notifications: bool,
434    /// Event transitions whose notifications this destination receives.
435    pub transitions: EventTransitionBits,
436}
437
438// ---------------------------------------------------------------------------
439// BACnetEventNotificationSubscription (Clause 21)
440// ---------------------------------------------------------------------------
441
442/// One entry of a Notification Forwarder's Subscribed_Recipients (Clause
443/// 12.51.9): a recipient that asked for forwarded notifications for a limited
444/// time.
445///
446/// The recipient and process identifier identify the entry: the list services
447/// match on those two members alone.
448#[derive(Debug, Clone, PartialEq, Eq)]
449pub struct BACnetEventNotificationSubscription {
450    /// Device or address that receives the notifications (`[0]`).
451    pub recipient: BACnetRecipient,
452    /// Process on the recipient that receives them (`[1]`).
453    pub process_identifier: u32,
454    /// `true` for confirmed notifications, `false` for unconfirmed ones (`[2]`).
455    pub issue_confirmed_notifications: bool,
456    /// Minutes left before the entry lapses (`[3]`). Unlike COV subscription
457    /// lifetimes this counts minutes, not seconds.
458    pub time_remaining: u32,
459}
460
461// ---------------------------------------------------------------------------
462// BACnetPortPermission (Clause 21)
463// ---------------------------------------------------------------------------
464
465/// One element of a Notification Forwarder's Port_Filter (Clause 12.51.11):
466/// whether notifications that arrive through one network port are forwarded.
467#[derive(Debug, Clone, Copy, PartialEq, Eq)]
468pub struct BACnetPortPermission {
469    /// The port, by its Clause 6 port ID; 0 on a node that does not route
470    /// (`[0]`).
471    pub port_id: u8,
472    /// `true` when notifications received through the port are forwarded
473    /// (`[1]`).
474    pub enabled: bool,
475}
476
477// ---------------------------------------------------------------------------
478// BACnetScale (Clause 21)
479// ---------------------------------------------------------------------------
480
481/// BACnet Scale: CHOICE { float-scale \[0\] Real, integer-scale \[1\] Integer }.
482#[derive(Debug, Clone, PartialEq)]
483pub enum BACnetScale {
484    /// Present_Value is multiplied by this factor to get engineering units.
485    FloatScale(f32),
486    /// Present_Value is multiplied by ten raised to this power to get engineering units.
487    IntegerScale(i32),
488}
489
490// ---------------------------------------------------------------------------
491// BACnetPrescale (Clause 21)
492// ---------------------------------------------------------------------------
493
494/// BACnet Prescale: SEQUENCE { multiplier Unsigned, modulo-divide Unsigned }.
495#[derive(Debug, Clone, PartialEq, Eq)]
496pub struct BACnetPrescale {
497    /// Numerator of the pulse-to-value conversion ratio, added to the accumulator per input pulse.
498    pub multiplier: u32,
499    /// Denominator of the conversion ratio; each time the accumulator reaches it, the value steps
500    /// by one.
501    pub modulo_divide: u32,
502}
503
504// ---------------------------------------------------------------------------
505// BACnetShedLevel (Clause 21 — used by LoadControl)
506// ---------------------------------------------------------------------------
507
508/// A Load Control shed level (`BACnetShedLevel`, Clause 21): the datatype of
509/// Requested_Shed_Level, Expected_Shed_Level and Actual_Shed_Level
510/// (Clause 12.28).
511///
512/// On the wire each alternative is one primitive context tag: percent `[0]`,
513/// level `[1]`, amount `[2]`. The codec is
514/// `bacnet_encoding::constructed::{encode_shed_level, decode_shed_level}`.
515#[derive(Debug, Clone, PartialEq)]
516pub enum BACnetShedLevel {
517    /// The load to run at, as a percentage of the baseline (Unsigned).
518    Percent(u64),
519    /// A preconfigured shed level (Unsigned). Level 0 means no shed.
520    Level(u64),
521    /// Kilowatts to take off the baseline (REAL).
522    Amount(f32),
523}
524
525// ---------------------------------------------------------------------------
526// BACnetDeviceObjectReference (Clause 21 -- used by Access Control objects)
527// ---------------------------------------------------------------------------
528
529/// BACnet Device Object Reference.
530///
531/// Clause 21 encodes the optional device identifier as context `[0]` followed
532/// by the required object identifier as context `[1]`. `None` identifies a
533/// local object, including Staging target references.
534#[derive(Debug, Clone, PartialEq, Eq)]
535pub struct BACnetDeviceObjectReference {
536    /// Optional device identifier (None = local device).
537    pub device_identifier: Option<ObjectIdentifier>,
538    /// The object being referenced.
539    pub object_identifier: ObjectIdentifier,
540}
541
542impl BACnetDeviceObjectReference {
543    /// Whether the device identifier is absent or names a Device object.
544    ///
545    /// The Clause 21 production gives the first member to the Device that
546    /// holds the object, so an identifier of any other object type can't make
547    /// a valid reference. Setters and write paths that take these references
548    /// refuse one that fails this check.
549    pub fn device_identifier_is_device(&self) -> bool {
550        device_identifier_is_device(self.device_identifier)
551    }
552}
553
554impl From<ObjectIdentifier> for BACnetDeviceObjectReference {
555    /// A reference to an object in this device: no device identifier.
556    fn from(object_identifier: ObjectIdentifier) -> Self {
557        Self {
558            device_identifier: None,
559            object_identifier,
560        }
561    }
562}
563
564// ---------------------------------------------------------------------------
565// FaultParameters (Clause 12.12 -- Fault_Parameters of Event Enrollment)
566// ---------------------------------------------------------------------------
567
568/// Fault parameter variants for configuring fault detection algorithms.
569#[derive(Debug, Clone, PartialEq)]
570pub enum FaultParameters {
571    /// No fault detection.
572    FaultNone,
573    /// Fault on characterstring match.
574    FaultCharacterString {
575        /// Strings that, when the monitored value matches one, indicate a fault.
576        fault_values: Vec<String>,
577    },
578    /// Vendor-defined fault algorithm.
579    FaultExtended {
580        /// Vendor identifier that owns the extended algorithm.
581        vendor_id: u16,
582        /// Vendor-defined identifier of the fault algorithm.
583        extended_fault_type: u32,
584        /// Pre-encoded, vendor-specific parameter bytes carried opaquely.
585        parameters: Vec<u8>,
586    },
587    /// Fault on life safety state match.
588    FaultLifeSafety {
589        /// Life safety states that indicate a fault.
590        fault_values: Vec<LifeSafetyState>,
591        /// Reference to the mode property consulted when evaluating these states.
592        mode_property_reference: BACnetDeviceObjectPropertyReference,
593    },
594    /// Fault on property state match.
595    FaultState {
596        /// Property states that indicate a fault when the monitored property takes one of them.
597        fault_values: Vec<BACnetPropertyStates>,
598    },
599    /// Fault on status flags change.
600    FaultStatusFlags {
601        /// Reference to the StatusFlags property whose FAULT bit is monitored.
602        reference: BACnetDeviceObjectPropertyReference,
603    },
604    /// Fault when value exceeds range.
605    FaultOutOfRange {
606        /// Lower bound of the normal range; values below it are a fault.
607        min_normal: f64,
608        /// Upper bound of the normal range; values above it are a fault.
609        max_normal: f64,
610    },
611    /// Fault from listed reference.
612    FaultListed {
613        /// BACnetLIST property whose entries carry the fault indications to watch.
614        reference: BACnetDeviceObjectPropertyReference,
615    },
616}
617
618// ---------------------------------------------------------------------------
619// BACnetRecipientProcess (Clause 21)
620// ---------------------------------------------------------------------------
621
622/// BACnet Recipient Process — a recipient with an associated process identifier.
623#[derive(Debug, Clone, PartialEq)]
624pub struct BACnetRecipientProcess {
625    /// Device or address that receives the notifications.
626    pub recipient: BACnetRecipient,
627    /// Process on the recipient that asked for the notifications; echoed back in each one.
628    pub process_identifier: u32,
629}
630
631// ---------------------------------------------------------------------------
632// BACnetCOVSubscription (Clause 21)
633// ---------------------------------------------------------------------------
634
635/// BACnet COV Subscription — represents an active COV subscription.
636///
637/// The `monitored_property_reference` is a `BACnetObjectPropertyReference`
638/// (object + property + optional index).
639#[derive(Debug, Clone, PartialEq)]
640pub struct BACnetCOVSubscription {
641    /// Subscriber (device or address plus process identifier) that receives the notifications.
642    pub recipient: BACnetRecipientProcess,
643    /// Object and property being watched.
644    pub monitored_property_reference: BACnetObjectPropertyReference,
645    /// `true` for confirmed notifications, `false` for unconfirmed ones.
646    pub issue_confirmed_notifications: bool,
647    /// Seconds left before the subscription lapses; 0 means it never lapses.
648    pub time_remaining: u32,
649    /// COV increment in use for a numeric monitored property: the requested one, else the
650    /// object's COV_Increment. `None` when the monitored property isn't numeric.
651    pub cov_increment: Option<f32>,
652}
653
654// ---------------------------------------------------------------------------
655// BACnetCOVMultipleSubscription (Clause 21)
656// ---------------------------------------------------------------------------
657
658/// One COV-multiple context of Device `Active_COV_Multiple_Subscriptions`:
659/// a recipient and notification form with one remaining lifetime and maximum
660/// notification delay shared by every nested COV reference.
661#[derive(Debug, Clone, PartialEq)]
662pub struct BACnetCOVMultipleSubscription {
663    /// COV-client address and subscriber process identifier (`[0]`).
664    pub recipient: BACnetRecipientProcess,
665    /// Notification form of this context (`[1]`).
666    pub issue_confirmed_notifications: bool,
667    /// Remaining context lifetime in seconds (`[2]`).
668    pub time_remaining: u32,
669    /// Maximum notification delay in seconds (`[3]`).
670    pub max_notification_delay: u32,
671    /// Monitored objects and their COV references (`[4]`).
672    pub list_of_cov_subscription_specifications: Vec<BACnetCOVSubscriptionSpecification>,
673}
674
675/// The COV references a COV-multiple context holds for one monitored object.
676#[derive(Debug, Clone, PartialEq)]
677pub struct BACnetCOVSubscriptionSpecification {
678    /// Monitored object (`[0]`).
679    pub monitored_object_identifier: ObjectIdentifier,
680    /// COV references on that object (`[1]`).
681    pub list_of_cov_references: Vec<BACnetCOVReference>,
682}
683
684/// One monitored property (a `BACnetPropertyReference`) of a COV-multiple
685/// subscription specification.
686#[derive(Debug, Clone, PartialEq)]
687pub struct BACnetCOVReference {
688    /// Monitored property (`[0]` property identifier).
689    pub property_identifier: crate::enums::PropertyIdentifier,
690    /// Optional array index; absence and zero are distinct.
691    pub property_array_index: Option<u32>,
692    /// COV increment in use (`[1]`), present for numeric monitored values.
693    pub cov_increment: Option<f32>,
694    /// Whether notifications carry the time of change (`[2]`).
695    pub timestamped: bool,
696}
697
698// ---------------------------------------------------------------------------
699// BACnetValueSource (Clause 21)
700// ---------------------------------------------------------------------------
701
702/// BACnet Value Source — identifies the source of a property value write.
703#[derive(Debug, Clone, PartialEq)]
704pub enum BACnetValueSource {
705    /// No identified source: context \[0\] NULL.
706    None,
707    /// Source object, optionally qualified by a device: constructed context \[1\].
708    Object(BACnetDeviceObjectReference),
709    /// Source network/MAC address: constructed context \[2\].
710    Address(BACnetAddress),
711}
712
713// ---------------------------------------------------------------------------
714// BACnetEventParameter -- Event_Parameters algorithm alternatives
715// ---------------------------------------------------------------------------
716
717mod event_parameter;
718pub use event_parameter::{event_parameter_tag, BACnetEventParameter, ChangeOfValueCriteria};
719
720mod property_states;
721pub use property_states::{
722    BACnetExtendedPropertyState, BACnetPropertyStates, BACnetProprietaryPropertyState,
723};
724
725// ---------------------------------------------------------------------------
726// Tests
727// ---------------------------------------------------------------------------
728
729#[cfg(test)]
730mod tests;