| name | shiny-wifi |
| description | Generate code using Shiny.Net.Wifi for cross-platform Wi-Fi - scanning for access points, connecting and disconnecting, listing/forgetting/rejoining the networks the device has saved, monitoring the current network (SSID, signal, IP, DNS), and hosting a hotspot with connected-client listing on Android, iOS, Mac Catalyst, macOS, Windows, and Linux |
| auto_invoke | true |
| triggers | ["wifi","Wi-Fi","WiFi","wireless network","scan for wifi","wifi scanner","list wifi networks","nearby networks","access point","SSID","BSSID","RSSI","GetCurrentNetwork","current network","which wifi am I on","ssid is null","bssid is null","unknown ssid","NEHotspotNetwork","fetchCurrent","CNCopyCurrentNetworkInfo","FLAG_INCLUDE_LOCATION_INFO","signal strength","connect to wifi","join a wifi network","disconnect wifi","wifi password","passphrase","WPA2","WPA3","hotspot","mobile hotspot","[Truncated]"] |
Shiny.Net.Wifi Skill
You are an expert in Shiny.Net.Wifi, a cross-platform Wi-Fi library covering scanning,
connect/disconnect, saved (known) network management, current-network monitoring and
hotspot hosting.
When to Use This Skill
Invoke this skill when the user wants to:
- List the Wi-Fi networks in range, with signal strength and security
- Join or leave a named network from code
- Read or watch the current network - SSID, BSSID, signal, IP, DNS, gateway, mask
- List, forget, or rejoin the networks the device has saved
- Raise a hotspot / access point and show the user its SSID and passphrase
- See which devices are connected to a hotspot
- Power the Wi-Fi radio on or off
- Ask why Wi-Fi scanning "does not work on iOS"
⚠️ Read this before writing anything
Wi-Fi is the most unevenly exposed capability across these platforms. Half of what users ask for
is impossible on at least one of them, and the impossibility is a platform policy decision, not a
gap in this library. Do not write code that assumes an operation exists everywhere, and do not
tell a user something will work on iOS when it will not.
Every manager publishes a WifiCapabilities flags property. Branch on it. Anything unavailable
throws WifiNotSupportedException with a message naming the exact limit.
Capability matrix
| Operation | Android | iOS / Mac Catalyst | macOS | Windows | Linux |
|---|
| Scan | ✅ (needs location) | ❌ no API | ✅ | ✅ | ✅ |
| Connect | ✅ (system dialog from API 29) | ✅ (system dialog) | ✅ | ✅ | ✅ |
| Disconnect | ✅ | ✅ (removes the config) | ✅ | ✅ | ✅ |
| Current network SSID | ✅ (needs location) | ✅ (needs entitlement + location) | ✅ (needs location) | ✅ | ✅ |
| IP / DNS / gateway | ✅ | ✅ | ✅ | ✅ | ✅ |
| Radio state | ✅ | ❌ | ✅ | ✅ | ✅ |
| Radio toggle | ⚠️ API ≤ 28 only | ❌ | ✅ | ✅ | ✅ |
| Hotspot | ⚠️ local-only, OS picks SSID | ❌ no API | ❌ no API | ✅ full tethering | ✅ AP mode |
| Hotspot clients | ❌ | ❌ | ❌ | ✅ | ✅ |
| List known networks | ⚠️ own app only | ⚠️ own app only | ✅ whole machine | ✅ whole machine | ✅ whole machine |
| Forget a known network | ⚠️ own app only | ⚠️ own app only | ⚠️ needs admin auth | ✅ | ✅ (polkit) |
| Connect by known id | ⚠️ API ≤ 28 only | ❌ | ✅ | ✅ | ✅ |
The three things users most often ask for that cannot be done
- Scanning on iOS. There is no public API.
NEHotspotHelper can list networks but its
entitlement is granted case by case by Apple to captive-network-assistant apps. Do not suggest
CNCopyCurrentNetworkInfo or NEHotspotNetwork.fetchCurrent as a substitute - both report the
joined network, not nearby ones.
- Reading the user's saved networks on a phone. Neither iOS nor Android will show an app the
networks the user saved.
GetKnownNetworks() on those two returns only what your own app
configured. Do not build a "manage all my Wi-Fi networks" screen for mobile.
- Naming an Android hotspot.
SoftApConfiguration.Builder exposes only the channel to
non-system apps - setSsid/setPassphrase are @SystemApi. The OS generates both and you read
them back off IHotspotSession.Info to show the user.
Library Overview
| Item | Value |
|---|
| GitHub | https://github.com/shinyorg/shiny |
| NuGet | Shiny.Net.Wifi, plus Shiny.Net.Wifi.Linux on Linux |
| Namespace | Shiny.Net.Wifi (types); Shiny (registration extensions) |
| Platforms | Android, iOS, Mac Catalyst, macOS, Windows, Linux |
How each platform is backed
| Platform | Manager | Known networks | Hotspot |
|---|
| Android | WifiManager + ConnectivityManager | WifiNetworkSuggestion (API 30+), WifiConfiguration below 29 | startLocalOnlyHotspot |
| iOS / Mac Catalyst | NEHotspotConfiguration + CaptiveNetwork | NEHotspotConfigurationManager.getConfiguredSSIDs | none |
| macOS | CoreWLAN (CWInterface) | CWConfiguration.networkProfiles | none |
| Windows | WiFiAdapter (WinRT) | wlanapi.dll - WinRT has no profile API | NetworkOperatorTetheringManager |
| Linux | NetworkManager / D-Bus | Settings.ListConnections (UUID-keyed) | NetworkManager AP mode + ipv4.method=shared |
| plain .NET | System.Net.NetworkInformation (addressing only) | none | none |
On Linux, reference Shiny.Net.Wifi.Linux instead of the base package. It registers
NetworkManager-backed implementations of the same interfaces. The base package's plain .NET
target reports IP/DNS off the wireless interface and raises Changed, but every Wi-Fi-specific call
throws - it is a deliberate stub, not a fallback.
Registration
builder.Services.AddWifi();
builder.Services.AddWifiHotspot();
Both are singletons. Register only what you use - each one costs a native watcher only once
something subscribes to its Changed event.
Scanning
public class NetworkPicker(IWifiManager wifi)
{
public async Task<IReadOnlyList<WifiNetwork>> Load(CancellationToken ct)
{
if (!wifi.Capabilities.HasFlag(WifiCapabilities.Scan))
return [];
var access = await wifi.RequestAccess(ct);
if (access != AccessState.Available)
throw new InvalidOperationException("Location access is needed to scan for networks");
var found = await wifi.Scan(ct);
return found
.GroupBy(x => x.Ssid)
.Select(g => g.MaxBy(x => x.SignalStrengthPercent)!)
.Where(x => !x.IsHidden)
.ToList();
}
}
WifiNetwork carries Ssid, Bssid, Security, SignalStrengthDbm (null where the platform
reports only a percentage), SignalStrengthPercent (0-100, always populated), FrequencyMhz,
IsHidden, and computed Band / Channel / IsOpen.
Always call RequestAccess before Scan. Android returns an empty list rather than an error
when location has not been granted; the library turns that into WifiPermissionException so it
does not look like an empty neighbourhood, but asking first is better than catching.
Connecting
var request = new WifiConnectionRequest("Kitchen")
{
Passphrase = "hunter2hunter2",
Remember = true,
Timeout = TimeSpan.FromSeconds(20)
};
try
{
var joined = await wifi.Connect(request, ct);
logger.LogInformation("On {Ssid} at {Ip}", joined.Ssid, joined.IPv4Address);
}
catch (WifiConnectionException ex)
{
}
Connect returns only once an address has been assigned, not when association completes - a
WifiNetworkInfo with no IP on it is useless to the caller.
Leave Security as Unknown unless you are joining a hidden network. The platform reads the
scheme off the beacon; a hidden network has no beacon to read, so it has to be told.
Android 10+ and iOS both show a system dialog naming the network. Neither lets an app join
silently, and on Android the join itself lasts only while your app holds the request. Remember
still does something everywhere: it writes an ordinary profile on Windows, macOS and Linux, keeps
the hotspot configuration on iOS, and on Android 11+ registers a WifiNetworkSuggestion next to
the join so the OS can come back to the network later. See Known (saved) networks below.
Known (saved) networks
if (wifi.Capabilities.HasFlag(WifiCapabilities.KnownNetworks))
{
foreach (var known in await wifi.GetKnownNetworks(ct))
Console.WriteLine($"{known.Ssid} ({known.Security}) id={known.Id}");
}
if (wifi.Capabilities.HasFlag(WifiCapabilities.ForgetNetwork))
await wifi.Forget(known.Id, ct);
if (wifi.Capabilities.HasFlag(WifiCapabilities.ConnectKnownNetwork))
await wifi.Connect(known.Id, ct);
KnownWifiNetwork carries Id, Ssid, Security, IsHidden and AddedByThisApp.
Id is opaque and platform-issued. A NetworkManager connection UUID on Linux, a numeric
network id on Android below API 29, the SSID everywhere else. Round-trip it; never parse it,
construct it, or persist it across platforms. Match on Ssid if you need to find a network by
name.
- The scope of "known" is not the same everywhere, and this is the thing to get right. iOS,
Mac Catalyst and Android disclose only your own app's entries -
NEHotspotConfigurationManager.getConfiguredSSIDs and network suggestions respectively. Windows,
macOS and Linux hand back every profile on the machine. AddedByThisApp tells the two apart; it
is false on the desktop platforms even for profiles your app created, because none of them record
who wrote an entry.
Connect(id) is desktop-plus-legacy-Android only. On iOS and Android 10+ a saved network is
a standing hint the OS acts on when it chooses - there is no call to force the join. Use
Connect(WifiConnectionRequest) with the passphrase there instead.
- Getting something into the list means
Remember = true on the join. On Android 11+ that
registers a WifiNetworkSuggestion alongside the specifier join; the specifier is still what
gets the device on the network now, and the suggestion only takes effect once the user approves
the notification Android raises.
- iOS reports names only. A stored hotspot configuration carries no security type or hidden
flag, so those stay at their defaults there.
- macOS
Forget usually throws. Editing the preferred-network list means committing a whole
CWConfiguration, which macOS gates behind an SFAuthorization a normal app cannot raise -
expect WifiPermissionException and have a fallback. Listing is unprivileged.
GetKnownNetworks() is not free on desktop. Windows reads one profile's XML per entry and
Linux makes one D-Bus round trip per profile, so cache the result rather than polling it.
Current network and change monitoring
public sealed class NetworkWatcher(IWifiManager wifi) : IDisposable
{
public void Start() => wifi.Changed += this.OnChanged;
public void Dispose() => wifi.Changed -= this.OnChanged;
void OnChanged(object? sender, WifiNetworkInfo? network)
{
if (network == null)
{
return;
}
Console.WriteLine($"{network.Ssid} ({network.SignalStrengthPercent}%) {network.IPv4Address}");
Console.WriteLine($"DNS: {String.Join(", ", network.DnsAddresses)}");
}
}
WifiNetworkInfo carries Ssid, Bssid, Security, SignalStrengthDbm,
SignalStrengthPercent, FrequencyMhz, Band, Channel, IpAddresses, DnsAddresses,
Gateway, SubnetMask, InterfaceName, and the IPv4Address / IPv6Address shortcuts.
Changed is de-duplicated. The native watchers behind it (Android's NetworkCallback,
Apple's NWPathMonitor, NetworkManager's PropertiesChanged) all fire several times per real
change; only genuine differences are raised. WifiNetworkInfo compares its address lists by
value for the same reason, so it is safe to diff yourself too.
- Unsubscribe. The native watcher is created on the first subscription and torn down on the
last, so a leaked handler keeps a radio callback alive.
- Subscribing delivers the current network once, then only real changes after that. A new
subscriber does not have to seed itself with a separate read.
GetCurrentNetwork(ct) is async and reads live on every call. Hold the result rather than
re-reading it in a loop. There is no CurrentNetwork property - it was removed because the two
mobile platforms stopped answering synchronously (see below).
- Addressing is available everywhere; the SSID is not. IP/DNS/gateway come from the managed
network stack, so they need no permission. Individual fields are still best-effort - a platform
may not implement one (
GatewayAddresses is unsupported on Android) and a refused field comes
back null or empty rather than throwing. Ssid and Bssid need WifiCapabilities.CurrentNetwork
and the platform permission behind it, and come back null otherwise.
Why reading the SSID is asynchronous
Both mobile platforms removed the synchronous answer, and on both the failure is silent - the call
succeeds and the SSID is simply null:
- iOS 14+ -
CNCopyCurrentNetworkInfo returns nothing unless your own app configured the
network being asked about. The replacement, NEHotspotNetwork.fetchCurrent, is async-only.
Shiny uses it, so iOS now also reports Security and SignalStrengthPercent, which
CaptiveNetwork never did.
- Android 12 (API 31)+ - the SSID and BSSID are redacted out of every pull-style read
(
getConnectionInfo, and the WifiInfo off getNetworkCapabilities) no matter what permissions
are held. Only a NetworkCallback registered with FLAG_INCLUDE_LOCATION_INFO gets them, and
that is push-based. Shiny registers one and serves reads from it.
If a user reports a null or <unknown ssid> SSID with permissions granted, this is why - check
they are on a Shiny version with GetCurrentNetwork rather than telling them to add permissions.
Hotspot
if (!hotspot.IsSupported)
return;
await using var session = await hotspot.Start(
new HotspotConfiguration { Ssid = "shiny-setup", Passphrase = "letmein12345" },
ct
);
ShowToUser(session.Info.Ssid, session.Info.Passphrase);
if (wifi.Capabilities.HasFlag(WifiCapabilities.HotspotClients))
{
foreach (var client in await session.GetClients(ct))
Console.WriteLine($"{client.MacAddress} {client.IpAddress}");
}
- The session is the hotspot's lifetime. Disposing it (or calling
Stop) brings the access
point down. Android in particular tears its reservation down when the owning process exits.
HotspotCustomConfiguration tells you whether the SSID/passphrase you passed were honoured.
Windows and Linux honour them; Android does not.
GetClients() is a snapshot, not a subscription - poll it for a live count. A client appears
once it has taken a DHCP lease, not the instant it associates. On Linux the list comes from the
kernel neighbour table, so entries linger for a minute or so after a device leaves. Android
throws - it has no client list and blocked the ARP table apps used to read in Android 10.
- Android's hotspot is local-only: clients reach the device but get no internet. Windows shares
the machine's existing internet connection, and fails if there is none to share. Linux runs
NetworkManager AP mode with DHCP and NAT.
- Raising a hotspot usually takes the radio out of station mode, dropping the device off any
network it was joined to.
Radio power
if (wifi.Capabilities.HasFlag(WifiCapabilities.RadioToggle))
await wifi.SetRadioEnabled(true, ct);
Android revoked setWifiEnabled for third-party apps in API 29 - the capability flag is only set
below that. Send the user to Settings.Panel.ACTION_WIFI instead. iOS never allowed it.
Platform setup
Android — AndroidManifest.xml
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />