nmrs 2.3.0

A Rust library for NetworkManager over D-Bus
Documentation
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
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
use tokio::sync::watch;
use zbus::Connection;

use crate::Result;
use crate::api::models::{Device, Network, NetworkInfo, WifiSecurity};
use crate::core::bluetooth::connect_bluetooth;
use crate::core::connection::{
    connect, connect_wired, disconnect, forget_by_name_and_type, get_device_by_interface,
    is_connected,
};
use crate::core::connection_settings::{
    get_saved_connection_path, has_saved_connection, list_saved_connections,
};
use crate::core::device::{
    is_connecting, list_bluetooth_devices, list_devices, set_wifi_enabled, wait_for_wifi_ready,
    wifi_enabled, wifi_hardware_enabled,
};
use crate::core::scan::{current_network, list_networks, scan_networks};
use crate::core::vpn::{connect_vpn, disconnect_vpn, get_vpn_info, list_vpn_connections};
use crate::models::{
    BluetoothDevice, BluetoothIdentity, VpnConnection, VpnConnectionInfo, VpnCredentials,
};
use crate::monitoring::device as device_monitor;
use crate::monitoring::info::show_details;
use crate::monitoring::network as network_monitor;
use crate::monitoring::wifi::{current_connection_info, current_ssid};
use crate::types::constants::device_type;

/// High-level interface to NetworkManager over D-Bus.
///
/// This is the main entry point for managing network connections on Linux systems.
/// It provides a safe, async Rust API over NetworkManager's D-Bus interface.
///
/// # Creating an Instance
///
/// ```no_run
/// use nmrs::NetworkManager;
///
/// # async fn example() -> nmrs::Result<()> {
/// let nm = NetworkManager::new().await?;
/// # Ok(())
/// # }
/// ```
///
/// # Capabilities
///
/// - **Device Management**: List devices, enable/disable WiFi
/// - **Network Scanning**: Discover available WiFi networks
/// - **Connection Management**: Connect to WiFi, Ethernet networks
/// - **Profile Management**: Save, retrieve, and delete connection profiles
/// - **Real-Time Monitoring**: Subscribe to network and device state changes
///
/// # Examples
///
/// ## Basic WiFi Connection
///
/// ```no_run
/// use nmrs::{NetworkManager, WifiSecurity};
///
/// # async fn example() -> nmrs::Result<()> {
/// let nm = NetworkManager::new().await?;
///
/// // Scan and list networks
/// let networks = nm.list_networks().await?;
/// for net in &networks {
///     println!("{}: {}%", net.ssid, net.strength.unwrap_or(0));
/// }
///
/// // Connect to a network
/// nm.connect("MyNetwork", WifiSecurity::WpaPsk {
///     psk: "password".into()
/// }).await?;
/// # Ok(())
/// # }
/// ```
///
/// ## Device Management
///
/// ```no_run
/// use nmrs::NetworkManager;
///
/// # async fn example() -> nmrs::Result<()> {
/// let nm = NetworkManager::new().await?;
///
/// // List all network devices
/// let devices = nm.list_devices().await?;
///
/// // Control WiFi
/// nm.set_wifi_enabled(false).await?;  // Disable WiFi
/// nm.set_wifi_enabled(true).await?;   // Enable WiFi
/// # Ok(())
/// # }
/// ```
///
/// ## Connection Profiles
///
/// ```no_run
/// use nmrs::NetworkManager;
///
/// # async fn example() -> nmrs::Result<()> {
/// let nm = NetworkManager::new().await?;
///
/// // Check for saved connection
/// if nm.has_saved_connection("MyNetwork").await? {
///     println!("Connection profile exists");
///     
///     // Delete it
///     nm.forget("MyNetwork").await?;
/// }
/// # Ok(())
/// # }
/// ```
///
/// # Thread Safety
///
/// `NetworkManager` is `Clone` and can be safely shared across async tasks.
/// Each clone shares the same underlying D-Bus connection.
///
/// # Concurrency
///
/// Concurrent connection operations (e.g. calling [`connect`](Self::connect)
/// from multiple tasks simultaneously) are **not supported** and may cause
/// race conditions. Use [`is_connecting`](Self::is_connecting) to check
/// whether a connection operation is already in progress before starting
/// a new one.
#[derive(Debug, Clone)]
pub struct NetworkManager {
    conn: Connection,
    timeout_config: crate::api::models::TimeoutConfig,
}

