Skip to main content

bacnet_objects/
timer.rs

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