Skip to main content

bacnet_ops/
destination.rs

1//! Turn a [`Target`] into a service destination.
2//!
3//! A local MAC is used as written. A device instance, or a device behind a
4//! router, goes through the client's discovery table, so the `*_from_device`
5//! client calls route it: they unicast to the router with the device's
6//! network and MAC in the NPDU (Clause 6.2.2). ReadProperty,
7//! ReadPropertyMultiple, WriteProperty, WritePropertyMultiple, SubscribeCOV,
8//! GetEventInformation, GetAlarmSummary, GetEnrollmentSummary, ReadRange
9//! (including log paging), AtomicReadFile and AtomicWriteFile,
10//! DeviceCommunicationControl, ReinitializeDevice and both
11//! time synchronization services, CreateObject, DeleteObject and canonical
12//! AcknowledgeAlarm use routed calls.
13//! Commands without a routed call take [`resolve_mac`], which refuses one.
14
15use std::cell::Cell;
16use std::ops::ControlFlow;
17use std::time::Duration;
18
19use bacnet_client::client::{BACnetClient, DeviceEventKind};
20use bacnet_client::discovery::DiscoveredDevice;
21use bacnet_client::log_reader::{LogCursor, LogPage};
22use bacnet_services::alarm_event::GetEventInformationAck;
23use bacnet_services::alarm_summary::GetAlarmSummaryAck;
24use bacnet_services::enrollment_summary::{GetEnrollmentSummaryAck, GetEnrollmentSummaryRequest};
25use bacnet_services::file::{AtomicReadFileAck, FileAccessMethod, FileWriteAccessMethod};
26use bacnet_services::read_property::ReadPropertyACK;
27use bacnet_services::read_range::{ReadRangeReply, ReadRangeRequest, ReadRangeValidation};
28use bacnet_services::rpm::ReadPropertyMultipleACK;
29use bacnet_services::who_is::DeviceInstanceRange;
30use bacnet_services::wpm::WriteAccessSpecification;
31use bacnet_transport::port::TransportPort;
32use bacnet_types::constructed::{PropertyReference, ReadAccessSpecification};
33use bacnet_types::enums::PropertyIdentifier;
34use bacnet_types::error::Error;
35use bacnet_types::primitives::ObjectIdentifier;
36use tokio::sync::broadcast::error::RecvError;
37
38use super::target::{Target, bip6_vmac, parse_target_on};
39use crate::engine::{RpmFailure, rpm_failure, rpm_rows, unreachable};
40use crate::format::{LogColumns, RawRow, RowError, StateTexts};
41
42/// Where a service request goes.
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub enum Destination {
45    /// A MAC on this network.
46    Mac(Vec<u8>),
47    /// A device in the client's discovery table, local or routed.
48    Device(u32),
49}
50
51/// The client's error as the CLI prints it.
52fn text(error: Error) -> String {
53    error.to_string()
54}
55
56fn not_found(instance: u32) -> String {
57    format!("Device {instance} not found. Run 'discover' first or use an address.")
58}
59
60/// Resolve `text` to a [`Destination`]. A routed target asks its network
61/// with a Who-Is when the discovery table doesn't hold it yet, and waits up
62/// to `wait` for the answer.
63pub async fn resolve<T: TransportPort + 'static>(
64    client: &BACnetClient<T>,
65    text: &str,
66    wait: Duration,
67) -> Result<Destination, String> {
68    match parse_target_on(client.transport(), text)? {
69        Target::Mac(mac) => Ok(Destination::Mac(mac)),
70        Target::Bip6(peer) => Ok(Destination::Mac(bip6_vmac(client.transport(), peer).await?)),
71        Target::Instance(instance) => match client.get_device(instance).await {
72            Some(_) => Ok(Destination::Device(instance)),
73            None => Err(not_found(instance)),
74        },
75        Target::Routed { dnet, instance } => {
76            find_routed(client, dnet, &Station::Instance(instance), wait).await
77        }
78        Target::RoutedMac { dnet, mac } => {
79            find_routed(client, dnet, &Station::Mac(mac), wait).await
80        }
81    }
82}
83
84/// Resolve `text` to a MAC on this network, for the commands that have no
85/// routed client call. A device instance is looked up in the discovery
86/// table; a routed target is refused.
87pub async fn resolve_mac<T: TransportPort + 'static>(
88    client: &BACnetClient<T>,
89    text: &str,
90) -> Result<Vec<u8>, String> {
91    match parse_target_on(client.transport(), text)? {
92        Target::Mac(mac) => Ok(mac),
93        Target::Bip6(peer) => bip6_vmac(client.transport(), peer).await,
94        Target::Instance(instance) => match client.get_device(instance).await {
95            Some(device) if device.source_network.is_some() => Err(format!(
96                "Device {instance} is behind a router; only read, readm, read-range, alarms, \
97                 alarm-summary, enrollment-summary, file-read, file-write, write, writem, subscribe, control, reinit, time-sync, create-object, delete-object and ack-alarm reach routed devices"
98            )),
99            Some(device) => Ok(device.mac_address.to_vec()),
100            None => Err(not_found(instance)),
101        },
102        Target::Routed { .. } | Target::RoutedMac { .. } => Err(format!(
103            "'{text}' is a routed target; only read, readm, read-range, alarms, \
104             alarm-summary, enrollment-summary, file-read, file-write, write, writem, subscribe, control, reinit, time-sync, create-object, delete-object and ack-alarm reach routed devices"
105        )),
106    }
107}
108
109/// How a routed target names its device.
110enum Station {
111    Instance(u32),
112    Mac(Vec<u8>),
113}
114
115impl Station {
116    fn matches(&self, dnet: u16, device: &DiscoveredDevice) -> bool {
117        device.source_network == Some(dnet)
118            && match self {
119                Self::Instance(instance) => device.object_identifier.instance_number() == *instance,
120                Self::Mac(mac) => device
121                    .source_address
122                    .as_ref()
123                    .is_some_and(|address| address.as_slice() == mac.as_slice()),
124            }
125    }
126}
127
128/// The instance of the device `station` names on network `dnet`, from the
129/// discovery table or, failing that, from its answer to a Who-Is sent to
130/// that network (to its one instance when the target names it).
131async fn find_routed<T: TransportPort + 'static>(
132    client: &BACnetClient<T>,
133    dnet: u16,
134    station: &Station,
135    wait: Duration,
136) -> Result<Destination, String> {
137    let known = |devices: Vec<DiscoveredDevice>| {
138        devices
139            .iter()
140            .find(|device| station.matches(dnet, device))
141            .map(|device| Destination::Device(device.object_identifier.instance_number()))
142    };
143    if let Some(found) = known(client.discovered_devices().await) {
144        return Ok(found);
145    }
146    // Subscribe before asking, so the answer can't slip in between.
147    let mut events = client.device_events();
148    let range = match station {
149        Station::Instance(instance) => {
150            Some(DeviceInstanceRange::new(*instance, *instance).map_err(text)?)
151        }
152        Station::Mac(_) => None,
153    };
154    client.who_is_network(dnet, range).await.map_err(text)?;
155    let deadline = tokio::time::Instant::now() + wait;
156    loop {
157        match tokio::time::timeout_at(deadline, events.recv()).await {
158            Err(_) => {
159                let name = match station {
160                    Station::Instance(instance) => format!("{dnet}:{instance}"),
161                    // The target parser takes no empty MAC, so this is
162                    // always a station; the fallback is never printed.
163                    Station::Mac(mac) => super::target::routed_station(dnet, mac)
164                        .unwrap_or_else(|| format!("{dnet}:*")),
165                };
166                return Err(format!(
167                    "device {name} did not answer a Who-Is on network {dnet} within {} ms",
168                    wait.as_millis()
169                ));
170            }
171            Ok(Ok(event)) => {
172                if event.kind != DeviceEventKind::Lost && station.matches(dnet, &event.device) {
173                    return Ok(Destination::Device(
174                        event.device.object_identifier.instance_number(),
175                    ));
176                }
177            }
178            // Missed events may include the answer; the table holds it.
179            Ok(Err(RecvError::Lagged(_))) => {
180                if let Some(found) = known(client.discovered_devices().await) {
181                    return Ok(found);
182                }
183            }
184            Ok(Err(RecvError::Closed)) => return Err("the client stopped".into()),
185        }
186    }
187}
188
189impl Destination {
190    /// One checked stream or record file window.
191    pub async fn atomic_read_file<T: TransportPort + 'static>(
192        &self,
193        client: &BACnetClient<T>,
194        file: ObjectIdentifier,
195        access: FileAccessMethod,
196    ) -> Result<AtomicReadFileAck, Error> {
197        match self {
198            Self::Mac(mac) => client.atomic_read_file_decoded(mac, file, access).await,
199            Self::Device(instance) => {
200                client
201                    .atomic_read_file_decoded_from_device(*instance, file, access)
202                    .await
203            }
204        }
205    }
206
207    /// AtomicWriteFile, retaining its raw service acknowledgement.
208    pub async fn atomic_write_file<T: TransportPort + 'static>(
209        &self,
210        client: &BACnetClient<T>,
211        file: ObjectIdentifier,
212        access: FileWriteAccessMethod,
213    ) -> Result<bytes::Bytes, Error> {
214        match self {
215            Self::Mac(mac) => client.atomic_write_file(mac, file, access).await,
216            Self::Device(instance) => {
217                client
218                    .atomic_write_file_from_device(*instance, file, access)
219                    .await
220            }
221        }
222    }
223    /// Create an object, returning the identifier its acknowledgement names.
224    pub async fn create_object<T: TransportPort + 'static>(
225        &self,
226        client: &BACnetClient<T>,
227        specifier: bacnet_services::object_mgmt::ObjectSpecifier,
228        initial_values: Vec<bacnet_services::common::BACnetPropertyValue>,
229    ) -> Result<bacnet_types::primitives::ObjectIdentifier, Error> {
230        match self {
231            Self::Mac(mac) => client.create_object(mac, specifier, initial_values).await,
232            Self::Device(instance) => {
233                client
234                    .create_object_from_device(*instance, specifier, initial_values)
235                    .await
236            }
237        }
238    }
239
240    /// Delete the identified object.
241    pub async fn delete_object<T: TransportPort + 'static>(
242        &self,
243        client: &BACnetClient<T>,
244        object: ObjectIdentifier,
245    ) -> Result<(), Error> {
246        match self {
247            Self::Mac(mac) => client.delete_object(mac, object).await,
248            Self::Device(instance) => client.delete_object_from_device(*instance, object).await,
249        }
250    }
251
252    /// Acknowledge an alarm with the caller's exact request.
253    pub async fn acknowledge_alarm_request<T: TransportPort + 'static>(
254        &self,
255        client: &BACnetClient<T>,
256        request: &bacnet_services::alarm_event::AcknowledgeAlarmRequest,
257    ) -> Result<(), Error> {
258        match self {
259            Self::Mac(mac) => client.acknowledge_alarm_request(mac, request).await,
260            Self::Device(instance) => {
261                client
262                    .acknowledge_alarm_request_from_device(*instance, request)
263                    .await
264            }
265        }
266    }
267
268    /// Dispatch device_communication_control through the resolved device or local MAC.
269    pub async fn device_communication_control<T: TransportPort + 'static>(
270        &self,
271        client: &BACnetClient<T>,
272        state: bacnet_types::enums::EnableDisable,
273        duration: Option<u16>,
274        password: Option<String>,
275    ) -> Result<(), Error> {
276        match self {
277            Self::Mac(mac) => {
278                client
279                    .device_communication_control(mac, state, duration, password)
280                    .await
281            }
282            Self::Device(instance) => {
283                client
284                    .device_communication_control_from_device(*instance, state, duration, password)
285                    .await
286            }
287        }
288    }
289
290    /// Dispatch reinitialize_device through the resolved device or local MAC.
291    pub async fn reinitialize_device<T: TransportPort + 'static>(
292        &self,
293        client: &BACnetClient<T>,
294        state: bacnet_types::enums::ReinitializedState,
295        password: Option<String>,
296    ) -> Result<(), Error> {
297        match self {
298            Self::Mac(mac) => client.reinitialize_device(mac, state, password).await,
299            Self::Device(instance) => {
300                client
301                    .reinitialize_device_from_device(*instance, state, password)
302                    .await
303            }
304        }
305    }
306
307    /// Send the selected unconfirmed time service once.
308    pub async fn time_synchronization<T: TransportPort + 'static>(
309        &self,
310        client: &BACnetClient<T>,
311        date: bacnet_types::primitives::Date,
312        time: bacnet_types::primitives::Time,
313        utc: bool,
314    ) -> Result<(), Error> {
315        match (self, utc) {
316            (Self::Mac(mac), false) => client.time_synchronization(mac, date, time).await,
317            (Self::Mac(mac), true) => client.utc_time_synchronization(mac, date, time).await,
318            (Self::Device(instance), false) => {
319                client
320                    .time_synchronization_from_device(*instance, date, time)
321                    .await
322            }
323            (Self::Device(instance), true) => {
324                client
325                    .utc_time_synchronization_from_device(*instance, date, time)
326                    .await
327            }
328        }
329    }
330
331    /// GetEventInformation from after `last`, decoded.
332    pub async fn get_event_information<T: TransportPort + 'static>(
333        &self,
334        client: &BACnetClient<T>,
335        last: Option<ObjectIdentifier>,
336    ) -> Result<GetEventInformationAck, Error> {
337        match self {
338            Self::Mac(mac) => client.get_event_information(mac, last).await,
339            Self::Device(instance) => {
340                client
341                    .get_event_information_from_device(*instance, last)
342                    .await
343            }
344        }
345    }
346
347    /// GetAlarmSummary, decoded.
348    pub async fn get_alarm_summary<T: TransportPort + 'static>(
349        &self,
350        client: &BACnetClient<T>,
351    ) -> Result<GetAlarmSummaryAck, Error> {
352        match self {
353            Self::Mac(mac) => client.get_alarm_summary(mac).await,
354            Self::Device(instance) => client.get_alarm_summary_from_device(*instance).await,
355        }
356    }
357
358    /// GetEnrollmentSummary with the caller's filters, decoded.
359    pub async fn get_enrollment_summary<T: TransportPort + 'static>(
360        &self,
361        client: &BACnetClient<T>,
362        request: &GetEnrollmentSummaryRequest,
363    ) -> Result<GetEnrollmentSummaryAck, Error> {
364        match self {
365            Self::Mac(mac) => client.get_enrollment_summary(mac, request).await,
366            Self::Device(instance) => {
367                client
368                    .get_enrollment_summary_from_device(*instance, request)
369                    .await
370            }
371        }
372    }
373
374    /// ReadRange with the selected acknowledgement validation.
375    pub async fn read_range<T: TransportPort + 'static>(
376        &self,
377        client: &BACnetClient<T>,
378        request: &ReadRangeRequest,
379        validation: ReadRangeValidation,
380    ) -> Result<ReadRangeReply, Error> {
381        match self {
382            Self::Mac(mac) => client.read_range_with(mac, request, validation).await,
383            Self::Device(instance) => {
384                client
385                    .read_range_from_device_with(*instance, request, validation)
386                    .await
387            }
388        }
389    }
390
391    /// One log page, including all metadata and range subrequests.
392    pub async fn read_log_page<T: TransportPort + 'static>(
393        &self,
394        client: &BACnetClient<T>,
395        log: ObjectIdentifier,
396        cursor: LogCursor,
397        page_size: u16,
398    ) -> Result<LogPage, Error> {
399        match self {
400            Self::Mac(mac) => client.read_log_page(mac, log, cursor, page_size).await,
401            Self::Device(instance) => {
402                client
403                    .read_log_page_from_device(*instance, log, cursor, page_size)
404                    .await
405            }
406        }
407    }
408
409    /// What each column of `log` samples, from its
410    /// Log_DeviceObjectProperty: one ReadProperty for a Trend Log or Trend
411    /// Log Multiple, none for another log. A refused, failed or empty read
412    /// names no column, so the values print as the log gives them (#2091).
413    pub async fn log_columns<T: TransportPort + 'static>(
414        &self,
415        client: &BACnetClient<T>,
416        log: ObjectIdentifier,
417    ) -> LogColumns {
418        if !LogColumns::applies(log.object_type()) {
419            return LogColumns::default();
420        }
421        self.read_property(
422            client,
423            log,
424            PropertyIdentifier::LOG_DEVICE_OBJECT_PROPERTY,
425            None,
426        )
427        .await
428        .map(|ack| LogColumns::decode(&ack.property_value))
429        .unwrap_or_default()
430    }
431
432    /// Name the states `columns` sample with their objects' state texts,
433    /// as [`Engine::log_state_texts`](crate::engine::Engine::log_state_texts)
434    /// does: one request an object, each object once, at most
435    /// [`MAX_STATE_TEXT_OBJECTS`](crate::format::MAX_STATE_TEXT_OBJECTS).
436    /// A reference that names a device is read only when it names this
437    /// destination's device: one addressed by instance, or a MAC the
438    /// client's discovery table holds for exactly one device. A refused
439    /// read leaves its columns named as before, a device that refuses
440    /// ReadPropertyMultiple is read by ReadProperty for the rest of the
441    /// objects, and a device that stops answering ends the reading (#2122).
442    pub async fn log_state_texts<T: TransportPort + 'static>(
443        &self,
444        client: &BACnetClient<T>,
445        columns: &mut LogColumns,
446    ) {
447        let log_device = match self {
448            Self::Device(instance) => Some(*instance),
449            Self::Mac(mac) => {
450                let devices = client.discovered_devices().await;
451                let mut at_mac = devices.iter().filter(|device| {
452                    device.source_network.is_none()
453                        && device.mac_address.as_slice() == mac.as_slice()
454                });
455                // Two instances at one MAC: which one the log is on is unknown.
456                match (at_mac.next(), at_mac.next()) {
457                    (Some(device), None) => Some(device.object_identifier.instance_number()),
458                    _ => None,
459                }
460            }
461        };
462        let no_rpm = Cell::new(false);
463        let no_rpm = &no_rpm;
464        columns
465            .read_state_texts(log_device, |object| async move {
466                match self.state_text_rows(client, object, no_rpm).await {
467                    Ok(rows) => ControlFlow::Continue(StateTexts::from_rows(&rows)),
468                    Err(error) if unreachable(&error) => ControlFlow::Break(()),
469                    Err(_) => ControlFlow::Continue(StateTexts::default()),
470                }
471            })
472            .await;
473    }
474
475    /// `object`'s state texts as the device sent them, for naming the
476    /// states a one-shot read prints (#2132): one ReadProperty of a
477    /// multi-state object's State_Text, or one ReadPropertyMultiple of a
478    /// binary object's Active_Text and Inactive_Text, and nothing for
479    /// another object type. `no_rpm` is the caller's memory of the device
480    /// refusing ReadPropertyMultiple: once set, by the first refusal, the
481    /// pair goes by ReadProperty each, so reading several objects costs
482    /// one refused RPM in all, not one an object.
483    /// Best effort: a refused or failed read gives no texts. The error
484    /// comes back only when the device, or the way to it, stopped
485    /// answering ([`unreachable()`]), so a caller reading several objects
486    /// stops asking. Clean the texts before showing them
487    /// ([`StateTexts::cleaned`]).
488    pub async fn state_texts<T: TransportPort + 'static>(
489        &self,
490        client: &BACnetClient<T>,
491        object: ObjectIdentifier,
492        no_rpm: &Cell<bool>,
493    ) -> Result<StateTexts, Error> {
494        if StateTexts::properties(object.object_type()).is_empty() {
495            return Ok(StateTexts::default());
496        }
497        match self.state_text_rows(client, object, no_rpm).await {
498            Ok(rows) => Ok(StateTexts::from_rows(&rows)),
499            Err(error) if unreachable(&error) => Err(error),
500            Err(_) => Ok(StateTexts::default()),
501        }
502    }
503
504    /// `object`'s state text properties: one ReadProperty for a single
505    /// one, else one ReadPropertyMultiple, or ReadProperty each when the
506    /// device refuses it. `no_rpm` is set once the device has refused
507    /// RPM, and skips it from then on.
508    async fn state_text_rows<T: TransportPort + 'static>(
509        &self,
510        client: &BACnetClient<T>,
511        object: ObjectIdentifier,
512        no_rpm: &Cell<bool>,
513    ) -> Result<Vec<RawRow>, Error> {
514        let properties = StateTexts::properties(object.object_type());
515        let read_each = async || {
516            let mut rows = Vec::with_capacity(properties.len());
517            for property in properties {
518                match self.read_property(client, object, *property, None).await {
519                    Ok(ack) => rows.push((*property, Ok(ack.property_value))),
520                    Err(error) if unreachable(&error) => return Err(error),
521                    Err(error) => rows.push((*property, Err(RowError::from(error)))),
522                }
523            }
524            Ok(rows)
525        };
526        if no_rpm.get() {
527            return read_each().await;
528        }
529        if let [property] = properties {
530            let ack = self.read_property(client, object, *property, None).await?;
531            return Ok(vec![(*property, Ok(ack.property_value))]);
532        }
533        let spec = ReadAccessSpecification {
534            object_identifier: object,
535            list_of_property_references: properties
536                .iter()
537                .map(|property| PropertyReference {
538                    property_identifier: *property,
539                    property_array_index: None,
540                })
541                .collect(),
542        };
543        match self.read_property_multiple(client, vec![spec]).await {
544            Ok(ack) => Ok(rpm_rows(ack)),
545            Err(error) => match rpm_failure(&error) {
546                RpmFailure::Other => Err(error),
547                RpmFailure::Refused => {
548                    no_rpm.set(true);
549                    read_each().await
550                }
551                RpmFailure::TooLarge => read_each().await,
552            },
553        }
554    }
555
556    /// ReadProperty.
557    pub async fn read_property<T: TransportPort + 'static>(
558        &self,
559        client: &BACnetClient<T>,
560        object: ObjectIdentifier,
561        property: PropertyIdentifier,
562        index: Option<u32>,
563    ) -> Result<ReadPropertyACK, Error> {
564        match self {
565            Self::Mac(mac) => client.read_property(mac, object, property, index).await,
566            Self::Device(instance) => {
567                client
568                    .read_property_from_device(*instance, object, property, index)
569                    .await
570            }
571        }
572    }
573
574    /// ReadPropertyMultiple.
575    pub async fn read_property_multiple<T: TransportPort + 'static>(
576        &self,
577        client: &BACnetClient<T>,
578        specs: Vec<ReadAccessSpecification>,
579    ) -> Result<ReadPropertyMultipleACK, Error> {
580        match self {
581            Self::Mac(mac) => client.read_property_multiple(mac, specs).await,
582            Self::Device(instance) => {
583                client
584                    .read_property_multiple_from_device(*instance, specs)
585                    .await
586            }
587        }
588    }
589
590    /// WriteProperty of an encoded value.
591    pub async fn write_property<T: TransportPort + 'static>(
592        &self,
593        client: &BACnetClient<T>,
594        object: ObjectIdentifier,
595        property: PropertyIdentifier,
596        index: Option<u32>,
597        value: Vec<u8>,
598        priority: Option<u8>,
599    ) -> Result<(), Error> {
600        match self {
601            Self::Mac(mac) => {
602                client
603                    .write_property(mac, object, property, index, value, priority)
604                    .await
605            }
606            Self::Device(instance) => {
607                client
608                    .write_property_to_device(*instance, object, property, index, value, priority)
609                    .await
610            }
611        }
612    }
613
614    /// WritePropertyMultiple.
615    pub async fn write_property_multiple<T: TransportPort + 'static>(
616        &self,
617        client: &BACnetClient<T>,
618        specs: Vec<WriteAccessSpecification>,
619    ) -> Result<(), Error> {
620        match self {
621            Self::Mac(mac) => client.write_property_multiple(mac, specs).await,
622            Self::Device(instance) => {
623                client
624                    .write_property_multiple_to_device(*instance, specs)
625                    .await
626            }
627        }
628    }
629
630    /// SubscribeCOV for `object`, from process `process`.
631    pub async fn subscribe_cov<T: TransportPort + 'static>(
632        &self,
633        client: &BACnetClient<T>,
634        process: u32,
635        object: ObjectIdentifier,
636        confirmed: bool,
637        lifetime: Option<u32>,
638    ) -> Result<(), Error> {
639        match self {
640            Self::Mac(mac) => {
641                client
642                    .subscribe_cov(mac, process, object, confirmed, lifetime)
643                    .await
644            }
645            Self::Device(instance) => {
646                client
647                    .subscribe_cov_to_device(*instance, process, object, confirmed, lifetime)
648                    .await
649            }
650        }
651    }
652
653    /// Cancel process `process`'s subscription to `object`.
654    pub async fn unsubscribe_cov<T: TransportPort + 'static>(
655        &self,
656        client: &BACnetClient<T>,
657        process: u32,
658        object: ObjectIdentifier,
659    ) -> Result<(), Error> {
660        match self {
661            Self::Mac(mac) => client.unsubscribe_cov(mac, process, object).await,
662            Self::Device(instance) => {
663                client
664                    .unsubscribe_cov_to_device(*instance, process, object)
665                    .await
666            }
667        }
668    }
669}