Tool Time - WiFi Info
I want to continue the introduction of a new set of PowerShell tools I've been working on. Today's tool was developed to solve a problem I had, although the solution may be useful to others. However, the reason I share this is that I want to share the process I used because that may be of more value to you than the tool itself.
The Problem
My family has a lake cabin that we often visit. I have a NAS device at the lake, as well as one at home. The NAS devices synchronize so I have the same shared folders, like Backup
, at both locations. When I am at the lake with my laptop, I want it to map PSDrives to the NAS at the lake. When I am at home, I want it to map PSDrives to the NAS at home. I don't want to rely solely on the IP Address because it is possible that I'll connect to a network while traveling that uses the same private address space.
I need to get WiFi connection information to determine if I am at the lake or at home. Unfortunately, even though there are PowerShell cmdlets that provide network adapter information, as far as I can tell, there's nothing that will tell me what WiFi network I am connected to.
Using netsh
Fortunately, I can get this information using the old-school netsh
command.
PS C:\> netsh wlan show interfaces
There is 1 interface on the system:
Name : Wi-Fi
Description : Intel(R) Wi-Fi 6 AX201 160MHz
GUID : 69deaa61-268f-42f0-abab-d115024094da
Physical address : 34:2e:b7:6d:e7:32
Interface type : Primary
State : connected
SSID : HomeOffice
BSSID : 78:45:58:fd:b3:76
Network type : Infrastructure
Radio type : 802.11ac
Authentication : WPA2-Personal
Cipher : CCMP
Connection mode : Auto Connect
Band : 5 GHz
Channel : 36
Receive rate (Mbps) : 866.7
Transmit rate (Mbps) : 866.7
Signal : 96%
Profile : HomeOffice
Hosted network status : Not available
This is exactly what I need. I can see the SSID of the network I am connected to. I can use this information to determine if I am at the lake or at home.
Now, I could simply parse this output. It isn't difficult to get the SSID using Select-String
.
PS C:\> netsh wlan show interfaces | Select-String "\bSSID\b"
SSID : HomeOffice
The \b
indicates a regular expression word boundary. This way I don't match BSSID
. I could even go so far as to split the string and get the value.
PS C:\> (netsh wlan show interfaces | Select-String "\bSSID\b").Line.Split(":")[1].Trim()
HomeOffice
However that leaves a lot of potentially useful information on the table. I can envision a situation at a hotel where I want to see my connection speed and signal strength. You don't have to go crazy, but try to avoid MVP coding, that is, the minimum viable product. You can't predict every way a user might want to use your command our its output, so try to make it as rich and useful as possible.
Mapping the Data
Unfortunately, unlike other command-line tools, netsh
lacks any sort of formatting option. My work would be so much easier if I could get JSON, or even XML, output. But I can't. So I need to parse the text output and create objects.
The output from netsh
already looks like a list of key-value pairs or object properties. I want to work with the lines with a colon (:) in them. I can use Select-String
to filter the lines I want.
PS C:\> netsh wlan show interfaces | where {$_ -match " : "}
Name : Wi-Fi
Description : Intel(R) Wi-Fi 6 AX201 160MHz
GUID : 69deaa61-268f-42f0-abab-d115024094da
Physical address : 34:2e:b7:6d:e7:32
Interface type : Primary
State : connected
SSID : HomeOffice
BSSID : 78:45:58:fd:b3:76
Network type : Infrastructure
Radio type : 802.11ac
Authentication : WPA2-Personal
Cipher : CCMP
Connection mode : Auto Connect
Band : 5 GHz
Channel : 36
Receive rate (Mbps) : 866.7
Transmit rate (Mbps) : 866.7
Signal : 96%
Profile : HomeOffice
Hosted network status : Not available
Notice the spaces in the Where-Object
filter. This is because I don't want to include the line There is 1 interface on the system:
.
What I want to do is create a mapping table that will convert the key-value pairs into properties of an object. I can use a hashtable for this.
netsh wlan show interfaces | where {$_ -match " : "} |
ForEach-Object -Begin { $h = [ordered]@{} } -Process {
$line = $_.Trim()
#split into 2 strings
$data = $line -split ':', 2
#need to trim spaces from everything and drop the % from the signal
$h.add($data[0].Trim(), $data[1].trim().replace('%', ''))
}
This code parses each line of the `netsh` output, splitting it into two strings at the colon. The first string is the key, and the second string is the value. I trim any leading or trailing spaces and remove any percent signs from the signal value. I then add the key-value pair to the hashtable. The `-Begin` block initializes the hashtable. The `-Process` block adds each key-value pair to the hashtable. The result is a hashtable that I can use.
```shell
PS C:\> $h
Name Value
---- -----
Name Wi-Fi
Description Intel(R) Wi-Fi 6 AX201 160MHz
GUID 69deaa61-268f-42f0-abab-d115024094da
Physical address 34:2e:b7:6d:e7:32
Interface type Primary
State connected
SSID HomeOffice
BSSID 78:45:58:fd:b3:76
Network type Infrastructure
Radio type 802.11ac
Authentication WPA2-Personal
Cipher CCMP
Connection mode Auto Connect
Band 5 GHz
Channel 36
Receive rate (Mbps) 866.7
Transmit rate (Mbps) 866.7
Signal 97
Profile HomeOffice
Hosted network status Not available
I may not have made it clear, but I'm not writing code that I can run at the prompt. I want to create a function that I, or you, can run. Part of that code will be a hashtable I can use in my script file to define the property names. These are preliminary steps I only need to do once. Eventually, I'll write code that will convert the netsh
output into an object.
The mapping hashtable I envision shouldn't have spaces or special characters. So I'll refine the data I have in $h
.
$h.keys | Foreach-Object -Begin { $o = "@{`n" } -Process {
$o += "$($_.Replace('\s','')) = '$($_.trim())'`n"
} -End { $o += '}' }
I suppose I could have simply modified $h
, but I decided to create another hashtable. This way, if I made a mistake I wouldn't have to recreate $h
. Here's where I am so far with $o
.
@{
Name = 'Name'
Description = 'Description'
GUID = 'GUID'
Physical address = 'Physical address'
Interface type = 'Interface type'
State = 'State'
SSID = 'SSID'
BSSID = 'BSSID'
Network type = 'Network type'
Radio type = 'Radio type'
Authentication = 'Authentication'
Cipher = 'Cipher'
Connection mode = 'Connection mode'
Band = 'Band'
Channel = 'Channel'
Receive rate (Mbps) = 'Receive rate (Mbps)'
Transmit rate (Mbps) = 'Transmit rate (Mbps)'
Signal = 'Signal'
Profile = 'Profile'
Hosted network status = 'Hosted network status'
}
I can copy and paste this into my script file and edit it. The keys will be the new object property names, and the values will be the corresponding values from the netsh
output.
$map = [ordered]@{
Name = 'Name'
Description = 'Description'
GUID = 'GUID'
PhysicalAddress = 'Physical address'
InterfaceType = 'Interface type'
State = 'State'
SSID = 'SSID'
BSSID = 'BSSID'
NetworkType = 'Network type'
RadioType = 'Radio type'
Authentication = 'Authentication'
Cipher = 'Cipher'
ConnectionMode = 'Connection mode'
Band = 'Band'
Channel = 'Channel'
ReceiveRate = 'Receive rate (Mbps)'
TransmitRate = 'Transmit rate (Mbps)'
Signal = 'Signal'
Profile = 'Profile'
HostedNetworkStatus = 'Hosted network status'
}
With this mapping table, I can now create a function that will convert the netsh
output into an object. I'll test with the data I have in $h
.
$map.GetEnumerator() |
ForEach-Object -Begin {
$n = [ordered]@{PSTypeName="PSWiFiInfo"}
} -Process {
$n.Add($_.Key,$h[$_.value])
} -End {
New-Object -TypeName PSObject -Property $n
} | Tee-Object -Variable w
The result is an object like this:
Name : Wi-Fi
Description : Intel(R) Wi-Fi 6 AX201 160MHz
GUID : 69deaa61-268f-42f0-abab-d115024094da
PhysicalAddress : 34:2e:b7:6d:e7:32
InterfaceType : Primary
State : connected
SSID : HomeOffice
BSSID : 78:45:58:fd:b3:76
NetworkType : Infrastructure
RadioType : 802.11ac
Authentication : WPA2-Personal
Cipher : CCMP
ConnectionMode : Auto Connect
Band : 5 GHz
Channel : 36
ReceiveRate : 866.7
TransmitRate : 866.7
Signal : 97
Profile : HomeOffice
HostedNetworkStatus : Not available
Every value is a string, which may not be that big a deal since I only have one object. But I can address that in a little bit.
Get-WifiInfo
Now that I have the core code, I can create a PowerShell function around all of this.
Function Get-WiFiInfo {
[CmdletBinding()]
[OutputType('PSWiFiInfo')]
[Alias("wifi")]
Param()
#Verify the wlan service is running
$svc = Get-Service wlansvc -ErrorAction SilentlyContinue
if ($svc.Status -eq 'running') {
$data = netsh wlan show interfaces
}
else {
Write-Warning 'The Wireless AutoConfig Service (wlansvc) is not running.'
#bail out
return
}
if ($data -match 'no wireless' ) {
Write-Warning 'No wireless interface detected on this computer.'
#bail out
return
}
# output might vary based on the wireless adapter or
$map = [ordered]@{
Name = 'Name'
Description = 'Description'
GUID = 'GUID'
PhysicalAddress = 'Physical address'
InterfaceType = 'Interface type'
State = 'State'
SSID = 'SSID'
BSSID = 'BSSID'
NetworkType = 'Network type'
RadioType = 'Radio type'
Authentication = 'Authentication'
Cipher = 'Cipher'
ConnectionMode = 'Connection mode'
Band = 'Band'
Channel = 'Channel'
ReceiveRate = 'Receive rate (Mbps)'
TransmitRate = 'Transmit rate (Mbps)'
Signal = 'Signal'
Profile = 'Profile'
HostedNetworkStatus = 'Hosted network status'
}
$data | Where-Object { $_ -match ' : ' } |
ForEach-Object -Begin { $h = [ordered]@{} } -Process {
$line = $_.Trim()
$data = $line -split ':', 2
#need to trim spaces from everything
$h.add($data[0].Trim(), $data[1].trim().replace('%', ''))
}
$map.GetEnumerator() | ForEach-Object -Begin {
$n = [ordered]@{PSTypeName = 'PSWiFiInfo' }
} -Process {
#$_ will change meaning based on scope
$mValue = $_.Value
#find the corresponding key from $h using the map value as a regex pattern
$MapValue = ($h.keys).Where({$_ -match "\b$mValue\b"})
Switch -regex ($_.Key) {
#Set values to appropriate types
# $_ here will be $_.Key
'ReceiveRate' { [double]$v = $h[$_] }
'Channel|Signal|TransmitRate' { [Int32]$v = $h[$_] }
Default { [String]$v = $h[$MapValue] }
} #Switch
$n.Add($_.Key, $v)
} -End {
#add the computer name
$n.Add('ComputerName', $env:COMPUTERNAME)
New-Object -TypeName PSObject -Property $n
}
}
During testing, I realized I should add error handling for the case where the wlansvc
service isn't running. In the ForEach-Object
loop, I'm using a Switch
statement to evaluate the key value from the netsh
output. I'm using the -regex
option so that I can use a regex pattern to compare the key.
Switch -regex ($_.Key) {
#Set values to appropriate types
# $_ here will be $_.Key
'ReceiveRate' { [double]$v = $h[$_] }
'Channel|Signal|TransmitRate' { [Int32]$v = $h[$_] }
Default { [String]$v = $h[$MapValue] }
} #Switch
This allows me to treat the keys as different object types.
I also discovered that the netsh
output can vary. On my desktop computer, which also has a WiFi adapter, I get slightly different output I had to adjust my regex pattern accordingly. And even though I had properties like QoS MSCS Configured
, I decided to not add them to my mapping hashtable.
Finally, I added a ComputerName
property to the object. This is a good practice because it can be useful to know where the data came from.
I even defined an alias for the function.
PS C:\> wifi
Name : Wi-Fi
Description : Intel(R) Wi-Fi 6 AX201 160MHz
GUID : 69deaa61-268f-42f0-abab-d115024094da
PhysicalAddress : 34:2e:b7:6d:e7:32
InterfaceType : Primary
State : connected
SSID : HomeOffice
BSSID : 78:45:58:fd:b3:76
NetworkType : Infrastructure
RadioType : 802.11ac
Authentication : WPA2-Personal
Cipher : CCMP
ConnectionMode : Auto Connect
Band : 5 GHz
Channel : 36
ReceiveRate : 0
TransmitRate : 0
Signal : 96
Profile : HomeOffice
HostedNetworkStatus : Not available
ComputerName : THINKX1-JH
Remember that I said to create rich objects? This is a lot, but because the object has a defined type name, I can create a custom format file to display the data in a more readable format.
Update-FormatData $PSScriptRoot\PSWiFiInfo.format.ps1xml
This is much nicer.
PS C:\> Get-WiFiInfo
SSID: HomeOffice
State : connected
Authentication : WPA2-Personal
Band : 5 GHz
Channel : 36
Signal : 96
RadioType : 802.11ac
I can always pipe the command to Select-Object *
to see all the properties.
In my profile script, I use the function to get the SSID and then map PSDrives accordingly.
Summary
I hope you were able to follow the process I went through to take command line output and turn it into a useful PowerShell function that writes a structured, rich object to the pipeline.
You can download a zip file with my function and the format file from Dropbox. Put both files in the same folder and dot-source the script file. It should work in Windows PowerShell and PowerShell 7.
If I lost you at any point, please let don't hesitate to post a comment.