impl NetworkManager {
    /// Creates a new `NetworkManager` connected to the system D-Bus with default timeout configuration.
    ///
    /// Uses default timeouts of 30 seconds for connection and 10 seconds for disconnection.
    /// To customize timeouts, use [`with_config()`](Self::with_config) instead.
    pub async fn new() -> Result<Self> {
        let conn = Connection::system().await?;
        Ok(Self {
            conn,
            timeout_config: crate::api::models::TimeoutConfig::default(),
        })
    }

    /// Creates a new `NetworkManager` with custom timeout configuration.
    ///
    /// This allows you to customize how long NetworkManager will wait for
    /// various operations to complete before timing out.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// use nmrs::{NetworkManager, TimeoutConfig};
    /// use std::time::Duration;
    ///
    /// # async fn example() -> nmrs::Result<()> {
    /// // Configure longer timeouts for slow networks
    /// let config = TimeoutConfig::new()
    ///     .with_connection_timeout(Duration::from_secs(60))
    ///     .with_disconnect_timeout(Duration::from_secs(20));
    ///
    /// let nm = NetworkManager::with_config(config).await?;
    /// # Ok(())
    /// # }
    /// ```
    pub async fn with_config(timeout_config: crate::api::models::TimeoutConfig) -> Result<Self> {
        let conn = Connection::system().await?;
        Ok(Self {
            conn,
            timeout_config,
        })
    }

    /// Returns the current timeout configuration.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// use nmrs::NetworkManager;
    ///
    /// # async fn example() -> nmrs::Result<()> {
    /// let nm = NetworkManager::new().await?;
    /// let config = nm.timeout_config();
    /// println!("Connection timeout: {:?}", config.connection_timeout);
    /// # Ok(())
    /// # }
    /// ```
    #[must_use]
    pub fn timeout_config(&self) -> crate::api::models::TimeoutConfig {
        self.timeout_config
    }

    /// List all network devices managed by NetworkManager.
    pub async fn list_devices(&self) -> Result<Vec<Device>> {
        list_devices(&self.conn).await
    }

    /// List all bluetooth devices.
    pub async fn list_bluetooth_devices(&self) -> Result<Vec<BluetoothDevice>> {
        list_bluetooth_devices(&self.conn).await
    }

    /// Lists all network devices managed by NetworkManager.
    pub async fn list_wireless_devices(&self) -> Result<Vec<Device>> {
        let devices = list_devices(&self.conn).await?;
        Ok(devices.into_iter().filter(|d| d.is_wireless()).collect())
    }

    /// List all wired (Ethernet) devices.
    pub async fn list_wired_devices(&self) -> Result<Vec<Device>> {
        let devices = list_devices(&self.conn).await?;
        Ok(devices.into_iter().filter(|d| d.is_wired()).collect())
    }

    /// Lists all visible Wi-Fi networks.
    pub async fn list_networks(&self) -> Result<Vec<Network>> {
        list_networks(&self.conn).await
    }

    /// Connects to a Wi-Fi network with the given credentials.
    ///
    /// # Errors
    ///
    /// Returns `ConnectionError::NotFound` if the network is not visible,
    /// `ConnectionError::AuthFailed` if authentication fails, or other
    /// variants for specific failure reasons.
    pub async fn connect(&self, ssid: &str, creds: WifiSecurity) -> Result<()> {
        connect(&self.conn, ssid, creds, Some(self.timeout_config)).await
    }

