Skip to main content

bacnet_objects/access_control/
zone.rs

1use bacnet_types::enums::AccessZoneOccupancyState;
2
3use super::zone_occupancy::{adjusted, Occupancy};
4use super::zone_out_of_service::ZoneState;
5use super::*;
6use crate::event::state_reporting::{ChangeOfStateReporting, ListedStates};
7
8// AccessZoneObject (type 36)
9// ---------------------------------------------------------------------------
10
11/// BACnet Access Zone object (type 36).
12///
13/// Represents a physical zone or area controlled by access points. Table
14/// 12-37 has neither Present_Value nor Access_Doors, so the object serves
15/// neither (#1064).
16///
17/// The zone counts occupancy (#1284): Occupancy_State follows
18/// Occupancy_Count, the occupancy limits and Occupancy_Count_Enable, and a
19/// client adjusts the count through Adjust_Value. The module
20/// `zone_occupancy` has the rules.
21///
22/// The zone reports intrinsically on Occupancy_State with the
23/// CHANGE_OF_STATE algorithm (Clause 12.32; #1305): Event_State goes
24/// OFFNORMAL once Occupancy_State has stayed in Alarm_Values for Time_Delay
25/// seconds and back to NORMAL once it has stayed out of them for
26/// Time_Delay_Normal, and a Reliability other than NO_FAULT_DETECTED puts it
27/// in FAULT. The event rows are served and written through
28/// `ChangeOfStateReporting`, and the server sends the notifications to the
29/// Notification Class recipients. Alarm_Values never holds NORMAL, the state
30/// with no limit crossed: listed, it would put the zone in alarm whenever its
31/// count sat inside its limits (#1401).
32///
33/// Entry_Points and Exit_Points list the Access Points leading into and out
34/// of the zone as `BACnetDeviceObjectReference` values (Clauses 12.32.23 and
35/// 12.32.24; #1306), set by the application and read-only over the network.
36///
37/// While Out_Of_Service is TRUE a client can simulate Occupancy_Count and
38/// Reliability by writing them, and the zone's own values come back on the
39/// return to service (Table 12-37 footnote 1; #1247). The module
40/// `zone_out_of_service` has the details.
41pub struct AccessZoneObject {
42    oid: ObjectIdentifier,
43    name: String,
44    description: String,
45    global_identifier: u64,
46    /// Occupancy_Count and Reliability as served: the zone's own, or a
47    /// client's simulation while Out_Of_Service is TRUE.
48    state: ZoneState,
49    /// The zone's own values, put aside while Out_Of_Service is TRUE.
50    device_state: Option<ZoneState>,
51    /// Occupancy_Count_Enable, Adjust_Value and the occupancy limits.
52    occupancy: Occupancy,
53    entry_points: Vec<BACnetDeviceObjectReference>,
54    exit_points: Vec<BACnetDeviceObjectReference>,
55    out_of_service: bool,
56    /// Intrinsic reporting on Occupancy_State.
57    reporting: ChangeOfStateReporting,
58}
59
60impl AccessZoneObject {
61    /// Create a new Access Zone object.
62    pub fn new(instance: u32, name: impl Into<String>) -> Result<Self, Error> {
63        let oid = ObjectIdentifier::new(ObjectType::ACCESS_ZONE, instance)?;
64        Ok(Self {
65            oid,
66            name: name.into(),
67            description: String::new(),
68            global_identifier: 0,
69            state: ZoneState {
70                occupancy_count: 0,
71                reliability: Reliability::NO_FAULT_DETECTED,
72            },
73            device_state: None,
74            occupancy: Occupancy::NEW,
75            entry_points: Vec::new(),
76            exit_points: Vec::new(),
77            out_of_service: false,
78            reporting: ChangeOfStateReporting::new(ALARM_STATES),
79        })
80    }
81
82    /// Set Entry_Points, the Access Points leading into the zone
83    /// (Clause 12.32.23). A reference with no device identifier names an
84    /// object in this device.
85    ///
86    /// Each reference has to name an Access Point object, and its device
87    /// identifier, when given, a Device object (#1285); a list breaking
88    /// either rule is refused with VALUE_OUT_OF_RANGE and the points set
89    /// before are kept. The list is read-only over the network.
90    pub fn set_entry_points(
91        &mut self,
92        points: impl IntoIterator<Item = impl Into<BACnetDeviceObjectReference>>,
93    ) -> Result<(), Error> {
94        self.entry_points = references_to(ObjectType::ACCESS_POINT, points)?;
95        Ok(())
96    }
97
98    /// Set Exit_Points, the Access Points leading out of the zone
99    /// (Clause 12.32.24), with the checks [`Self::set_entry_points`] makes.
100    pub fn set_exit_points(
101        &mut self,
102        points: impl IntoIterator<Item = impl Into<BACnetDeviceObjectReference>>,
103    ) -> Result<(), Error> {
104        self.exit_points = references_to(ObjectType::ACCESS_POINT, points)?;
105        Ok(())
106    }
107
108    /// Set Alarm_Values, the occupancy states the zone reports as offnormal
109    /// (Clause 12.32.27). NORMAL, or a value outside the
110    /// BACnetAccessZoneOccupancyState production, named or proprietary (64
111    /// to 65535), is refused with VALUE_OUT_OF_RANGE naming the element, and
112    /// the values set before are kept. Clients can write the list too, as
113    /// they can the zone's other event configuration (Time_Delay,
114    /// Notification_Class, Event_Enable and the rest), and meet the same
115    /// checks.
116    pub fn set_alarm_values(
117        &mut self,
118        states: impl IntoIterator<Item = AccessZoneOccupancyState>,
119    ) -> Result<(), Error> {
120        self.reporting
121            .set_alarm_values(states.into_iter().map(|state| state.to_raw()).collect())
122    }
123
124    /// Record the zone's occupancy count, as the application's counting
125    /// works it out (Clause 12.32.11). Over the network Occupancy_Count takes
126    /// writes only while Out_Of_Service is TRUE; meanwhile the client's
127    /// simulated count keeps being served, and this one takes over on the
128    /// return to service. While counting is off the count stays zero and the
129    /// call changes nothing.
130    pub fn set_occupancy_count(&mut self, count: u64) {
131        if self.occupancy.enabled {
132            self.device_state_mut().occupancy_count = count;
133        }
134    }
135
136    /// Set Occupancy_Count_Enable (Clause 12.32.12), read-only over the
137    /// network. Turning counting off zeroes Occupancy_Count, the count set
138    /// aside while out of service and Adjust_Value, and Occupancy_State reads
139    /// DISABLED until counting is turned on again, starting from zero.
140    pub fn set_occupancy_count_enable(&mut self, enabled: bool) {
141        self.occupancy.enabled = enabled;
142        if !enabled {
143            self.occupancy.adjust_value = 0;
144            self.state.occupancy_count = 0;
145            if let Some(own) = &mut self.device_state {
146                own.occupancy_count = 0;
147            }
148        }
149    }
150
151    /// Set Occupancy_Lower_Limit and Occupancy_Upper_Limit together, zero
152    /// meaning no limit (Clauses 12.32.14 and 12.32.15). Both are read-only
153    /// over the network. A nonzero upper limit at or below the lower one is
154    /// refused with VALUE_OUT_OF_RANGE and both limits are kept.
155    pub fn set_occupancy_limits(&mut self, lower: u64, upper: u64) -> Result<(), Error> {
156        self.occupancy.set_limits(lower, upper)
157    }
158
159    /// Occupancy_State as served: DISABLED while counting is off, else the
160    /// served count against the limits (Clause 12.32.6).
161    pub fn occupancy_state(&self) -> AccessZoneOccupancyState {
162        self.occupancy.state(self.state.occupancy_count)
163    }
164
165    /// The value the event algorithm watches: Occupancy_State, raw.
166    fn watched_state(&self) -> u32 {
167        self.occupancy_state().to_raw()
168    }
169
170    /// The Reliability served, which the algorithm's fault check reads.
171    fn served_reliability(&self) -> Reliability {
172        self.state.reliability
173    }
174
175    /// A client's Adjust_Value write (Clause 12.32.13): an Integer, else
176    /// INVALID_DATA_TYPE. With counting on, the value is kept and moves the
177    /// count served, unless Out_Of_Service is TRUE (12.32.10); with counting
178    /// off, Adjust_Value stays zero.
179    fn write_adjust_value(&mut self, value: &PropertyValue) -> Result<(), Error> {
180        let PropertyValue::Signed(adjust) = *value else {
181            return Err(common::invalid_data_type_error());
182        };
183        if !self.occupancy.enabled {
184            return Ok(());
185        }
186        self.occupancy.adjust_value = adjust;
187        if !self.out_of_service {
188            self.state.occupancy_count = adjusted(self.state.occupancy_count, adjust);
189        }
190        Ok(())
191    }
192
193    /// The zone's own values: the ones put aside while out of service, else
194    /// the ones served.
195    fn device_state_mut(&mut self) -> &mut ZoneState {
196        self.device_state.as_mut().unwrap_or(&mut self.state)
197    }
198}
199
200impl BACnetObject for AccessZoneObject {
201    fn object_identifier(&self) -> ObjectIdentifier {
202        self.oid
203    }
204
205    fn object_name(&self) -> &str {
206        &self.name
207    }
208
209    fn read_property(
210        &self,
211        property: PropertyIdentifier,
212        array_index: Option<u32>,
213    ) -> Result<PropertyValue, Error> {
214        if let Some(result) = crate::common::read_identity_properties!(self, property, array_index)
215        {
216            return result;
217        }
218        if let Some(result) = self.reporting.read(property, array_index) {
219            return result;
220        }
221        match property {
222            p if p == PropertyIdentifier::OBJECT_TYPE => {
223                Ok(PropertyValue::Enumerated(ObjectType::ACCESS_ZONE.to_raw()))
224            }
225            // OVERRIDDEN stays clear (Clause 12.32.7).
226            p if p == PropertyIdentifier::STATUS_FLAGS => Ok(common::compute_status_flags(
227                StatusFlags::empty(),
228                self.state.reliability,
229                self.out_of_service,
230                self.reporting.event_state(),
231            )),
232            p if p == PropertyIdentifier::OUT_OF_SERVICE => {
233                Ok(PropertyValue::Boolean(self.out_of_service))
234            }
235            p if p == PropertyIdentifier::RELIABILITY => {
236                Ok(PropertyValue::Enumerated(self.state.reliability.to_raw()))
237            }
238            p if p == PropertyIdentifier::GLOBAL_IDENTIFIER => {
239                Ok(PropertyValue::Unsigned(self.global_identifier))
240            }
241            p if p == PropertyIdentifier::OCCUPANCY_COUNT => {
242                Ok(PropertyValue::Unsigned(self.state.occupancy_count))
243            }
244            p if p == PropertyIdentifier::OCCUPANCY_STATE => {
245                Ok(PropertyValue::Enumerated(self.occupancy_state().to_raw()))
246            }
247            p if p == PropertyIdentifier::OCCUPANCY_COUNT_ENABLE => {
248                Ok(PropertyValue::Boolean(self.occupancy.enabled))
249            }
250            p if p == PropertyIdentifier::ADJUST_VALUE => {
251                Ok(PropertyValue::Signed(self.occupancy.adjust_value))
252            }
253            p if p == PropertyIdentifier::OCCUPANCY_UPPER_LIMIT => {
254                Ok(PropertyValue::Unsigned(self.occupancy.upper_limit))
255            }
256            p if p == PropertyIdentifier::OCCUPANCY_LOWER_LIMIT => {
257                Ok(PropertyValue::Unsigned(self.occupancy.lower_limit))
258            }
259            p if p == PropertyIdentifier::ENTRY_POINTS => {
260                Ok(crate::device_reference::reference_list(&self.entry_points))
261            }
262            p if p == PropertyIdentifier::EXIT_POINTS => {
263                Ok(crate::device_reference::reference_list(&self.exit_points))
264            }
265            _ => Err(common::unknown_property_error()),
266        }
267    }
268
269    fn write_property(
270        &mut self,
271        property: PropertyIdentifier,
272        array_index: Option<u32>,
273        value: PropertyValue,
274        _priority: Option<u8>,
275    ) -> Result<(), Error> {
276        // The zone starts in service and only this write moves Out_Of_Service,
277        // so the entry edge is always seen and there is no fallback.
278        if let Some(result) = common::write_out_of_service_with_restore(
279            &mut self.out_of_service,
280            &mut self.state,
281            &mut self.device_state,
282            None,
283            property,
284            &value,
285        ) {
286            return result;
287        }
288        if let Some(result) = common::write_description(&mut self.description, property, &value) {
289            return result;
290        }
291        if let Some(result) = self.state.write(
292            self.out_of_service,
293            self.occupancy.enabled,
294            property,
295            &value,
296        ) {
297            return result;
298        }
299        if property == PropertyIdentifier::ADJUST_VALUE {
300            return self.write_adjust_value(&value);
301        }
302        if let Some(result) = self.reporting.write(property, array_index, &value) {
303            return result;
304        }
305        match property {
306            p if p == PropertyIdentifier::GLOBAL_IDENTIFIER => {
307                if let PropertyValue::Unsigned(v) = value {
308                    self.global_identifier = v;
309                    Ok(())
310                } else {
311                    Err(common::invalid_data_type_error())
312                }
313            }
314            _ => Err(common::unhandled_write_error(
315                self.property_metadata().as_ref(),
316                property,
317                array_index,
318            )),
319        }
320    }
321
322    fn property_metadata(&self) -> Cow<'_, [crate::property_metadata::PropertyMetadata]> {
323        super::metadata_topology::for_access_zone_object(self)
324    }
325
326    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
327        crate::property_metadata::property_list_from_metadata(self.property_metadata().as_ref())
328    }
329
330    crate::event::impl_change_of_state_reporting!(
331        reporting,
332        Self::watched_state,
333        Self::served_reliability
334    );
335
336    /// The Reliability the zone's fault detection works out. Like the other
337    /// Reliability carriers, the zone refuses it with WRITE_ACCESS_DENIED
338    /// while Out_Of_Service is TRUE, so a client's simulated value stays
339    /// served; the return to service brings back the value from before. A
340    /// value outside the BACnetReliability production is refused with
341    /// VALUE_OUT_OF_RANGE.
342    fn set_reliability_internal(&mut self, reliability: Reliability) -> Result<(), Error> {
343        if self.out_of_service {
344            return Err(common::write_access_denied_error());
345        }
346        self.state.reliability = checked_reliability(reliability)?;
347        Ok(())
348    }
349}
350
351/// The states Alarm_Values can hold: a BACnetAccessZoneOccupancyState other
352/// than NORMAL.
353const ALARM_STATES: ListedStates = ListedStates {
354    normal: AccessZoneOccupancyState::NORMAL.to_raw(),
355    in_range: occupancy_state_in_range,
356};
357
358/// Whether `raw` is a BACnetAccessZoneOccupancyState: a named state or a
359/// proprietary one from 64 to 65535 (Clause 23.1).
360fn occupancy_state_in_range(raw: u32) -> bool {
361    AccessZoneOccupancyState::ALL_NAMED
362        .iter()
363        .any(|&(_, named)| named.to_raw() == raw)
364        || (64..=65_535).contains(&raw)
365}
366
367// ---------------------------------------------------------------------------