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