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//!
4//! R1 matrix (PR-0803 sub-slice 1) — Event_State / Status_Flags post-state for
5//! each LifeSafetyOperation on Point and Zone. Verdict: outcome (b), pins only
6//! with zero state change. The Standard keeps Event_State purely intrinsic —
7//! driven by the object's event algorithm off the monitored LifeSafetyState,
8//! mode changes, delays, and re-alert — and no service clause mandates an
9//! LSO-driven Event_State or Status_Flags transition:
10//!
11//! | Operation         | Point Event_State | Point IN_ALARM | Zone Event_State | Zone IN_ALARM |
12//! |-------------------|-------------------|----------------|------------------|---------------|
13//! | SILENCE           | NORMAL (0)        | FALSE          | NORMAL (0)       | FALSE         |
14//! | SILENCE_AUDIBLE   | NORMAL (0)        | FALSE          | NORMAL (0)       | FALSE         |
15//! | SILENCE_VISUAL    | NORMAL (0)        | FALSE          | NORMAL (0)       | FALSE         |
16//! | UNSILENCE         | NORMAL (0)        | FALSE          | NORMAL (0)       | FALSE         |
17//! | UNSILENCE_AUDIBLE | NORMAL (0)        | FALSE          | NORMAL (0)       | FALSE         |
18//! | UNSILENCE_VISUAL  | NORMAL (0)        | FALSE          | NORMAL (0)       | FALSE         |
19//! | RESET             | NORMAL (0)        | FALSE          | NORMAL (0)       | FALSE         |
20//! | RESET_ALARM       | NORMAL (0)        | FALSE          | NORMAL (0)       | FALSE         |
21//! | RESET_FAULT       | NORMAL (0)        | FALSE          | NORMAL (0)       | FALSE         |
22//!
23//! Rationale (paraphrased; see cited pages, never normative text): the object
24//! clauses describe Event_State as read-only, mirroring the event algorithm
25//! only when intrinsic reporting is supported and otherwise staying NORMAL —
26//! and this crate implements no intrinsic reporting, so NORMAL is the
27//! spec-correct value. The IN_ALARM flag mirrors a non-NORMAL Event_State.
28//! Present_Value latching until reset and Tracking_Value continuous tracking
29//! are local matters; Silenced records whether the latest audible/visual
30//! transition was silenced via service request or local means, and
31//! Operation_Expected names the next operation the local situation calls for.
32//! The LifeSafetyOperation service clause only silences/resets/unsilences the
33//! addressed (or all applicable) objects and answers Result(+/-), rejecting a
34//! reset the object is not ready for; it carries no Event_State/Status_Flags
35//! rows. The CHANGE_OF_LIFE_SAFETY algorithm keys transitions off the
36//! monitored state versus the alarm lists (plus mode/delay/re-alert) while
37//! Operation_Expected and Status_Flags travel as notification inputs only.
38//! Page cites in the local licensed PDF (`_spec/2020_ASHRAE_...pdf`):
39//! object clauses printed pp. 245-256 (PDF pp. 247-258), LifeSafetyOperation
40//! service printed pp. 701-702 (PDF pp. 703-704), CHANGE_OF_LIFE_SAFETY
41//! printed pp. 657-658 (PDF pp. 659-660). Refs #177 (no claim change here).
42//!
43//! Point and Zone both serve Tracking_Value (Clauses 12.15.5 and 12.16.5): the
44//! application keeps it current through `set_tracking_value` or a reset
45//! commit, and both objects offer it for property COV. While Out_Of_Service is
46//! TRUE a client may write Tracking_Value and Reliability instead (#1108); see
47//! `out_of_service.rs` for how the simulated values interact with the rest of
48//! the object. Once a server holds the object, the application reaches
49//! Present_Value and Tracking_Value through `set_present_value_internal` and
50//! `set_tracking_value_internal` (#1123); `application.rs` records how that
51//! route meets latching, Silenced, the reset executor and Out_Of_Service.
52//!
53//! Accepted_Modes (Clauses 12.15.13 and 12.16.13) is the configured set of
54//! modes a network write of Mode may select. It starts as every standard
55//! LifeSafetyMode and an application narrows it with `set_accepted_modes`. A
56//! Mode write naming any other value fails with PROPERTY / VALUE_OUT_OF_RANGE
57//! and leaves the object unchanged. The local `set_mode` ignores the list,
58//! because the object's own logic may move Mode outside it.
59
60use crate::prelude::*;
61use alloc::borrow::Cow;
62use bacnet_types::constructed::BACnetDeviceObjectReference;
63use bacnet_types::enums::{
64    ErrorClass, ErrorCode, EventState, LifeSafetyMode, LifeSafetyOperation, LifeSafetyState,
65    ObjectType, PropertyIdentifier, Reliability, SilencedState,
66};
67use bacnet_types::error::Error;
68use bacnet_types::primitives::{ObjectIdentifier, PropertyValue, StatusFlags};
69
70use crate::common::{self, read_common_properties};
71use crate::device_reference::reference_list;
72use crate::traits::{BACnetObject, LifeSafetyOperationEffect, LifeSafetyOperationOutcome};
73
74mod application;
75mod membership;
76mod metadata;
77mod out_of_service;
78mod reset;
79
80use out_of_service::{DeviceValues, Simulation};
81
82pub use reset::{
83    LifeSafetyPointResetCommit, LifeSafetyPointResetContext, LifeSafetyPointResetExecutor,
84    LifeSafetyResetError, LifeSafetyZoneResetCommit, LifeSafetyZoneResetContext,
85    LifeSafetyZoneResetExecutor,
86};
87
88/// Whether a state is one BACnetLifeSafetyState defines or one from the
89/// proprietary range 256..=65535 (Clause 21).
90fn valid_life_safety_state(state: LifeSafetyState) -> bool {
91    LifeSafetyState::ALL_NAMED
92        .iter()
93        .any(|&(_, named)| named == state)
94        || (256..=65_535).contains(&state.to_raw())
95}
96
97fn life_safety_error(code: ErrorCode) -> Error {
98    Error::protocol(ErrorClass::OBJECT, code)
99}
100
101fn apply_silenced_operation(
102    silenced: &mut SilencedState,
103    operation_expected: &mut LifeSafetyOperation,
104    operation: LifeSafetyOperation,
105) -> Result<LifeSafetyOperationEffect, Error> {
106    // The four standard SilencedState values form a two-bit audible/visible
107    // set, so the partial operations are bit operations on the wire value. A
108    // reserved or proprietary state has no such decomposition and is refused.
109    let current = silenced.to_raw();
110    if current > SilencedState::ALL_SILENCED.to_raw() {
111        return Err(life_safety_error(
112            ErrorCode::INVALID_OPERATION_IN_THIS_STATE,
113        ));
114    }
115
116    let desired = if operation == LifeSafetyOperation::SILENCE {
117        SilencedState::ALL_SILENCED
118    } else if operation == LifeSafetyOperation::SILENCE_AUDIBLE {
119        SilencedState::from_raw(current | SilencedState::AUDIBLE_SILENCED.to_raw())
120    } else if operation == LifeSafetyOperation::SILENCE_VISUAL {
121        SilencedState::from_raw(current | SilencedState::VISIBLE_SILENCED.to_raw())
122    } else if operation == LifeSafetyOperation::UNSILENCE {
123        SilencedState::UNSILENCED
124    } else if operation == LifeSafetyOperation::UNSILENCE_AUDIBLE {
125        SilencedState::from_raw(current & !SilencedState::AUDIBLE_SILENCED.to_raw())
126    } else if operation == LifeSafetyOperation::UNSILENCE_VISUAL {
127        SilencedState::from_raw(current & !SilencedState::VISIBLE_SILENCED.to_raw())
128    } else {
129        return Err(life_safety_error(ErrorCode::VALUE_OUT_OF_RANGE));
130    };
131
132    if *operation_expected != operation {
133        return Err(life_safety_error(
134            ErrorCode::INVALID_OPERATION_IN_THIS_STATE,
135        ));
136    }
137
138    *silenced = desired;
139    *operation_expected = LifeSafetyOperation::NONE;
140    Ok(LifeSafetyOperationEffect::Applied)
141}
142
143/// Properties a Point or Zone offers for property COV, in the order an
144/// operation outcome reports their deltas.
145const COV_PROPERTIES: [PropertyIdentifier; 5] = [
146    PropertyIdentifier::PRESENT_VALUE,
147    PropertyIdentifier::TRACKING_VALUE,
148    PropertyIdentifier::SILENCED,
149    PropertyIdentifier::OPERATION_EXPECTED,
150    PropertyIdentifier::STATUS_FLAGS,
151];
152
153/// The Accepted_Modes a new Point or Zone starts with: every standard mode.
154fn standard_life_safety_modes() -> Vec<LifeSafetyMode> {
155    LifeSafetyMode::ALL_NAMED
156        .iter()
157        .map(|&(_, mode)| mode)
158        .collect()
159}
160
161/// Keep each mode once, in the caller's order.
162fn distinct_modes(modes: impl IntoIterator<Item = LifeSafetyMode>) -> Vec<LifeSafetyMode> {
163    let mut distinct = Vec::new();
164    for mode in modes {
165        if !distinct.contains(&mode) {
166            distinct.push(mode);
167        }
168    }
169    distinct
170}
171
172fn read_accepted_modes(accepted_modes: &[LifeSafetyMode]) -> PropertyValue {
173    PropertyValue::List(
174        accepted_modes
175            .iter()
176            .map(|mode| PropertyValue::Enumerated(mode.to_raw()))
177            .collect(),
178    )
179}
180
181/// Apply a network write of Mode. A mode missing from Accepted_Modes is
182/// refused with PROPERTY / VALUE_OUT_OF_RANGE (Clauses 12.15.13 and 12.16.13)
183/// and Mode keeps its value.
184fn write_mode(
185    mode: &mut LifeSafetyMode,
186    accepted_modes: &[LifeSafetyMode],
187    value: &PropertyValue,
188) -> Result<(), Error> {
189    let PropertyValue::Enumerated(raw) = value else {
190        return Err(common::invalid_data_type_error());
191    };
192    let requested = LifeSafetyMode::from_raw(*raw);
193    if !accepted_modes.contains(&requested) {
194        return Err(common::value_out_of_range_error());
195    }
196    *mode = requested;
197    Ok(())
198}
199
200fn operation_outcome(
201    object: &dyn BACnetObject,
202    before: Vec<(PropertyIdentifier, PropertyValue)>,
203    effect: LifeSafetyOperationEffect,
204) -> LifeSafetyOperationOutcome {
205    let changed_properties = before
206        .into_iter()
207        .filter_map(|(property, previous)| {
208            object
209                .read_property(property, None)
210                .is_ok_and(|current| current != previous)
211                .then_some(property)
212        })
213        .collect();
214    LifeSafetyOperationOutcome {
215        effect,
216        changed_properties,
217    }
218}
219
220// ---------------------------------------------------------------------------
221// LifeSafetyPointObject (type 21)
222// ---------------------------------------------------------------------------
223
224/// BACnet Life Safety Point object.
225///
226/// Represents a single life-safety sensor or detector (e.g. smoke detector,
227/// pull station). Present_Value is an enumerated LifeSafetyState, set by the
228/// application via [`set_present_value`](Self::set_present_value), or through
229/// [`BACnetObject::set_present_value_internal`] once a server holds the point.
230pub struct LifeSafetyPointObject {
231    oid: ObjectIdentifier,
232    name: String,
233    description: String,
234    /// Present value (read-only via protocol).
235    present_value: LifeSafetyState,
236    /// Operating mode.
237    mode: LifeSafetyMode,
238    /// Modes a network write of Mode may select.
239    accepted_modes: Vec<LifeSafetyMode>,
240    /// Silenced state.
241    silenced: SilencedState,
242    /// Expected operation.
243    operation_expected: LifeSafetyOperation,
244    /// Tracking value.
245    tracking_value: LifeSafetyState,
246    /// Zones this point belongs to (Member_Of).
247    member_of: Vec<BACnetDeviceObjectReference>,
248    /// Raw sensor reading.
249    direct_reading: f32,
250    /// Whether maintenance is required.
251    maintenance_required: bool,
252    /// Event_State.
253    event_state: EventState,
254    status_flags: StatusFlags,
255    out_of_service: bool,
256    /// Reliability; NO_FAULT_DETECTED until a fault is evaluated or simulated.
257    reliability: Reliability,
258    /// The application's Tracking_Value and Reliability while a client
259    /// simulates them.
260    device_values: DeviceValues,
261    /// Application-owned physical reset integration, configured before insertion.
262    reset_executor: Option<LifeSafetyPointResetExecutor>,
263}
264
265impl LifeSafetyPointObject {
266    /// Create a new Life Safety Point object.
267    ///
268    /// Defaults: present_value = QUIET, mode = OFF, accepted_modes = every
269    /// standard mode, silenced = UNSILENCED, operation_expected = NONE,
270    /// tracking_value = QUIET.
271    pub fn new(instance: u32, name: impl Into<String>) -> Result<Self, Error> {
272        let oid = ObjectIdentifier::new(ObjectType::LIFE_SAFETY_POINT, instance)?;
273        Ok(Self {
274            oid,
275            name: name.into(),
276            description: String::new(),
277            present_value: LifeSafetyState::QUIET,
278            mode: LifeSafetyMode::OFF,
279            accepted_modes: standard_life_safety_modes(),
280            silenced: SilencedState::UNSILENCED,
281            operation_expected: LifeSafetyOperation::NONE,
282            tracking_value: LifeSafetyState::QUIET,
283            member_of: Vec::new(),
284            direct_reading: 0.0,
285            maintenance_required: false,
286            event_state: EventState::NORMAL,
287            status_flags: StatusFlags::empty(),
288            out_of_service: false,
289            reliability: Reliability::NO_FAULT_DETECTED,
290            device_values: DeviceValues::default(),
291            reset_executor: None,
292        })
293    }
294
295    /// Set the present value.
296    pub fn set_present_value(&mut self, state: LifeSafetyState) {
297        self.present_value = state;
298    }
299
300    /// Set the operating mode from local logic, which Accepted_Modes does not
301    /// constrain.
302    pub fn set_mode(&mut self, mode: LifeSafetyMode) {
303        self.mode = mode;
304    }
305
306    /// Configure Accepted_Modes, the modes a network write of Mode may select.
307    ///
308    /// A repeated mode is kept once. The current Mode stays as it is even when
309    /// the new list leaves it out.
310    pub fn set_accepted_modes(&mut self, modes: impl IntoIterator<Item = LifeSafetyMode>) {
311        self.accepted_modes = distinct_modes(modes);
312    }
313
314    /// Set the tracking value the device reports.
315    ///
316    /// While Out_Of_Service is TRUE a client's simulated value keeps being
317    /// served, and this one takes over on the return to service.
318    pub fn set_tracking_value(&mut self, state: LifeSafetyState) {
319        self.simulation().track(state);
320    }
321
322    fn simulation(&mut self) -> Simulation<'_> {
323        Simulation {
324            out_of_service: &mut self.out_of_service,
325            reliability: &mut self.reliability,
326            tracking_value: &mut self.tracking_value,
327            device_values: &mut self.device_values,
328        }
329    }
330
331    /// Set the locally determined silenced state.
332    pub fn set_silenced(&mut self, state: SilencedState) {
333        self.silenced = state;
334    }
335
336    /// Set the next LifeSafetyOperation expected by local device logic.
337    pub fn set_operation_expected(&mut self, operation: LifeSafetyOperation) {
338        self.operation_expected = operation;
339    }
340
341    /// Configure the application-owned reset executor before database insertion.
342    ///
343    /// The executor is used only for `RESET`, `RESET_ALARM`, and `RESET_FAULT`.
344    /// See [`LifeSafetyPointResetExecutor`] for its synchronous execution contract.
345    pub fn set_reset_executor(&mut self, executor: LifeSafetyPointResetExecutor) {
346        self.reset_executor = Some(executor);
347    }
348
349    /// Configure and return this point for insertion into an object database.
350    pub fn with_reset_executor(mut self, executor: LifeSafetyPointResetExecutor) -> Self {
351        self.set_reset_executor(executor);
352        self
353    }
354
355    /// Set the direct reading (raw sensor value).
356    pub fn set_direct_reading(&mut self, value: f32) {
357        self.direct_reading = value;
358    }
359
360    /// Set the description.
361    pub fn set_description(&mut self, desc: impl Into<String>) {
362        self.description = desc.into();
363    }
364
365    /// Add a zone to Member_Of: a Life Safety Zone, in this device (an
366    /// `ObjectIdentifier` converts) or in the device the reference names.
367    ///
368    /// A zone already listed stays listed once. Any other object type, or a
369    /// Device member that isn't a Device identifier, fails with PROPERTY /
370    /// VALUE_OUT_OF_RANGE and changes nothing.
371    pub fn add_member(
372        &mut self,
373        zone: impl Into<BACnetDeviceObjectReference>,
374    ) -> Result<(), Error> {
375        membership::add(&mut self.member_of, zone.into(), membership::ZONES)
376    }
377}
378
379impl BACnetObject for LifeSafetyPointObject {
380    fn object_identifier(&self) -> ObjectIdentifier {
381        self.oid
382    }
383
384    fn object_name(&self) -> &str {
385        &self.name
386    }
387
388    fn read_property(
389        &self,
390        property: PropertyIdentifier,
391        array_index: Option<u32>,
392    ) -> Result<PropertyValue, Error> {
393        if let Some(result) = read_common_properties!(self, property, array_index) {
394            return result;
395        }
396        match property {
397            p if p == PropertyIdentifier::OBJECT_TYPE => Ok(PropertyValue::Enumerated(
398                ObjectType::LIFE_SAFETY_POINT.to_raw(),
399            )),
400            p if p == PropertyIdentifier::PRESENT_VALUE => {
401                Ok(PropertyValue::Enumerated(self.present_value.to_raw()))
402            }
403            p if p == PropertyIdentifier::MODE => Ok(PropertyValue::Enumerated(self.mode.to_raw())),
404            p if p == PropertyIdentifier::ACCEPTED_MODES => {
405                Ok(read_accepted_modes(&self.accepted_modes))
406            }
407            p if p == PropertyIdentifier::SILENCED => {
408                Ok(PropertyValue::Enumerated(self.silenced.to_raw()))
409            }
410            p if p == PropertyIdentifier::OPERATION_EXPECTED => {
411                Ok(PropertyValue::Enumerated(self.operation_expected.to_raw()))
412            }
413            p if p == PropertyIdentifier::TRACKING_VALUE => {
414                Ok(PropertyValue::Enumerated(self.tracking_value.to_raw()))
415            }
416            p if p == PropertyIdentifier::MEMBER_OF => Ok(reference_list(&self.member_of)),
417            p if p == PropertyIdentifier::DIRECT_READING => {
418                Ok(PropertyValue::Real(self.direct_reading))
419            }
420            p if p == PropertyIdentifier::MAINTENANCE_REQUIRED => {
421                Ok(PropertyValue::Boolean(self.maintenance_required))
422            }
423            p if p == PropertyIdentifier::EVENT_STATE => {
424                Ok(PropertyValue::Enumerated(self.event_state.to_raw()))
425            }
426            _ => Err(common::unknown_property_error()),
427        }
428    }
429
430    fn write_property(
431        &mut self,
432        property: PropertyIdentifier,
433        _array_index: Option<u32>,
434        value: PropertyValue,
435        _priority: Option<u8>,
436    ) -> Result<(), Error> {
437        // Present value is read-only via protocol
438        if property == PropertyIdentifier::PRESENT_VALUE {
439            return Err(common::write_access_denied_error());
440        }
441        if property == PropertyIdentifier::MODE {
442            return write_mode(&mut self.mode, &self.accepted_modes, &value);
443        }
444        if property == PropertyIdentifier::SILENCED
445            || property == PropertyIdentifier::OPERATION_EXPECTED
446        {
447            return Err(common::write_access_denied_error());
448        }
449        if property == PropertyIdentifier::DIRECT_READING {
450            if let PropertyValue::Real(v) = value {
451                common::reject_non_finite(v)?;
452                self.direct_reading = v;
453                return Ok(());
454            }
455            return Err(common::invalid_data_type_error());
456        }
457        if property == PropertyIdentifier::MAINTENANCE_REQUIRED {
458            if let PropertyValue::Boolean(v) = value {
459                self.maintenance_required = v;
460                return Ok(());
461            }
462            return Err(common::invalid_data_type_error());
463        }
464        if let Some(result) = self.simulation().write(property, &value) {
465            return result;
466        }
467        if let Some(result) = common::write_description(&mut self.description, property, &value) {
468            return result;
469        }
470        Err(crate::common::unhandled_write_error(
471            self.property_metadata().as_ref(),
472            property,
473            _array_index,
474        ))
475    }
476
477    fn property_metadata(&self) -> Cow<'_, [crate::property_metadata::PropertyMetadata]> {
478        metadata::for_point(self)
479    }
480
481    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
482        crate::property_metadata::property_list_from_metadata(self.property_metadata().as_ref())
483    }
484
485    fn supports_cov(&self) -> bool {
486        true
487    }
488
489    fn supports_cov_property(&self, property: PropertyIdentifier) -> bool {
490        COV_PROPERTIES.contains(&property)
491    }
492
493    fn apply_life_safety_operation(
494        &mut self,
495        operation: LifeSafetyOperation,
496    ) -> Result<LifeSafetyOperationOutcome, Error> {
497        let before = COV_PROPERTIES
498            .into_iter()
499            .filter_map(|property| {
500                self.read_property(property, None)
501                    .ok()
502                    .map(|value| (property, value))
503            })
504            .collect();
505        let effect = if reset::is_reset_operation(operation) {
506            self.apply_reset_operation(operation)?
507        } else {
508            apply_silenced_operation(&mut self.silenced, &mut self.operation_expected, operation)?
509        };
510        Ok(operation_outcome(self, before, effect))
511    }
512
513    fn set_life_safety_operation_expected_internal(
514        &mut self,
515        operation: LifeSafetyOperation,
516    ) -> Result<(), Error> {
517        self.set_operation_expected(operation);
518        Ok(())
519    }
520
521    fn set_reliability_internal(&mut self, reliability: Reliability) -> Result<(), Error> {
522        self.simulation().set_reliability(reliability)
523    }
524
525    fn set_present_value_internal(&mut self, value: PropertyValue) -> Result<(), Error> {
526        self.present_value = application::life_safety_state(&value)?;
527        Ok(())
528    }
529
530    fn set_tracking_value_internal(&mut self, value: PropertyValue) -> Result<(), Error> {
531        let state = application::life_safety_state(&value)?;
532        self.set_tracking_value(state);
533        Ok(())
534    }
535}
536
537// ---------------------------------------------------------------------------
538// LifeSafetyZoneObject (type 22)
539// ---------------------------------------------------------------------------
540
541/// BACnet Life Safety Zone object.
542///
543/// Aggregates one or more Life Safety Point objects into a zone.
544/// Present_Value is an enumerated LifeSafetyState, set by the application
545/// (typically the worst-case state among zone members) via
546/// [`set_present_value`](Self::set_present_value), or through
547/// [`BACnetObject::set_present_value_internal`] once a server holds the zone.
548pub struct LifeSafetyZoneObject {
549    oid: ObjectIdentifier,
550    name: String,
551    description: String,
552    /// Present value (read-only via protocol).
553    present_value: LifeSafetyState,
554    /// Operating mode.
555    mode: LifeSafetyMode,
556    /// Modes a network write of Mode may select.
557    accepted_modes: Vec<LifeSafetyMode>,
558    /// Silenced state.
559    silenced: SilencedState,
560    /// Expected operation.
561    operation_expected: LifeSafetyOperation,
562    /// Tracking value.
563    tracking_value: LifeSafetyState,
564    /// Points and zones belonging to this zone (Zone_Members).
565    zone_members: Vec<BACnetDeviceObjectReference>,
566    /// Zones this zone belongs to (Member_Of).
567    member_of: Vec<BACnetDeviceObjectReference>,
568    /// Event_State.
569    event_state: EventState,
570    status_flags: StatusFlags,
571    out_of_service: bool,
572    /// Reliability; NO_FAULT_DETECTED until a fault is evaluated or simulated.
573    reliability: Reliability,
574    /// The application's Tracking_Value and Reliability while a client
575    /// simulates them.
576    device_values: DeviceValues,
577    /// Application-owned physical reset integration, configured before insertion.
578    reset_executor: Option<LifeSafetyZoneResetExecutor>,
579}
580
581impl LifeSafetyZoneObject {
582    /// Create a new Life Safety Zone object.
583    ///
584    /// Defaults: present_value = QUIET, mode = OFF, accepted_modes = every
585    /// standard mode, silenced = UNSILENCED, operation_expected = NONE,
586    /// tracking_value = QUIET.
587    pub fn new(instance: u32, name: impl Into<String>) -> Result<Self, Error> {
588        let oid = ObjectIdentifier::new(ObjectType::LIFE_SAFETY_ZONE, instance)?;
589        Ok(Self {
590            oid,
591            name: name.into(),
592            description: String::new(),
593            present_value: LifeSafetyState::QUIET,
594            mode: LifeSafetyMode::OFF,
595            accepted_modes: standard_life_safety_modes(),
596            silenced: SilencedState::UNSILENCED,
597            operation_expected: LifeSafetyOperation::NONE,
598            tracking_value: LifeSafetyState::QUIET,
599            zone_members: Vec::new(),
600            member_of: Vec::new(),
601            event_state: EventState::NORMAL,
602            status_flags: StatusFlags::empty(),
603            out_of_service: false,
604            reliability: Reliability::NO_FAULT_DETECTED,
605            device_values: DeviceValues::default(),
606            reset_executor: None,
607        })
608    }
609
610    /// Set the present value.
611    pub fn set_present_value(&mut self, state: LifeSafetyState) {
612        self.present_value = state;
613    }
614
615    /// Set the operating mode from local logic, which Accepted_Modes does not
616    /// constrain.
617    pub fn set_mode(&mut self, mode: LifeSafetyMode) {
618        self.mode = mode;
619    }
620
621    /// Configure Accepted_Modes, the modes a network write of Mode may select.
622    ///
623    /// A repeated mode is kept once. The current Mode stays as it is even when
624    /// the new list leaves it out.
625    pub fn set_accepted_modes(&mut self, modes: impl IntoIterator<Item = LifeSafetyMode>) {
626        self.accepted_modes = distinct_modes(modes);
627    }
628
629    /// Set the tracking value the device reports.
630    ///
631    /// While Out_Of_Service is TRUE a client's simulated value keeps being
632    /// served, and this one takes over on the return to service.
633    pub fn set_tracking_value(&mut self, state: LifeSafetyState) {
634        self.simulation().track(state);
635    }
636
637    fn simulation(&mut self) -> Simulation<'_> {
638        Simulation {
639            out_of_service: &mut self.out_of_service,
640            reliability: &mut self.reliability,
641            tracking_value: &mut self.tracking_value,
642            device_values: &mut self.device_values,
643        }
644    }
645
646    /// Set the locally determined silenced state.
647    pub fn set_silenced(&mut self, state: SilencedState) {
648        self.silenced = state;
649    }
650
651    /// Set the next LifeSafetyOperation expected by local device logic.
652    pub fn set_operation_expected(&mut self, operation: LifeSafetyOperation) {
653        self.operation_expected = operation;
654    }
655
656    /// Configure the application-owned reset executor before database insertion.
657    ///
658    /// The executor is used only for `RESET`, `RESET_ALARM`, and `RESET_FAULT`.
659    /// See [`LifeSafetyZoneResetExecutor`] for its synchronous execution contract.
660    pub fn set_reset_executor(&mut self, executor: LifeSafetyZoneResetExecutor) {
661        self.reset_executor = Some(executor);
662    }
663
664    /// Configure and return this zone for insertion into an object database.
665    pub fn with_reset_executor(mut self, executor: LifeSafetyZoneResetExecutor) -> Self {
666        self.set_reset_executor(executor);
667        self
668    }
669
670    /// Set the description.
671    pub fn set_description(&mut self, desc: impl Into<String>) {
672        self.description = desc.into();
673    }
674
675    /// Add a member to Zone_Members: a Life Safety Point or Zone, in this
676    /// device (an `ObjectIdentifier` converts) or in the device the reference
677    /// names.
678    ///
679    /// A member already listed stays listed once. Any other object type, or a
680    /// Device member that isn't a Device identifier, fails with PROPERTY /
681    /// VALUE_OUT_OF_RANGE and changes nothing.
682    pub fn add_zone_member(
683        &mut self,
684        member: impl Into<BACnetDeviceObjectReference>,
685    ) -> Result<(), Error> {
686        membership::add(
687            &mut self.zone_members,
688            member.into(),
689            membership::ZONE_MEMBERS,
690        )
691    }
692
693    /// Add a zone to Member_Of: a Life Safety Zone this zone belongs to, with
694    /// the same refusals as [`add_zone_member`](Self::add_zone_member).
695    pub fn add_member(
696        &mut self,
697        zone: impl Into<BACnetDeviceObjectReference>,
698    ) -> Result<(), Error> {
699        membership::add(&mut self.member_of, zone.into(), membership::ZONES)
700    }
701}
702
703impl BACnetObject for LifeSafetyZoneObject {
704    fn object_identifier(&self) -> ObjectIdentifier {
705        self.oid
706    }
707
708    fn object_name(&self) -> &str {
709        &self.name
710    }
711
712    fn read_property(
713        &self,
714        property: PropertyIdentifier,
715        array_index: Option<u32>,
716    ) -> Result<PropertyValue, Error> {
717        if let Some(result) = read_common_properties!(self, property, array_index) {
718            return result;
719        }
720        match property {
721            p if p == PropertyIdentifier::OBJECT_TYPE => Ok(PropertyValue::Enumerated(
722                ObjectType::LIFE_SAFETY_ZONE.to_raw(),
723            )),
724            p if p == PropertyIdentifier::PRESENT_VALUE => {
725                Ok(PropertyValue::Enumerated(self.present_value.to_raw()))
726            }
727            p if p == PropertyIdentifier::MODE => Ok(PropertyValue::Enumerated(self.mode.to_raw())),
728            p if p == PropertyIdentifier::ACCEPTED_MODES => {
729                Ok(read_accepted_modes(&self.accepted_modes))
730            }
731            p if p == PropertyIdentifier::SILENCED => {
732                Ok(PropertyValue::Enumerated(self.silenced.to_raw()))
733            }
734            p if p == PropertyIdentifier::OPERATION_EXPECTED => {
735                Ok(PropertyValue::Enumerated(self.operation_expected.to_raw()))
736            }
737            p if p == PropertyIdentifier::TRACKING_VALUE => {
738                Ok(PropertyValue::Enumerated(self.tracking_value.to_raw()))
739            }
740            p if p == PropertyIdentifier::ZONE_MEMBERS => Ok(reference_list(&self.zone_members)),
741            p if p == PropertyIdentifier::MEMBER_OF => Ok(reference_list(&self.member_of)),
742            p if p == PropertyIdentifier::EVENT_STATE => {
743                Ok(PropertyValue::Enumerated(self.event_state.to_raw()))
744            }
745            _ => Err(common::unknown_property_error()),
746        }
747    }
748
749    fn write_property(
750        &mut self,
751        property: PropertyIdentifier,
752        _array_index: Option<u32>,
753        value: PropertyValue,
754        _priority: Option<u8>,
755    ) -> Result<(), Error> {
756        // Present value is read-only via protocol
757        if property == PropertyIdentifier::PRESENT_VALUE {
758            return Err(common::write_access_denied_error());
759        }
760        if property == PropertyIdentifier::MODE {
761            return write_mode(&mut self.mode, &self.accepted_modes, &value);
762        }
763        if property == PropertyIdentifier::SILENCED
764            || property == PropertyIdentifier::OPERATION_EXPECTED
765        {
766            return Err(common::write_access_denied_error());
767        }
768        if let Some(result) = self.simulation().write(property, &value) {
769            return result;
770        }
771        if let Some(result) = common::write_description(&mut self.description, property, &value) {
772            return result;
773        }
774        Err(crate::common::unhandled_write_error(
775            self.property_metadata().as_ref(),
776            property,
777            _array_index,
778        ))
779    }
780
781    fn property_metadata(&self) -> Cow<'_, [crate::property_metadata::PropertyMetadata]> {
782        metadata::for_zone(self)
783    }
784
785    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
786        crate::property_metadata::property_list_from_metadata(self.property_metadata().as_ref())
787    }
788
789    fn supports_cov(&self) -> bool {
790        true
791    }
792
793    fn supports_cov_property(&self, property: PropertyIdentifier) -> bool {
794        COV_PROPERTIES.contains(&property)
795    }
796
797    fn apply_life_safety_operation(
798        &mut self,
799        operation: LifeSafetyOperation,
800    ) -> Result<LifeSafetyOperationOutcome, Error> {
801        let before = COV_PROPERTIES
802            .into_iter()
803            .filter_map(|property| {
804                self.read_property(property, None)
805                    .ok()
806                    .map(|value| (property, value))
807            })
808            .collect();
809        let effect = if reset::is_reset_operation(operation) {
810            self.apply_reset_operation(operation)?
811        } else {
812            apply_silenced_operation(&mut self.silenced, &mut self.operation_expected, operation)?
813        };
814        Ok(operation_outcome(self, before, effect))
815    }
816
817    fn set_life_safety_operation_expected_internal(
818        &mut self,
819        operation: LifeSafetyOperation,
820    ) -> Result<(), Error> {
821        self.set_operation_expected(operation);
822        Ok(())
823    }
824
825    fn set_reliability_internal(&mut self, reliability: Reliability) -> Result<(), Error> {
826        self.simulation().set_reliability(reliability)
827    }
828
829    fn set_present_value_internal(&mut self, value: PropertyValue) -> Result<(), Error> {
830        self.present_value = application::life_safety_state(&value)?;
831        Ok(())
832    }
833
834    fn set_tracking_value_internal(&mut self, value: PropertyValue) -> Result<(), Error> {
835        let state = application::life_safety_state(&value)?;
836        self.set_tracking_value(state);
837        Ok(())
838    }
839}
840
841// ===========================================================================
842// Tests
843// ===========================================================================
844
845#[cfg(test)]
846mod tests;
847
848#[cfg(test)]
849mod reset_tests;
850
851#[cfg(test)]
852mod event_state_tests;
853
854#[cfg(test)]
855mod accepted_modes_tests;
856
857#[cfg(test)]
858mod out_of_service_tests;
859
860#[cfg(test)]
861mod application_tests;
862
863#[cfg(test)]
864mod membership_tests;