Skip to main content

bacnet_objects/
life_safety.rs

1//! Life Safety Point (type 21) and Life Safety Zone (type 22) objects
2//! per ASHRAE 135-2020 Clauses 12.15 and 12.16.
3
4use bacnet_types::enums::{ObjectType, PropertyIdentifier};
5use bacnet_types::error::Error;
6use bacnet_types::primitives::{ObjectIdentifier, PropertyValue, StatusFlags};
7use std::borrow::Cow;
8
9use crate::common::{self, read_common_properties};
10use crate::traits::BACnetObject;
11
12// ---------------------------------------------------------------------------
13// LifeSafetyPointObject (type 21)
14// ---------------------------------------------------------------------------
15
16/// BACnet Life Safety Point object.
17///
18/// Represents a single life-safety sensor or detector (e.g. smoke detector,
19/// pull station). Present_Value is an enumerated LifeSafetyState, set by the
20/// application via [`set_present_value`](Self::set_present_value).
21pub struct LifeSafetyPointObject {
22    oid: ObjectIdentifier,
23    name: String,
24    description: String,
25    /// Present value — LifeSafetyState enumeration (read-only via protocol).
26    present_value: u32,
27    /// Operating mode — LifeSafetyMode enumeration.
28    mode: u32,
29    /// Silenced state — SilencedState enumeration.
30    silenced: u32,
31    /// Expected operation — LifeSafetyOperation enumeration.
32    operation_expected: u32,
33    /// Tracking value — LifeSafetyState enumeration.
34    tracking_value: u32,
35    /// Zones this point belongs to.
36    member_of: Vec<ObjectIdentifier>,
37    /// Raw sensor reading.
38    direct_reading: f32,
39    /// Whether maintenance is required.
40    maintenance_required: bool,
41    /// Event state (0 = NORMAL).
42    event_state: u32,
43    status_flags: StatusFlags,
44    out_of_service: bool,
45    /// Reliability (0 = NO_FAULT_DETECTED).
46    reliability: u32,
47}
48
49impl LifeSafetyPointObject {
50    /// Create a new Life Safety Point object.
51    ///
52    /// Defaults: present_value = QUIET (0), mode = OFF (0), silenced = UNSILENCED (0),
53    /// operation_expected = NONE (0), tracking_value = QUIET (0).
54    pub fn new(instance: u32, name: impl Into<String>) -> Result<Self, Error> {
55        let oid = ObjectIdentifier::new(ObjectType::LIFE_SAFETY_POINT, instance)?;
56        Ok(Self {
57            oid,
58            name: name.into(),
59            description: String::new(),
60            present_value: 0,      // QUIET
61            mode: 0,               // OFF
62            silenced: 0,           // UNSILENCED
63            operation_expected: 0, // NONE
64            tracking_value: 0,     // QUIET
65            member_of: Vec::new(),
66            direct_reading: 0.0,
67            maintenance_required: false,
68            event_state: 0, // NORMAL
69            status_flags: StatusFlags::empty(),
70            out_of_service: false,
71            reliability: 0,
72        })
73    }
74
75    /// Set the present value (LifeSafetyState enumeration).
76    pub fn set_present_value(&mut self, state: u32) {
77        self.present_value = state;
78    }
79
80    /// Set the operating mode (LifeSafetyMode enumeration).
81    pub fn set_mode(&mut self, mode: u32) {
82        self.mode = mode;
83    }
84
85    /// Set the tracking value (LifeSafetyState enumeration).
86    pub fn set_tracking_value(&mut self, state: u32) {
87        self.tracking_value = state;
88    }
89
90    /// Set the direct reading (raw sensor value).
91    pub fn set_direct_reading(&mut self, value: f32) {
92        self.direct_reading = value;
93    }
94
95    /// Set the description.
96    pub fn set_description(&mut self, desc: impl Into<String>) {
97        self.description = desc.into();
98    }
99
100    /// Add a zone membership (ObjectIdentifier of a LifeSafetyZone).
101    pub fn add_member(&mut self, zone_oid: ObjectIdentifier) {
102        self.member_of.push(zone_oid);
103    }
104}
105
106impl BACnetObject for LifeSafetyPointObject {
107    fn object_identifier(&self) -> ObjectIdentifier {
108        self.oid
109    }
110
111    fn object_name(&self) -> &str {
112        &self.name
113    }
114
115    fn read_property(
116        &self,
117        property: PropertyIdentifier,
118        array_index: Option<u32>,
119    ) -> Result<PropertyValue, Error> {
120        if let Some(result) = read_common_properties!(self, property, array_index) {
121            return result;
122        }
123        match property {
124            p if p == PropertyIdentifier::OBJECT_TYPE => Ok(PropertyValue::Enumerated(
125                ObjectType::LIFE_SAFETY_POINT.to_raw(),
126            )),
127            p if p == PropertyIdentifier::PRESENT_VALUE => {
128                Ok(PropertyValue::Enumerated(self.present_value))
129            }
130            p if p == PropertyIdentifier::MODE => Ok(PropertyValue::Enumerated(self.mode)),
131            p if p == PropertyIdentifier::SILENCED => Ok(PropertyValue::Enumerated(self.silenced)),
132            p if p == PropertyIdentifier::OPERATION_EXPECTED => {
133                Ok(PropertyValue::Enumerated(self.operation_expected))
134            }
135            p if p == PropertyIdentifier::TRACKING_VALUE => {
136                Ok(PropertyValue::Enumerated(self.tracking_value))
137            }
138            p if p == PropertyIdentifier::MEMBER_OF => Ok(PropertyValue::List(
139                self.member_of
140                    .iter()
141                    .map(|oid| PropertyValue::ObjectIdentifier(*oid))
142                    .collect(),
143            )),
144            p if p == PropertyIdentifier::DIRECT_READING => {
145                Ok(PropertyValue::Real(self.direct_reading))
146            }
147            p if p == PropertyIdentifier::MAINTENANCE_REQUIRED => {
148                Ok(PropertyValue::Boolean(self.maintenance_required))
149            }
150            p if p == PropertyIdentifier::EVENT_STATE => {
151                Ok(PropertyValue::Enumerated(self.event_state))
152            }
153            _ => Err(common::unknown_property_error()),
154        }
155    }
156
157    fn write_property(
158        &mut self,
159        property: PropertyIdentifier,
160        _array_index: Option<u32>,
161        value: PropertyValue,
162        _priority: Option<u8>,
163    ) -> Result<(), Error> {
164        // Present value is read-only via protocol
165        if property == PropertyIdentifier::PRESENT_VALUE {
166            return Err(common::write_access_denied_error());
167        }
168        if property == PropertyIdentifier::MODE {
169            if let PropertyValue::Enumerated(v) = value {
170                self.mode = v;
171                return Ok(());
172            }
173            return Err(common::invalid_data_type_error());
174        }
175        if property == PropertyIdentifier::SILENCED {
176            if let PropertyValue::Enumerated(v) = value {
177                self.silenced = v;
178                return Ok(());
179            }
180            return Err(common::invalid_data_type_error());
181        }
182        if property == PropertyIdentifier::OPERATION_EXPECTED {
183            if let PropertyValue::Enumerated(v) = value {
184                self.operation_expected = v;
185                return Ok(());
186            }
187            return Err(common::invalid_data_type_error());
188        }
189        if property == PropertyIdentifier::DIRECT_READING {
190            if let PropertyValue::Real(v) = value {
191                common::reject_non_finite(v)?;
192                self.direct_reading = v;
193                return Ok(());
194            }
195            return Err(common::invalid_data_type_error());
196        }
197        if property == PropertyIdentifier::MAINTENANCE_REQUIRED {
198            if let PropertyValue::Boolean(v) = value {
199                self.maintenance_required = v;
200                return Ok(());
201            }
202            return Err(common::invalid_data_type_error());
203        }
204        if let Some(result) =
205            common::write_out_of_service(&mut self.out_of_service, property, &value)
206        {
207            return result;
208        }
209        if let Some(result) = common::write_description(&mut self.description, property, &value) {
210            return result;
211        }
212        Err(common::write_access_denied_error())
213    }
214
215    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
216        static PROPS: &[PropertyIdentifier] = &[
217            PropertyIdentifier::OBJECT_IDENTIFIER,
218            PropertyIdentifier::OBJECT_NAME,
219            PropertyIdentifier::OBJECT_TYPE,
220            PropertyIdentifier::DESCRIPTION,
221            PropertyIdentifier::PRESENT_VALUE,
222            PropertyIdentifier::MODE,
223            PropertyIdentifier::SILENCED,
224            PropertyIdentifier::OPERATION_EXPECTED,
225            PropertyIdentifier::TRACKING_VALUE,
226            PropertyIdentifier::MEMBER_OF,
227            PropertyIdentifier::DIRECT_READING,
228            PropertyIdentifier::MAINTENANCE_REQUIRED,
229            PropertyIdentifier::EVENT_STATE,
230            PropertyIdentifier::STATUS_FLAGS,
231            PropertyIdentifier::OUT_OF_SERVICE,
232            PropertyIdentifier::RELIABILITY,
233        ];
234        Cow::Borrowed(PROPS)
235    }
236
237    fn supports_cov(&self) -> bool {
238        true
239    }
240}
241
242// ---------------------------------------------------------------------------
243// LifeSafetyZoneObject (type 22)
244// ---------------------------------------------------------------------------
245
246/// BACnet Life Safety Zone object.
247///
248/// Aggregates one or more Life Safety Point objects into a zone.
249/// Present_Value is an enumerated LifeSafetyState, set by the application
250/// (typically the worst-case state among zone members).
251pub struct LifeSafetyZoneObject {
252    oid: ObjectIdentifier,
253    name: String,
254    description: String,
255    /// Present value — LifeSafetyState enumeration (read-only via protocol).
256    present_value: u32,
257    /// Operating mode — LifeSafetyMode enumeration.
258    mode: u32,
259    /// Silenced state — SilencedState enumeration.
260    silenced: u32,
261    /// Expected operation — LifeSafetyOperation enumeration.
262    operation_expected: u32,
263    /// Points belonging to this zone.
264    zone_members: Vec<ObjectIdentifier>,
265    /// Event state (0 = NORMAL).
266    event_state: u32,
267    status_flags: StatusFlags,
268    out_of_service: bool,
269    /// Reliability (0 = NO_FAULT_DETECTED).
270    reliability: u32,
271}
272
273impl LifeSafetyZoneObject {
274    /// Create a new Life Safety Zone object.
275    ///
276    /// Defaults: present_value = QUIET (0), mode = OFF (0), silenced = UNSILENCED (0),
277    /// operation_expected = NONE (0).
278    pub fn new(instance: u32, name: impl Into<String>) -> Result<Self, Error> {
279        let oid = ObjectIdentifier::new(ObjectType::LIFE_SAFETY_ZONE, instance)?;
280        Ok(Self {
281            oid,
282            name: name.into(),
283            description: String::new(),
284            present_value: 0,      // QUIET
285            mode: 0,               // OFF
286            silenced: 0,           // UNSILENCED
287            operation_expected: 0, // NONE
288            zone_members: Vec::new(),
289            event_state: 0, // NORMAL
290            status_flags: StatusFlags::empty(),
291            out_of_service: false,
292            reliability: 0,
293        })
294    }
295
296    /// Set the present value (LifeSafetyState enumeration).
297    pub fn set_present_value(&mut self, state: u32) {
298        self.present_value = state;
299    }
300
301    /// Set the operating mode (LifeSafetyMode enumeration).
302    pub fn set_mode(&mut self, mode: u32) {
303        self.mode = mode;
304    }
305
306    /// Set the description.
307    pub fn set_description(&mut self, desc: impl Into<String>) {
308        self.description = desc.into();
309    }
310
311    /// Add a point to this zone (ObjectIdentifier of a LifeSafetyPoint).
312    pub fn add_zone_member(&mut self, point_oid: ObjectIdentifier) {
313        self.zone_members.push(point_oid);
314    }
315}
316
317impl BACnetObject for LifeSafetyZoneObject {
318    fn object_identifier(&self) -> ObjectIdentifier {
319        self.oid
320    }
321
322    fn object_name(&self) -> &str {
323        &self.name
324    }
325
326    fn read_property(
327        &self,
328        property: PropertyIdentifier,
329        array_index: Option<u32>,
330    ) -> Result<PropertyValue, Error> {
331        if let Some(result) = read_common_properties!(self, property, array_index) {
332            return result;
333        }
334        match property {
335            p if p == PropertyIdentifier::OBJECT_TYPE => Ok(PropertyValue::Enumerated(
336                ObjectType::LIFE_SAFETY_ZONE.to_raw(),
337            )),
338            p if p == PropertyIdentifier::PRESENT_VALUE => {
339                Ok(PropertyValue::Enumerated(self.present_value))
340            }
341            p if p == PropertyIdentifier::MODE => Ok(PropertyValue::Enumerated(self.mode)),
342            p if p == PropertyIdentifier::SILENCED => Ok(PropertyValue::Enumerated(self.silenced)),
343            p if p == PropertyIdentifier::OPERATION_EXPECTED => {
344                Ok(PropertyValue::Enumerated(self.operation_expected))
345            }
346            p if p == PropertyIdentifier::ZONE_MEMBERS => Ok(PropertyValue::List(
347                self.zone_members
348                    .iter()
349                    .map(|oid| PropertyValue::ObjectIdentifier(*oid))
350                    .collect(),
351            )),
352            p if p == PropertyIdentifier::EVENT_STATE => {
353                Ok(PropertyValue::Enumerated(self.event_state))
354            }
355            _ => Err(common::unknown_property_error()),
356        }
357    }
358
359    fn write_property(
360        &mut self,
361        property: PropertyIdentifier,
362        _array_index: Option<u32>,
363        value: PropertyValue,
364        _priority: Option<u8>,
365    ) -> Result<(), Error> {
366        // Present value is read-only via protocol
367        if property == PropertyIdentifier::PRESENT_VALUE {
368            return Err(common::write_access_denied_error());
369        }
370        if property == PropertyIdentifier::MODE {
371            if let PropertyValue::Enumerated(v) = value {
372                self.mode = v;
373                return Ok(());
374            }
375            return Err(common::invalid_data_type_error());
376        }
377        if property == PropertyIdentifier::SILENCED {
378            if let PropertyValue::Enumerated(v) = value {
379                self.silenced = v;
380                return Ok(());
381            }
382            return Err(common::invalid_data_type_error());
383        }
384        if property == PropertyIdentifier::OPERATION_EXPECTED {
385            if let PropertyValue::Enumerated(v) = value {
386                self.operation_expected = v;
387                return Ok(());
388            }
389            return Err(common::invalid_data_type_error());
390        }
391        if let Some(result) =
392            common::write_out_of_service(&mut self.out_of_service, property, &value)
393        {
394            return result;
395        }
396        if let Some(result) = common::write_description(&mut self.description, property, &value) {
397            return result;
398        }
399        Err(common::write_access_denied_error())
400    }
401
402    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
403        static PROPS: &[PropertyIdentifier] = &[
404            PropertyIdentifier::OBJECT_IDENTIFIER,
405            PropertyIdentifier::OBJECT_NAME,
406            PropertyIdentifier::OBJECT_TYPE,
407            PropertyIdentifier::DESCRIPTION,
408            PropertyIdentifier::PRESENT_VALUE,
409            PropertyIdentifier::MODE,
410            PropertyIdentifier::SILENCED,
411            PropertyIdentifier::OPERATION_EXPECTED,
412            PropertyIdentifier::ZONE_MEMBERS,
413            PropertyIdentifier::EVENT_STATE,
414            PropertyIdentifier::STATUS_FLAGS,
415            PropertyIdentifier::OUT_OF_SERVICE,
416            PropertyIdentifier::RELIABILITY,
417        ];
418        Cow::Borrowed(PROPS)
419    }
420
421    fn supports_cov(&self) -> bool {
422        true
423    }
424}
425
426// ===========================================================================
427// Tests
428// ===========================================================================
429
430#[cfg(test)]
431mod tests {
432    use super::*;
433    use bacnet_types::enums::{LifeSafetyMode, LifeSafetyState, ObjectType};
434
435    // -----------------------------------------------------------------------
436    // LifeSafetyPointObject
437    // -----------------------------------------------------------------------
438
439    #[test]
440    fn point_object_type() {
441        let pt = LifeSafetyPointObject::new(1, "LSP-1").unwrap();
442        assert_eq!(
443            pt.object_identifier().object_type(),
444            ObjectType::LIFE_SAFETY_POINT
445        );
446        assert_eq!(pt.object_identifier().instance_number(), 1);
447        assert_eq!(pt.object_name(), "LSP-1");
448    }
449
450    #[test]
451    fn point_read_present_value_default() {
452        let pt = LifeSafetyPointObject::new(1, "LSP-1").unwrap();
453        let val = pt
454            .read_property(PropertyIdentifier::PRESENT_VALUE, None)
455            .unwrap();
456        assert_eq!(
457            val,
458            PropertyValue::Enumerated(LifeSafetyState::QUIET.to_raw())
459        );
460    }
461
462    #[test]
463    fn point_set_and_read_present_value() {
464        let mut pt = LifeSafetyPointObject::new(1, "LSP-1").unwrap();
465        pt.set_present_value(LifeSafetyState::ALARM.to_raw());
466        let val = pt
467            .read_property(PropertyIdentifier::PRESENT_VALUE, None)
468            .unwrap();
469        assert_eq!(
470            val,
471            PropertyValue::Enumerated(LifeSafetyState::ALARM.to_raw())
472        );
473    }
474
475    #[test]
476    fn point_present_value_write_denied() {
477        let mut pt = LifeSafetyPointObject::new(1, "LSP-1").unwrap();
478        let result = pt.write_property(
479            PropertyIdentifier::PRESENT_VALUE,
480            None,
481            PropertyValue::Enumerated(2),
482            None,
483        );
484        assert!(result.is_err());
485    }
486
487    #[test]
488    fn point_read_mode_default() {
489        let pt = LifeSafetyPointObject::new(1, "LSP-1").unwrap();
490        let val = pt.read_property(PropertyIdentifier::MODE, None).unwrap();
491        assert_eq!(val, PropertyValue::Enumerated(LifeSafetyMode::OFF.to_raw()));
492    }
493
494    #[test]
495    fn point_set_mode() {
496        let mut pt = LifeSafetyPointObject::new(1, "LSP-1").unwrap();
497        pt.set_mode(LifeSafetyMode::ON.to_raw());
498        let val = pt.read_property(PropertyIdentifier::MODE, None).unwrap();
499        assert_eq!(val, PropertyValue::Enumerated(LifeSafetyMode::ON.to_raw()));
500    }
501
502    #[test]
503    fn point_write_mode() {
504        let mut pt = LifeSafetyPointObject::new(1, "LSP-1").unwrap();
505        pt.write_property(
506            PropertyIdentifier::MODE,
507            None,
508            PropertyValue::Enumerated(LifeSafetyMode::ARMED.to_raw()),
509            None,
510        )
511        .unwrap();
512        let val = pt.read_property(PropertyIdentifier::MODE, None).unwrap();
513        assert_eq!(
514            val,
515            PropertyValue::Enumerated(LifeSafetyMode::ARMED.to_raw())
516        );
517    }
518
519    #[test]
520    fn point_read_silenced_default() {
521        let pt = LifeSafetyPointObject::new(1, "LSP-1").unwrap();
522        let val = pt
523            .read_property(PropertyIdentifier::SILENCED, None)
524            .unwrap();
525        assert_eq!(val, PropertyValue::Enumerated(0)); // UNSILENCED
526    }
527
528    #[test]
529    fn point_read_tracking_value() {
530        let mut pt = LifeSafetyPointObject::new(1, "LSP-1").unwrap();
531        pt.set_tracking_value(LifeSafetyState::PRE_ALARM.to_raw());
532        let val = pt
533            .read_property(PropertyIdentifier::TRACKING_VALUE, None)
534            .unwrap();
535        assert_eq!(
536            val,
537            PropertyValue::Enumerated(LifeSafetyState::PRE_ALARM.to_raw())
538        );
539    }
540
541    #[test]
542    fn point_read_direct_reading() {
543        let mut pt = LifeSafetyPointObject::new(1, "LSP-1").unwrap();
544        pt.set_direct_reading(42.5);
545        let val = pt
546            .read_property(PropertyIdentifier::DIRECT_READING, None)
547            .unwrap();
548        assert_eq!(val, PropertyValue::Real(42.5));
549    }
550
551    #[test]
552    fn point_read_maintenance_required() {
553        let pt = LifeSafetyPointObject::new(1, "LSP-1").unwrap();
554        let val = pt
555            .read_property(PropertyIdentifier::MAINTENANCE_REQUIRED, None)
556            .unwrap();
557        assert_eq!(val, PropertyValue::Boolean(false));
558    }
559
560    #[test]
561    fn point_add_member_and_read() {
562        let mut pt = LifeSafetyPointObject::new(1, "LSP-1").unwrap();
563        let zone1 = ObjectIdentifier::new(ObjectType::LIFE_SAFETY_ZONE, 1).unwrap();
564        let zone2 = ObjectIdentifier::new(ObjectType::LIFE_SAFETY_ZONE, 2).unwrap();
565        pt.add_member(zone1);
566        pt.add_member(zone2);
567
568        let val = pt
569            .read_property(PropertyIdentifier::MEMBER_OF, None)
570            .unwrap();
571        assert_eq!(
572            val,
573            PropertyValue::List(vec![
574                PropertyValue::ObjectIdentifier(zone1),
575                PropertyValue::ObjectIdentifier(zone2),
576            ])
577        );
578    }
579
580    #[test]
581    fn point_member_of_empty() {
582        let pt = LifeSafetyPointObject::new(1, "LSP-1").unwrap();
583        let val = pt
584            .read_property(PropertyIdentifier::MEMBER_OF, None)
585            .unwrap();
586        assert_eq!(val, PropertyValue::List(vec![]));
587    }
588
589    #[test]
590    fn point_read_event_state_default() {
591        let pt = LifeSafetyPointObject::new(1, "LSP-1").unwrap();
592        let val = pt
593            .read_property(PropertyIdentifier::EVENT_STATE, None)
594            .unwrap();
595        assert_eq!(val, PropertyValue::Enumerated(0)); // NORMAL
596    }
597
598    #[test]
599    fn point_read_object_type() {
600        let pt = LifeSafetyPointObject::new(1, "LSP-1").unwrap();
601        let val = pt
602            .read_property(PropertyIdentifier::OBJECT_TYPE, None)
603            .unwrap();
604        assert_eq!(
605            val,
606            PropertyValue::Enumerated(ObjectType::LIFE_SAFETY_POINT.to_raw())
607        );
608    }
609
610    #[test]
611    fn point_property_list() {
612        let pt = LifeSafetyPointObject::new(1, "LSP-1").unwrap();
613        let props = pt.property_list();
614        assert!(props.contains(&PropertyIdentifier::PRESENT_VALUE));
615        assert!(props.contains(&PropertyIdentifier::MODE));
616        assert!(props.contains(&PropertyIdentifier::SILENCED));
617        assert!(props.contains(&PropertyIdentifier::OPERATION_EXPECTED));
618        assert!(props.contains(&PropertyIdentifier::TRACKING_VALUE));
619        assert!(props.contains(&PropertyIdentifier::MEMBER_OF));
620        assert!(props.contains(&PropertyIdentifier::DIRECT_READING));
621        assert!(props.contains(&PropertyIdentifier::MAINTENANCE_REQUIRED));
622        assert!(props.contains(&PropertyIdentifier::EVENT_STATE));
623        assert!(props.contains(&PropertyIdentifier::STATUS_FLAGS));
624        assert!(props.contains(&PropertyIdentifier::OUT_OF_SERVICE));
625        assert!(props.contains(&PropertyIdentifier::RELIABILITY));
626    }
627
628    #[test]
629    fn point_write_mode_wrong_type() {
630        let mut pt = LifeSafetyPointObject::new(1, "LSP-1").unwrap();
631        let result = pt.write_property(
632            PropertyIdentifier::MODE,
633            None,
634            PropertyValue::Real(1.0),
635            None,
636        );
637        assert!(result.is_err());
638    }
639
640    // -----------------------------------------------------------------------
641    // LifeSafetyZoneObject
642    // -----------------------------------------------------------------------
643
644    #[test]
645    fn zone_object_type() {
646        let z = LifeSafetyZoneObject::new(1, "LSZ-1").unwrap();
647        assert_eq!(
648            z.object_identifier().object_type(),
649            ObjectType::LIFE_SAFETY_ZONE
650        );
651        assert_eq!(z.object_identifier().instance_number(), 1);
652        assert_eq!(z.object_name(), "LSZ-1");
653    }
654
655    #[test]
656    fn zone_read_present_value_default() {
657        let z = LifeSafetyZoneObject::new(1, "LSZ-1").unwrap();
658        let val = z
659            .read_property(PropertyIdentifier::PRESENT_VALUE, None)
660            .unwrap();
661        assert_eq!(
662            val,
663            PropertyValue::Enumerated(LifeSafetyState::QUIET.to_raw())
664        );
665    }
666
667    #[test]
668    fn zone_set_and_read_present_value() {
669        let mut z = LifeSafetyZoneObject::new(1, "LSZ-1").unwrap();
670        z.set_present_value(LifeSafetyState::ALARM.to_raw());
671        let val = z
672            .read_property(PropertyIdentifier::PRESENT_VALUE, None)
673            .unwrap();
674        assert_eq!(
675            val,
676            PropertyValue::Enumerated(LifeSafetyState::ALARM.to_raw())
677        );
678    }
679
680    #[test]
681    fn zone_present_value_write_denied() {
682        let mut z = LifeSafetyZoneObject::new(1, "LSZ-1").unwrap();
683        let result = z.write_property(
684            PropertyIdentifier::PRESENT_VALUE,
685            None,
686            PropertyValue::Enumerated(2),
687            None,
688        );
689        assert!(result.is_err());
690    }
691
692    #[test]
693    fn zone_read_mode_default() {
694        let z = LifeSafetyZoneObject::new(1, "LSZ-1").unwrap();
695        let val = z.read_property(PropertyIdentifier::MODE, None).unwrap();
696        assert_eq!(val, PropertyValue::Enumerated(LifeSafetyMode::OFF.to_raw()));
697    }
698
699    #[test]
700    fn zone_set_mode() {
701        let mut z = LifeSafetyZoneObject::new(1, "LSZ-1").unwrap();
702        z.set_mode(LifeSafetyMode::ARMED.to_raw());
703        let val = z.read_property(PropertyIdentifier::MODE, None).unwrap();
704        assert_eq!(
705            val,
706            PropertyValue::Enumerated(LifeSafetyMode::ARMED.to_raw())
707        );
708    }
709
710    #[test]
711    fn zone_add_zone_member_and_read() {
712        let mut z = LifeSafetyZoneObject::new(1, "LSZ-1").unwrap();
713        let pt1 = ObjectIdentifier::new(ObjectType::LIFE_SAFETY_POINT, 1).unwrap();
714        let pt2 = ObjectIdentifier::new(ObjectType::LIFE_SAFETY_POINT, 2).unwrap();
715        let pt3 = ObjectIdentifier::new(ObjectType::LIFE_SAFETY_POINT, 3).unwrap();
716        z.add_zone_member(pt1);
717        z.add_zone_member(pt2);
718        z.add_zone_member(pt3);
719
720        let val = z
721            .read_property(PropertyIdentifier::ZONE_MEMBERS, None)
722            .unwrap();
723        assert_eq!(
724            val,
725            PropertyValue::List(vec![
726                PropertyValue::ObjectIdentifier(pt1),
727                PropertyValue::ObjectIdentifier(pt2),
728                PropertyValue::ObjectIdentifier(pt3),
729            ])
730        );
731    }
732
733    #[test]
734    fn zone_members_empty() {
735        let z = LifeSafetyZoneObject::new(1, "LSZ-1").unwrap();
736        let val = z
737            .read_property(PropertyIdentifier::ZONE_MEMBERS, None)
738            .unwrap();
739        assert_eq!(val, PropertyValue::List(vec![]));
740    }
741
742    #[test]
743    fn zone_read_event_state_default() {
744        let z = LifeSafetyZoneObject::new(1, "LSZ-1").unwrap();
745        let val = z
746            .read_property(PropertyIdentifier::EVENT_STATE, None)
747            .unwrap();
748        assert_eq!(val, PropertyValue::Enumerated(0)); // NORMAL
749    }
750
751    #[test]
752    fn zone_read_object_type() {
753        let z = LifeSafetyZoneObject::new(1, "LSZ-1").unwrap();
754        let val = z
755            .read_property(PropertyIdentifier::OBJECT_TYPE, None)
756            .unwrap();
757        assert_eq!(
758            val,
759            PropertyValue::Enumerated(ObjectType::LIFE_SAFETY_ZONE.to_raw())
760        );
761    }
762
763    #[test]
764    fn zone_property_list() {
765        let z = LifeSafetyZoneObject::new(1, "LSZ-1").unwrap();
766        let props = z.property_list();
767        assert!(props.contains(&PropertyIdentifier::PRESENT_VALUE));
768        assert!(props.contains(&PropertyIdentifier::MODE));
769        assert!(props.contains(&PropertyIdentifier::SILENCED));
770        assert!(props.contains(&PropertyIdentifier::OPERATION_EXPECTED));
771        assert!(props.contains(&PropertyIdentifier::ZONE_MEMBERS));
772        assert!(props.contains(&PropertyIdentifier::EVENT_STATE));
773        assert!(props.contains(&PropertyIdentifier::STATUS_FLAGS));
774        assert!(props.contains(&PropertyIdentifier::OUT_OF_SERVICE));
775        assert!(props.contains(&PropertyIdentifier::RELIABILITY));
776    }
777
778    #[test]
779    fn zone_write_mode() {
780        let mut z = LifeSafetyZoneObject::new(1, "LSZ-1").unwrap();
781        z.write_property(
782            PropertyIdentifier::MODE,
783            None,
784            PropertyValue::Enumerated(LifeSafetyMode::ON.to_raw()),
785            None,
786        )
787        .unwrap();
788        let val = z.read_property(PropertyIdentifier::MODE, None).unwrap();
789        assert_eq!(val, PropertyValue::Enumerated(LifeSafetyMode::ON.to_raw()));
790    }
791
792    #[test]
793    fn zone_write_out_of_service() {
794        let mut z = LifeSafetyZoneObject::new(1, "LSZ-1").unwrap();
795        z.write_property(
796            PropertyIdentifier::OUT_OF_SERVICE,
797            None,
798            PropertyValue::Boolean(true),
799            None,
800        )
801        .unwrap();
802        let val = z
803            .read_property(PropertyIdentifier::OUT_OF_SERVICE, None)
804            .unwrap();
805        assert_eq!(val, PropertyValue::Boolean(true));
806    }
807
808    #[test]
809    fn zone_write_unknown_property_denied() {
810        let mut z = LifeSafetyZoneObject::new(1, "LSZ-1").unwrap();
811        let result = z.write_property(
812            PropertyIdentifier::TRACKING_VALUE,
813            None,
814            PropertyValue::Enumerated(0),
815            None,
816        );
817        assert!(result.is_err());
818    }
819}