    /// Connects to a wired (Ethernet) device.
    ///
    /// Finds the first available wired device and either activates an existing
    /// saved connection or creates a new one. The connection will activate
    /// when a cable is plugged in.
    ///
    /// # Errors
    ///
    /// Returns `ConnectionError::NoWiredDevice` if no wired device is found.
    pub async fn connect_wired(&self) -> Result<()> {
        connect_wired(&self.conn, Some(self.timeout_config)).await
    }

    /// Connects to a bluetooth device using the provided identity.
    ///
    /// # Example
    ///
    /// ```no_run
    /// use nmrs::{NetworkManager, models::BluetoothIdentity, models::BluetoothNetworkRole};
    ///
    /// # async fn example() -> nmrs::Result<()> {
    ///    let nm = NetworkManager::new().await?;
    ///
    ///    let identity = BluetoothIdentity::new(
    ///         "C8:1F:E8:F0:51:57".into(),
    ///         BluetoothNetworkRole::PanU,
    ///     )?;
    ///
    ///    nm.connect_bluetooth("My Phone", &identity).await?;
    ///    Ok(())
    /// }
    ///
    /// ```
    pub async fn connect_bluetooth(&self, name: &str, identity: &BluetoothIdentity) -> Result<()> {
        connect_bluetooth(&self.conn, name, identity, Some(self.timeout_config)).await
    }

    /// Connects to a VPN using the provided credentials.
    ///
    /// Currently supports WireGuard VPN connections. The function checks for an
    /// existing saved VPN connection by name. If found, it activates the saved
    /// connection. If not found, it creates a new VPN connection with the provided
    /// credentials.
    ///
    /// # Example
    ///
    /// ```rust
    /// use nmrs::{NetworkManager, VpnCredentials, VpnType, WireGuardPeer};
    ///
    /// # async fn example() -> nmrs::Result<()> {
    /// let nm = NetworkManager::new().await?;
    ///
    /// let peer = WireGuardPeer::new(
    ///     "peer_public_key",
    ///     "vpn.example.com:51820",
    ///     vec!["0.0.0.0/0".into()],
    /// ).with_persistent_keepalive(25);
    ///
    /// let creds = VpnCredentials::new(
    ///     VpnType::WireGuard,
    ///     "MyVPN",
    ///     "vpn.example.com:51820",
    ///     "your_private_key",
    ///     "10.0.0.2/24",
    ///     vec![peer],
    /// ).with_dns(vec!["1.1.1.1".into()]);
    ///
    /// nm.connect_vpn(creds).await?;
    /// # Ok(())
    /// # }
    /// ```
    ///
    /// # Errors
    ///
    /// Returns an error if:
    /// - NetworkManager is not running or accessible
    /// - The credentials are invalid or incomplete
    /// - The VPN connection fails to activate
    pub async fn connect_vpn(&self, creds: VpnCredentials) -> Result<()> {
        connect_vpn(&self.conn, creds, Some(self.timeout_config)).await
    }

    /// Disconnects from an active VPN connection by name.
    ///
    /// Searches through active connections for a VPN matching the given name.
    /// If found, deactivates the connection. If not found or already disconnected,
    /// returns success.
    ///
    /// # Example
    ///
    /// ```no_run
    /// use nmrs::NetworkManager;
    ///
    /// # async fn example() -> nmrs::Result<()> {
    /// let nm = NetworkManager::new().await?;
    /// nm.disconnect_vpn("MyVPN").await?;
    /// # Ok(())
    /// # }
    /// ```
    pub async fn disconnect_vpn(&self, name: &str) -> Result<()> {
        disconnect_vpn(&self.conn, name).await
    }

    /// Lists all saved VPN connections.
    ///
    /// Returns a list of all VPN connection profiles saved in NetworkManager,
    /// including their name, type, and current state. Only VPN connections with
    /// recognized types (currently WireGuard) are returned.
    ///
    /// # Example
    ///
    /// ```no_run
    /// use nmrs::NetworkManager;
    ///
    /// # async fn example() -> nmrs::Result<()> {
    /// let nm = NetworkManager::new().await?;
    /// let vpns = nm.list_vpn_connections().await?;
    ///
    /// for vpn in vpns {
    ///     println!("{}: {:?}", vpn.name, vpn.vpn_type);
    /// }
    /// # Ok(())
    /// # }
    /// ```
    pub async fn list_vpn_connections(&self) -> Result<Vec<VpnConnection>> {
        list_vpn_connections(&self.conn).await
    }

