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