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
238// ---------------------------------------------------------------------------
239// LifeSafetyZoneObject (type 22)
240// ---------------------------------------------------------------------------
241
242/// BACnet Life Safety Zone object.
243///
244/// Aggregates one or more Life Safety Point objects into a zone.
245/// Present_Value is an enumerated LifeSafetyState, set by the application
246/// (typically the worst-case state among zone members).
247pub struct LifeSafetyZoneObject {
248    oid: ObjectIdentifier,
249    name: String,
250    description: String,
251    /// Present value — LifeSafetyState enumeration (read-only via protocol).
252    present_value: u32,
253    /// Operating mode — LifeSafetyMode enumeration.
254    mode: u32,
255    /// Silenced state — SilencedState enumeration.
256    silenced: u32,
257    /// Expected operation — LifeSafetyOperation enumeration.
258    operation_expected: u32,
259    /// Points belonging to this zone.
260    zone_members: Vec<ObjectIdentifier>,
261    /// Event state (0 = NORMAL).
262    event_state: u32,
263    status_flags: StatusFlags,
264    out_of_service: bool,
265    /// Reliability (0 = NO_FAULT_DETECTED).
266    reliability: u32,
267}
268
269impl LifeSafetyZoneObject {
270    /// Create a new Life Safety Zone object.
271    ///
272    /// Defaults: present_value = QUIET (0), mode = OFF (0), silenced = UNSILENCED (0),
273    /// operation_expected = NONE (0).
274    pub fn new(instance: u32, name: impl Into<String>) -> Result<Self, Error> {
275        let oid = ObjectIdentifier::new(ObjectType::LIFE_SAFETY_ZONE, instance)?;
276        Ok(Self {
277            oid,
278            name: name.into(),
279            description: String::new(),
280            present_value: 0,      // QUIET
281            mode: 0,               // OFF
282            silenced: 0,           // UNSILENCED
283            operation_expected: 0, // NONE
284            zone_members: Vec::new(),
285            event_state: 0, // NORMAL
286            status_flags: StatusFlags::empty(),
287            out_of_service: false,
288            reliability: 0,
289        })
290    }
291
292    /// Set the present value (LifeSafetyState enumeration).
293    pub fn set_present_value(&mut self, state: u32) {
294        self.present_value = state;
295    }
296
297    /// Set the operating mode (LifeSafetyMode enumeration).
298    pub fn set_mode(&mut self, mode: u32) {
299        self.mode = mode;
300    }
301
302    /// Set the description.
303    pub fn set_description(&mut self, desc: impl Into<String>) {
304        self.description = desc.into();
305    }
306
307    /// Add a point to this zone (ObjectIdentifier of a LifeSafetyPoint).
308    pub fn add_zone_member(&mut self, point_oid: ObjectIdentifier) {
309        self.zone_members.push(point_oid);
310    }
311}
312
313impl BACnetObject for LifeSafetyZoneObject {
314    fn object_identifier(&self) -> ObjectIdentifier {
315        self.oid
316    }
317
318    fn object_name(&self) -> &str {
319        &self.name
320    }
321
322    fn read_property(
323        &self,
324        property: PropertyIdentifier,
325        array_index: Option<u32>,
326    ) -> Result<PropertyValue, Error> {
327        if let Some(result) = read_common_properties!(self, property, array_index) {
328            return result;
329        }
330        match property {
331            p if p == PropertyIdentifier::OBJECT_TYPE => Ok(PropertyValue::Enumerated(
332                ObjectType::LIFE_SAFETY_ZONE.to_raw(),
333            )),
334            p if p == PropertyIdentifier::PRESENT_VALUE => {
335                Ok(PropertyValue::Enumerated(self.present_value))
336            }
337            p if p == PropertyIdentifier::MODE => Ok(PropertyValue::Enumerated(self.mode)),
338            p if p == PropertyIdentifier::SILENCED => Ok(PropertyValue::Enumerated(self.silenced)),
339            p if p == PropertyIdentifier::OPERATION_EXPECTED => {
340                Ok(PropertyValue::Enumerated(self.operation_expected))
341            }
342            p if p == PropertyIdentifier::ZONE_MEMBERS => Ok(PropertyValue::List(
343                self.zone_members
344                    .iter()
345                    .map(|oid| PropertyValue::ObjectIdentifier(*oid))
346                    .collect(),
347            )),
348            p if p == PropertyIdentifier::EVENT_STATE => {
349                Ok(PropertyValue::Enumerated(self.event_state))
350            }
351            _ => Err(common::unknown_property_error()),
352        }
353    }
354
355    fn write_property(
356        &mut self,
357        property: PropertyIdentifier,
358        _array_index: Option<u32>,
359        value: PropertyValue,
360        _priority: Option<u8>,
361    ) -> Result<(), Error> {
362        // Present value is read-only via protocol
363        if property == PropertyIdentifier::PRESENT_VALUE {
364            return Err(common::write_access_denied_error());
365        }
366        if property == PropertyIdentifier::MODE {
367            if let PropertyValue::Enumerated(v) = value {
368                self.mode = v;
369                return Ok(());
370            }
371            return Err(common::invalid_data_type_error());
372        }
373        if property == PropertyIdentifier::SILENCED {
374            if let PropertyValue::Enumerated(v) = value {
375                self.silenced = v;
376                return Ok(());
377            }
378            return Err(common::invalid_data_type_error());
379        }
380        if property == PropertyIdentifier::OPERATION_EXPECTED {
381            if let PropertyValue::Enumerated(v) = value {
382                self.operation_expected = v;
383                return Ok(());
384            }
385            return Err(common::invalid_data_type_error());
386        }
387        if let Some(result) =
388            common::write_out_of_service(&mut self.out_of_service, property, &value)
389        {
390            return result;
391        }
392        if let Some(result) = common::write_description(&mut self.description, property, &value) {
393            return result;
394        }
395        Err(common::write_access_denied_error())
396    }
397
398    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
399        static PROPS: &[PropertyIdentifier] = &[
400            PropertyIdentifier::OBJECT_IDENTIFIER,
401            PropertyIdentifier::OBJECT_NAME,
402            PropertyIdentifier::OBJECT_TYPE,
403            PropertyIdentifier::DESCRIPTION,
404            PropertyIdentifier::PRESENT_VALUE,
405            PropertyIdentifier::MODE,
406            PropertyIdentifier::SILENCED,
407            PropertyIdentifier::OPERATION_EXPECTED,
408            PropertyIdentifier::ZONE_MEMBERS,
409            PropertyIdentifier::EVENT_STATE,
410            PropertyIdentifier::STATUS_FLAGS,
411            PropertyIdentifier::OUT_OF_SERVICE,
412            PropertyIdentifier::RELIABILITY,
413        ];
414        Cow::Borrowed(PROPS)
415    }
416}
417
418// ===========================================================================
419// Tests
420// ===========================================================================
421
422#[cfg(test)]
423mod tests {
424    use super::*;
425    use bacnet_types::enums::{LifeSafetyMode, LifeSafetyState, ObjectType};
426
427    // -----------------------------------------------------------------------
428    // LifeSafetyPointObject
429    // -----------------------------------------------------------------------
430
431    #[test]
432    fn point_object_type() {
433        let pt = LifeSafetyPointObject::new(1, "LSP-1").unwrap();
434        assert_eq!(
435            pt.object_identifier().object_type(),
436            ObjectType::LIFE_SAFETY_POINT
437        );
438        assert_eq!(pt.object_identifier().instance_number(), 1);
439        assert_eq!(pt.object_name(), "LSP-1");
440    }
441
442    #[test]
443    fn point_read_present_value_default() {
444        let pt = LifeSafetyPointObject::new(1, "LSP-1").unwrap();
445        let val = pt
446            .read_property(PropertyIdentifier::PRESENT_VALUE, None)
447            .unwrap();
448        assert_eq!(
449            val,
450            PropertyValue::Enumerated(LifeSafetyState::QUIET.to_raw())
451        );
452    }
453
454    #[test]
455    fn point_set_and_read_present_value() {
456        let mut pt = LifeSafetyPointObject::new(1, "LSP-1").unwrap();
457        pt.set_present_value(LifeSafetyState::ALARM.to_raw());
458        let val = pt
459            .read_property(PropertyIdentifier::PRESENT_VALUE, None)
460            .unwrap();
461        assert_eq!(
462            val,
463            PropertyValue::Enumerated(LifeSafetyState::ALARM.to_raw())
464        );
465    }
466
467    #[test]
468    fn point_present_value_write_denied() {
469        let mut pt = LifeSafetyPointObject::new(1, "LSP-1").unwrap();
470        let result = pt.write_property(
471            PropertyIdentifier::PRESENT_VALUE,
472            None,
473            PropertyValue::Enumerated(2),
474            None,
475        );
476        assert!(result.is_err());
477    }
478
479    #[test]
480    fn point_read_mode_default() {
481        let pt = LifeSafetyPointObject::new(1, "LSP-1").unwrap();
482        let val = pt.read_property(PropertyIdentifier::MODE, None).unwrap();
483        assert_eq!(val, PropertyValue::Enumerated(LifeSafetyMode::OFF.to_raw()));
484    }
485
486    #[test]
487    fn point_set_mode() {
488        let mut pt = LifeSafetyPointObject::new(1, "LSP-1").unwrap();
489        pt.set_mode(LifeSafetyMode::ON.to_raw());
490        let val = pt.read_property(PropertyIdentifier::MODE, None).unwrap();
491        assert_eq!(val, PropertyValue::Enumerated(LifeSafetyMode::ON.to_raw()));
492    }
493
494    #[test]
495    fn point_write_mode() {
496        let mut pt = LifeSafetyPointObject::new(1, "LSP-1").unwrap();
497        pt.write_property(
498            PropertyIdentifier::MODE,
499            None,
500            PropertyValue::Enumerated(LifeSafetyMode::ARMED.to_raw()),
501            None,
502        )
503        .unwrap();
504        let val = pt.read_property(PropertyIdentifier::MODE, None).unwrap();
505        assert_eq!(
506            val,
507            PropertyValue::Enumerated(LifeSafetyMode::ARMED.to_raw())
508        );
509    }
510
511    #[test]
512    fn point_read_silenced_default() {
513        let pt = LifeSafetyPointObject::new(1, "LSP-1").unwrap();
514        let val = pt
515            .read_property(PropertyIdentifier::SILENCED, None)
516            .unwrap();
517        assert_eq!(val, PropertyValue::Enumerated(0)); // UNSILENCED
518    }
519
520    #[test]
521    fn point_read_tracking_value() {
522        let mut pt = LifeSafetyPointObject::new(1, "LSP-1").unwrap();
523        pt.set_tracking_value(LifeSafetyState::PRE_ALARM.to_raw());
524        let val = pt
525            .read_property(PropertyIdentifier::TRACKING_VALUE, None)
526            .unwrap();
527        assert_eq!(
528            val,
529            PropertyValue::Enumerated(LifeSafetyState::PRE_ALARM.to_raw())
530        );
531    }
532
533    #[test]
534    fn point_read_direct_reading() {
535        let mut pt = LifeSafetyPointObject::new(1, "LSP-1").unwrap();
536        pt.set_direct_reading(42.5);
537        let val = pt
538            .read_property(PropertyIdentifier::DIRECT_READING, None)
539            .unwrap();
540        assert_eq!(val, PropertyValue::Real(42.5));
541    }
542
543    #[test]
544    fn point_read_maintenance_required() {
545        let pt = LifeSafetyPointObject::new(1, "LSP-1").unwrap();
546        let val = pt
547            .read_property(PropertyIdentifier::MAINTENANCE_REQUIRED, None)
548            .unwrap();
549        assert_eq!(val, PropertyValue::Boolean(false));
550    }
551
552    #[test]
553    fn point_add_member_and_read() {
554        let mut pt = LifeSafetyPointObject::new(1, "LSP-1").unwrap();
555        let zone1 = ObjectIdentifier::new(ObjectType::LIFE_SAFETY_ZONE, 1).unwrap();
556        let zone2 = ObjectIdentifier::new(ObjectType::LIFE_SAFETY_ZONE, 2).unwrap();
557        pt.add_member(zone1);
558        pt.add_member(zone2);
559
560        let val = pt
561            .read_property(PropertyIdentifier::MEMBER_OF, None)
562            .unwrap();
563        assert_eq!(
564            val,
565            PropertyValue::List(vec![
566                PropertyValue::ObjectIdentifier(zone1),
567                PropertyValue::ObjectIdentifier(zone2),
568            ])
569        );
570    }
571
572    #[test]
573    fn point_member_of_empty() {
574        let pt = LifeSafetyPointObject::new(1, "LSP-1").unwrap();
575        let val = pt
576            .read_property(PropertyIdentifier::MEMBER_OF, None)
577            .unwrap();
578        assert_eq!(val, PropertyValue::List(vec![]));
579    }
580
581    #[test]
582    fn point_read_event_state_default() {
583        let pt = LifeSafetyPointObject::new(1, "LSP-1").unwrap();
584        let val = pt
585            .read_property(PropertyIdentifier::EVENT_STATE, None)
586            .unwrap();
587        assert_eq!(val, PropertyValue::Enumerated(0)); // NORMAL
588    }
589
590    #[test]
591    fn point_read_object_type() {
592        let pt = LifeSafetyPointObject::new(1, "LSP-1").unwrap();
593        let val = pt
594            .read_property(PropertyIdentifier::OBJECT_TYPE, None)
595            .unwrap();
596        assert_eq!(
597            val,
598            PropertyValue::Enumerated(ObjectType::LIFE_SAFETY_POINT.to_raw())
599        );
600    }
601
602    #[test]
603    fn point_property_list() {
604        let pt = LifeSafetyPointObject::new(1, "LSP-1").unwrap();
605        let props = pt.property_list();
606        assert!(props.contains(&PropertyIdentifier::PRESENT_VALUE));
607        assert!(props.contains(&PropertyIdentifier::MODE));
608        assert!(props.contains(&PropertyIdentifier::SILENCED));
609        assert!(props.contains(&PropertyIdentifier::OPERATION_EXPECTED));
610        assert!(props.contains(&PropertyIdentifier::TRACKING_VALUE));
611        assert!(props.contains(&PropertyIdentifier::MEMBER_OF));
612        assert!(props.contains(&PropertyIdentifier::DIRECT_READING));
613        assert!(props.contains(&PropertyIdentifier::MAINTENANCE_REQUIRED));
614        assert!(props.contains(&PropertyIdentifier::EVENT_STATE));
615        assert!(props.contains(&PropertyIdentifier::STATUS_FLAGS));
616        assert!(props.contains(&PropertyIdentifier::OUT_OF_SERVICE));
617        assert!(props.contains(&PropertyIdentifier::RELIABILITY));
618    }
619
620    #[test]
621    fn point_write_mode_wrong_type() {
622        let mut pt = LifeSafetyPointObject::new(1, "LSP-1").unwrap();
623        let result = pt.write_property(
624            PropertyIdentifier::MODE,
625            None,
626            PropertyValue::Real(1.0),
627            None,
628        );
629        assert!(result.is_err());
630    }
631
632    // -----------------------------------------------------------------------
633    // LifeSafetyZoneObject
634    // -----------------------------------------------------------------------
635
636    #[test]
637    fn zone_object_type() {
638        let z = LifeSafetyZoneObject::new(1, "LSZ-1").unwrap();
639        assert_eq!(
640            z.object_identifier().object_type(),
641            ObjectType::LIFE_SAFETY_ZONE
642        );
643        assert_eq!(z.object_identifier().instance_number(), 1);
644        assert_eq!(z.object_name(), "LSZ-1");
645    }
646
647    #[test]
648    fn zone_read_present_value_default() {
649        let z = LifeSafetyZoneObject::new(1, "LSZ-1").unwrap();
650        let val = z
651            .read_property(PropertyIdentifier::PRESENT_VALUE, None)
652            .unwrap();
653        assert_eq!(
654            val,
655            PropertyValue::Enumerated(LifeSafetyState::QUIET.to_raw())
656        );
657    }
658
659    #[test]
660    fn zone_set_and_read_present_value() {
661        let mut z = LifeSafetyZoneObject::new(1, "LSZ-1").unwrap();
662        z.set_present_value(LifeSafetyState::ALARM.to_raw());
663        let val = z
664            .read_property(PropertyIdentifier::PRESENT_VALUE, None)
665            .unwrap();
666        assert_eq!(
667            val,
668            PropertyValue::Enumerated(LifeSafetyState::ALARM.to_raw())
669        );
670    }
671
672    #[test]
673    fn zone_present_value_write_denied() {
674        let mut z = LifeSafetyZoneObject::new(1, "LSZ-1").unwrap();
675        let result = z.write_property(
676            PropertyIdentifier::PRESENT_VALUE,
677            None,
678            PropertyValue::Enumerated(2),
679            None,
680        );
681        assert!(result.is_err());
682    }
683
684    #[test]
685    fn zone_read_mode_default() {
686        let z = LifeSafetyZoneObject::new(1, "LSZ-1").unwrap();
687        let val = z.read_property(PropertyIdentifier::MODE, None).unwrap();
688        assert_eq!(val, PropertyValue::Enumerated(LifeSafetyMode::OFF.to_raw()));
689    }
690
691    #[test]
692    fn zone_set_mode() {
693        let mut z = LifeSafetyZoneObject::new(1, "LSZ-1").unwrap();
694        z.set_mode(LifeSafetyMode::ARMED.to_raw());
695        let val = z.read_property(PropertyIdentifier::MODE, None).unwrap();
696        assert_eq!(
697            val,
698            PropertyValue::Enumerated(LifeSafetyMode::ARMED.to_raw())
699        );
700    }
701
702    #[test]
703    fn zone_add_zone_member_and_read() {
704        let mut z = LifeSafetyZoneObject::new(1, "LSZ-1").unwrap();
705        let pt1 = ObjectIdentifier::new(ObjectType::LIFE_SAFETY_POINT, 1).unwrap();
706        let pt2 = ObjectIdentifier::new(ObjectType::LIFE_SAFETY_POINT, 2).unwrap();
707        let pt3 = ObjectIdentifier::new(ObjectType::LIFE_SAFETY_POINT, 3).unwrap();
708        z.add_zone_member(pt1);
709        z.add_zone_member(pt2);
710        z.add_zone_member(pt3);
711
712        let val = z
713            .read_property(PropertyIdentifier::ZONE_MEMBERS, None)
714            .unwrap();
715        assert_eq!(
716            val,
717            PropertyValue::List(vec![
718                PropertyValue::ObjectIdentifier(pt1),
719                PropertyValue::ObjectIdentifier(pt2),
720                PropertyValue::ObjectIdentifier(pt3),
721            ])
722        );
723    }
724
725    #[test]
726    fn zone_members_empty() {
727        let z = LifeSafetyZoneObject::new(1, "LSZ-1").unwrap();
728        let val = z
729            .read_property(PropertyIdentifier::ZONE_MEMBERS, None)
730            .unwrap();
731        assert_eq!(val, PropertyValue::List(vec![]));
732    }
733
734    #[test]
735    fn zone_read_event_state_default() {
736        let z = LifeSafetyZoneObject::new(1, "LSZ-1").unwrap();
737        let val = z
738            .read_property(PropertyIdentifier::EVENT_STATE, None)
739            .unwrap();
740        assert_eq!(val, PropertyValue::Enumerated(0)); // NORMAL
741    }
742
743    #[test]
744    fn zone_read_object_type() {
745        let z = LifeSafetyZoneObject::new(1, "LSZ-1").unwrap();
746        let val = z
747            .read_property(PropertyIdentifier::OBJECT_TYPE, None)
748            .unwrap();
749        assert_eq!(
750            val,
751            PropertyValue::Enumerated(ObjectType::LIFE_SAFETY_ZONE.to_raw())
752        );
753    }
754
755    #[test]
756    fn zone_property_list() {
757        let z = LifeSafetyZoneObject::new(1, "LSZ-1").unwrap();
758        let props = z.property_list();
759        assert!(props.contains(&PropertyIdentifier::PRESENT_VALUE));
760        assert!(props.contains(&PropertyIdentifier::MODE));
761        assert!(props.contains(&PropertyIdentifier::SILENCED));
762        assert!(props.contains(&PropertyIdentifier::OPERATION_EXPECTED));
763        assert!(props.contains(&PropertyIdentifier::ZONE_MEMBERS));
764        assert!(props.contains(&PropertyIdentifier::EVENT_STATE));
765        assert!(props.contains(&PropertyIdentifier::STATUS_FLAGS));
766        assert!(props.contains(&PropertyIdentifier::OUT_OF_SERVICE));
767        assert!(props.contains(&PropertyIdentifier::RELIABILITY));
768    }
769
770    #[test]
771    fn zone_write_mode() {
772        let mut z = LifeSafetyZoneObject::new(1, "LSZ-1").unwrap();
773        z.write_property(
774            PropertyIdentifier::MODE,
775            None,
776            PropertyValue::Enumerated(LifeSafetyMode::ON.to_raw()),
777            None,
778        )
779        .unwrap();
780        let val = z.read_property(PropertyIdentifier::MODE, None).unwrap();
781        assert_eq!(val, PropertyValue::Enumerated(LifeSafetyMode::ON.to_raw()));
782    }
783
784    #[test]
785    fn zone_write_out_of_service() {
786        let mut z = LifeSafetyZoneObject::new(1, "LSZ-1").unwrap();
787        z.write_property(
788            PropertyIdentifier::OUT_OF_SERVICE,
789            None,
790            PropertyValue::Boolean(true),
791            None,
792        )
793        .unwrap();
794        let val = z
795            .read_property(PropertyIdentifier::OUT_OF_SERVICE, None)
796            .unwrap();
797        assert_eq!(val, PropertyValue::Boolean(true));
798    }
799
800    #[test]
801    fn zone_write_unknown_property_denied() {
802        let mut z = LifeSafetyZoneObject::new(1, "LSZ-1").unwrap();
803        let result = z.write_property(
804            PropertyIdentifier::TRACKING_VALUE,
805            None,
806            PropertyValue::Enumerated(0),
807            None,
808        );
809        assert!(result.is_err());
810    }
811}