    /// Forgets (deletes) a saved VPN connection by name.
    ///
    /// Searches through saved connections for a VPN matching the given name.
    /// If found, deletes the connection profile. If currently connected, the
    /// VPN will be disconnected first before deletion.
    ///
    /// # Example
    ///
    /// ```no_run
    /// use nmrs::NetworkManager;
    ///
    /// # async fn example() -> nmrs::Result<()> {
    /// let nm = NetworkManager::new().await?;
    /// nm.forget_vpn("MyVPN").await?;
    /// # Ok(())
    /// # }
    /// ```
    ///
    /// # Errors
    ///
    /// Returns an error only if the operation fails unexpectedly.
    /// Returns `Ok(())` if no matching VPN connection is found.
    pub async fn forget_vpn(&self, name: &str) -> Result<()> {
        crate::core::vpn::forget_vpn(&self.conn, name).await
    }

    /// Gets detailed information about an active VPN connection.
    ///
    /// Retrieves comprehensive information about a VPN connection, including
    /// IP configuration, DNS servers, gateway, interface, and connection state.
    /// The VPN must be actively connected to retrieve this information.
    ///
    /// # Example
    ///
    /// ```no_run
    /// use nmrs::NetworkManager;
    ///
    /// # async fn example() -> nmrs::Result<()> {
    /// let nm = NetworkManager::new().await?;
    /// let info = nm.get_vpn_info("MyVPN").await?;
    ///
    /// println!("VPN: {}", info.name);
    /// println!("Interface: {:?}", info.interface);
    /// println!("IP Address: {:?}", info.ip4_address);
    /// println!("DNS Servers: {:?}", info.dns_servers);
    /// println!("State: {:?}", info.state);
    /// # Ok(())
    /// # }
    /// ```
    ///
    /// # Errors
    ///
    /// Returns `ConnectionError::NoVpnConnection` if the VPN is not found
    /// or not currently active.
    pub async fn get_vpn_info(&self, name: &str) -> Result<VpnConnectionInfo> {
        get_vpn_info(&self.conn, name).await
    }

    /// Returns whether Wi-Fi is currently enabled.
    pub async fn wifi_enabled(&self) -> Result<bool> {
        wifi_enabled(&self.conn).await
    }

    /// Enables or disables Wi-Fi.
    pub async fn set_wifi_enabled(&self, value: bool) -> Result<()> {
        set_wifi_enabled(&self.conn, value).await
    }

    /// Returns whether wireless hardware is currently enabled.
    /// Reflects rfkill state which helps check if the radio is enabled or blocked.
    pub async fn wifi_hardware_enabled(&self) -> Result<bool> {
        wifi_hardware_enabled(&self.conn).await
    }

    /// Waits for a Wi-Fi device to become ready (disconnected or activated).
    pub async fn wait_for_wifi_ready(&self) -> Result<()> {
        wait_for_wifi_ready(&self.conn).await
    }

    /// Triggers a Wi-Fi scan on all wireless devices.
    pub async fn scan_networks(&self) -> Result<()> {
        scan_networks(&self.conn).await
    }

    /// Returns whether any network device is currently in a transitional state.
    ///
    /// A device is considered "connecting" when its state is one of:
    /// Prepare, Config, NeedAuth, IpConfig, IpCheck, Secondaries, or Deactivating.
    ///
    /// Use this to guard against concurrent connection attempts, which are
    /// not supported and may cause undefined behavior.
    ///
    /// # Example
    ///
    /// ```no_run
    /// use nmrs::NetworkManager;
    ///
    /// # async fn example() -> nmrs::Result<()> {
    /// let nm = NetworkManager::new().await?;
    ///
    /// if nm.is_connecting().await? {
    ///     eprintln!("A connection operation is already in progress");
    /// }
    /// # Ok(())
    /// # }
    /// ```
    pub async fn is_connecting(&self) -> Result<bool> {
        is_connecting(&self.conn).await
    }

