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::{
5    ErrorClass, ErrorCode, LifeSafetyOperation, ObjectType, PropertyIdentifier, SilencedState,
6};
7use bacnet_types::error::Error;
8use bacnet_types::primitives::{ObjectIdentifier, PropertyValue, StatusFlags};
9use std::borrow::Cow;
10
11use crate::common::{self, read_common_properties};
12use crate::traits::{BACnetObject, LifeSafetyOperationEffect, LifeSafetyOperationOutcome};
13
14mod reset;
15
16pub use reset::{
17    LifeSafetyPointResetCommit, LifeSafetyPointResetContext, LifeSafetyPointResetExecutor,
18    LifeSafetyResetError, LifeSafetyZoneResetCommit, LifeSafetyZoneResetContext,
19    LifeSafetyZoneResetExecutor,
20};
21
22fn life_safety_error(code: ErrorCode) -> Error {
23    Error::Protocol {
24        class: ErrorClass::OBJECT.to_raw() as u32,
25        code: code.to_raw() as u32,
26    }
27}
28
29fn apply_silenced_operation(
30    silenced: &mut u32,
31    operation_expected: &mut u32,
32    operation: LifeSafetyOperation,
33) -> Result<LifeSafetyOperationEffect, Error> {
34    let current = *silenced;
35    if current > SilencedState::ALL_SILENCED.to_raw() {
36        return Err(life_safety_error(
37            ErrorCode::INVALID_OPERATION_IN_THIS_STATE,
38        ));
39    }
40
41    let desired = if operation == LifeSafetyOperation::SILENCE {
42        SilencedState::ALL_SILENCED.to_raw()
43    } else if operation == LifeSafetyOperation::SILENCE_AUDIBLE {
44        current | SilencedState::AUDIBLE_SILENCED.to_raw()
45    } else if operation == LifeSafetyOperation::SILENCE_VISUAL {
46        current | SilencedState::VISIBLE_SILENCED.to_raw()
47    } else if operation == LifeSafetyOperation::UNSILENCE {
48        SilencedState::UNSILENCED.to_raw()
49    } else if operation == LifeSafetyOperation::UNSILENCE_AUDIBLE {
50        current & !SilencedState::AUDIBLE_SILENCED.to_raw()
51    } else if operation == LifeSafetyOperation::UNSILENCE_VISUAL {
52        current & !SilencedState::VISIBLE_SILENCED.to_raw()
53    } else {
54        return Err(life_safety_error(ErrorCode::VALUE_OUT_OF_RANGE));
55    };
56
57    if *operation_expected != operation.to_raw() {
58        return Err(life_safety_error(
59            ErrorCode::INVALID_OPERATION_IN_THIS_STATE,
60        ));
61    }
62
63    *silenced = desired;
64    *operation_expected = LifeSafetyOperation::NONE.to_raw();
65    Ok(LifeSafetyOperationEffect::Applied)
66}
67
68const POINT_COV_PROPERTIES: [PropertyIdentifier; 5] = [
69    PropertyIdentifier::PRESENT_VALUE,
70    PropertyIdentifier::TRACKING_VALUE,
71    PropertyIdentifier::SILENCED,
72    PropertyIdentifier::OPERATION_EXPECTED,
73    PropertyIdentifier::STATUS_FLAGS,
74];
75
76const ZONE_COV_PROPERTIES: [PropertyIdentifier; 4] = [
77    PropertyIdentifier::PRESENT_VALUE,
78    PropertyIdentifier::SILENCED,
79    PropertyIdentifier::OPERATION_EXPECTED,
80    PropertyIdentifier::STATUS_FLAGS,
81];
82
83fn operation_outcome(
84    object: &dyn BACnetObject,
85    before: Vec<(PropertyIdentifier, PropertyValue)>,
86    effect: LifeSafetyOperationEffect,
87) -> LifeSafetyOperationOutcome {
88    let changed_properties = before
89        .into_iter()
90        .filter_map(|(property, previous)| {
91            object
92                .read_property(property, None)
93                .is_ok_and(|current| current != previous)
94                .then_some(property)
95        })
96        .collect();
97    LifeSafetyOperationOutcome {
98        effect,
99        changed_properties,
100    }
101}
102
103// ---------------------------------------------------------------------------
104// LifeSafetyPointObject (type 21)
105// ---------------------------------------------------------------------------
106
107/// BACnet Life Safety Point object.
108///
109/// Represents a single life-safety sensor or detector (e.g. smoke detector,
110/// pull station). Present_Value is an enumerated LifeSafetyState, set by the
111/// application via [`set_present_value`](Self::set_present_value).
112pub struct LifeSafetyPointObject {
113    oid: ObjectIdentifier,
114    name: String,
115    description: String,
116    /// Present value — LifeSafetyState enumeration (read-only via protocol).
117    present_value: u32,
118    /// Operating mode — LifeSafetyMode enumeration.
119    mode: u32,
120    /// Silenced state — SilencedState enumeration.
121    silenced: u32,
122    /// Expected operation — LifeSafetyOperation enumeration.
123    operation_expected: u32,
124    /// Tracking value — LifeSafetyState enumeration.
125    tracking_value: u32,
126    /// Zones this point belongs to.
127    member_of: Vec<ObjectIdentifier>,
128    /// Raw sensor reading.
129    direct_reading: f32,
130    /// Whether maintenance is required.
131    maintenance_required: bool,
132    /// Event state (0 = NORMAL).
133    event_state: u32,
134    status_flags: StatusFlags,
135    out_of_service: bool,
136    /// Reliability (0 = NO_FAULT_DETECTED).
137    reliability: u32,
138    /// Application-owned physical reset integration, configured before insertion.
139    reset_executor: Option<LifeSafetyPointResetExecutor>,
140}
141
142impl LifeSafetyPointObject {
143    /// Create a new Life Safety Point object.
144    ///
145    /// Defaults: present_value = QUIET (0), mode = OFF (0), silenced = UNSILENCED (0),
146    /// operation_expected = NONE (0), tracking_value = QUIET (0).
147    pub fn new(instance: u32, name: impl Into<String>) -> Result<Self, Error> {
148        let oid = ObjectIdentifier::new(ObjectType::LIFE_SAFETY_POINT, instance)?;
149        Ok(Self {
150            oid,
151            name: name.into(),
152            description: String::new(),
153            present_value: 0,      // QUIET
154            mode: 0,               // OFF
155            silenced: 0,           // UNSILENCED
156            operation_expected: 0, // NONE
157            tracking_value: 0,     // QUIET
158            member_of: Vec::new(),
159            direct_reading: 0.0,
160            maintenance_required: false,
161            event_state: 0, // NORMAL
162            status_flags: StatusFlags::empty(),
163            out_of_service: false,
164            reliability: 0,
165            reset_executor: None,
166        })
167    }
168
169    /// Set the present value (LifeSafetyState enumeration).
170    pub fn set_present_value(&mut self, state: u32) {
171        self.present_value = state;
172    }
173
174    /// Set the operating mode (LifeSafetyMode enumeration).
175    pub fn set_mode(&mut self, mode: u32) {
176        self.mode = mode;
177    }
178
179    /// Set the tracking value (LifeSafetyState enumeration).
180    pub fn set_tracking_value(&mut self, state: u32) {
181        self.tracking_value = state;
182    }
183
184    /// Set the locally determined silenced state.
185    pub fn set_silenced(&mut self, state: SilencedState) {
186        self.silenced = state.to_raw();
187    }
188
189    /// Set the next LifeSafetyOperation expected by local device logic.
190    pub fn set_operation_expected(&mut self, operation: LifeSafetyOperation) {
191        self.operation_expected = operation.to_raw();
192    }
193
194    /// Configure the application-owned reset executor before database insertion.
195    ///
196    /// The executor is used only for `RESET`, `RESET_ALARM`, and `RESET_FAULT`.
197    /// See [`LifeSafetyPointResetExecutor`] for its synchronous execution contract.
198    pub fn set_reset_executor(&mut self, executor: LifeSafetyPointResetExecutor) {
199        self.reset_executor = Some(executor);
200    }
201
202    /// Configure and return this point for insertion into an object database.
203    pub fn with_reset_executor(mut self, executor: LifeSafetyPointResetExecutor) -> Self {
204        self.set_reset_executor(executor);
205        self
206    }
207
208    /// Set the direct reading (raw sensor value).
209    pub fn set_direct_reading(&mut self, value: f32) {
210        self.direct_reading = value;
211    }
212
213    /// Set the description.
214    pub fn set_description(&mut self, desc: impl Into<String>) {
215        self.description = desc.into();
216    }
217
218    /// Add a zone membership (ObjectIdentifier of a LifeSafetyZone).
219    pub fn add_member(&mut self, zone_oid: ObjectIdentifier) {
220        self.member_of.push(zone_oid);
221    }
222}
223
224impl BACnetObject for LifeSafetyPointObject {
225    fn object_identifier(&self) -> ObjectIdentifier {
226        self.oid
227    }
228
229    fn object_name(&self) -> &str {
230        &self.name
231    }
232
233    fn read_property(
234        &self,
235        property: PropertyIdentifier,
236        array_index: Option<u32>,
237    ) -> Result<PropertyValue, Error> {
238        if let Some(result) = read_common_properties!(self, property, array_index) {
239            return result;
240        }
241        match property {
242            p if p == PropertyIdentifier::OBJECT_TYPE => Ok(PropertyValue::Enumerated(
243                ObjectType::LIFE_SAFETY_POINT.to_raw(),
244            )),
245            p if p == PropertyIdentifier::PRESENT_VALUE => {
246                Ok(PropertyValue::Enumerated(self.present_value))
247            }
248            p if p == PropertyIdentifier::MODE => Ok(PropertyValue::Enumerated(self.mode)),
249            p if p == PropertyIdentifier::SILENCED => Ok(PropertyValue::Enumerated(self.silenced)),
250            p if p == PropertyIdentifier::OPERATION_EXPECTED => {
251                Ok(PropertyValue::Enumerated(self.operation_expected))
252            }
253            p if p == PropertyIdentifier::TRACKING_VALUE => {
254                Ok(PropertyValue::Enumerated(self.tracking_value))
255            }
256            p if p == PropertyIdentifier::MEMBER_OF => Ok(PropertyValue::List(
257                self.member_of
258                    .iter()
259                    .map(|oid| PropertyValue::ObjectIdentifier(*oid))
260                    .collect(),
261            )),
262            p if p == PropertyIdentifier::DIRECT_READING => {
263                Ok(PropertyValue::Real(self.direct_reading))
264            }
265            p if p == PropertyIdentifier::MAINTENANCE_REQUIRED => {
266                Ok(PropertyValue::Boolean(self.maintenance_required))
267            }
268            p if p == PropertyIdentifier::EVENT_STATE => {
269                Ok(PropertyValue::Enumerated(self.event_state))
270            }
271            _ => Err(common::unknown_property_error()),
272        }
273    }
274
275    fn write_property(
276        &mut self,
277        property: PropertyIdentifier,
278        _array_index: Option<u32>,
279        value: PropertyValue,
280        _priority: Option<u8>,
281    ) -> Result<(), Error> {
282        // Present value is read-only via protocol
283        if property == PropertyIdentifier::PRESENT_VALUE {
284            return Err(common::write_access_denied_error());
285        }
286        if property == PropertyIdentifier::MODE {
287            if let PropertyValue::Enumerated(v) = value {
288                self.mode = v;
289                return Ok(());
290            }
291            return Err(common::invalid_data_type_error());
292        }
293        if property == PropertyIdentifier::SILENCED
294            || property == PropertyIdentifier::OPERATION_EXPECTED
295        {
296            return Err(common::write_access_denied_error());
297        }
298        if property == PropertyIdentifier::DIRECT_READING {
299            if let PropertyValue::Real(v) = value {
300                common::reject_non_finite(v)?;
301                self.direct_reading = v;
302                return Ok(());
303            }
304            return Err(common::invalid_data_type_error());
305        }
306        if property == PropertyIdentifier::MAINTENANCE_REQUIRED {
307            if let PropertyValue::Boolean(v) = value {
308                self.maintenance_required = v;
309                return Ok(());
310            }
311            return Err(common::invalid_data_type_error());
312        }
313        if let Some(result) =
314            common::write_out_of_service(&mut self.out_of_service, property, &value)
315        {
316            return result;
317        }
318        if let Some(result) = common::write_description(&mut self.description, property, &value) {
319            return result;
320        }
321        Err(common::write_access_denied_error())
322    }
323
324    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
325        static PROPS: &[PropertyIdentifier] = &[
326            PropertyIdentifier::OBJECT_IDENTIFIER,
327            PropertyIdentifier::OBJECT_NAME,
328            PropertyIdentifier::OBJECT_TYPE,
329            PropertyIdentifier::DESCRIPTION,
330            PropertyIdentifier::PRESENT_VALUE,
331            PropertyIdentifier::MODE,
332            PropertyIdentifier::SILENCED,
333            PropertyIdentifier::OPERATION_EXPECTED,
334            PropertyIdentifier::TRACKING_VALUE,
335            PropertyIdentifier::MEMBER_OF,
336            PropertyIdentifier::DIRECT_READING,
337            PropertyIdentifier::MAINTENANCE_REQUIRED,
338            PropertyIdentifier::EVENT_STATE,
339            PropertyIdentifier::STATUS_FLAGS,
340            PropertyIdentifier::OUT_OF_SERVICE,
341            PropertyIdentifier::RELIABILITY,
342        ];
343        Cow::Borrowed(PROPS)
344    }
345
346    fn supports_cov(&self) -> bool {
347        true
348    }
349
350    fn supports_cov_property(&self, property: PropertyIdentifier) -> bool {
351        POINT_COV_PROPERTIES.contains(&property)
352    }
353
354    fn is_writable_property(&self, property: PropertyIdentifier) -> bool {
355        matches!(
356            property,
357            PropertyIdentifier::MODE
358                | PropertyIdentifier::DIRECT_READING
359                | PropertyIdentifier::MAINTENANCE_REQUIRED
360                | PropertyIdentifier::DESCRIPTION
361                | PropertyIdentifier::OUT_OF_SERVICE
362        )
363    }
364
365    fn apply_life_safety_operation(
366        &mut self,
367        operation: LifeSafetyOperation,
368    ) -> Result<LifeSafetyOperationEffect, Error> {
369        if reset::is_reset_operation(operation) {
370            self.apply_reset_operation(operation)
371        } else {
372            apply_silenced_operation(&mut self.silenced, &mut self.operation_expected, operation)
373        }
374    }
375
376    fn apply_life_safety_operation_detailed(
377        &mut self,
378        operation: LifeSafetyOperation,
379    ) -> Result<LifeSafetyOperationOutcome, Error> {
380        let before = POINT_COV_PROPERTIES
381            .into_iter()
382            .filter_map(|property| {
383                self.read_property(property, None)
384                    .ok()
385                    .map(|value| (property, value))
386            })
387            .collect();
388        let effect = self.apply_life_safety_operation(operation)?;
389        Ok(operation_outcome(self, before, effect))
390    }
391
392    fn set_life_safety_operation_expected_internal(
393        &mut self,
394        operation: LifeSafetyOperation,
395    ) -> Result<(), Error> {
396        self.set_operation_expected(operation);
397        Ok(())
398    }
399}
400
401// ---------------------------------------------------------------------------
402// LifeSafetyZoneObject (type 22)
403// ---------------------------------------------------------------------------
404
405/// BACnet Life Safety Zone object.
406///
407/// Aggregates one or more Life Safety Point objects into a zone.
408/// Present_Value is an enumerated LifeSafetyState, set by the application
409/// (typically the worst-case state among zone members).
410pub struct LifeSafetyZoneObject {
411    oid: ObjectIdentifier,
412    name: String,
413    description: String,
414    /// Present value — LifeSafetyState enumeration (read-only via protocol).
415    present_value: u32,
416    /// Operating mode — LifeSafetyMode enumeration.
417    mode: u32,
418    /// Silenced state — SilencedState enumeration.
419    silenced: u32,
420    /// Expected operation — LifeSafetyOperation enumeration.
421    operation_expected: u32,
422    /// Points belonging to this zone.
423    zone_members: Vec<ObjectIdentifier>,
424    /// Event state (0 = NORMAL).
425    event_state: u32,
426    status_flags: StatusFlags,
427    out_of_service: bool,
428    /// Reliability (0 = NO_FAULT_DETECTED).
429    reliability: u32,
430    /// Application-owned physical reset integration, configured before insertion.
431    reset_executor: Option<LifeSafetyZoneResetExecutor>,
432}
433
434impl LifeSafetyZoneObject {
435    /// Create a new Life Safety Zone object.
436    ///
437    /// Defaults: present_value = QUIET (0), mode = OFF (0), silenced = UNSILENCED (0),
438    /// operation_expected = NONE (0).
439    pub fn new(instance: u32, name: impl Into<String>) -> Result<Self, Error> {
440        let oid = ObjectIdentifier::new(ObjectType::LIFE_SAFETY_ZONE, instance)?;
441        Ok(Self {
442            oid,
443            name: name.into(),
444            description: String::new(),
445            present_value: 0,      // QUIET
446            mode: 0,               // OFF
447            silenced: 0,           // UNSILENCED
448            operation_expected: 0, // NONE
449            zone_members: Vec::new(),
450            event_state: 0, // NORMAL
451            status_flags: StatusFlags::empty(),
452            out_of_service: false,
453            reliability: 0,
454            reset_executor: None,
455        })
456    }
457
458    /// Set the present value (LifeSafetyState enumeration).
459    pub fn set_present_value(&mut self, state: u32) {
460        self.present_value = state;
461    }
462
463    /// Set the operating mode (LifeSafetyMode enumeration).
464    pub fn set_mode(&mut self, mode: u32) {
465        self.mode = mode;
466    }
467
468    /// Set the locally determined silenced state.
469    pub fn set_silenced(&mut self, state: SilencedState) {
470        self.silenced = state.to_raw();
471    }
472
473    /// Set the next LifeSafetyOperation expected by local device logic.
474    pub fn set_operation_expected(&mut self, operation: LifeSafetyOperation) {
475        self.operation_expected = operation.to_raw();
476    }
477
478    /// Configure the application-owned reset executor before database insertion.
479    ///
480    /// The executor is used only for `RESET`, `RESET_ALARM`, and `RESET_FAULT`.
481    /// See [`LifeSafetyZoneResetExecutor`] for its synchronous execution contract.
482    pub fn set_reset_executor(&mut self, executor: LifeSafetyZoneResetExecutor) {
483        self.reset_executor = Some(executor);
484    }
485
486    /// Configure and return this zone for insertion into an object database.
487    pub fn with_reset_executor(mut self, executor: LifeSafetyZoneResetExecutor) -> Self {
488        self.set_reset_executor(executor);
489        self
490    }
491
492    /// Set the description.
493    pub fn set_description(&mut self, desc: impl Into<String>) {
494        self.description = desc.into();
495    }
496
497    /// Add a point to this zone (ObjectIdentifier of a LifeSafetyPoint).
498    pub fn add_zone_member(&mut self, point_oid: ObjectIdentifier) {
499        self.zone_members.push(point_oid);
500    }
501}
502
503impl BACnetObject for LifeSafetyZoneObject {
504    fn object_identifier(&self) -> ObjectIdentifier {
505        self.oid
506    }
507
508    fn object_name(&self) -> &str {
509        &self.name
510    }
511
512    fn read_property(
513        &self,
514        property: PropertyIdentifier,
515        array_index: Option<u32>,
516    ) -> Result<PropertyValue, Error> {
517        if let Some(result) = read_common_properties!(self, property, array_index) {
518            return result;
519        }
520        match property {
521            p if p == PropertyIdentifier::OBJECT_TYPE => Ok(PropertyValue::Enumerated(
522                ObjectType::LIFE_SAFETY_ZONE.to_raw(),
523            )),
524            p if p == PropertyIdentifier::PRESENT_VALUE => {
525                Ok(PropertyValue::Enumerated(self.present_value))
526            }
527            p if p == PropertyIdentifier::MODE => Ok(PropertyValue::Enumerated(self.mode)),
528            p if p == PropertyIdentifier::SILENCED => Ok(PropertyValue::Enumerated(self.silenced)),
529            p if p == PropertyIdentifier::OPERATION_EXPECTED => {
530                Ok(PropertyValue::Enumerated(self.operation_expected))
531            }
532            p if p == PropertyIdentifier::ZONE_MEMBERS => Ok(PropertyValue::List(
533                self.zone_members
534                    .iter()
535                    .map(|oid| PropertyValue::ObjectIdentifier(*oid))
536                    .collect(),
537            )),
538            p if p == PropertyIdentifier::EVENT_STATE => {
539                Ok(PropertyValue::Enumerated(self.event_state))
540            }
541            _ => Err(common::unknown_property_error()),
542        }
543    }
544
545    fn write_property(
546        &mut self,
547        property: PropertyIdentifier,
548        _array_index: Option<u32>,
549        value: PropertyValue,
550        _priority: Option<u8>,
551    ) -> Result<(), Error> {
552        // Present value is read-only via protocol
553        if property == PropertyIdentifier::PRESENT_VALUE {
554            return Err(common::write_access_denied_error());
555        }
556        if property == PropertyIdentifier::MODE {
557            if let PropertyValue::Enumerated(v) = value {
558                self.mode = v;
559                return Ok(());
560            }
561            return Err(common::invalid_data_type_error());
562        }
563        if property == PropertyIdentifier::SILENCED
564            || property == PropertyIdentifier::OPERATION_EXPECTED
565        {
566            return Err(common::write_access_denied_error());
567        }
568        if let Some(result) =
569            common::write_out_of_service(&mut self.out_of_service, property, &value)
570        {
571            return result;
572        }
573        if let Some(result) = common::write_description(&mut self.description, property, &value) {
574            return result;
575        }
576        Err(common::write_access_denied_error())
577    }
578
579    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
580        static PROPS: &[PropertyIdentifier] = &[
581            PropertyIdentifier::OBJECT_IDENTIFIER,
582            PropertyIdentifier::OBJECT_NAME,
583            PropertyIdentifier::OBJECT_TYPE,
584            PropertyIdentifier::DESCRIPTION,
585            PropertyIdentifier::PRESENT_VALUE,
586            PropertyIdentifier::MODE,
587            PropertyIdentifier::SILENCED,
588            PropertyIdentifier::OPERATION_EXPECTED,
589            PropertyIdentifier::ZONE_MEMBERS,
590            PropertyIdentifier::EVENT_STATE,
591            PropertyIdentifier::STATUS_FLAGS,
592            PropertyIdentifier::OUT_OF_SERVICE,
593            PropertyIdentifier::RELIABILITY,
594        ];
595        Cow::Borrowed(PROPS)
596    }
597
598    fn supports_cov(&self) -> bool {
599        true
600    }
601
602    fn supports_cov_property(&self, property: PropertyIdentifier) -> bool {
603        ZONE_COV_PROPERTIES.contains(&property)
604    }
605
606    fn is_writable_property(&self, property: PropertyIdentifier) -> bool {
607        matches!(
608            property,
609            PropertyIdentifier::MODE
610                | PropertyIdentifier::DESCRIPTION
611                | PropertyIdentifier::OUT_OF_SERVICE
612        )
613    }
614
615    fn apply_life_safety_operation(
616        &mut self,
617        operation: LifeSafetyOperation,
618    ) -> Result<LifeSafetyOperationEffect, Error> {
619        if reset::is_reset_operation(operation) {
620            self.apply_reset_operation(operation)
621        } else {
622            apply_silenced_operation(&mut self.silenced, &mut self.operation_expected, operation)
623        }
624    }
625
626    fn apply_life_safety_operation_detailed(
627        &mut self,
628        operation: LifeSafetyOperation,
629    ) -> Result<LifeSafetyOperationOutcome, Error> {
630        let before = ZONE_COV_PROPERTIES
631            .into_iter()
632            .filter_map(|property| {
633                self.read_property(property, None)
634                    .ok()
635                    .map(|value| (property, value))
636            })
637            .collect();
638        let effect = self.apply_life_safety_operation(operation)?;
639        Ok(operation_outcome(self, before, effect))
640    }
641
642    fn set_life_safety_operation_expected_internal(
643        &mut self,
644        operation: LifeSafetyOperation,
645    ) -> Result<(), Error> {
646        self.set_operation_expected(operation);
647        Ok(())
648    }
649}
650
651// ===========================================================================
652// Tests
653// ===========================================================================
654
655#[cfg(test)]
656mod tests;
657
658#[cfg(test)]
659mod reset_tests;