Skip to main content

bacnet_objects/
trend.rs

1//! TrendLog (type 20) and TrendLogMultiple (type 27) objects per ASHRAE 135-2020.
2
3use std::borrow::Cow;
4use std::collections::VecDeque;
5
6use bacnet_types::constructed::{BACnetDeviceObjectPropertyReference, BACnetLogRecord, LogDatum};
7use bacnet_types::enums::{ErrorClass, ErrorCode, ObjectType, PropertyIdentifier};
8use bacnet_types::error::Error;
9use bacnet_types::primitives::{ObjectIdentifier, PropertyValue, StatusFlags};
10
11use crate::common::{self, read_property_list_property};
12use crate::traits::BACnetObject;
13
14/// BACnet TrendLog object.
15///
16/// Ring buffer of timestamped property values. The application calls
17/// `add_record()` to log values at `log_interval` intervals.
18pub struct TrendLogObject {
19    oid: ObjectIdentifier,
20    name: String,
21    description: String,
22    log_enable: bool,
23    log_interval: u32,
24    stop_when_full: bool,
25    buffer_size: u32,
26    buffer: VecDeque<BACnetLogRecord>,
27    total_record_count: u64,
28    out_of_service: bool,
29    reliability: u32,
30    status_flags: StatusFlags,
31    log_device_object_property: Option<BACnetDeviceObjectPropertyReference>,
32    logging_type: u32, // 0=polled, 1=cov, 2=triggered
33}
34
35impl TrendLogObject {
36    pub fn new(instance: u32, name: impl Into<String>, buffer_size: u32) -> Result<Self, Error> {
37        let oid = ObjectIdentifier::new(ObjectType::TREND_LOG, instance)?;
38        Ok(Self {
39            oid,
40            name: name.into(),
41            description: String::new(),
42            log_enable: true,
43            log_interval: 0,
44            stop_when_full: false,
45            buffer_size,
46            buffer: VecDeque::new(),
47            total_record_count: 0,
48            out_of_service: false,
49            reliability: 0,
50            status_flags: StatusFlags::empty(),
51            log_device_object_property: None,
52            logging_type: 0,
53        })
54    }
55
56    /// Add a BACnetLogRecord to the trend log buffer.
57    pub fn add_record(&mut self, record: BACnetLogRecord) {
58        if !self.log_enable {
59            return;
60        }
61        if self.buffer.len() >= self.buffer_size as usize {
62            if self.stop_when_full {
63                return;
64            }
65            self.buffer.pop_front();
66        }
67        self.buffer.push_back(record);
68        self.total_record_count += 1;
69    }
70
71    /// Get the current buffer contents.
72    pub fn records(&self) -> &VecDeque<BACnetLogRecord> {
73        &self.buffer
74    }
75
76    /// Clear the buffer.
77    pub fn clear(&mut self) {
78        self.buffer.clear();
79    }
80
81    /// Set the description string.
82    pub fn set_description(&mut self, desc: impl Into<String>) {
83        self.description = desc.into();
84    }
85
86    /// Set the log device object property reference.
87    pub fn set_log_device_object_property(
88        &mut self,
89        reference: Option<BACnetDeviceObjectPropertyReference>,
90    ) {
91        self.log_device_object_property = reference;
92    }
93
94    /// Set the logging type (0=polled, 1=cov, 2=triggered).
95    pub fn set_logging_type(&mut self, logging_type: u32) {
96        self.logging_type = logging_type;
97    }
98}
99
100impl BACnetObject for TrendLogObject {
101    fn object_identifier(&self) -> ObjectIdentifier {
102        self.oid
103    }
104
105    fn object_name(&self) -> &str {
106        &self.name
107    }
108
109    fn read_property(
110        &self,
111        property: PropertyIdentifier,
112        array_index: Option<u32>,
113    ) -> Result<PropertyValue, Error> {
114        match property {
115            p if p == PropertyIdentifier::OBJECT_IDENTIFIER => {
116                Ok(PropertyValue::ObjectIdentifier(self.oid))
117            }
118            p if p == PropertyIdentifier::OBJECT_NAME => {
119                Ok(PropertyValue::CharacterString(self.name.clone()))
120            }
121            p if p == PropertyIdentifier::DESCRIPTION => {
122                Ok(PropertyValue::CharacterString(self.description.clone()))
123            }
124            p if p == PropertyIdentifier::OBJECT_TYPE => {
125                Ok(PropertyValue::Enumerated(ObjectType::TREND_LOG.to_raw()))
126            }
127            p if p == PropertyIdentifier::LOG_ENABLE => Ok(PropertyValue::Boolean(self.log_enable)),
128            p if p == PropertyIdentifier::LOG_INTERVAL => {
129                Ok(PropertyValue::Unsigned(self.log_interval as u64))
130            }
131            p if p == PropertyIdentifier::STOP_WHEN_FULL => {
132                Ok(PropertyValue::Boolean(self.stop_when_full))
133            }
134            p if p == PropertyIdentifier::BUFFER_SIZE => {
135                Ok(PropertyValue::Unsigned(self.buffer_size as u64))
136            }
137            p if p == PropertyIdentifier::RECORD_COUNT => {
138                Ok(PropertyValue::Unsigned(self.buffer.len() as u64))
139            }
140            p if p == PropertyIdentifier::TOTAL_RECORD_COUNT => {
141                Ok(PropertyValue::Unsigned(self.total_record_count))
142            }
143            p if p == PropertyIdentifier::STATUS_FLAGS => Ok(PropertyValue::BitString {
144                unused_bits: 4,
145                data: vec![self.status_flags.bits() << 4],
146            }),
147            p if p == PropertyIdentifier::EVENT_STATE => Ok(PropertyValue::Enumerated(0)),
148            p if p == PropertyIdentifier::RELIABILITY => {
149                Ok(PropertyValue::Enumerated(self.reliability))
150            }
151            p if p == PropertyIdentifier::OUT_OF_SERVICE => {
152                Ok(PropertyValue::Boolean(self.out_of_service))
153            }
154            p if p == PropertyIdentifier::LOG_BUFFER => {
155                let records = self
156                    .buffer
157                    .iter()
158                    .map(|record| {
159                        let datum_value = match &record.log_datum {
160                            LogDatum::LogStatus(v) => PropertyValue::Unsigned(*v as u64),
161                            LogDatum::BooleanValue(v) => PropertyValue::Boolean(*v),
162                            LogDatum::RealValue(v) => PropertyValue::Real(*v),
163                            LogDatum::EnumValue(v) => PropertyValue::Enumerated(*v),
164                            LogDatum::UnsignedValue(v) => PropertyValue::Unsigned(*v),
165                            LogDatum::SignedValue(v) => PropertyValue::Signed(*v as i32),
166                            LogDatum::BitstringValue { unused_bits, data } => {
167                                PropertyValue::BitString {
168                                    unused_bits: *unused_bits,
169                                    data: data.clone(),
170                                }
171                            }
172                            LogDatum::NullValue => PropertyValue::Null,
173                            LogDatum::Failure {
174                                error_class,
175                                error_code,
176                            } => PropertyValue::List(vec![
177                                PropertyValue::Unsigned(*error_class as u64),
178                                PropertyValue::Unsigned(*error_code as u64),
179                            ]),
180                            LogDatum::TimeChange(v) => PropertyValue::Real(*v),
181                            LogDatum::AnyValue(bytes) => PropertyValue::OctetString(bytes.clone()),
182                        };
183                        PropertyValue::List(vec![
184                            PropertyValue::Date(record.date),
185                            PropertyValue::Time(record.time),
186                            datum_value,
187                        ])
188                    })
189                    .collect();
190                Ok(PropertyValue::List(records))
191            }
192            p if p == PropertyIdentifier::LOGGING_TYPE => {
193                Ok(PropertyValue::Enumerated(self.logging_type))
194            }
195            p if p == PropertyIdentifier::LOG_DEVICE_OBJECT_PROPERTY => {
196                match &self.log_device_object_property {
197                    None => Ok(PropertyValue::Null),
198                    Some(r) => Ok(PropertyValue::List(vec![
199                        PropertyValue::ObjectIdentifier(r.object_identifier),
200                        PropertyValue::Unsigned(r.property_identifier as u64),
201                        match r.property_array_index {
202                            Some(idx) => PropertyValue::Unsigned(idx as u64),
203                            None => PropertyValue::Null,
204                        },
205                        match r.device_identifier {
206                            Some(dev) => PropertyValue::ObjectIdentifier(dev),
207                            None => PropertyValue::Null,
208                        },
209                    ])),
210                }
211            }
212            p if p == PropertyIdentifier::PROPERTY_LIST => {
213                read_property_list_property(&self.property_list(), array_index)
214            }
215            _ => Err(Error::Protocol {
216                class: ErrorClass::PROPERTY.to_raw() as u32,
217                code: ErrorCode::UNKNOWN_PROPERTY.to_raw() as u32,
218            }),
219        }
220    }
221
222    fn write_property(
223        &mut self,
224        property: PropertyIdentifier,
225        _array_index: Option<u32>,
226        value: PropertyValue,
227        _priority: Option<u8>,
228    ) -> Result<(), Error> {
229        if property == PropertyIdentifier::LOG_ENABLE {
230            if let PropertyValue::Boolean(v) = value {
231                self.log_enable = v;
232                return Ok(());
233            }
234            return Err(Error::Protocol {
235                class: ErrorClass::PROPERTY.to_raw() as u32,
236                code: ErrorCode::INVALID_DATA_TYPE.to_raw() as u32,
237            });
238        }
239        if property == PropertyIdentifier::LOG_INTERVAL {
240            if let PropertyValue::Unsigned(v) = value {
241                self.log_interval = common::u64_to_u32(v)?;
242                return Ok(());
243            }
244            return Err(Error::Protocol {
245                class: ErrorClass::PROPERTY.to_raw() as u32,
246                code: ErrorCode::INVALID_DATA_TYPE.to_raw() as u32,
247            });
248        }
249        if property == PropertyIdentifier::STOP_WHEN_FULL {
250            if let PropertyValue::Boolean(v) = value {
251                self.stop_when_full = v;
252                return Ok(());
253            }
254            return Err(Error::Protocol {
255                class: ErrorClass::PROPERTY.to_raw() as u32,
256                code: ErrorCode::INVALID_DATA_TYPE.to_raw() as u32,
257            });
258        }
259        if property == PropertyIdentifier::RECORD_COUNT {
260            // Writing 0 clears the buffer
261            if let PropertyValue::Unsigned(0) = value {
262                self.buffer.clear();
263                return Ok(());
264            }
265            return Err(Error::Protocol {
266                class: ErrorClass::PROPERTY.to_raw() as u32,
267                code: ErrorCode::INVALID_DATA_TYPE.to_raw() as u32,
268            });
269        }
270        if property == PropertyIdentifier::RELIABILITY {
271            if let PropertyValue::Enumerated(v) = value {
272                self.reliability = v;
273                return Ok(());
274            }
275            return Err(Error::Protocol {
276                class: ErrorClass::PROPERTY.to_raw() as u32,
277                code: ErrorCode::INVALID_DATA_TYPE.to_raw() as u32,
278            });
279        }
280        if property == PropertyIdentifier::OUT_OF_SERVICE {
281            if let PropertyValue::Boolean(v) = value {
282                self.out_of_service = v;
283                return Ok(());
284            }
285            return Err(Error::Protocol {
286                class: ErrorClass::PROPERTY.to_raw() as u32,
287                code: ErrorCode::INVALID_DATA_TYPE.to_raw() as u32,
288            });
289        }
290        if property == PropertyIdentifier::DESCRIPTION {
291            if let PropertyValue::CharacterString(s) = value {
292                self.description = s;
293                return Ok(());
294            }
295            return Err(Error::Protocol {
296                class: ErrorClass::PROPERTY.to_raw() as u32,
297                code: ErrorCode::INVALID_DATA_TYPE.to_raw() as u32,
298            });
299        }
300        Err(Error::Protocol {
301            class: ErrorClass::PROPERTY.to_raw() as u32,
302            code: ErrorCode::WRITE_ACCESS_DENIED.to_raw() as u32,
303        })
304    }
305
306    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
307        static PROPS: &[PropertyIdentifier] = &[
308            PropertyIdentifier::OBJECT_IDENTIFIER,
309            PropertyIdentifier::OBJECT_NAME,
310            PropertyIdentifier::DESCRIPTION,
311            PropertyIdentifier::OBJECT_TYPE,
312            PropertyIdentifier::LOG_ENABLE,
313            PropertyIdentifier::LOG_INTERVAL,
314            PropertyIdentifier::STOP_WHEN_FULL,
315            PropertyIdentifier::BUFFER_SIZE,
316            PropertyIdentifier::LOG_BUFFER,
317            PropertyIdentifier::RECORD_COUNT,
318            PropertyIdentifier::TOTAL_RECORD_COUNT,
319            PropertyIdentifier::STATUS_FLAGS,
320            PropertyIdentifier::EVENT_STATE,
321            PropertyIdentifier::RELIABILITY,
322            PropertyIdentifier::OUT_OF_SERVICE,
323            PropertyIdentifier::LOGGING_TYPE,
324            PropertyIdentifier::LOG_DEVICE_OBJECT_PROPERTY,
325        ];
326        Cow::Borrowed(PROPS)
327    }
328
329    fn add_trend_record(&mut self, record: BACnetLogRecord) {
330        self.add_record(record);
331    }
332}
333
334// ---------------------------------------------------------------------------
335// TrendLogMultiple (type 27)
336// ---------------------------------------------------------------------------
337
338/// BACnet TrendLogMultiple object (type 27).
339///
340/// Multi-channel trending. Logs values from multiple properties simultaneously.
341/// Unlike TrendLog which monitors a single property, TrendLogMultiple monitors
342/// a list of device-object-property references per record.
343pub struct TrendLogMultipleObject {
344    oid: ObjectIdentifier,
345    name: String,
346    description: String,
347    log_enable: bool,
348    log_interval: u32,
349    stop_when_full: bool,
350    buffer_size: u32,
351    buffer: VecDeque<BACnetLogRecord>,
352    total_record_count: u64,
353    status_flags: StatusFlags,
354    log_device_object_property: Vec<BACnetDeviceObjectPropertyReference>,
355    logging_type: u32, // 0=polled, 1=cov, 2=triggered
356    out_of_service: bool,
357    reliability: u32,
358}
359
360impl TrendLogMultipleObject {
361    pub fn new(instance: u32, name: impl Into<String>, buffer_size: u32) -> Result<Self, Error> {
362        let oid = ObjectIdentifier::new(ObjectType::TREND_LOG_MULTIPLE, instance)?;
363        Ok(Self {
364            oid,
365            name: name.into(),
366            description: String::new(),
367            log_enable: true,
368            log_interval: 0,
369            stop_when_full: false,
370            buffer_size,
371            buffer: VecDeque::new(),
372            total_record_count: 0,
373            status_flags: StatusFlags::empty(),
374            log_device_object_property: Vec::new(),
375            logging_type: 0,
376            out_of_service: false,
377            reliability: 0,
378        })
379    }
380
381    /// Add a BACnetLogRecord to the trend log buffer.
382    pub fn add_record(&mut self, record: BACnetLogRecord) {
383        if !self.log_enable {
384            return;
385        }
386        if self.buffer.len() >= self.buffer_size as usize {
387            if self.stop_when_full {
388                return;
389            }
390            self.buffer.pop_front();
391        }
392        self.buffer.push_back(record);
393        self.total_record_count += 1;
394    }
395
396    /// Add a property reference to the monitored list.
397    pub fn add_property_reference(&mut self, reference: BACnetDeviceObjectPropertyReference) {
398        self.log_device_object_property.push(reference);
399    }
400
401    /// Get the current buffer contents.
402    pub fn records(&self) -> &VecDeque<BACnetLogRecord> {
403        &self.buffer
404    }
405
406    /// Clear the buffer.
407    pub fn clear(&mut self) {
408        self.buffer.clear();
409    }
410
411    /// Set the description string.
412    pub fn set_description(&mut self, desc: impl Into<String>) {
413        self.description = desc.into();
414    }
415
416    /// Set the logging type (0=polled, 1=cov, 2=triggered).
417    pub fn set_logging_type(&mut self, logging_type: u32) {
418        self.logging_type = logging_type;
419    }
420}
421
422impl BACnetObject for TrendLogMultipleObject {
423    fn object_identifier(&self) -> ObjectIdentifier {
424        self.oid
425    }
426
427    fn object_name(&self) -> &str {
428        &self.name
429    }
430
431    fn read_property(
432        &self,
433        property: PropertyIdentifier,
434        array_index: Option<u32>,
435    ) -> Result<PropertyValue, Error> {
436        match property {
437            p if p == PropertyIdentifier::OBJECT_IDENTIFIER => {
438                Ok(PropertyValue::ObjectIdentifier(self.oid))
439            }
440            p if p == PropertyIdentifier::OBJECT_NAME => {
441                Ok(PropertyValue::CharacterString(self.name.clone()))
442            }
443            p if p == PropertyIdentifier::DESCRIPTION => {
444                Ok(PropertyValue::CharacterString(self.description.clone()))
445            }
446            p if p == PropertyIdentifier::OBJECT_TYPE => Ok(PropertyValue::Enumerated(
447                ObjectType::TREND_LOG_MULTIPLE.to_raw(),
448            )),
449            p if p == PropertyIdentifier::LOG_ENABLE => Ok(PropertyValue::Boolean(self.log_enable)),
450            p if p == PropertyIdentifier::LOG_INTERVAL => {
451                Ok(PropertyValue::Unsigned(self.log_interval as u64))
452            }
453            p if p == PropertyIdentifier::STOP_WHEN_FULL => {
454                Ok(PropertyValue::Boolean(self.stop_when_full))
455            }
456            p if p == PropertyIdentifier::BUFFER_SIZE => {
457                Ok(PropertyValue::Unsigned(self.buffer_size as u64))
458            }
459            p if p == PropertyIdentifier::RECORD_COUNT => {
460                Ok(PropertyValue::Unsigned(self.buffer.len() as u64))
461            }
462            p if p == PropertyIdentifier::TOTAL_RECORD_COUNT => {
463                Ok(PropertyValue::Unsigned(self.total_record_count))
464            }
465            p if p == PropertyIdentifier::STATUS_FLAGS => Ok(PropertyValue::BitString {
466                unused_bits: 4,
467                data: vec![self.status_flags.bits() << 4],
468            }),
469            p if p == PropertyIdentifier::EVENT_STATE => Ok(PropertyValue::Enumerated(0)),
470            p if p == PropertyIdentifier::OUT_OF_SERVICE => {
471                Ok(PropertyValue::Boolean(self.out_of_service))
472            }
473            p if p == PropertyIdentifier::RELIABILITY => {
474                Ok(PropertyValue::Enumerated(self.reliability))
475            }
476            p if p == PropertyIdentifier::LOG_BUFFER => {
477                let records = self
478                    .buffer
479                    .iter()
480                    .map(|record| {
481                        let datum_value = match &record.log_datum {
482                            LogDatum::LogStatus(v) => PropertyValue::Unsigned(*v as u64),
483                            LogDatum::BooleanValue(v) => PropertyValue::Boolean(*v),
484                            LogDatum::RealValue(v) => PropertyValue::Real(*v),
485                            LogDatum::EnumValue(v) => PropertyValue::Enumerated(*v),
486                            LogDatum::UnsignedValue(v) => PropertyValue::Unsigned(*v),
487                            LogDatum::SignedValue(v) => PropertyValue::Signed(*v as i32),
488                            LogDatum::BitstringValue { unused_bits, data } => {
489                                PropertyValue::BitString {
490                                    unused_bits: *unused_bits,
491                                    data: data.clone(),
492                                }
493                            }
494                            LogDatum::NullValue => PropertyValue::Null,
495                            LogDatum::Failure {
496                                error_class,
497                                error_code,
498                            } => PropertyValue::List(vec![
499                                PropertyValue::Unsigned(*error_class as u64),
500                                PropertyValue::Unsigned(*error_code as u64),
501                            ]),
502                            LogDatum::TimeChange(v) => PropertyValue::Real(*v),
503                            LogDatum::AnyValue(bytes) => PropertyValue::OctetString(bytes.clone()),
504                        };
505                        PropertyValue::List(vec![
506                            PropertyValue::Date(record.date),
507                            PropertyValue::Time(record.time),
508                            datum_value,
509                        ])
510                    })
511                    .collect();
512                Ok(PropertyValue::List(records))
513            }
514            p if p == PropertyIdentifier::LOGGING_TYPE => {
515                Ok(PropertyValue::Enumerated(self.logging_type))
516            }
517            p if p == PropertyIdentifier::LOG_DEVICE_OBJECT_PROPERTY => {
518                let refs: Vec<PropertyValue> = self
519                    .log_device_object_property
520                    .iter()
521                    .map(|r| {
522                        PropertyValue::List(vec![
523                            PropertyValue::ObjectIdentifier(r.object_identifier),
524                            PropertyValue::Unsigned(r.property_identifier as u64),
525                            match r.property_array_index {
526                                Some(idx) => PropertyValue::Unsigned(idx as u64),
527                                None => PropertyValue::Null,
528                            },
529                            match r.device_identifier {
530                                Some(dev) => PropertyValue::ObjectIdentifier(dev),
531                                None => PropertyValue::Null,
532                            },
533                        ])
534                    })
535                    .collect();
536                Ok(PropertyValue::List(refs))
537            }
538            p if p == PropertyIdentifier::PROPERTY_LIST => {
539                read_property_list_property(&self.property_list(), array_index)
540            }
541            _ => Err(Error::Protocol {
542                class: ErrorClass::PROPERTY.to_raw() as u32,
543                code: ErrorCode::UNKNOWN_PROPERTY.to_raw() as u32,
544            }),
545        }
546    }
547
548    fn write_property(
549        &mut self,
550        property: PropertyIdentifier,
551        _array_index: Option<u32>,
552        value: PropertyValue,
553        _priority: Option<u8>,
554    ) -> Result<(), Error> {
555        if property == PropertyIdentifier::LOG_ENABLE {
556            if let PropertyValue::Boolean(v) = value {
557                self.log_enable = v;
558                return Ok(());
559            }
560            return Err(Error::Protocol {
561                class: ErrorClass::PROPERTY.to_raw() as u32,
562                code: ErrorCode::INVALID_DATA_TYPE.to_raw() as u32,
563            });
564        }
565        if property == PropertyIdentifier::LOG_INTERVAL {
566            if let PropertyValue::Unsigned(v) = value {
567                self.log_interval = common::u64_to_u32(v)?;
568                return Ok(());
569            }
570            return Err(Error::Protocol {
571                class: ErrorClass::PROPERTY.to_raw() as u32,
572                code: ErrorCode::INVALID_DATA_TYPE.to_raw() as u32,
573            });
574        }
575        if property == PropertyIdentifier::STOP_WHEN_FULL {
576            if let PropertyValue::Boolean(v) = value {
577                self.stop_when_full = v;
578                return Ok(());
579            }
580            return Err(Error::Protocol {
581                class: ErrorClass::PROPERTY.to_raw() as u32,
582                code: ErrorCode::INVALID_DATA_TYPE.to_raw() as u32,
583            });
584        }
585        if property == PropertyIdentifier::RECORD_COUNT {
586            // Writing 0 clears the buffer
587            if let PropertyValue::Unsigned(0) = value {
588                self.buffer.clear();
589                return Ok(());
590            }
591            return Err(Error::Protocol {
592                class: ErrorClass::PROPERTY.to_raw() as u32,
593                code: ErrorCode::INVALID_DATA_TYPE.to_raw() as u32,
594            });
595        }
596        if property == PropertyIdentifier::DESCRIPTION {
597            if let PropertyValue::CharacterString(s) = value {
598                self.description = s;
599                return Ok(());
600            }
601            return Err(Error::Protocol {
602                class: ErrorClass::PROPERTY.to_raw() as u32,
603                code: ErrorCode::INVALID_DATA_TYPE.to_raw() as u32,
604            });
605        }
606        Err(Error::Protocol {
607            class: ErrorClass::PROPERTY.to_raw() as u32,
608            code: ErrorCode::WRITE_ACCESS_DENIED.to_raw() as u32,
609        })
610    }
611
612    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
613        static PROPS: &[PropertyIdentifier] = &[
614            PropertyIdentifier::OBJECT_IDENTIFIER,
615            PropertyIdentifier::OBJECT_NAME,
616            PropertyIdentifier::DESCRIPTION,
617            PropertyIdentifier::OBJECT_TYPE,
618            PropertyIdentifier::LOG_ENABLE,
619            PropertyIdentifier::LOG_INTERVAL,
620            PropertyIdentifier::STOP_WHEN_FULL,
621            PropertyIdentifier::BUFFER_SIZE,
622            PropertyIdentifier::LOG_BUFFER,
623            PropertyIdentifier::RECORD_COUNT,
624            PropertyIdentifier::TOTAL_RECORD_COUNT,
625            PropertyIdentifier::STATUS_FLAGS,
626            PropertyIdentifier::EVENT_STATE,
627            PropertyIdentifier::OUT_OF_SERVICE,
628            PropertyIdentifier::RELIABILITY,
629            PropertyIdentifier::LOGGING_TYPE,
630            PropertyIdentifier::LOG_DEVICE_OBJECT_PROPERTY,
631        ];
632        Cow::Borrowed(PROPS)
633    }
634
635    fn add_trend_record(&mut self, record: BACnetLogRecord) {
636        self.add_record(record);
637    }
638}
639
640#[cfg(test)]
641mod tests {
642    use super::*;
643    use bacnet_types::primitives::{Date, Time};
644
645    fn make_record(hour: u8, value: f32) -> BACnetLogRecord {
646        BACnetLogRecord {
647            date: Date {
648                year: 124,
649                month: 3,
650                day: 15,
651                day_of_week: 5,
652            },
653            time: Time {
654                hour,
655                minute: 0,
656                second: 0,
657                hundredths: 0,
658            },
659            log_datum: LogDatum::RealValue(value),
660            status_flags: None,
661        }
662    }
663
664    #[test]
665    fn trendlog_add_records() {
666        let mut tl = TrendLogObject::new(1, "TL-1", 100).unwrap();
667        tl.add_record(make_record(10, 72.5));
668        tl.add_record(make_record(11, 73.0));
669        assert_eq!(tl.records().len(), 2);
670        let val = tl
671            .read_property(PropertyIdentifier::RECORD_COUNT, None)
672            .unwrap();
673        assert_eq!(val, PropertyValue::Unsigned(2));
674        let val = tl
675            .read_property(PropertyIdentifier::TOTAL_RECORD_COUNT, None)
676            .unwrap();
677        assert_eq!(val, PropertyValue::Unsigned(2));
678    }
679
680    #[test]
681    fn trendlog_ring_buffer_wraps() {
682        let mut tl = TrendLogObject::new(1, "TL-1", 3).unwrap();
683        for i in 0..5u8 {
684            tl.add_record(BACnetLogRecord {
685                date: Date {
686                    year: 124,
687                    month: 3,
688                    day: 15,
689                    day_of_week: 5,
690                },
691                time: Time {
692                    hour: i,
693                    minute: 0,
694                    second: 0,
695                    hundredths: 0,
696                },
697                log_datum: LogDatum::UnsignedValue(i as u64),
698                status_flags: None,
699            });
700        }
701        assert_eq!(tl.records().len(), 3);
702        // Oldest records should have been evicted; first remaining is hour=2
703        assert_eq!(tl.records()[0].time.hour, 2);
704        let val = tl
705            .read_property(PropertyIdentifier::TOTAL_RECORD_COUNT, None)
706            .unwrap();
707        assert_eq!(val, PropertyValue::Unsigned(5));
708    }
709
710    #[test]
711    fn trendlog_stop_when_full() {
712        let mut tl = TrendLogObject::new(1, "TL-1", 2).unwrap();
713        tl.write_property(
714            PropertyIdentifier::STOP_WHEN_FULL,
715            None,
716            PropertyValue::Boolean(true),
717            None,
718        )
719        .unwrap();
720        for i in 0..5u8 {
721            tl.add_record(make_record(i, i as f32));
722        }
723        assert_eq!(tl.records().len(), 2);
724        assert_eq!(tl.total_record_count, 2); // Only 2 accepted
725    }
726
727    #[test]
728    fn trendlog_disable_logging() {
729        let mut tl = TrendLogObject::new(1, "TL-1", 100).unwrap();
730        tl.write_property(
731            PropertyIdentifier::LOG_ENABLE,
732            None,
733            PropertyValue::Boolean(false),
734            None,
735        )
736        .unwrap();
737        tl.add_record(make_record(10, 72.5));
738        assert_eq!(tl.records().len(), 0);
739    }
740
741    #[test]
742    fn trendlog_clear_buffer() {
743        let mut tl = TrendLogObject::new(1, "TL-1", 100).unwrap();
744        tl.add_record(make_record(10, 72.5));
745        assert_eq!(tl.records().len(), 1);
746        tl.write_property(
747            PropertyIdentifier::RECORD_COUNT,
748            None,
749            PropertyValue::Unsigned(0),
750            None,
751        )
752        .unwrap();
753        assert_eq!(tl.records().len(), 0);
754    }
755
756    #[test]
757    fn trendlog_read_object_type() {
758        let tl = TrendLogObject::new(1, "TL-1", 100).unwrap();
759        let val = tl
760            .read_property(PropertyIdentifier::OBJECT_TYPE, None)
761            .unwrap();
762        assert_eq!(
763            val,
764            PropertyValue::Enumerated(ObjectType::TREND_LOG.to_raw())
765        );
766    }
767
768    #[test]
769    fn trendlog_description_read_write() {
770        let mut tl = TrendLogObject::new(1, "TL-1", 100).unwrap();
771        // Default is empty string
772        assert_eq!(
773            tl.read_property(PropertyIdentifier::DESCRIPTION, None)
774                .unwrap(),
775            PropertyValue::CharacterString(String::new())
776        );
777        tl.write_property(
778            PropertyIdentifier::DESCRIPTION,
779            None,
780            PropertyValue::CharacterString("Zone temperature trend".into()),
781            None,
782        )
783        .unwrap();
784        assert_eq!(
785            tl.read_property(PropertyIdentifier::DESCRIPTION, None)
786                .unwrap(),
787            PropertyValue::CharacterString("Zone temperature trend".into())
788        );
789    }
790
791    #[test]
792    fn trendlog_set_description_convenience() {
793        let mut tl = TrendLogObject::new(1, "TL-1", 100).unwrap();
794        tl.set_description("Outdoor air temperature log");
795        assert_eq!(
796            tl.read_property(PropertyIdentifier::DESCRIPTION, None)
797                .unwrap(),
798            PropertyValue::CharacterString("Outdoor air temperature log".into())
799        );
800    }
801
802    #[test]
803    fn trendlog_description_in_property_list() {
804        let tl = TrendLogObject::new(1, "TL-1", 100).unwrap();
805        assert!(tl
806            .property_list()
807            .contains(&PropertyIdentifier::DESCRIPTION));
808    }
809
810    #[test]
811    fn trendlog_read_log_buffer() {
812        let mut tl = TrendLogObject::new(1, "TL-1", 100).unwrap();
813        tl.add_record(make_record(10, 72.5));
814        tl.add_record(make_record(11, 73.0));
815        let val = tl
816            .read_property(PropertyIdentifier::LOG_BUFFER, None)
817            .unwrap();
818        if let PropertyValue::List(records) = val {
819            assert_eq!(records.len(), 2);
820            // First record
821            if let PropertyValue::List(fields) = &records[0] {
822                assert_eq!(fields.len(), 3);
823                assert_eq!(fields[0], PropertyValue::Date(make_record(10, 72.5).date));
824                assert_eq!(fields[1], PropertyValue::Time(make_record(10, 72.5).time));
825                assert_eq!(fields[2], PropertyValue::Real(72.5));
826            } else {
827                panic!("Expected List for log record");
828            }
829            // Second record
830            if let PropertyValue::List(fields) = &records[1] {
831                assert_eq!(fields[2], PropertyValue::Real(73.0));
832            } else {
833                panic!("Expected List for log record");
834            }
835        } else {
836            panic!("Expected List for LOG_BUFFER");
837        }
838    }
839
840    #[test]
841    fn trendlog_log_buffer_empty() {
842        let tl = TrendLogObject::new(1, "TL-1", 100).unwrap();
843        let val = tl
844            .read_property(PropertyIdentifier::LOG_BUFFER, None)
845            .unwrap();
846        assert_eq!(val, PropertyValue::List(vec![]));
847    }
848
849    #[test]
850    fn trendlog_log_buffer_overflow_stop_when_full() {
851        let mut tl = TrendLogObject::new(1, "TL-1", 3).unwrap();
852        tl.write_property(
853            PropertyIdentifier::STOP_WHEN_FULL,
854            None,
855            PropertyValue::Boolean(true),
856            None,
857        )
858        .unwrap();
859        for i in 0..5u8 {
860            tl.add_record(make_record(i, i as f32 * 10.0));
861        }
862        // Buffer capped at 3; only first 3 records accepted
863        let val = tl
864            .read_property(PropertyIdentifier::LOG_BUFFER, None)
865            .unwrap();
866        if let PropertyValue::List(records) = val {
867            assert_eq!(records.len(), 3);
868            if let PropertyValue::List(fields) = &records[0] {
869                assert_eq!(fields[2], PropertyValue::Real(0.0));
870            } else {
871                panic!("Expected List");
872            }
873            if let PropertyValue::List(fields) = &records[2] {
874                assert_eq!(fields[2], PropertyValue::Real(20.0));
875            } else {
876                panic!("Expected List");
877            }
878        } else {
879            panic!("Expected List for LOG_BUFFER");
880        }
881    }
882
883    #[test]
884    fn trendlog_read_logging_type() {
885        let tl = TrendLogObject::new(1, "TL-1", 100).unwrap();
886        let val = tl
887            .read_property(PropertyIdentifier::LOGGING_TYPE, None)
888            .unwrap();
889        // Default is 0 (polled)
890        assert_eq!(val, PropertyValue::Enumerated(0));
891    }
892
893    #[test]
894    fn trendlog_set_logging_type() {
895        let mut tl = TrendLogObject::new(1, "TL-1", 100).unwrap();
896        tl.set_logging_type(1); // COV
897        let val = tl
898            .read_property(PropertyIdentifier::LOGGING_TYPE, None)
899            .unwrap();
900        assert_eq!(val, PropertyValue::Enumerated(1));
901    }
902
903    #[test]
904    fn trendlog_log_buffer_in_property_list() {
905        let tl = TrendLogObject::new(1, "TL-1", 100).unwrap();
906        let props = tl.property_list();
907        assert!(props.contains(&PropertyIdentifier::LOG_BUFFER));
908        assert!(props.contains(&PropertyIdentifier::LOGGING_TYPE));
909        assert!(props.contains(&PropertyIdentifier::LOG_DEVICE_OBJECT_PROPERTY));
910    }
911
912    #[test]
913    fn trendlog_log_device_object_property_null_by_default() {
914        let tl = TrendLogObject::new(1, "TL-1", 100).unwrap();
915        let val = tl
916            .read_property(PropertyIdentifier::LOG_DEVICE_OBJECT_PROPERTY, None)
917            .unwrap();
918        assert_eq!(val, PropertyValue::Null);
919    }
920
921    #[test]
922    fn trendlog_log_buffer_various_datum_types() {
923        use bacnet_types::constructed::LogDatum;
924        let mut tl = TrendLogObject::new(1, "TL-1", 100).unwrap();
925
926        let date = Date {
927            year: 124,
928            month: 3,
929            day: 15,
930            day_of_week: 5,
931        };
932        let time = Time {
933            hour: 8,
934            minute: 0,
935            second: 0,
936            hundredths: 0,
937        };
938
939        tl.add_record(BACnetLogRecord {
940            date,
941            time,
942            log_datum: LogDatum::BooleanValue(true),
943            status_flags: None,
944        });
945        tl.add_record(BACnetLogRecord {
946            date,
947            time,
948            log_datum: LogDatum::EnumValue(42),
949            status_flags: Some(0b0100),
950        });
951        tl.add_record(BACnetLogRecord {
952            date,
953            time,
954            log_datum: LogDatum::NullValue,
955            status_flags: None,
956        });
957
958        let val = tl
959            .read_property(PropertyIdentifier::LOG_BUFFER, None)
960            .unwrap();
961        if let PropertyValue::List(records) = val {
962            assert_eq!(records.len(), 3);
963            if let PropertyValue::List(fields) = &records[0] {
964                assert_eq!(fields[2], PropertyValue::Boolean(true));
965            } else {
966                panic!("Expected List");
967            }
968            if let PropertyValue::List(fields) = &records[1] {
969                assert_eq!(fields[2], PropertyValue::Enumerated(42));
970            } else {
971                panic!("Expected List");
972            }
973            if let PropertyValue::List(fields) = &records[2] {
974                assert_eq!(fields[2], PropertyValue::Null);
975            } else {
976                panic!("Expected List");
977            }
978        } else {
979            panic!("Expected List for LOG_BUFFER");
980        }
981    }
982
983    // -----------------------------------------------------------------------
984    // TrendLogMultiple tests
985    // -----------------------------------------------------------------------
986
987    #[test]
988    fn trendlog_multiple_create() {
989        let tlm = TrendLogMultipleObject::new(1, "TLM-1", 200).unwrap();
990        assert_eq!(
991            tlm.read_property(PropertyIdentifier::OBJECT_NAME, None)
992                .unwrap(),
993            PropertyValue::CharacterString("TLM-1".into())
994        );
995        assert_eq!(
996            tlm.read_property(PropertyIdentifier::OBJECT_TYPE, None)
997                .unwrap(),
998            PropertyValue::Enumerated(ObjectType::TREND_LOG_MULTIPLE.to_raw())
999        );
1000        assert_eq!(
1001            tlm.read_property(PropertyIdentifier::BUFFER_SIZE, None)
1002                .unwrap(),
1003            PropertyValue::Unsigned(200)
1004        );
1005    }
1006
1007    #[test]
1008    fn trendlog_multiple_add_records() {
1009        let mut tlm = TrendLogMultipleObject::new(1, "TLM-1", 100).unwrap();
1010        tlm.add_record(make_record(10, 72.5));
1011        tlm.add_record(make_record(11, 73.0));
1012        assert_eq!(tlm.records().len(), 2);
1013        assert_eq!(
1014            tlm.read_property(PropertyIdentifier::RECORD_COUNT, None)
1015                .unwrap(),
1016            PropertyValue::Unsigned(2)
1017        );
1018        assert_eq!(
1019            tlm.read_property(PropertyIdentifier::TOTAL_RECORD_COUNT, None)
1020                .unwrap(),
1021            PropertyValue::Unsigned(2)
1022        );
1023    }
1024
1025    #[test]
1026    fn trendlog_multiple_ring_buffer() {
1027        let mut tlm = TrendLogMultipleObject::new(1, "TLM-1", 3).unwrap();
1028        for i in 0..5u8 {
1029            tlm.add_record(BACnetLogRecord {
1030                date: Date {
1031                    year: 124,
1032                    month: 3,
1033                    day: 15,
1034                    day_of_week: 5,
1035                },
1036                time: Time {
1037                    hour: i,
1038                    minute: 0,
1039                    second: 0,
1040                    hundredths: 0,
1041                },
1042                log_datum: LogDatum::UnsignedValue(i as u64),
1043                status_flags: None,
1044            });
1045        }
1046        assert_eq!(tlm.records().len(), 3);
1047        assert_eq!(tlm.records()[0].time.hour, 2);
1048        assert_eq!(
1049            tlm.read_property(PropertyIdentifier::TOTAL_RECORD_COUNT, None)
1050                .unwrap(),
1051            PropertyValue::Unsigned(5)
1052        );
1053    }
1054
1055    #[test]
1056    fn trendlog_multiple_read_log_buffer() {
1057        let mut tlm = TrendLogMultipleObject::new(1, "TLM-1", 100).unwrap();
1058        tlm.add_record(make_record(10, 72.5));
1059        let val = tlm
1060            .read_property(PropertyIdentifier::LOG_BUFFER, None)
1061            .unwrap();
1062        if let PropertyValue::List(records) = val {
1063            assert_eq!(records.len(), 1);
1064            if let PropertyValue::List(fields) = &records[0] {
1065                assert_eq!(fields[2], PropertyValue::Real(72.5));
1066            } else {
1067                panic!("Expected List for log record");
1068            }
1069        } else {
1070            panic!("Expected List for LOG_BUFFER");
1071        }
1072    }
1073
1074    #[test]
1075    fn trendlog_multiple_property_list() {
1076        let tlm = TrendLogMultipleObject::new(1, "TLM-1", 100).unwrap();
1077        let props = tlm.property_list();
1078        assert!(props.contains(&PropertyIdentifier::LOG_BUFFER));
1079        assert!(props.contains(&PropertyIdentifier::LOGGING_TYPE));
1080        assert!(props.contains(&PropertyIdentifier::LOG_DEVICE_OBJECT_PROPERTY));
1081        assert!(props.contains(&PropertyIdentifier::OUT_OF_SERVICE));
1082        assert!(props.contains(&PropertyIdentifier::RELIABILITY));
1083    }
1084
1085    #[test]
1086    fn trendlog_multiple_add_property_references() {
1087        let mut tlm = TrendLogMultipleObject::new(1, "TLM-1", 100).unwrap();
1088
1089        let oid1 = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
1090        let oid2 = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 2).unwrap();
1091        let pv_raw = PropertyIdentifier::PRESENT_VALUE.to_raw();
1092
1093        tlm.add_property_reference(BACnetDeviceObjectPropertyReference {
1094            object_identifier: oid1,
1095            property_identifier: pv_raw,
1096            property_array_index: None,
1097            device_identifier: None,
1098        });
1099        tlm.add_property_reference(BACnetDeviceObjectPropertyReference {
1100            object_identifier: oid2,
1101            property_identifier: pv_raw,
1102            property_array_index: Some(3),
1103            device_identifier: None,
1104        });
1105
1106        let val = tlm
1107            .read_property(PropertyIdentifier::LOG_DEVICE_OBJECT_PROPERTY, None)
1108            .unwrap();
1109        if let PropertyValue::List(refs) = val {
1110            assert_eq!(refs.len(), 2);
1111            // First reference
1112            if let PropertyValue::List(fields) = &refs[0] {
1113                assert_eq!(fields[0], PropertyValue::ObjectIdentifier(oid1));
1114                assert_eq!(fields[1], PropertyValue::Unsigned(pv_raw as u64));
1115                assert_eq!(fields[2], PropertyValue::Null);
1116                assert_eq!(fields[3], PropertyValue::Null);
1117            } else {
1118                panic!("Expected List for property reference");
1119            }
1120            // Second reference with array index
1121            if let PropertyValue::List(fields) = &refs[1] {
1122                assert_eq!(fields[0], PropertyValue::ObjectIdentifier(oid2));
1123                assert_eq!(fields[1], PropertyValue::Unsigned(pv_raw as u64));
1124                assert_eq!(fields[2], PropertyValue::Unsigned(3));
1125                assert_eq!(fields[3], PropertyValue::Null);
1126            } else {
1127                panic!("Expected List for property reference");
1128            }
1129        } else {
1130            panic!("Expected List for LOG_DEVICE_OBJECT_PROPERTY");
1131        }
1132    }
1133
1134    #[test]
1135    fn trendlog_multiple_empty_property_references() {
1136        let tlm = TrendLogMultipleObject::new(1, "TLM-1", 100).unwrap();
1137        let val = tlm
1138            .read_property(PropertyIdentifier::LOG_DEVICE_OBJECT_PROPERTY, None)
1139            .unwrap();
1140        assert_eq!(val, PropertyValue::List(vec![]));
1141    }
1142
1143    #[test]
1144    fn trendlog_multiple_write_log_enable() {
1145        let mut tlm = TrendLogMultipleObject::new(1, "TLM-1", 100).unwrap();
1146        tlm.write_property(
1147            PropertyIdentifier::LOG_ENABLE,
1148            None,
1149            PropertyValue::Boolean(false),
1150            None,
1151        )
1152        .unwrap();
1153        assert_eq!(
1154            tlm.read_property(PropertyIdentifier::LOG_ENABLE, None)
1155                .unwrap(),
1156            PropertyValue::Boolean(false)
1157        );
1158        // Records should not be added when disabled
1159        tlm.add_record(make_record(10, 72.5));
1160        assert_eq!(tlm.records().len(), 0);
1161    }
1162}