    /// Check if a network is connected
    pub async fn is_connected(&self, ssid: &str) -> Result<bool> {
        is_connected(&self.conn, ssid).await
    }

    /// Disconnects from the current network.
    ///
    /// If currently connected to a WiFi network, this will deactivate
    /// the connection and wait for the device to reach disconnected state.
    ///
    /// Returns `Ok(())` if disconnected successfully or if no active connection exists.
    ///
    /// # Example
    ///
    /// ```no_run
    /// use nmrs::NetworkManager;
    ///
    /// # async fn example() -> nmrs::Result<()> {
    /// let nm = NetworkManager::new().await?;
    /// nm.disconnect().await?;
    /// # Ok(())
    /// # }
    /// ```
    pub async fn disconnect(&self) -> Result<()> {
        disconnect(&self.conn, Some(self.timeout_config)).await
    }

    /// Returns the full `Network` object for the currently connected WiFi network.
    ///
    /// This provides detailed information about the active connection including
    /// signal strength, frequency, security type, and BSSID.
    ///
    /// # Example
    ///
    /// ```no_run
    /// use nmrs::NetworkManager;
    ///
    /// # async fn example() -> nmrs::Result<()> {
    /// let nm = NetworkManager::new().await?;
    /// if let Some(network) = nm.current_network().await? {
    ///     println!("Connected to: {} ({}%)", network.ssid, network.strength.unwrap_or(0));
    /// } else {
    ///     println!("Not connected");
    /// }
    /// # Ok(())
    /// # }
    /// ```
    pub async fn current_network(&self) -> Result<Option<Network>> {
        current_network(&self.conn).await
    }

    /// Lists all saved connection profiles.
    ///
    /// Returns the names (IDs) of all saved connection profiles in NetworkManager,
    /// including WiFi, Ethernet, VPN, and other connection types.
    ///
    /// # Example
    ///
    /// ```no_run
    /// use nmrs::NetworkManager;
    ///
    /// # async fn example() -> nmrs::Result<()> {
    /// let nm = NetworkManager::new().await?;
    /// let connections = nm.list_saved_connections().await?;
    /// for name in connections {
    ///     println!("Saved connection: {}", name);
    /// }
    /// # Ok(())
    /// # }
    /// ```
    pub async fn list_saved_connections(&self) -> Result<Vec<String>> {
        list_saved_connections(&self.conn).await
    }

    /// Finds a device by its interface name (e.g., "wlan0", "eth0").
    ///
    /// Returns the D-Bus object path of the device if found.
    ///
    /// # Example
    ///
    /// ```no_run
    /// use nmrs::NetworkManager;
    ///
    /// # async fn example() -> nmrs::Result<()> {
    /// let nm = NetworkManager::new().await?;
    /// let device_path = nm.get_device_by_interface("wlan0").await?;
    /// println!("Device path: {}", device_path.as_str());
    /// # Ok(())
    /// # }
    /// ```
    pub async fn get_device_by_interface(&self, name: &str) -> Result<zvariant::OwnedObjectPath> {
        get_device_by_interface(&self.conn, name).await
    }

    /// Returns the SSID of the currently connected network, if any.
    #[must_use]
    pub async fn current_ssid(&self) -> Option<String> {
        current_ssid(&self.conn).await
    }

    /// Returns the SSID and frequency of the current connection, if any.
    #[must_use]
    pub async fn current_connection_info(&self) -> Option<(String, Option<u32>)> {
        current_connection_info(&self.conn).await
    }

    /// Returns detailed information about a specific network.
    pub async fn show_details(&self, net: &Network) -> Result<NetworkInfo> {
        show_details(&self.conn, net).await
    }

