Skip to main content

bacnet_types/
constructed.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::{vec, vec::Vec};
9
10use crate::error::Error;
11use crate::primitives::{Date, ObjectIdentifier, Time};
12use crate::MacAddr;
13
14// ---------------------------------------------------------------------------
15// BACnetDateRange (Clause 21 -- used by CalendarEntry and BACnetSpecialEvent)
16// ---------------------------------------------------------------------------
17
18/// BACnet date range: a SEQUENCE of start and end Date values.
19///
20/// Encoded as 8 bytes: 4 bytes for start_date followed by 4 bytes for end_date.
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct BACnetDateRange {
23    /// The start of the date range (inclusive).
24    pub start_date: Date,
25    /// The end of the date range (inclusive).
26    pub end_date: Date,
27}
28
29impl BACnetDateRange {
30    /// Encode to 8 bytes (start_date || end_date).
31    pub fn encode(&self) -> [u8; 8] {
32        let mut out = [0u8; 8];
33        out[..4].copy_from_slice(&self.start_date.encode());
34        out[4..].copy_from_slice(&self.end_date.encode());
35        out
36    }
37
38    /// Decode from at least 8 bytes.
39    pub fn decode(data: &[u8]) -> Result<Self, Error> {
40        if data.len() < 8 {
41            return Err(Error::buffer_too_short(8, data.len()));
42        }
43        Ok(Self {
44            start_date: Date::decode(&data[0..4])?,
45            end_date: Date::decode(&data[4..8])?,
46        })
47    }
48}
49
50// ---------------------------------------------------------------------------
51// BACnetWeekNDay (Clause 21 -- used by CalendarEntry)
52// ---------------------------------------------------------------------------
53
54/// BACnet Week-And-Day: OCTET STRING(3) encoding month, week_of_month,
55/// and day_of_week.
56///
57/// Each field may be `0xFF` to mean "any" (wildcard).
58///
59/// - `month`: 1-12, 13=odd, 14=even, 0xFF=any
60/// - `week_of_month`: 1=first, 2=second, ..., 5=last, 6=any-in-first,
61///   0xFF=any
62/// - `day_of_week`: 1=Monday..7=Sunday, 0xFF=any
63#[derive(Debug, Clone, PartialEq, Eq)]
64pub struct BACnetWeekNDay {
65    /// Month (1-14, or 0xFF for any).
66    pub month: u8,
67    /// Week of month (1-6, or 0xFF for any).
68    pub week_of_month: u8,
69    /// Day of week (1-7, or 0xFF for any).
70    pub day_of_week: u8,
71}
72
73impl BACnetWeekNDay {
74    /// Wildcard value indicating "any" for any field.
75    pub const ANY: u8 = 0xFF;
76
77    /// Encode to 3 bytes.
78    pub fn encode(&self) -> [u8; 3] {
79        [self.month, self.week_of_month, self.day_of_week]
80    }
81
82    /// Decode from at least 3 bytes.
83    pub fn decode(data: &[u8]) -> Result<Self, Error> {
84        if data.len() < 3 {
85            return Err(Error::buffer_too_short(3, data.len()));
86        }
87        Ok(Self {
88            month: data[0],
89            week_of_month: data[1],
90            day_of_week: data[2],
91        })
92    }
93}
94
95// ---------------------------------------------------------------------------
96// BACnetCalendarEntry (Clause 12.6.3 -- property list of Calendar object)
97// ---------------------------------------------------------------------------
98
99/// BACnet calendar entry: a CHOICE between a specific date, a date range,
100/// or a week-and-day pattern.
101///
102/// Context tags per spec:
103/// - `[0]` Date
104/// - `[1]` DateRange
105/// - `[2]` WeekNDay
106#[derive(Debug, Clone, PartialEq, Eq)]
107pub enum BACnetCalendarEntry {
108    /// A single specific date (context tag 0).
109    Date(Date),
110    /// A contiguous date range (context tag 1).
111    DateRange(BACnetDateRange),
112    /// A recurring week-and-day pattern (context tag 2).
113    WeekNDay(BACnetWeekNDay),
114}
115
116// ---------------------------------------------------------------------------
117// BACnetTimeValue (Clause 12.17.4 -- used by Schedule weekly_schedule)
118// ---------------------------------------------------------------------------
119
120/// BACnet time-value pair: a Time followed by an application-tagged value.
121///
122/// The `value` field holds raw application-tagged bytes because the value
123/// type is polymorphic (Real, Boolean, Unsigned, Null, etc.) and the Schedule
124/// object stores them opaquely for later dispatch.
125#[derive(Debug, Clone, PartialEq)]
126pub struct BACnetTimeValue {
127    /// The time at which the value applies.
128    pub time: Time,
129    /// Raw application-tagged BACnet encoding of the value.
130    pub value: Vec<u8>,
131}
132
133// ---------------------------------------------------------------------------
134// SpecialEventPeriod (Clause 12.17.5 -- used by BACnetSpecialEvent)
135// ---------------------------------------------------------------------------
136
137/// The period portion of a BACnetSpecialEvent: either an inline
138/// CalendarEntry or a reference to an existing Calendar object.
139///
140/// Context tags per spec:
141/// - `[0]` CalendarEntry (constructed)
142/// - `[1]` CalendarReference (ObjectIdentifier)
143#[derive(Debug, Clone, PartialEq, Eq)]
144pub enum SpecialEventPeriod {
145    /// An inline calendar entry (context tag 0).
146    CalendarEntry(BACnetCalendarEntry),
147    /// A reference to a Calendar object (context tag 1).
148    CalendarReference(ObjectIdentifier),
149}
150
151// ---------------------------------------------------------------------------
152// BACnetSpecialEvent (Clause 12.17.5 -- exception_schedule of Schedule)
153// ---------------------------------------------------------------------------
154
155/// BACnet special event: an exception schedule entry combining a period
156/// definition, a list of time-value pairs, and a priority.
157#[derive(Debug, Clone, PartialEq)]
158pub struct BACnetSpecialEvent {
159    /// The period this special event applies to.
160    pub period: SpecialEventPeriod,
161    /// Ordered list of time-value pairs to apply during this period.
162    pub list_of_time_values: Vec<BACnetTimeValue>,
163    /// Priority for conflict resolution (1=highest..16=lowest).
164    pub event_priority: u8,
165}
166
167// ---------------------------------------------------------------------------
168// BACnetObjectPropertyReference (Clause 21 -- used by Loop and others)
169// ---------------------------------------------------------------------------
170
171/// A reference to a specific property (and optionally an array index) on
172/// a specific object within the same device.
173#[derive(Debug, Clone, PartialEq, Eq)]
174pub struct BACnetObjectPropertyReference {
175    /// The object being referenced.
176    pub object_identifier: ObjectIdentifier,
177    /// The property being referenced (PropertyIdentifier raw value).
178    pub property_identifier: u32,
179    /// Optional array index within the property.
180    pub property_array_index: Option<u32>,
181}
182
183impl BACnetObjectPropertyReference {
184    /// Create a reference without an array index.
185    pub fn new(object_identifier: ObjectIdentifier, property_identifier: u32) -> Self {
186        Self {
187            object_identifier,
188            property_identifier,
189            property_array_index: None,
190        }
191    }
192
193    /// Create a reference with an array index.
194    pub fn new_indexed(
195        object_identifier: ObjectIdentifier,
196        property_identifier: u32,
197        array_index: u32,
198    ) -> Self {
199        Self {
200            object_identifier,
201            property_identifier,
202            property_array_index: Some(array_index),
203        }
204    }
205}
206
207// ---------------------------------------------------------------------------
208// BACnetDeviceObjectPropertyReference (Clause 21 -- used by several objects)
209// ---------------------------------------------------------------------------
210
211/// Like `BACnetObjectPropertyReference` but may also specify a remote device.
212///
213/// When `device_identifier` is `None`, the reference is to an object in the
214/// local device.
215#[derive(Debug, Clone, PartialEq, Eq)]
216pub struct BACnetDeviceObjectPropertyReference {
217    /// The object being referenced.
218    pub object_identifier: ObjectIdentifier,
219    /// The property being referenced (PropertyIdentifier raw value).
220    pub property_identifier: u32,
221    /// Optional array index within the property.
222    pub property_array_index: Option<u32>,
223    /// Optional device identifier (None = local device).
224    pub device_identifier: Option<ObjectIdentifier>,
225}
226
227impl BACnetDeviceObjectPropertyReference {
228    /// Create a local-device reference without an array index.
229    pub fn new_local(object_identifier: ObjectIdentifier, property_identifier: u32) -> Self {
230        Self {
231            object_identifier,
232            property_identifier,
233            property_array_index: None,
234            device_identifier: None,
235        }
236    }
237
238    /// Create a remote-device reference without an array index.
239    pub fn new_remote(
240        object_identifier: ObjectIdentifier,
241        property_identifier: u32,
242        device_identifier: ObjectIdentifier,
243    ) -> Self {
244        Self {
245            object_identifier,
246            property_identifier,
247            property_array_index: None,
248            device_identifier: Some(device_identifier),
249        }
250    }
251
252    /// Create a reference with an array index (may be local or remote).
253    pub fn with_index(mut self, array_index: u32) -> Self {
254        self.property_array_index = Some(array_index);
255        self
256    }
257}
258
259// ---------------------------------------------------------------------------
260// BACnetAddress (Clause 21 -- network address used by BACnetRecipient)
261// ---------------------------------------------------------------------------
262
263/// A BACnet network address: network number (0 = local) plus MAC address.
264#[derive(Debug, Clone, PartialEq, Eq)]
265pub struct BACnetAddress {
266    /// Network number (0 = local network, 1-65534 = remote, 65535 = broadcast).
267    pub network_number: u16,
268    /// MAC-layer address (variable length; empty = local broadcast).
269    pub mac_address: MacAddr,
270}
271
272impl BACnetAddress {
273    /// Create a local-broadcast address.
274    pub fn local_broadcast() -> Self {
275        Self {
276            network_number: 0,
277            mac_address: MacAddr::new(),
278        }
279    }
280
281    /// Create a BACnet/IP address from a 6-byte octet-string (4-byte IPv4 + 2-byte port).
282    pub fn from_ip(ip_port_bytes: [u8; 6]) -> Self {
283        Self {
284            network_number: 0,
285            mac_address: MacAddr::from_slice(&ip_port_bytes),
286        }
287    }
288}
289
290// ---------------------------------------------------------------------------
291// BACnetRecipient (Clause 21 -- used by BACnetDestination / NotificationClass)
292// ---------------------------------------------------------------------------
293
294/// A BACnet notification recipient: either a Device object reference or a
295/// network address.
296///
297/// Context tags per spec:
298/// - `[0]` Device (ObjectIdentifier)
299/// - `[1]` Address (BACnetAddress)
300#[derive(Debug, Clone, PartialEq, Eq)]
301pub enum BACnetRecipient {
302    /// A device identified by its Object Identifier (context tag 0).
303    Device(ObjectIdentifier),
304    /// A device identified by its network address (context tag 1).
305    Address(BACnetAddress),
306}
307
308// ---------------------------------------------------------------------------
309// BACnetDestination (Clause 12.15.5 -- recipient_list of NotificationClass)
310// ---------------------------------------------------------------------------
311
312/// A single entry in a NotificationClass recipient list.
313///
314/// Specifies *who* receives the notification, *when* (days/times), and *how*
315/// (confirmed vs. unconfirmed, which transition types).
316#[derive(Debug, Clone, PartialEq, Eq)]
317pub struct BACnetDestination {
318    /// Bitmask of valid days (bit 0 = Monday ... bit 6 = Sunday), 7 bits used.
319    pub valid_days: u8,
320    /// Start of the daily time window during which this destination is active.
321    pub from_time: Time,
322    /// End of the daily time window.
323    pub to_time: Time,
324    /// The notification recipient.
325    pub recipient: BACnetRecipient,
326    /// Process identifier on the receiving device.
327    pub process_identifier: u32,
328    /// If true, use ConfirmedEventNotification; otherwise unconfirmed.
329    pub issue_confirmed_notifications: bool,
330    /// Bitmask of event transitions to send (bit 0=ToOffNormal, bit 1=ToFault,
331    /// bit 2=ToNormal), 3 bits used.
332    pub transitions: u8,
333}
334
335// ---------------------------------------------------------------------------
336// LogDatum (Clause 12.20.5 -- log_buffer element datum of TrendLog)
337// ---------------------------------------------------------------------------
338
339/// The datum field of a BACnetLogRecord: a CHOICE covering all possible
340/// logged value types.
341///
342/// Context tags per spec:
343/// - `[0]` log-status (BACnetLogStatus, 8-bit flags)
344/// - `[1]` boolean-value
345/// - `[2]` real-value
346/// - `[3]` enum-value (unsigned)
347/// - `[4]` unsigned-value
348/// - `[5]` signed-value
349/// - `[6]` bitstring-value
350/// - `[7]` null-value
351/// - `[8]` failure (BACnetError)
352/// - `[9]` time-change (REAL, clock-adjustment seconds)
353/// - `[10]` any-value (raw application-tagged bytes)
354#[derive(Debug, Clone, PartialEq)]
355pub enum LogDatum {
356    /// Log-status flags (context tag 0).  Bit 0=log-disabled, bit 1=buffer-purged,
357    /// bit 2=log-interrupted.
358    LogStatus(u8),
359    /// Boolean value (context tag 1).
360    BooleanValue(bool),
361    /// Real (f32) value (context tag 2).
362    RealValue(f32),
363    /// Enumerated value (context tag 3).
364    EnumValue(u32),
365    /// Unsigned integer value (context tag 4).
366    UnsignedValue(u64),
367    /// Signed integer value (context tag 5).
368    SignedValue(i64),
369    /// Bit-string value (context tag 6).
370    BitstringValue {
371        /// Number of unused bits in the last byte.
372        unused_bits: u8,
373        /// The bit data.
374        data: Vec<u8>,
375    },
376    /// Null value (context tag 7).
377    NullValue,
378    /// Error (context tag 8): error class + error code.
379    Failure {
380        /// Raw BACnet error class value.
381        error_class: u32,
382        /// Raw BACnet error code value.
383        error_code: u32,
384    },
385    /// Time-change: clock-adjustment amount in seconds (context tag 9).
386    TimeChange(f32),
387    /// Any-value: raw application-tagged bytes for types not enumerated above
388    /// (context tag 10).
389    AnyValue(Vec<u8>),
390}
391
392// ---------------------------------------------------------------------------
393// BACnetLogRecord (Clause 12.20.5 -- log_buffer of TrendLog)
394// ---------------------------------------------------------------------------
395
396/// A single record stored in a TrendLog object's log buffer.
397///
398/// Contains a timestamp (date + time), the logged datum, and optional
399/// status flags that were in effect at logging time.
400#[derive(Debug, Clone, PartialEq)]
401pub struct BACnetLogRecord {
402    /// The date at which this record was logged.
403    pub date: Date,
404    /// The time at which this record was logged.
405    pub time: Time,
406    /// The logged datum.
407    pub log_datum: LogDatum,
408    /// Optional status flags at time of logging (4-bit BACnet StatusFlags).
409    pub status_flags: Option<u8>,
410}
411
412// ---------------------------------------------------------------------------
413// BACnetScale (Clause 21)
414// ---------------------------------------------------------------------------
415
416/// BACnet Scale: CHOICE { float-scale [0] Real, integer-scale [1] Integer }.
417#[derive(Debug, Clone, PartialEq)]
418pub enum BACnetScale {
419    FloatScale(f32),
420    IntegerScale(i32),
421}
422
423// ---------------------------------------------------------------------------
424// BACnetPrescale (Clause 21)
425// ---------------------------------------------------------------------------
426
427/// BACnet Prescale: SEQUENCE { multiplier Unsigned, modulo-divide Unsigned }.
428#[derive(Debug, Clone, PartialEq, Eq)]
429pub struct BACnetPrescale {
430    pub multiplier: u32,
431    pub modulo_divide: u32,
432}
433
434// ---------------------------------------------------------------------------
435// BACnetPropertyStates (Clause 21)
436// ---------------------------------------------------------------------------
437
438/// BACnet Property States — CHOICE type with 40+ variants.
439/// We represent common variants typed, uncommon as raw bytes.
440#[derive(Debug, Clone, PartialEq)]
441pub enum BACnetPropertyStates {
442    BooleanValue(bool),      // [0]
443    BinaryValue(u32),        // [1] BACnetBinaryPV
444    EventType(u32),          // [2]
445    Polarity(u32),           // [3]
446    ProgramChange(u32),      // [4]
447    ProgramState(u32),       // [5]
448    ReasonForHalt(u32),      // [6]
449    Reliability(u32),        // [7]
450    State(u32),              // [8] BACnetEventState
451    SystemStatus(u32),       // [9]
452    Units(u32),              // [10]
453    UnsignedValue(u32),      // [11]
454    LifeSafetyMode(u32),     // [12]
455    LifeSafetyState(u32),    // [13]
456    DoorAlarmState(u32),     // [14]
457    Action(u32),             // [15]
458    DoorSecuredStatus(u32),  // [16]
459    DoorStatus(u32),         // [17]
460    DoorValue(u32),          // [18]
461    LiftCarDirection(u32),   // [40]
462    LiftCarDoorCommand(u32), // [42]
463    TimerState(u32),         // [38]
464    TimerTransition(u32),    // [39]
465    /// Catch-all for uncommon variants.
466    Other {
467        tag: u8,
468        data: Vec<u8>,
469    },
470}
471
472// ---------------------------------------------------------------------------
473// BACnetShedLevel (Clause 12 — used by LoadControl)
474// ---------------------------------------------------------------------------
475
476/// BACnet ShedLevel — CHOICE for LoadControl.
477#[derive(Debug, Clone, PartialEq)]
478pub enum BACnetShedLevel {
479    /// Shed level as a percentage (0–100).
480    Percent(u32),
481    /// Shed level as an abstract level value.
482    Level(u32),
483    /// Shed level as a floating-point amount.
484    Amount(f32),
485}
486
487// ---------------------------------------------------------------------------
488// BACnetLightingCommand (Clause 21 -- used by LightingOutput)
489// ---------------------------------------------------------------------------
490
491/// BACnet Lighting Command -- controls lighting operations.
492///
493/// Per ASHRAE 135-2020 Clause 21, this type is used by the LightingOutput
494/// object's LIGHTING_COMMAND property to specify a lighting operation
495/// (e.g., fade, ramp, step) with optional parameters.
496#[derive(Debug, Clone, PartialEq)]
497pub struct BACnetLightingCommand {
498    /// The lighting operation (LightingOperation enum raw value).
499    pub operation: u32,
500    /// Optional target brightness level (0.0 to 100.0 percent).
501    pub target_level: Option<f32>,
502    /// Optional ramp rate (percent per second).
503    pub ramp_rate: Option<f32>,
504    /// Optional step increment (percent).
505    pub step_increment: Option<f32>,
506    /// Optional fade time (milliseconds).
507    pub fade_time: Option<u32>,
508    /// Optional priority (1-16).
509    pub priority: Option<u32>,
510}
511
512// ---------------------------------------------------------------------------
513// BACnetDeviceObjectReference (Clause 21 -- used by Access Control objects)
514// ---------------------------------------------------------------------------
515
516/// BACnet Device Object Reference (simplified).
517///
518/// References an object, optionally on a specific device. Used by access
519/// control objects (e.g., BACnetAccessRule location).
520#[derive(Debug, Clone, PartialEq, Eq)]
521pub struct BACnetDeviceObjectReference {
522    /// Optional device identifier (None = local device).
523    pub device_identifier: Option<ObjectIdentifier>,
524    /// The object being referenced.
525    pub object_identifier: ObjectIdentifier,
526}
527
528// ---------------------------------------------------------------------------
529// BACnetAccessRule (Clause 12 -- used by AccessRights object)
530// ---------------------------------------------------------------------------
531
532/// BACnet Access Rule for access control objects.
533///
534/// Specifies a time range and location with an enable/disable flag,
535/// used in positive and negative access rules lists.
536#[derive(Debug, Clone, PartialEq)]
537pub struct BACnetAccessRule {
538    /// Time range specifier: 0 = specified, 1 = always.
539    pub time_range_specifier: u32,
540    /// Optional time range (start date, start time, end date, end time).
541    /// Present only when `time_range_specifier` is 0 (specified).
542    pub time_range: Option<(Date, Time, Date, Time)>,
543    /// Location specifier: 0 = specified, 1 = all.
544    pub location_specifier: u32,
545    /// Optional location reference. Present only when `location_specifier` is 0 (specified).
546    pub location: Option<BACnetDeviceObjectReference>,
547    /// Whether access is enabled or disabled by this rule.
548    pub enable: bool,
549}
550
551// ---------------------------------------------------------------------------
552// BACnetAssignedAccessRights (Clause 12 -- used by AccessCredential/AccessUser)
553// ---------------------------------------------------------------------------
554
555/// BACnet Assigned Access Rights.
556///
557/// Associates a reference to an AccessRights object with an enable flag.
558#[derive(Debug, Clone, PartialEq, Eq)]
559pub struct BACnetAssignedAccessRights {
560    /// Reference to an AccessRights object.
561    pub assigned_access_rights: ObjectIdentifier,
562    /// Whether these access rights are currently enabled.
563    pub enable: bool,
564}
565
566// ---------------------------------------------------------------------------
567// BACnetAssignedLandingCalls (Clause 12 -- used by ElevatorGroup)
568// ---------------------------------------------------------------------------
569
570/// BACnet Assigned Landing Calls for elevator group.
571#[derive(Debug, Clone, PartialEq)]
572pub struct BACnetAssignedLandingCalls {
573    /// The floor number for this landing call.
574    pub floor_number: u8,
575    /// Direction: 0=up, 1=down, 2=unknown.
576    pub direction: u32,
577}
578
579// ---------------------------------------------------------------------------
580// FaultParameters (Clause 12.12.50)
581// ---------------------------------------------------------------------------
582
583/// Fault parameter variants for configuring fault detection algorithms.
584#[derive(Debug, Clone, PartialEq)]
585pub enum FaultParameters {
586    /// No fault detection.
587    FaultNone,
588    /// Fault on characterstring match.
589    FaultCharacterString { fault_values: Vec<String> },
590    /// Vendor-defined fault algorithm.
591    FaultExtended {
592        vendor_id: u16,
593        extended_fault_type: u32,
594        parameters: Vec<u8>,
595    },
596    /// Fault on life safety state match.
597    FaultLifeSafety {
598        fault_values: Vec<u32>,
599        mode_for_reference: BACnetDeviceObjectPropertyReference,
600    },
601    /// Fault on property state match.
602    FaultState {
603        fault_values: Vec<BACnetPropertyStates>,
604    },
605    /// Fault on status flags change.
606    FaultStatusFlags {
607        reference: BACnetDeviceObjectPropertyReference,
608    },
609    /// Fault when value exceeds range.
610    FaultOutOfRange { min_normal: f64, max_normal: f64 },
611    /// Fault from listed reference.
612    FaultListed {
613        reference: BACnetDeviceObjectPropertyReference,
614    },
615}
616
617// ---------------------------------------------------------------------------
618// BACnetRecipientProcess (Clause 21)
619// ---------------------------------------------------------------------------
620
621/// BACnet Recipient Process — a recipient with an associated process identifier.
622#[derive(Debug, Clone, PartialEq)]
623pub struct BACnetRecipientProcess {
624    pub recipient: BACnetRecipient,
625    pub process_identifier: u32,
626}
627
628// ---------------------------------------------------------------------------
629// BACnetCOVSubscription (Clause 21)
630// ---------------------------------------------------------------------------
631
632/// BACnet COV Subscription — represents an active COV subscription.
633///
634/// The `monitored_property_reference` is a `BACnetObjectPropertyReference`
635/// (object + property + optional index).
636#[derive(Debug, Clone, PartialEq)]
637pub struct BACnetCOVSubscription {
638    pub recipient: BACnetRecipientProcess,
639    pub monitored_property_reference: BACnetObjectPropertyReference,
640    pub issue_confirmed_notifications: bool,
641    pub time_remaining: u32,
642    pub cov_increment: Option<f32>,
643}
644
645// ---------------------------------------------------------------------------
646// BACnetValueSource (Clause 21)
647// ---------------------------------------------------------------------------
648
649/// BACnet Value Source — identifies the source of a property value write.
650#[derive(Debug, Clone, PartialEq)]
651pub enum BACnetValueSource {
652    None,
653    Object(ObjectIdentifier),
654    Address(BACnetAddress),
655}
656
657// ---------------------------------------------------------------------------
658// Tests
659// ---------------------------------------------------------------------------
660
661#[cfg(test)]
662mod tests {
663    use super::*;
664    use crate::enums::{ObjectType, PropertyIdentifier};
665
666    // --- BACnetDateRange ---
667
668    #[test]
669    fn date_range_encode_decode_round_trip() {
670        let range = BACnetDateRange {
671            start_date: Date {
672                year: 124,
673                month: 1,
674                day: 1,
675                day_of_week: 1,
676            },
677            end_date: Date {
678                year: 124,
679                month: 12,
680                day: 31,
681                day_of_week: 2,
682            },
683        };
684        let encoded = range.encode();
685        assert_eq!(encoded.len(), 8);
686        let decoded = BACnetDateRange::decode(&encoded).unwrap();
687        assert_eq!(range, decoded);
688    }
689
690    #[test]
691    fn date_range_encode_decode_all_unspecified() {
692        let range = BACnetDateRange {
693            start_date: Date {
694                year: Date::UNSPECIFIED,
695                month: Date::UNSPECIFIED,
696                day: Date::UNSPECIFIED,
697                day_of_week: Date::UNSPECIFIED,
698            },
699            end_date: Date {
700                year: Date::UNSPECIFIED,
701                month: Date::UNSPECIFIED,
702                day: Date::UNSPECIFIED,
703                day_of_week: Date::UNSPECIFIED,
704            },
705        };
706        let encoded = range.encode();
707        let decoded = BACnetDateRange::decode(&encoded).unwrap();
708        assert_eq!(range, decoded);
709    }
710
711    #[test]
712    fn date_range_buffer_too_short() {
713        // 7 bytes — one short
714        let result = BACnetDateRange::decode(&[0; 7]);
715        assert!(result.is_err());
716        match result.unwrap_err() {
717            Error::BufferTooShort { need, have } => {
718                assert_eq!(need, 8);
719                assert_eq!(have, 7);
720            }
721            other => panic!("unexpected error: {other:?}"),
722        }
723    }
724
725    #[test]
726    fn date_range_buffer_empty() {
727        let result = BACnetDateRange::decode(&[]);
728        assert!(result.is_err());
729    }
730
731    #[test]
732    fn date_range_extra_bytes_ignored() {
733        let range = BACnetDateRange {
734            start_date: Date {
735                year: 100,
736                month: 6,
737                day: 15,
738                day_of_week: 5,
739            },
740            end_date: Date {
741                year: 100,
742                month: 6,
743                day: 30,
744                day_of_week: 6,
745            },
746        };
747        let encoded = range.encode();
748        let mut extended = encoded.to_vec();
749        extended.extend_from_slice(&[0xFF, 0xFF]); // extra bytes
750        let decoded = BACnetDateRange::decode(&extended).unwrap();
751        assert_eq!(range, decoded);
752    }
753
754    // --- BACnetWeekNDay ---
755
756    #[test]
757    fn week_n_day_encode_decode_round_trip() {
758        let wnd = BACnetWeekNDay {
759            month: 3,
760            week_of_month: 2,
761            day_of_week: 5, // Friday
762        };
763        let encoded = wnd.encode();
764        assert_eq!(encoded.len(), 3);
765        let decoded = BACnetWeekNDay::decode(&encoded).unwrap();
766        assert_eq!(wnd, decoded);
767    }
768
769    #[test]
770    fn week_n_day_encode_decode_all_any() {
771        let wnd = BACnetWeekNDay {
772            month: BACnetWeekNDay::ANY,
773            week_of_month: BACnetWeekNDay::ANY,
774            day_of_week: BACnetWeekNDay::ANY,
775        };
776        let encoded = wnd.encode();
777        assert_eq!(encoded, [0xFF, 0xFF, 0xFF]);
778        let decoded = BACnetWeekNDay::decode(&encoded).unwrap();
779        assert_eq!(wnd, decoded);
780    }
781
782    #[test]
783    fn week_n_day_buffer_too_short() {
784        // 2 bytes — one short
785        let result = BACnetWeekNDay::decode(&[0x03, 0x02]);
786        assert!(result.is_err());
787        match result.unwrap_err() {
788            Error::BufferTooShort { need, have } => {
789                assert_eq!(need, 3);
790                assert_eq!(have, 2);
791            }
792            other => panic!("unexpected error: {other:?}"),
793        }
794    }
795
796    #[test]
797    fn week_n_day_buffer_empty() {
798        let result = BACnetWeekNDay::decode(&[]);
799        assert!(result.is_err());
800    }
801
802    // --- BACnetObjectPropertyReference ---
803
804    #[test]
805    fn object_property_reference_basic_construction() {
806        let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
807        let opr = BACnetObjectPropertyReference::new(oid, 85); // prop 85 = present-value
808        assert_eq!(opr.object_identifier, oid);
809        assert_eq!(opr.property_identifier, 85);
810        assert_eq!(opr.property_array_index, None);
811    }
812
813    #[test]
814    fn object_property_reference_with_index() {
815        let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
816        let opr = BACnetObjectPropertyReference::new_indexed(oid, 85, 3);
817        assert_eq!(opr.property_array_index, Some(3));
818    }
819
820    #[test]
821    fn object_property_reference_equality() {
822        let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 5).unwrap();
823        let a = BACnetObjectPropertyReference::new(oid, 85);
824        let b = BACnetObjectPropertyReference::new(oid, 85);
825        assert_eq!(a, b);
826
827        let c = BACnetObjectPropertyReference::new(oid, 77); // different property
828        assert_ne!(a, c);
829    }
830
831    // --- BACnetDeviceObjectPropertyReference ---
832
833    #[test]
834    fn device_object_property_reference_local() {
835        let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 10).unwrap();
836        let dopr = BACnetDeviceObjectPropertyReference::new_local(oid, 85);
837        assert_eq!(dopr.object_identifier, oid);
838        assert_eq!(dopr.property_identifier, 85);
839        assert_eq!(dopr.property_array_index, None);
840        assert_eq!(dopr.device_identifier, None);
841    }
842
843    #[test]
844    fn device_object_property_reference_remote() {
845        let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 10).unwrap();
846        let dev_oid = ObjectIdentifier::new(ObjectType::DEVICE, 1234).unwrap();
847        let dopr = BACnetDeviceObjectPropertyReference::new_remote(oid, 85, dev_oid);
848        assert_eq!(dopr.device_identifier, Some(dev_oid));
849    }
850
851    #[test]
852    fn device_object_property_reference_with_index() {
853        let oid = ObjectIdentifier::new(ObjectType::MULTI_STATE_INPUT, 3).unwrap();
854        let dopr = BACnetDeviceObjectPropertyReference::new_local(oid, 74).with_index(2); // prop 74 = state-text
855        assert_eq!(dopr.property_array_index, Some(2));
856        assert_eq!(dopr.device_identifier, None);
857    }
858
859    // --- BACnetAddress ---
860
861    #[test]
862    fn bacnet_address_local_broadcast() {
863        let addr = BACnetAddress::local_broadcast();
864        assert_eq!(addr.network_number, 0);
865        assert!(addr.mac_address.is_empty());
866    }
867
868    #[test]
869    fn bacnet_address_from_ip() {
870        let ip_port: [u8; 6] = [192, 168, 1, 100, 0xBA, 0xC0]; // 192.168.1.100:47808
871        let addr = BACnetAddress::from_ip(ip_port);
872        assert_eq!(addr.network_number, 0);
873        assert_eq!(addr.mac_address.as_slice(), &ip_port);
874    }
875
876    // --- BACnetRecipient ---
877
878    #[test]
879    fn bacnet_recipient_device_variant() {
880        let dev_oid = ObjectIdentifier::new(ObjectType::DEVICE, 42).unwrap();
881        let recipient = BACnetRecipient::Device(dev_oid);
882        match recipient {
883            BACnetRecipient::Device(oid) => assert_eq!(oid.instance_number(), 42),
884            BACnetRecipient::Address(_) => panic!("wrong variant"),
885        }
886    }
887
888    #[test]
889    fn bacnet_recipient_address_variant() {
890        let addr = BACnetAddress {
891            network_number: 100,
892            mac_address: MacAddr::from_slice(&[0x01, 0x02, 0x03]),
893        };
894        let recipient = BACnetRecipient::Address(addr.clone());
895        match recipient {
896            BACnetRecipient::Device(_) => panic!("wrong variant"),
897            BACnetRecipient::Address(a) => assert_eq!(a, addr),
898        }
899    }
900
901    // --- BACnetDestination ---
902
903    #[test]
904    fn bacnet_destination_construction() {
905        let dev_oid = ObjectIdentifier::new(ObjectType::DEVICE, 99).unwrap();
906        let dest = BACnetDestination {
907            valid_days: 0b0111_1111, // all days
908            from_time: Time {
909                hour: 0,
910                minute: 0,
911                second: 0,
912                hundredths: 0,
913            },
914            to_time: Time {
915                hour: 23,
916                minute: 59,
917                second: 59,
918                hundredths: 99,
919            },
920            recipient: BACnetRecipient::Device(dev_oid),
921            process_identifier: 1,
922            issue_confirmed_notifications: true,
923            transitions: 0b0000_0111, // all transitions
924        };
925        assert_eq!(dest.valid_days & 0x7F, 0x7F);
926        assert!(dest.issue_confirmed_notifications);
927        assert_eq!(dest.transitions & 0x07, 0x07);
928    }
929
930    // --- LogDatum ---
931
932    #[test]
933    fn log_datum_variants_clone_eq() {
934        let real = LogDatum::RealValue(72.5_f32);
935        assert_eq!(real.clone(), LogDatum::RealValue(72.5_f32));
936
937        let bits = LogDatum::BitstringValue {
938            unused_bits: 3,
939            data: vec![0b1010_0000],
940        };
941        assert_eq!(bits.clone(), bits);
942
943        let fail = LogDatum::Failure {
944            error_class: 2,
945            error_code: 31,
946        };
947        assert_eq!(fail.clone(), fail);
948
949        assert_eq!(LogDatum::NullValue, LogDatum::NullValue);
950        assert_ne!(LogDatum::BooleanValue(true), LogDatum::BooleanValue(false));
951    }
952
953    // --- BACnetLogRecord ---
954
955    #[test]
956    fn log_record_construction() {
957        let record = BACnetLogRecord {
958            date: Date {
959                year: 124,
960                month: 3,
961                day: 15,
962                day_of_week: 5,
963            },
964            time: Time {
965                hour: 10,
966                minute: 30,
967                second: 0,
968                hundredths: 0,
969            },
970            log_datum: LogDatum::RealValue(23.4_f32),
971            status_flags: None,
972        };
973        assert_eq!(record.date.year, 124);
974        assert_eq!(record.status_flags, None);
975    }
976
977    #[test]
978    fn log_record_with_status_flags() {
979        let record = BACnetLogRecord {
980            date: Date {
981                year: 124,
982                month: 1,
983                day: 1,
984                day_of_week: 1,
985            },
986            time: Time {
987                hour: 0,
988                minute: 0,
989                second: 0,
990                hundredths: 0,
991            },
992            log_datum: LogDatum::LogStatus(0b010), // buffer-purged
993            status_flags: Some(0b0100),            // FAULT set
994        };
995        assert_eq!(record.status_flags, Some(0b0100));
996        match record.log_datum {
997            LogDatum::LogStatus(s) => assert_eq!(s, 0b010),
998            _ => panic!("wrong datum variant"),
999        }
1000    }
1001
1002    // --- BACnetCalendarEntry ---
1003
1004    #[test]
1005    fn calendar_entry_variants() {
1006        let date_entry = BACnetCalendarEntry::Date(Date {
1007            year: 124,
1008            month: 6,
1009            day: 15,
1010            day_of_week: 6,
1011        });
1012        let range_entry = BACnetCalendarEntry::DateRange(BACnetDateRange {
1013            start_date: Date {
1014                year: 124,
1015                month: 1,
1016                day: 1,
1017                day_of_week: 1,
1018            },
1019            end_date: Date {
1020                year: 124,
1021                month: 12,
1022                day: 31,
1023                day_of_week: 2,
1024            },
1025        });
1026        let wnd_entry = BACnetCalendarEntry::WeekNDay(BACnetWeekNDay {
1027            month: BACnetWeekNDay::ANY,
1028            week_of_month: 1,
1029            day_of_week: 1, // first Monday of every month
1030        });
1031        // Just verify they can be constructed and cloned
1032        let _a = date_entry.clone();
1033        let _b = range_entry.clone();
1034        let _c = wnd_entry.clone();
1035    }
1036
1037    // --- BACnetSpecialEvent ---
1038
1039    #[test]
1040    fn special_event_inline_calendar_entry() {
1041        let event = BACnetSpecialEvent {
1042            period: SpecialEventPeriod::CalendarEntry(BACnetCalendarEntry::WeekNDay(
1043                BACnetWeekNDay {
1044                    month: 12,
1045                    week_of_month: BACnetWeekNDay::ANY,
1046                    day_of_week: BACnetWeekNDay::ANY,
1047                },
1048            )),
1049            list_of_time_values: vec![BACnetTimeValue {
1050                time: Time {
1051                    hour: 8,
1052                    minute: 0,
1053                    second: 0,
1054                    hundredths: 0,
1055                },
1056                value: vec![0x10, 0x00], // raw-tagged Null
1057            }],
1058            event_priority: 16, // lowest priority
1059        };
1060        assert_eq!(event.event_priority, 16);
1061        assert_eq!(event.list_of_time_values.len(), 1);
1062    }
1063
1064    #[test]
1065    fn special_event_calendar_reference() {
1066        let cal_oid = ObjectIdentifier::new(ObjectType::CALENDAR, 0).unwrap();
1067        let event = BACnetSpecialEvent {
1068            period: SpecialEventPeriod::CalendarReference(cal_oid),
1069            list_of_time_values: vec![],
1070            event_priority: 1, // highest priority
1071        };
1072        match &event.period {
1073            SpecialEventPeriod::CalendarReference(oid) => {
1074                assert_eq!(oid.instance_number(), 0);
1075            }
1076            SpecialEventPeriod::CalendarEntry(_) => panic!("wrong period variant"),
1077        }
1078    }
1079
1080    // --- BACnetRecipientProcess ---
1081
1082    #[test]
1083    fn recipient_process_construction() {
1084        let dev_oid = ObjectIdentifier::new(ObjectType::DEVICE, 100).unwrap();
1085        let rp = BACnetRecipientProcess {
1086            recipient: BACnetRecipient::Device(dev_oid),
1087            process_identifier: 42,
1088        };
1089        assert_eq!(rp.process_identifier, 42);
1090        match &rp.recipient {
1091            BACnetRecipient::Device(oid) => assert_eq!(oid.instance_number(), 100),
1092            BACnetRecipient::Address(_) => panic!("wrong variant"),
1093        }
1094    }
1095
1096    // --- BACnetCOVSubscription ---
1097
1098    #[test]
1099    fn cov_subscription_creation() {
1100        let dev_oid = ObjectIdentifier::new(ObjectType::DEVICE, 200).unwrap();
1101        let ai_oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
1102        let sub = BACnetCOVSubscription {
1103            recipient: BACnetRecipientProcess {
1104                recipient: BACnetRecipient::Device(dev_oid),
1105                process_identifier: 7,
1106            },
1107            monitored_property_reference: BACnetObjectPropertyReference::new(
1108                ai_oid,
1109                PropertyIdentifier::PRESENT_VALUE.to_raw(),
1110            ),
1111            issue_confirmed_notifications: true,
1112            time_remaining: 300,
1113            cov_increment: Some(0.5),
1114        };
1115        assert_eq!(sub.recipient.process_identifier, 7);
1116        assert_eq!(
1117            sub.monitored_property_reference
1118                .object_identifier
1119                .instance_number(),
1120            1
1121        );
1122        assert!(sub.issue_confirmed_notifications);
1123        assert_eq!(sub.time_remaining, 300);
1124        assert_eq!(sub.cov_increment, Some(0.5));
1125    }
1126
1127    #[test]
1128    fn cov_subscription_without_increment() {
1129        let dev_oid = ObjectIdentifier::new(ObjectType::DEVICE, 50).unwrap();
1130        let bv_oid = ObjectIdentifier::new(ObjectType::BINARY_VALUE, 3).unwrap();
1131        let sub = BACnetCOVSubscription {
1132            recipient: BACnetRecipientProcess {
1133                recipient: BACnetRecipient::Device(dev_oid),
1134                process_identifier: 1,
1135            },
1136            monitored_property_reference: BACnetObjectPropertyReference::new(
1137                bv_oid,
1138                PropertyIdentifier::PRESENT_VALUE.to_raw(),
1139            ),
1140            issue_confirmed_notifications: false,
1141            time_remaining: 0,
1142            cov_increment: None,
1143        };
1144        assert!(!sub.issue_confirmed_notifications);
1145        assert_eq!(sub.cov_increment, None);
1146    }
1147
1148    // --- BACnetValueSource ---
1149
1150    #[test]
1151    fn value_source_none_variant() {
1152        let vs = BACnetValueSource::None;
1153        assert_eq!(vs, BACnetValueSource::None);
1154    }
1155
1156    #[test]
1157    fn value_source_object_variant() {
1158        let dev_oid = ObjectIdentifier::new(ObjectType::DEVICE, 1).unwrap();
1159        let vs = BACnetValueSource::Object(dev_oid);
1160        match vs {
1161            BACnetValueSource::Object(oid) => assert_eq!(oid.instance_number(), 1),
1162            _ => panic!("wrong variant"),
1163        }
1164    }
1165
1166    #[test]
1167    fn value_source_address_variant() {
1168        let addr = BACnetAddress::from_ip([192, 168, 1, 10, 0xBA, 0xC0]);
1169        let vs = BACnetValueSource::Address(addr.clone());
1170        match vs {
1171            BACnetValueSource::Address(a) => assert_eq!(a, addr),
1172            _ => panic!("wrong variant"),
1173        }
1174    }
1175}