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
1 change: 1 addition & 0 deletions docs/src/api/network-manager.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ let config = nm.timeout_config();
| `reload_saved_connections()` | `Result<()>` | Re-read profiles from disk |
| `has_saved_connection(ssid)` | `Result<bool>` | Check if a Wi-Fi profile exists |
| `get_saved_connection_path(ssid)` | `Result<Option<OwnedObjectPath>>` | Get profile D-Bus path |
| `get_saved_connection_uuid(name)` | `Result<Option<String>>` | Get profile UUID by `connection.id` (usually SSID) |
| `forget(ssid)` | `Result<()>` | Delete a Wi-Fi profile |

## Monitoring Methods
Expand Down
49 changes: 49 additions & 0 deletions docs/src/guide/profiles.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,55 @@ into an existing profile via NM's `Update` / `UpdateUnsaved` methods.
This is the right call to flip `autoconnect`, change a priority, or
update DNS without rebuilding the entire profile.

**Important:** the first argument is the profile **UUID** (`connection.uuid`),
not the Wi-Fi SSID. [`Network`](../api/types.md#network) values from a scan
do not include the profile UUID — resolve it first.

### Look up UUID by profile name (SSID)

For Wi-Fi profiles, `connection.id` is usually the SSID. The same string
works with `has_saved_connection`, `forget`, and
[`get_saved_connection_uuid`](../api/network-manager.md#connection-profile-methods):

```rust
use nmrs::{NetworkManager, SettingsPatch};

let nm = NetworkManager::new().await?;

if let Some(uuid) = nm.get_saved_connection_uuid("HomeWiFi").await? {
let mut patch = SettingsPatch::default();
patch.autoconnect = Some(false);
nm.update_saved_connection(&uuid, patch).await?;
}
```

### Update while listing saved profiles

When iterating [`list_saved_connections`](../api/network-manager.md#connection-profile-methods),
each [`SavedConnection`](../api/models.md#savedconnection) already carries `uuid`
and `id`. Match on `id` (or compare against your target SSID) and pass
**`saved.uuid`** to `update_saved_connection`:

```rust
use nmrs::{NetworkManager, SettingsPatch};

let nm = NetworkManager::new().await?;
let target = "HomeWiFi";

for saved in nm.list_saved_connections().await? {
if saved.id == target {
let mut patch = SettingsPatch::default();
patch.autoconnect = Some(!saved.autoconnect);
nm.update_saved_connection(&saved.uuid, patch).await?;
}
}
```

Common mistake: using a scanned [`Network`](../api/types.md#network)'s `ssid`
where a UUID is required, or calling `update_saved_connection(&network.ssid, …)`.
There is no `uuid` field on `Network` — use `get_saved_connection_uuid` or
`SavedConnection::uuid` instead.

## Deleting by UUID

When the profile UUID is known, you can delete it directly:
Expand Down
3 changes: 2 additions & 1 deletion nmrs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,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))
## [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
48 changes: 47 additions & 1 deletion nmrs/src/api/network_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ use crate::core::connection::{
connect, connect_to_bssid, 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};
use crate::core::connection_settings::{
get_saved_connection_path, get_saved_connection_uuid, has_saved_connection,
};
use crate::core::device::{
is_connecting, list_bluetooth_devices, list_devices, wait_for_wifi_ready,
};
Expand Down Expand Up @@ -1170,6 +1172,27 @@ impl NetworkManager {
}

/// Merges a [`SettingsPatch`] into an existing profile (`Update` / `UpdateUnsaved`).
///
/// `uuid` is the profile's `connection.uuid` (see [`SavedConnection::uuid`]), **not**
/// the Wi-Fi SSID from a scan [`Network`]. Use [`Self::get_saved_connection_uuid`] or
/// [`Self::list_saved_connections`] to resolve the UUID from a profile name / SSID.
///
/// # Example
///
/// ```no_run
/// use nmrs::{NetworkManager, SettingsPatch};
///
/// # async fn example() -> nmrs::Result<()> {
/// let nm = NetworkManager::new().await?;
///
/// if let Some(uuid) = nm.get_saved_connection_uuid("HomeWiFi").await? {
/// let mut patch = SettingsPatch::default();
/// patch.autoconnect = Some(false);
/// nm.update_saved_connection(&uuid, patch).await?;
/// }
/// # Ok(())
/// # }
/// ```
pub async fn update_saved_connection(&self, uuid: &str, patch: SettingsPatch) -> Result<()> {
saved_profiles::update_saved_connection(&self.conn, uuid, &patch).await
}
Expand Down Expand Up @@ -1229,6 +1252,29 @@ impl NetworkManager {
get_saved_connection_path(&self.conn, ssid).await
}

/// Returns the profile UUID for a saved connection whose `connection.id` matches `name`.
///
/// For Wi-Fi profiles, `name` is usually the SSID. Use the returned UUID with
/// [`Self::update_saved_connection`] or [`Self::delete_saved_connection`].
///
/// # Example
///
/// ```no_run
/// use nmrs::NetworkManager;
///
/// # async fn example() -> nmrs::Result<()> {
/// let nm = NetworkManager::new().await?;
///
/// if let Some(uuid) = nm.get_saved_connection_uuid("HomeWiFi").await? {
/// println!("Profile UUID: {uuid}");
/// }
/// # Ok(())
/// # }
/// ```
pub async fn get_saved_connection_uuid(&self, name: &str) -> Result<Option<String>> {
get_saved_connection_uuid(&self.conn, name).await
}

/// Forgets (deletes) a saved WiFi connection for the given SSID.
///
/// If currently connected to this network, disconnects first, then deletes
Expand Down
59 changes: 46 additions & 13 deletions nmrs/src/core/connection_settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,13 @@ use crate::api::models::ConnectionError;
use crate::util::utils::{connection_settings_proxy, settings_proxy};
use crate::util::validation::validate_connection_name;

/// Finds the D-Bus path of a saved connection by SSID or connection name.
///
/// Iterates through all saved connections in NetworkManager's settings
/// and returns the path of the first one whose connection ID matches
/// the given SSID or name.
/// Finds a saved profile whose `connection.id` matches `name` (SSID for typical Wi-Fi).
///
/// Returns `None` if no saved connection exists for this SSID/name.
pub(crate) async fn get_saved_connection_path(
/// Returns the D-Bus path and `connection.uuid` of the first match.
async fn find_saved_connection_by_name(
conn: &Connection,
name: &str,
) -> Result<Option<OwnedObjectPath>> {
if should_skip_lookup(name)? {
return Ok(None);
}

) -> Result<Option<(OwnedObjectPath, String)>> {
let settings = settings_proxy(conn).await?;

let reply = settings
Expand Down Expand Up @@ -57,14 +49,55 @@ pub(crate) async fn get_saved_connection_path(
if let Some(conn_section) = all.get("connection")
&& let Some(Value::Str(id)) = conn_section.get("id")
&& id == name
&& let Some(Value::Str(uuid)) = conn_section.get("uuid")
{
return Ok(Some(cpath));
return Ok(Some((cpath, uuid.to_string())));
}
}

Ok(None)
}

/// Finds the D-Bus path of a saved connection by SSID or connection name.
///
/// Iterates through all saved connections in NetworkManager's settings
/// and returns the path of the first one whose connection ID matches
/// the given SSID or name.
///
/// Returns `None` if no saved connection exists for this SSID/name.
pub(crate) async fn get_saved_connection_path(
conn: &Connection,
name: &str,
) -> Result<Option<OwnedObjectPath>> {
if should_skip_lookup(name)? {
return Ok(None);
}

Ok(find_saved_connection_by_name(conn, name)
.await?
.map(|(path, _)| path))
}

/// Returns the profile UUID for a saved connection whose `connection.id` matches `name`.
///
/// For Wi-Fi profiles created by nmrs, `connection.id` is usually the SSID — the same
/// string accepted by [`has_saved_connection`](crate::NetworkManager::has_saved_connection)
/// and [`forget`](crate::NetworkManager::forget).
///
/// Returns `None` when no profile matches.
pub(crate) async fn get_saved_connection_uuid(
conn: &Connection,
name: &str,
) -> Result<Option<String>> {
if should_skip_lookup(name)? {
return Ok(None);
}

Ok(find_saved_connection_by_name(conn, name)
.await?
.map(|(_, uuid)| uuid))
}

fn should_skip_lookup(name: &str) -> Result<bool> {
if name.trim().is_empty() {
return Ok(true);
Expand Down
29 changes: 28 additions & 1 deletion nmrs/tests/integration_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -435,7 +435,34 @@ async fn test_get_saved_connection_path() {
.await
.expect("Failed to get saved connection path for empty SSID");
// Result can be Some or None depending on system state
assert!(result.is_some() || result.is_none());
let _ = result;
}

/// Test getting the UUID of a saved connection
#[tokio::test]
#[serial]
async fn test_get_saved_connection_uuid() {
require_networkmanager!();

let nm = NetworkManager::new()
.await
.expect("Failed to create NetworkManager");
require_wifi!(&nm);

let result = nm
.get_saved_connection_uuid("__NONEXISTENT_TEST_SSID__")
.await
.expect("Failed to get saved connection UUID");
assert!(
result.is_none(),
"Non-existent SSID should not have saved connection UUID"
);

let result = nm
.get_saved_connection_uuid("")
.await
.expect("Failed to get saved connection UUID for empty SSID");
let _ = result;
}

/// Test connecting to an open network
Expand Down