1use std::collections::HashMap;
4use std::time::Instant;
5
6use bacnet_encoding::npdu::NpduAddress;
7use bacnet_types::enums::PropertyIdentifier;
8use bacnet_types::primitives::ObjectIdentifier;
9use bacnet_types::MacAddr;
10
11#[derive(Debug, Clone)]
13pub struct CovSubscription {
14 pub subscriber_mac: MacAddr,
16 pub subscriber_network: Option<NpduAddress>,
18 pub subscriber_process_identifier: u32,
20 pub monitored_object_identifier: ObjectIdentifier,
22 pub issue_confirmed_notifications: bool,
24 pub expires_at: Option<Instant>,
26 pub last_notified_value: Option<f32>,
29 pub monitored_property: Option<PropertyIdentifier>,
31 pub monitored_property_array_index: Option<u32>,
33 pub cov_increment: Option<f32>,
35 pub notification_kind: CovNotificationKind,
37 pub timestamped: bool,
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum CovNotificationKind {
44 Single,
46 Multiple,
48}
49
50type SubKey = (
55 MacAddr,
56 Option<NpduAddress>,
57 u32,
58 ObjectIdentifier,
59 Option<PropertyIdentifier>,
60);
61
62#[derive(Debug, Default)]
64pub struct CovSubscriptionTable {
65 subs: HashMap<SubKey, CovSubscription>,
66}
67
68impl CovSubscriptionTable {
69 pub fn new() -> Self {
70 Self {
71 subs: HashMap::new(),
72 }
73 }
74
75 pub fn subscribe(&mut self, sub: CovSubscription) {
77 let key = (
78 sub.subscriber_mac.clone(),
79 sub.subscriber_network.clone(),
80 sub.subscriber_process_identifier,
81 sub.monitored_object_identifier,
82 sub.monitored_property,
83 );
84 self.subs.insert(key, sub);
85 }
86
87 pub fn contains(
89 &self,
90 mac: &MacAddr,
91 network: Option<&NpduAddress>,
92 process_id: u32,
93 monitored_object: ObjectIdentifier,
94 monitored_property: Option<PropertyIdentifier>,
95 ) -> bool {
96 self.subs.contains_key(&(
97 mac.clone(),
98 network.cloned(),
99 process_id,
100 monitored_object,
101 monitored_property,
102 ))
103 }
104
105 pub fn unsubscribe(
107 &mut self,
108 mac: &[u8],
109 process_id: u32,
110 monitored_object: ObjectIdentifier,
111 ) -> bool {
112 self.unsubscribe_at(mac, None, process_id, monitored_object)
113 }
114
115 pub fn unsubscribe_at(
117 &mut self,
118 mac: &[u8],
119 network: Option<&NpduAddress>,
120 process_id: u32,
121 monitored_object: ObjectIdentifier,
122 ) -> bool {
123 let key = (
124 MacAddr::from_slice(mac),
125 network.cloned(),
126 process_id,
127 monitored_object,
128 None,
129 );
130 self.subs.remove(&key).is_some()
131 }
132
133 pub fn unsubscribe_property(
135 &mut self,
136 mac: &[u8],
137 process_id: u32,
138 monitored_object: ObjectIdentifier,
139 monitored_property: PropertyIdentifier,
140 ) -> bool {
141 self.unsubscribe_property_at(mac, None, process_id, monitored_object, monitored_property)
142 }
143
144 pub fn unsubscribe_property_at(
146 &mut self,
147 mac: &[u8],
148 network: Option<&NpduAddress>,
149 process_id: u32,
150 monitored_object: ObjectIdentifier,
151 monitored_property: PropertyIdentifier,
152 ) -> bool {
153 let key = (
154 MacAddr::from_slice(mac),
155 network.cloned(),
156 process_id,
157 monitored_object,
158 Some(monitored_property),
159 );
160 self.subs.remove(&key).is_some()
161 }
162
163 pub fn unsubscribe_cov_multiple_context(
165 &mut self,
166 mac: &[u8],
167 network: Option<&NpduAddress>,
168 process_id: u32,
169 confirmed: bool,
170 ) {
171 let mac = MacAddr::from_slice(mac);
172 self.subs.retain(|_, sub| {
173 !(sub.notification_kind == CovNotificationKind::Multiple
174 && sub.subscriber_mac == mac
175 && sub.subscriber_network == network.cloned()
176 && sub.subscriber_process_identifier == process_id
177 && sub.issue_confirmed_notifications == confirmed)
178 });
179 }
180
181 pub fn unsubscribe_cov_multiple_property_at(
183 &mut self,
184 mac: &[u8],
185 network: Option<&NpduAddress>,
186 process_id: u32,
187 confirmed: bool,
188 monitored_object: ObjectIdentifier,
189 monitored_property: PropertyIdentifier,
190 ) {
191 let mac = MacAddr::from_slice(mac);
192 self.subs.retain(|_, sub| {
193 !(sub.notification_kind == CovNotificationKind::Multiple
194 && sub.subscriber_mac == mac
195 && sub.subscriber_network == network.cloned()
196 && sub.subscriber_process_identifier == process_id
197 && sub.issue_confirmed_notifications == confirmed
198 && sub.monitored_object_identifier == monitored_object
199 && sub.monitored_property == Some(monitored_property))
200 });
201 }
202
203 pub fn refresh_cov_multiple_context_lifetime(
205 &mut self,
206 mac: &[u8],
207 network: Option<&NpduAddress>,
208 process_id: u32,
209 confirmed: bool,
210 expires_at: Option<Instant>,
211 ) {
212 let mac = MacAddr::from_slice(mac);
213 for sub in self.subs.values_mut() {
214 if sub.notification_kind == CovNotificationKind::Multiple
215 && sub.subscriber_mac == mac
216 && sub.subscriber_network == network.cloned()
217 && sub.subscriber_process_identifier == process_id
218 && sub.issue_confirmed_notifications == confirmed
219 {
220 sub.expires_at = expires_at;
221 }
222 }
223 }
224
225 pub fn remove_for_object(&mut self, oid: ObjectIdentifier) {
227 self.subs.retain(|k, _| k.3 != oid);
228 }
229
230 pub fn subscriptions_for(&mut self, oid: &ObjectIdentifier) -> Vec<&CovSubscription> {
232 let now = Instant::now();
233 self.subs
234 .retain(|_, sub| sub.expires_at.is_none_or(|exp| exp > now));
235 self.subs
236 .values()
237 .filter(|sub| sub.monitored_object_identifier == *oid)
238 .collect()
239 }
240
241 pub fn set_last_notified_value(
243 &mut self,
244 mac: &[u8],
245 network: Option<&NpduAddress>,
246 process_id: u32,
247 monitored_object: ObjectIdentifier,
248 monitored_property: Option<PropertyIdentifier>,
249 value: f32,
250 ) {
251 let key = (
252 MacAddr::from_slice(mac),
253 network.cloned(),
254 process_id,
255 monitored_object,
256 monitored_property,
257 );
258 if let Some(sub) = self.subs.get_mut(&key) {
259 sub.last_notified_value = Some(value);
260 }
261 }
262
263 pub fn should_notify(
271 sub: &CovSubscription,
272 current_value: Option<f32>,
273 cov_increment: Option<f32>,
274 ) -> bool {
275 match (cov_increment, current_value) {
276 (Some(increment), Some(current)) => {
277 match sub.last_notified_value {
278 None => true, Some(last) => (current - last).abs() >= increment,
280 }
281 }
282 _ => true, }
284 }
285
286 pub fn len(&self) -> usize {
288 self.subs.len()
289 }
290
291 pub fn is_empty(&self) -> bool {
293 self.subs.is_empty()
294 }
295
296 pub fn purge_expired(&mut self) -> usize {
298 let before = self.subs.len();
299 let now = Instant::now();
300 self.subs
301 .retain(|_, sub| sub.expires_at.is_none_or(|exp| exp > now));
302 before - self.subs.len()
303 }
304}
305
306#[cfg(test)]
307mod tests {
308 use super::*;
309 use bacnet_types::enums::ObjectType;
310 use std::time::Duration;
311
312 fn ai1() -> ObjectIdentifier {
313 ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap()
314 }
315
316 fn ai2() -> ObjectIdentifier {
317 ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 2).unwrap()
318 }
319
320 fn make_sub(mac: &[u8], process_id: u32, oid: ObjectIdentifier) -> CovSubscription {
321 CovSubscription {
322 subscriber_mac: MacAddr::from_slice(mac),
323 subscriber_network: None,
324 subscriber_process_identifier: process_id,
325 monitored_object_identifier: oid,
326 issue_confirmed_notifications: false,
327 expires_at: None,
328 last_notified_value: None,
329 monitored_property: None,
330 monitored_property_array_index: None,
331 cov_increment: None,
332 notification_kind: CovNotificationKind::Single,
333 timestamped: false,
334 }
335 }
336
337 #[test]
338 fn subscribe_and_lookup() {
339 let mut table = CovSubscriptionTable::new();
340 table.subscribe(make_sub(&[1, 2, 3], 1, ai1()));
341 assert_eq!(table.len(), 1);
342 assert_eq!(table.subscriptions_for(&ai1()).len(), 1);
343 assert_eq!(table.subscriptions_for(&ai2()).len(), 0);
344 }
345
346 #[test]
347 fn unsubscribe() {
348 let mut table = CovSubscriptionTable::new();
349 table.subscribe(make_sub(&[1, 2, 3], 1, ai1()));
350 assert!(table.unsubscribe(&[1, 2, 3], 1, ai1()));
351 assert!(!table.unsubscribe(&[1, 2, 3], 1, ai1())); assert!(table.is_empty());
353 }
354
355 #[test]
356 fn expired_subscriptions_purged_on_lookup() {
357 let mut table = CovSubscriptionTable::new();
358 let mut sub = make_sub(&[1, 2, 3], 1, ai1());
359 sub.expires_at = Some(Instant::now() - Duration::from_secs(1)); table.subscribe(sub);
361 assert_eq!(table.subscriptions_for(&ai1()).len(), 0);
362 assert!(table.is_empty());
363 }
364
365 #[test]
366 fn multiple_subscribers_same_object() {
367 let mut table = CovSubscriptionTable::new();
368 table.subscribe(make_sub(&[1, 2, 3], 1, ai1()));
369 table.subscribe(make_sub(&[4, 5, 6], 2, ai1()));
370 assert_eq!(table.subscriptions_for(&ai1()).len(), 2);
371 }
372
373 #[test]
374 fn should_notify_no_increment_always_fires() {
375 let sub = make_sub(&[1, 2, 3], 1, ai1());
376 assert!(CovSubscriptionTable::should_notify(&sub, Some(1.0), None));
378 }
379
380 #[test]
381 fn should_notify_first_notification_always_fires() {
382 let sub = make_sub(&[1, 2, 3], 1, ai1());
383 assert!(CovSubscriptionTable::should_notify(
385 &sub,
386 Some(72.5),
387 Some(1.0)
388 ));
389 }
390
391 #[test]
392 fn should_notify_change_exceeds_increment() {
393 let mut sub = make_sub(&[1, 2, 3], 1, ai1());
394 sub.last_notified_value = Some(70.0);
395 assert!(CovSubscriptionTable::should_notify(
397 &sub,
398 Some(72.5),
399 Some(1.0)
400 ));
401 }
402
403 #[test]
404 fn should_notify_change_below_increment() {
405 let mut sub = make_sub(&[1, 2, 3], 1, ai1());
406 sub.last_notified_value = Some(72.0);
407 assert!(!CovSubscriptionTable::should_notify(
409 &sub,
410 Some(72.3),
411 Some(1.0)
412 ));
413 }
414
415 #[test]
416 fn should_notify_exact_increment() {
417 let mut sub = make_sub(&[1, 2, 3], 1, ai1());
418 sub.last_notified_value = Some(70.0);
419 assert!(CovSubscriptionTable::should_notify(
421 &sub,
422 Some(71.0),
423 Some(1.0)
424 ));
425 }
426
427 #[test]
428 fn should_notify_zero_increment_always_fires() {
429 let mut sub = make_sub(&[1, 2, 3], 1, ai1());
430 sub.last_notified_value = Some(72.0);
431 assert!(CovSubscriptionTable::should_notify(
433 &sub,
434 Some(72.001),
435 Some(0.0)
436 ));
437 }
438
439 #[test]
440 fn set_last_notified_value_updates() {
441 let mut table = CovSubscriptionTable::new();
442 table.subscribe(make_sub(&[1, 2, 3], 1, ai1()));
443 table.set_last_notified_value(&[1, 2, 3], None, 1, ai1(), None, 72.5);
444
445 let subs = table.subscriptions_for(&ai1());
446 assert_eq!(subs[0].last_notified_value, Some(72.5));
447 }
448
449 #[test]
450 fn upsert_replaces_existing() {
451 let mut table = CovSubscriptionTable::new();
452 let mut sub = make_sub(&[1, 2, 3], 1, ai1());
453 sub.issue_confirmed_notifications = false;
454 table.subscribe(sub);
455 let mut sub2 = make_sub(&[1, 2, 3], 1, ai1());
457 sub2.issue_confirmed_notifications = true;
458 table.subscribe(sub2);
459 assert_eq!(table.len(), 1);
460 let subs = table.subscriptions_for(&ai1());
461 assert!(subs[0].issue_confirmed_notifications);
462 }
463
464 #[test]
465 fn same_subscriber_different_objects_both_exist() {
466 let mut table = CovSubscriptionTable::new();
467 table.subscribe(make_sub(&[1, 2, 3], 1, ai1()));
469 table.subscribe(make_sub(&[1, 2, 3], 1, ai2()));
470 assert_eq!(table.len(), 2);
471 assert_eq!(table.subscriptions_for(&ai1()).len(), 1);
472 assert_eq!(table.subscriptions_for(&ai2()).len(), 1);
473 }
474
475 #[test]
476 fn purge_expired_removes_stale_subscriptions() {
477 let mut table = CovSubscriptionTable::new();
478 let mut sub1 = make_sub(&[1, 2, 3], 1, ai1());
479 sub1.expires_at = Some(Instant::now() - Duration::from_secs(10));
480 table.subscribe(sub1);
481
482 let mut sub2 = make_sub(&[4, 5, 6], 2, ai1());
483 sub2.expires_at = None; table.subscribe(sub2);
485
486 let purged = table.purge_expired();
487 assert_eq!(purged, 1);
488 assert_eq!(table.len(), 1);
489 }
490
491 #[test]
492 fn cov_multiple_context_lifetime_refreshes_and_expires() {
493 let mut table = CovSubscriptionTable::new();
494 let original_expiry = Instant::now() + Duration::from_secs(30);
495 let refreshed_expiry = Instant::now() + Duration::from_secs(60);
496
497 let mut present_value = make_sub(&[1, 2, 3], 1, ai1());
498 present_value.notification_kind = CovNotificationKind::Multiple;
499 present_value.monitored_property = Some(PropertyIdentifier::PRESENT_VALUE);
500 present_value.expires_at = Some(original_expiry);
501 table.subscribe(present_value);
502
503 let mut status_flags = make_sub(&[1, 2, 3], 1, ai1());
504 status_flags.notification_kind = CovNotificationKind::Multiple;
505 status_flags.monitored_property = Some(PropertyIdentifier::STATUS_FLAGS);
506 status_flags.expires_at = Some(original_expiry);
507 table.subscribe(status_flags);
508
509 let mut single = make_sub(&[1, 2, 3], 1, ai1());
510 single.expires_at = Some(original_expiry);
511 table.subscribe(single);
512
513 table.refresh_cov_multiple_context_lifetime(
514 &[1, 2, 3],
515 None,
516 1,
517 false,
518 Some(refreshed_expiry),
519 );
520
521 let multiple_expiries: Vec<_> = table
522 .subs
523 .values()
524 .filter(|sub| sub.notification_kind == CovNotificationKind::Multiple)
525 .map(|sub| sub.expires_at)
526 .collect();
527 assert_eq!(
528 multiple_expiries,
529 vec![Some(refreshed_expiry), Some(refreshed_expiry)]
530 );
531 assert!(table
532 .subs
533 .values()
534 .any(|sub| sub.notification_kind == CovNotificationKind::Single
535 && sub.expires_at == Some(original_expiry)));
536
537 table.refresh_cov_multiple_context_lifetime(
538 &[1, 2, 3],
539 None,
540 1,
541 false,
542 Some(Instant::now() - Duration::from_secs(1)),
543 );
544
545 assert_eq!(table.purge_expired(), 2);
546 assert_eq!(table.len(), 1);
547 assert!(table.subs.values().all(|sub| {
548 sub.notification_kind == CovNotificationKind::Single
549 && sub.expires_at == Some(original_expiry)
550 }));
551 }
552
553 #[test]
554 fn purge_expired_returns_zero_when_none_expired() {
555 let mut table = CovSubscriptionTable::new();
556 table.subscribe(make_sub(&[1, 2, 3], 1, ai1()));
557 let purged = table.purge_expired();
558 assert_eq!(purged, 0);
559 assert_eq!(table.len(), 1);
560 }
561}