Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions nmrs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
25 changes: 25 additions & 0 deletions nmrs/src/api/models/device.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ pub struct Device {
pub ip4_address: Option<String>,
/// Assigned IPv6 address with CIDR notation (only present when connected)
pub ip6_address: Option<String>,
/// Operating frequency in MHz for the active Wi-Fi connection, if known.
pub frequency: Option<u32>,
// Link speed in Mb/s (wired devices)
// pub speed: Option<u32>,
}
Expand Down Expand Up @@ -94,6 +96,8 @@ pub struct WifiDevice {
pub is_active: bool,
/// SSID of the currently active AP, if any.
pub active_ssid: Option<String>,
/// Operating frequency in MHz of the currently active AP, if any.
pub active_frequency_mhz: Option<u32>,
}

/// Represents the hardware identity of a network device.
Expand Down Expand Up @@ -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 {
Expand Down
29 changes: 29 additions & 0 deletions nmrs/src/api/models/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion nmrs/src/api/network_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Vec<WifiDevice>> {
list_wifi_devices(&self.conn).await
}
Expand Down
37 changes: 36 additions & 1 deletion nmrs/src/core/device.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -94,6 +94,40 @@ pub(crate) async fn list_devices(conn: &Connection) -> Result<Vec<Device>> {
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) =
Expand Down Expand Up @@ -129,6 +163,7 @@ pub(crate) async fn list_devices(conn: &Connection) -> Result<Vec<Device>> {
driver,
ip4_address,
ip6_address,
frequency,
// speed,
});
}
Expand Down
22 changes: 14 additions & 8 deletions nmrs/src/core/wifi_device.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Vec<WifiDevice>> {
let nm = NMProxy::new(conn).await?;
let paths = nm.get_devices().await?;
Expand Down Expand Up @@ -47,21 +47,26 @@ pub(crate) async fn list_wifi_devices(conn: &Connection) -> Result<Vec<WifiDevic
.build()
.await?;
let active_ap_path = wifi.active_access_point().await.ok();
let (is_active, active_ssid) = match active_ap_path {
let (is_active, active_ssid, active_frequency_mhz) = match active_ap_path {
Some(ap_path) if ap_path.as_str() != "/" => {
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 {
Expand All @@ -75,6 +80,7 @@ pub(crate) async fn list_wifi_devices(conn: &Connection) -> Result<Vec<WifiDevic
autoconnect,
is_active,
active_ssid,
active_frequency_mhz,
});
}

Expand Down