diff --git a/nmrs/CHANGELOG.md b/nmrs/CHANGELOG.md index e1c10a6d..1335155f 100644 --- a/nmrs/CHANGELOG.md +++ b/nmrs/CHANGELOG.md @@ -5,6 +5,8 @@ All notable changes to the `nmrs` crate will be documented in this file. ## [Unreleased] ### Added - `NetworkManager::get_saved_connection_uuid()` — resolve a profile UUID from `connection.id` (usually the Wi-Fi SSID) for use with `update_saved_connection` ([#442](https://github.com/networkmanager-rs/nmrs/issues/442)) +- `DeviceState::is_enabled()`, `Device.frequency`, and `WifiDevice.active_frequency_mhz` expose device usability and active Wi-Fi AP frequency without requiring separate AP lookups ([#445](https://github.com/networkmanager-rs/nmrs/pull/445)) + ## [3.2.0] - 2026-05-31 ### Added - Add EAP-TLS support for WPA-Enterprise Wi-Fi, including TLS certificate/key path or blob configuration on `EapOptions` and `EapMethod::Tls` ([#434](https://github.com/networkmanager-rs/nmrs/pull/434)) diff --git a/nmrs/src/api/models/device.rs b/nmrs/src/api/models/device.rs index a9800aa8..3e221079 100644 --- a/nmrs/src/api/models/device.rs +++ b/nmrs/src/api/models/device.rs @@ -59,6 +59,8 @@ pub struct Device { pub ip4_address: Option, /// Assigned IPv6 address with CIDR notation (only present when connected) pub ip6_address: Option, + /// Operating frequency in MHz for the active Wi-Fi connection, if known. + pub frequency: Option, // Link speed in Mb/s (wired devices) // pub speed: Option, } @@ -94,6 +96,8 @@ pub struct WifiDevice { pub is_active: bool, /// SSID of the currently active AP, if any. pub active_ssid: Option, + /// Operating frequency in MHz of the currently active AP, if any. + pub active_frequency_mhz: Option, } /// Represents the hardware identity of a network device. @@ -286,6 +290,27 @@ impl DeviceState { | Self::Deactivating ) } + + /// Returns `true` if the device state indicates the device is usable. + /// + /// This is derived only from the NetworkManager device state. For actual + /// Wi-Fi radio power and rfkill state, use + /// [`NetworkManager::wifi_state`](crate::NetworkManager::wifi_state). + #[must_use] + pub fn is_enabled(&self) -> bool { + matches!( + self, + Self::Disconnected + | Self::Prepare + | Self::Config + | Self::NeedAuth + | Self::IpConfig + | Self::IpCheck + | Self::Secondaries + | Self::Activated + | Self::Deactivating + ) + } } impl Device { diff --git a/nmrs/src/api/models/tests.rs b/nmrs/src/api/models/tests.rs index fbf5e548..541615dc 100644 --- a/nmrs/src/api/models/tests.rs +++ b/nmrs/src/api/models/tests.rs @@ -603,6 +603,7 @@ fn test_device_is_bluetooth() { driver: Some("btusb".into()), ip4_address: None, ip6_address: None, + frequency: None, }; assert!(bt_device.is_bluetooth()); @@ -1260,6 +1261,34 @@ fn test_device_state_is_transitional() { } } +#[test] +fn test_device_state_is_enabled() { + let enabled = [ + DeviceState::Disconnected, + DeviceState::Prepare, + DeviceState::Config, + DeviceState::NeedAuth, + DeviceState::IpConfig, + DeviceState::IpCheck, + DeviceState::Secondaries, + DeviceState::Activated, + DeviceState::Deactivating, + ]; + for state in &enabled { + assert!(state.is_enabled(), "{state:?} should be enabled"); + } + + let disabled = [ + DeviceState::Unmanaged, + DeviceState::Unavailable, + DeviceState::Failed, + DeviceState::Other(999), + ]; + for state in &disabled { + assert!(!state.is_enabled(), "{state:?} should not be enabled"); + } +} + #[test] fn test_device_state_from_u32_intermediate_states() { assert_eq!(DeviceState::from(40), DeviceState::Prepare); diff --git a/nmrs/src/api/network_manager.rs b/nmrs/src/api/network_manager.rs index 01542488..408a59d3 100644 --- a/nmrs/src/api/network_manager.rs +++ b/nmrs/src/api/network_manager.rs @@ -265,7 +265,7 @@ impl NetworkManager { /// Lists every managed Wi-Fi device on the system. /// /// Each [`WifiDevice`] includes its interface name, MAC, current state, - /// and the SSID of any active connection. + /// and the SSID/frequency of any active connection. pub async fn list_wifi_devices(&self) -> Result> { list_wifi_devices(&self.conn).await } diff --git a/nmrs/src/core/device.rs b/nmrs/src/core/device.rs index 071bc843..8d041ab8 100644 --- a/nmrs/src/core/device.rs +++ b/nmrs/src/core/device.rs @@ -12,7 +12,7 @@ use crate::api::models::{BluetoothDevice, ConnectionError, Device, DeviceIdentit use crate::core::bluetooth::populate_bluez_info; use crate::core::connection::get_device_by_interface; use crate::core::state_wait::wait_for_wifi_device_ready; -use crate::dbus::{NMBluetoothProxy, NMDeviceProxy, NMProxy}; +use crate::dbus::{NMAccessPointProxy, NMBluetoothProxy, NMDeviceProxy, NMProxy, NMWirelessProxy}; use crate::types::constants::device_type; use crate::util::utils::get_ip_addresses_from_active_connection; @@ -94,6 +94,40 @@ pub(crate) async fn list_devices(conn: &Connection) -> Result> { None } }; + let frequency = if raw_type == device_type::WIFI { + match NMWirelessProxy::builder(conn) + .path(p.clone())? + .build() + .await + { + Ok(wifi) => match wifi.active_access_point().await { + Ok(ap_path) if ap_path.as_str() != "/" => { + match NMAccessPointProxy::builder(conn) + .path(ap_path)? + .build() + .await + { + Ok(ap) => ap.frequency().await.ok(), + Err(e) => { + debug!("Failed to build active AP proxy for {}: {}", interface, e); + None + } + } + } + Ok(_) => None, + Err(e) => { + debug!("Failed to get active AP for {}: {}", interface, e); + None + } + }, + Err(e) => { + debug!("Failed to build wireless proxy for {}: {}", interface, e); + None + } + } + } else { + None + }; // Get IP addresses from active connection let (ip4_address, ip6_address) = @@ -129,6 +163,7 @@ pub(crate) async fn list_devices(conn: &Connection) -> Result> { driver, ip4_address, ip6_address, + frequency, // speed, }); } diff --git a/nmrs/src/core/wifi_device.rs b/nmrs/src/core/wifi_device.rs index 20a7eccd..e27fc262 100644 --- a/nmrs/src/core/wifi_device.rs +++ b/nmrs/src/core/wifi_device.rs @@ -16,7 +16,7 @@ use crate::dbus::{NMAccessPointProxy, NMDeviceProxy, NMProxy, NMWirelessProxy}; use crate::types::constants::device_type; use crate::util::utils::decode_ssid_or_hidden; -/// Lists every managed Wi-Fi device with current MAC, state, and active SSID. +/// Lists every managed Wi-Fi device with current MAC, state, and active AP info. pub(crate) async fn list_wifi_devices(conn: &Connection) -> Result> { let nm = NMProxy::new(conn).await?; let paths = nm.get_devices().await?; @@ -47,21 +47,26 @@ pub(crate) async fn list_wifi_devices(conn: &Connection) -> Result { match NMAccessPointProxy::builder(conn) .path(ap_path)? .build() .await { - Ok(ap) => match ap.ssid().await { - Ok(bytes) => (true, Some(decode_ssid_or_hidden(&bytes).into_owned())), - Err(_) => (true, None), - }, - Err(_) => (true, None), + Ok(ap) => { + let active_ssid = ap + .ssid() + .await + .ok() + .map(|bytes| decode_ssid_or_hidden(&bytes).into_owned()); + let active_frequency_mhz = ap.frequency().await.ok(); + (true, active_ssid, active_frequency_mhz) + } + Err(_) => (true, None, None), } } - _ => (false, None), + _ => (false, None, None), }; out.push(WifiDevice { @@ -75,6 +80,7 @@ pub(crate) async fn list_wifi_devices(conn: &Connection) -> Result