Skip to main content

bacnet_objects/
timer.rs

1//! Timer object (type 31) per ASHRAE 135-2020 Clause 12.
2//!
3//! The Timer object represents a countdown or count-up timer. Its present value
4//! is an Enumerated representing the timer state: 0=idle, 1=running, 2=expired.
5
6use bacnet_types::enums::{ObjectType, PropertyIdentifier};
7use bacnet_types::error::Error;
8use bacnet_types::primitives::{Date, ObjectIdentifier, PropertyValue, StatusFlags, Time};
9use std::borrow::Cow;
10
11use crate::common::{self, read_common_properties};
12use crate::traits::BACnetObject;
13
14/// Timer state enumeration values.
15const TIMER_STATE_IDLE: u32 = 0;
16const TIMER_STATE_RUNNING: u32 = 1;
17const TIMER_STATE_EXPIRED: u32 = 2;
18
19/// BACnet Timer object — countdown/count-up timer.
20pub struct TimerObject {
21    oid: ObjectIdentifier,
22    name: String,
23    description: String,
24    present_value: u32,
25    timer_running: bool,
26    initial_timeout: u64,
27    update_time: (Date, Time),
28    expiration_time: (Date, Time),
29    status_flags: StatusFlags,
30    out_of_service: bool,
31    reliability: u32,
32}
33
34impl TimerObject {
35    /// Create a new Timer object with default values (idle state).
36    pub fn new(instance: u32, name: impl Into<String>) -> Result<Self, Error> {
37        let oid = ObjectIdentifier::new(ObjectType::TIMER, instance)?;
38        Ok(Self {
39            oid,
40            name: name.into(),
41            description: String::new(),
42            present_value: TIMER_STATE_IDLE,
43            timer_running: false,
44            initial_timeout: 0,
45            update_time: (
46                Date {
47                    year: 0xFF,
48                    month: 0xFF,
49                    day: 0xFF,
50                    day_of_week: 0xFF,
51                },
52                Time {
53                    hour: 0xFF,
54                    minute: 0xFF,
55                    second: 0xFF,
56                    hundredths: 0xFF,
57                },
58            ),
59            expiration_time: (
60                Date {
61                    year: 0xFF,
62                    month: 0xFF,
63                    day: 0xFF,
64                    day_of_week: 0xFF,
65                },
66                Time {
67                    hour: 0xFF,
68                    minute: 0xFF,
69                    second: 0xFF,
70                    hundredths: 0xFF,
71                },
72            ),
73            status_flags: StatusFlags::empty(),
74            out_of_service: false,
75            reliability: 0,
76        })
77    }
78
79    /// Start the timer — transitions to running state.
80    pub fn start(&mut self) {
81        self.present_value = TIMER_STATE_RUNNING;
82        self.timer_running = true;
83    }
84
85    /// Stop the timer — transitions to idle state.
86    pub fn stop(&mut self) {
87        self.present_value = TIMER_STATE_IDLE;
88        self.timer_running = false;
89    }
90
91    /// Set the initial timeout in milliseconds.
92    pub fn set_initial_timeout(&mut self, timeout_ms: u64) {
93        self.initial_timeout = timeout_ms;
94    }
95
96    /// Set the update time as a (Date, Time) tuple.
97    pub fn set_update_time(&mut self, date: Date, time: Time) {
98        self.update_time = (date, time);
99    }
100
101    /// Set the expiration time as a (Date, Time) tuple.
102    pub fn set_expiration_time(&mut self, date: Date, time: Time) {
103        self.expiration_time = (date, time);
104    }
105}
106
107impl BACnetObject for TimerObject {
108    fn object_identifier(&self) -> ObjectIdentifier {
109        self.oid
110    }
111
112    fn object_name(&self) -> &str {
113        &self.name
114    }
115
116    fn read_property(
117        &self,
118        property: PropertyIdentifier,
119        array_index: Option<u32>,
120    ) -> Result<PropertyValue, Error> {
121        if let Some(result) = read_common_properties!(self, property, array_index) {
122            return result;
123        }
124        match property {
125            p if p == PropertyIdentifier::OBJECT_TYPE => {
126                Ok(PropertyValue::Enumerated(ObjectType::TIMER.to_raw()))
127            }
128            p if p == PropertyIdentifier::PRESENT_VALUE => {
129                Ok(PropertyValue::Enumerated(self.present_value))
130            }
131            p if p == PropertyIdentifier::TIMER_STATE => {
132                Ok(PropertyValue::Enumerated(self.present_value))
133            }
134            p if p == PropertyIdentifier::TIMER_RUNNING => {
135                Ok(PropertyValue::Boolean(self.timer_running))
136            }
137            p if p == PropertyIdentifier::INITIAL_TIMEOUT => {
138                Ok(PropertyValue::Unsigned(self.initial_timeout))
139            }
140            p if p == PropertyIdentifier::UPDATE_TIME => Ok(PropertyValue::List(vec![
141                PropertyValue::Date(self.update_time.0),
142                PropertyValue::Time(self.update_time.1),
143            ])),
144            p if p == PropertyIdentifier::EXPIRATION_TIME => Ok(PropertyValue::List(vec![
145                PropertyValue::Date(self.expiration_time.0),
146                PropertyValue::Time(self.expiration_time.1),
147            ])),
148            _ => Err(common::unknown_property_error()),
149        }
150    }
151
152    fn write_property(
153        &mut self,
154        property: PropertyIdentifier,
155        _array_index: Option<u32>,
156        value: PropertyValue,
157        _priority: Option<u8>,
158    ) -> Result<(), Error> {
159        if let Some(result) =
160            common::write_out_of_service(&mut self.out_of_service, property, &value)
161        {
162            return result;
163        }
164        if let Some(result) = common::write_description(&mut self.description, property, &value) {
165            return result;
166        }
167        match property {
168            p if p == PropertyIdentifier::PRESENT_VALUE => {
169                if let PropertyValue::Enumerated(v) = value {
170                    if v > TIMER_STATE_EXPIRED {
171                        return Err(common::value_out_of_range_error());
172                    }
173                    self.present_value = v;
174                    self.timer_running = v == TIMER_STATE_RUNNING;
175                    Ok(())
176                } else {
177                    Err(common::invalid_data_type_error())
178                }
179            }
180            p if p == PropertyIdentifier::INITIAL_TIMEOUT => {
181                if let PropertyValue::Unsigned(v) = value {
182                    self.initial_timeout = v;
183                    Ok(())
184                } else {
185                    Err(common::invalid_data_type_error())
186                }
187            }
188            _ => Err(common::write_access_denied_error()),
189        }
190    }
191
192    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
193        static PROPS: &[PropertyIdentifier] = &[
194            PropertyIdentifier::OBJECT_IDENTIFIER,
195            PropertyIdentifier::OBJECT_NAME,
196            PropertyIdentifier::DESCRIPTION,
197            PropertyIdentifier::OBJECT_TYPE,
198            PropertyIdentifier::PRESENT_VALUE,
199            PropertyIdentifier::TIMER_STATE,
200            PropertyIdentifier::TIMER_RUNNING,
201            PropertyIdentifier::INITIAL_TIMEOUT,
202            PropertyIdentifier::UPDATE_TIME,
203            PropertyIdentifier::EXPIRATION_TIME,
204            PropertyIdentifier::STATUS_FLAGS,
205            PropertyIdentifier::OUT_OF_SERVICE,
206            PropertyIdentifier::RELIABILITY,
207        ];
208        Cow::Borrowed(PROPS)
209    }
210}
211
212// ---------------------------------------------------------------------------
213// Tests
214// ---------------------------------------------------------------------------
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219
220    #[test]
221    fn timer_create_and_read_defaults() {
222        let timer = TimerObject::new(1, "TMR-1").unwrap();
223        assert_eq!(timer.object_name(), "TMR-1");
224        assert_eq!(
225            timer
226                .read_property(PropertyIdentifier::PRESENT_VALUE, None)
227                .unwrap(),
228            PropertyValue::Enumerated(TIMER_STATE_IDLE)
229        );
230        assert_eq!(
231            timer
232                .read_property(PropertyIdentifier::TIMER_RUNNING, None)
233                .unwrap(),
234            PropertyValue::Boolean(false)
235        );
236    }
237
238    #[test]
239    fn timer_object_type() {
240        let timer = TimerObject::new(1, "TMR-1").unwrap();
241        assert_eq!(
242            timer
243                .read_property(PropertyIdentifier::OBJECT_TYPE, None)
244                .unwrap(),
245            PropertyValue::Enumerated(ObjectType::TIMER.to_raw())
246        );
247    }
248
249    #[test]
250    fn timer_start_stop() {
251        let mut timer = TimerObject::new(1, "TMR-1").unwrap();
252        timer.start();
253        assert_eq!(
254            timer
255                .read_property(PropertyIdentifier::PRESENT_VALUE, None)
256                .unwrap(),
257            PropertyValue::Enumerated(TIMER_STATE_RUNNING)
258        );
259        assert_eq!(
260            timer
261                .read_property(PropertyIdentifier::TIMER_RUNNING, None)
262                .unwrap(),
263            PropertyValue::Boolean(true)
264        );
265
266        timer.stop();
267        assert_eq!(
268            timer
269                .read_property(PropertyIdentifier::PRESENT_VALUE, None)
270                .unwrap(),
271            PropertyValue::Enumerated(TIMER_STATE_IDLE)
272        );
273        assert_eq!(
274            timer
275                .read_property(PropertyIdentifier::TIMER_RUNNING, None)
276                .unwrap(),
277            PropertyValue::Boolean(false)
278        );
279    }
280
281    #[test]
282    fn timer_write_present_value() {
283        let mut timer = TimerObject::new(1, "TMR-1").unwrap();
284        timer
285            .write_property(
286                PropertyIdentifier::PRESENT_VALUE,
287                None,
288                PropertyValue::Enumerated(TIMER_STATE_RUNNING),
289                None,
290            )
291            .unwrap();
292        assert_eq!(
293            timer
294                .read_property(PropertyIdentifier::PRESENT_VALUE, None)
295                .unwrap(),
296            PropertyValue::Enumerated(TIMER_STATE_RUNNING)
297        );
298        assert_eq!(
299            timer
300                .read_property(PropertyIdentifier::TIMER_RUNNING, None)
301                .unwrap(),
302            PropertyValue::Boolean(true)
303        );
304    }
305
306    #[test]
307    fn timer_write_present_value_out_of_range() {
308        let mut timer = TimerObject::new(1, "TMR-1").unwrap();
309        let result = timer.write_property(
310            PropertyIdentifier::PRESENT_VALUE,
311            None,
312            PropertyValue::Enumerated(99),
313            None,
314        );
315        assert!(result.is_err());
316    }
317
318    #[test]
319    fn timer_write_present_value_wrong_type() {
320        let mut timer = TimerObject::new(1, "TMR-1").unwrap();
321        let result = timer.write_property(
322            PropertyIdentifier::PRESENT_VALUE,
323            None,
324            PropertyValue::Unsigned(1),
325            None,
326        );
327        assert!(result.is_err());
328    }
329
330    #[test]
331    fn timer_read_initial_timeout() {
332        let mut timer = TimerObject::new(1, "TMR-1").unwrap();
333        timer.set_initial_timeout(5000);
334        assert_eq!(
335            timer
336                .read_property(PropertyIdentifier::INITIAL_TIMEOUT, None)
337                .unwrap(),
338            PropertyValue::Unsigned(5000)
339        );
340    }
341
342    #[test]
343    fn timer_write_initial_timeout() {
344        let mut timer = TimerObject::new(1, "TMR-1").unwrap();
345        timer
346            .write_property(
347                PropertyIdentifier::INITIAL_TIMEOUT,
348                None,
349                PropertyValue::Unsigned(10000),
350                None,
351            )
352            .unwrap();
353        assert_eq!(
354            timer
355                .read_property(PropertyIdentifier::INITIAL_TIMEOUT, None)
356                .unwrap(),
357            PropertyValue::Unsigned(10000)
358        );
359    }
360
361    #[test]
362    fn timer_read_update_time() {
363        let timer = TimerObject::new(1, "TMR-1").unwrap();
364        let val = timer
365            .read_property(PropertyIdentifier::UPDATE_TIME, None)
366            .unwrap();
367        let unspec_date = Date {
368            year: 0xFF,
369            month: 0xFF,
370            day: 0xFF,
371            day_of_week: 0xFF,
372        };
373        let unspec_time = Time {
374            hour: 0xFF,
375            minute: 0xFF,
376            second: 0xFF,
377            hundredths: 0xFF,
378        };
379        assert_eq!(
380            val,
381            PropertyValue::List(vec![
382                PropertyValue::Date(unspec_date),
383                PropertyValue::Time(unspec_time),
384            ])
385        );
386    }
387
388    #[test]
389    fn timer_read_timer_state_matches_pv() {
390        let mut timer = TimerObject::new(1, "TMR-1").unwrap();
391        timer.start();
392        let pv = timer
393            .read_property(PropertyIdentifier::PRESENT_VALUE, None)
394            .unwrap();
395        let ts = timer
396            .read_property(PropertyIdentifier::TIMER_STATE, None)
397            .unwrap();
398        assert_eq!(pv, ts);
399    }
400
401    #[test]
402    fn timer_property_list() {
403        let timer = TimerObject::new(1, "TMR-1").unwrap();
404        let list = timer.property_list();
405        assert!(list.contains(&PropertyIdentifier::PRESENT_VALUE));
406        assert!(list.contains(&PropertyIdentifier::TIMER_STATE));
407        assert!(list.contains(&PropertyIdentifier::TIMER_RUNNING));
408        assert!(list.contains(&PropertyIdentifier::INITIAL_TIMEOUT));
409        assert!(list.contains(&PropertyIdentifier::UPDATE_TIME));
410        assert!(list.contains(&PropertyIdentifier::EXPIRATION_TIME));
411    }
412}