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