    /// Returns whether a saved connection exists for the given SSID.
    pub async fn has_saved_connection(&self, ssid: &str) -> Result<bool> {
        has_saved_connection(&self.conn, ssid).await
    }

    /// Returns the D-Bus object path of a saved connection for the given SSID.
    pub async fn get_saved_connection_path(
        &self,
        ssid: &str,
    ) -> Result<Option<zvariant::OwnedObjectPath>> {
        get_saved_connection_path(&self.conn, ssid).await
    }

    /// Forgets (deletes) a saved WiFi connection for the given SSID.
    ///
    /// If currently connected to this network, disconnects first, then deletes
    /// all saved connection profiles matching the SSID.
    ///
    /// # Returns
    ///
    /// Returns `Ok(())` if one or more connections were deleted successfully,
    /// or if no matching connections were found.
    pub async fn forget(&self, ssid: &str) -> Result<()> {
        forget_by_name_and_type(
            &self.conn,
            ssid,
            Some(device_type::WIFI),
            Some(self.timeout_config),
        )
        .await
    }

    /// Forgets (deletes) a saved Bluetooth connection.
    ///
    /// If currently connected to this device, it will disconnect first before
    /// deleting the connection profile. Can match by connection name or bdaddr.
    ///
    /// # Arguments
    ///
    /// * `name` - Connection name or bdaddr to forget
    ///
    /// # Returns
    ///
    /// Returns `Ok(())` if the connection was deleted successfully.
    /// Returns `NoSavedConnection` if no matching connection was found.
    pub async fn forget_bluetooth(&self, name: &str) -> Result<()> {
        forget_by_name_and_type(
            &self.conn,
            name,
            Some(device_type::BLUETOOTH),
            Some(self.timeout_config),
        )
        .await
    }
    ///
    /// Subscribes to D-Bus signals for access point additions and removals
    /// on all Wi-Fi devices. Invokes the callback whenever the network list
    /// changes, enabling live UI updates without polling.
    ///
    /// This function runs indefinitely until an error occurs. Run it in a
    /// background task.
    ///
    /// # Example
    ///
    /// ```ignore
    /// # use nmrs::NetworkManager;
    /// # async fn example() -> nmrs::Result<()> {
    /// let nm = NetworkManager::new().await?;
    ///
    /// // Spawn monitoring task
    /// glib::MainContext::default().spawn_local({
    ///     let nm = nm.clone();
    ///     async move {
    ///         nm.monitor_network_changes(|| {
    ///             println!("Networks changed!");
    ///         }).await
    ///     }
    /// });
    /// # Ok(())
    /// # }
    /// ```
    pub async fn monitor_network_changes<F>(&self, callback: F) -> Result<()>
    where
        F: Fn() + 'static,
    {
        let (_tx, rx) = watch::channel(());
        network_monitor::monitor_network_changes(&self.conn, rx, callback).await
    }

    /// Monitors device state changes in real-time.
    ///
    /// Subscribes to D-Bus signals for device state changes on all network
    /// devices (both wired and wireless). Invokes the callback whenever a
    /// device state changes (e.g., cable plugged in, device activated),
    /// enabling live UI updates without polling.
    ///
    /// This function runs indefinitely until an error occurs. Run it in a
    /// background task.
    ///
    /// # Example
    ///
    /// ```ignore
    /// # use nmrs::NetworkManager;
    /// # async fn example() -> nmrs::Result<()> {
    /// let nm = NetworkManager::new().await?;
    ///
    /// // Spawn monitoring task
    /// glib::MainContext::default().spawn_local({
    ///     let nm = nm.clone();
    ///     async move {
    ///         nm.monitor_device_changes(|| {
    ///             println!("Device state changed!");
    ///         }).await
    ///     }
    /// });
    /// # Ok(())
    /// # }
    /// ```
    pub async fn monitor_device_changes<F>(&self, callback: F) -> Result<()>
    where
        F: Fn() + 'static,
    {
        let (_tx, rx) = watch::channel(());
        device_monitor::monitor_device_changes(&self.conn, rx, callback).await
    }
}