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