Monitoring UniFi WiFi Controllers with PRTG

With remote UniFi WiFi sites, I like to maintain a centralized monitoring dashboard. My PRTG server connects to sites over OpenVPN management tunnels. This also has the added advantage of monitoring end-to-end internet connectivity and latency.

Paessler’s KB has a sample script that enumerates connected access points. I go a bit further and enumerate connected wireless clients, and alert on devices that don’t have an appropriate IP, as this is indicative of a user-visible outage/connectivity issue.

Follow the instructions in the KB article above if you’re unsure how to deploy this script on your PRTG server.

Data I collect:

  1. Number of connected access points.
  2. Number of connected wireless clients.
  3. Number of devices with valid IP addresses.
  4. Number of devices with invalid IP addresses.
  5. Invalid client hostnames (if applicable).
  6. UniFi controller version.
  7. UniFi controller update status.

PRTG-UniFi-Graph

param(
	[string]$server = 'unifi.domain.com',
	[string]$port = '8443',
	[string]$site = 'default',
	[string]$username = 'admin',
	[string]$password = '123456',
	[string]$validIPRegex = '192.168*',
	[switch]$debug = $false
)

#Ignore SSL Errors
[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {$true}  

#Define supported Protocols
[System.Net.ServicePointManager]::SecurityProtocol = @("Tls12","Tls11","Tls","Ssl3")

# Confirm Powershell Version.
if ($PSVersionTable.PSVersion.Major -lt 3) {
	Write-Output "<prtg>"
	Write-Output "<error>1</error>"
	Write-Output "<text>Powershell Version is $($PSVersionTable.PSVersion.Major) Requires at least 3. </text>"
	Write-Output "</prtg>"
	Exit
}

# Create $controller and $credential using multiple variables/parameters.
[string]$controller = "https://$($server):$($port)"
[string]$credential = "`{`"username`":`"$username`",`"password`":`"$password`"`}"

# Start debug timer
$queryMeasurement = [System.Diagnostics.Stopwatch]::StartNew()

# Perform the authentication and store the token to myWebSession
try {
$null = Invoke-Restmethod -Uri "$controller/api/login" -method post -body $credential -ContentType "application/json; charset=utf-8"  -SessionVariable myWebSession
}catch{
	Write-Output "<prtg>"
	Write-Output "<error>1</error>"
	Write-Output "<text>Authentication Failed: $($_.Exception.Message)</text>"
	Write-Output "</prtg>"
	Exit
}

#Query API providing token from first query.
try {
$jsonresultat = Invoke-Restmethod -Uri "$controller/api/s/$site/stat/device/" -WebSession $myWebSession
$jsonresultat2 = Invoke-Restmethod -Uri "$controller/api/s/$site/stat/sta/" -WebSession $myWebSession
$jsonresultat3 = Invoke-Restmethod -Uri "$controller/api/s/$site/stat/sysinfo/" -WebSession $myWebSession

}catch{
	Write-Output "<prtg>"
	Write-Output "<error>1</error>"
	Write-Output "<text>API Query Failed: $($_.Exception.Message)</text>"
	Write-Output "</prtg>"
	Exit
}

# Stop debug timer
$queryMeasurement.Stop()


# Iterate jsonresultat and count the number of APs. 
#   $_.state -eq "1" = Connected 
#   $_.type -like "uap" = Access Point ?

$apCount = 0
Foreach ($entry in ($jsonresultat.data | where-object { $_.state -eq "1" -and $_.type -like "uap"})){
	$apCount ++
}

$apUpgradeable = 0
Foreach ($entry in ($jsonresultat.data | where-object { $_.state -eq "1" -and $_.type -like "uap" -and $_.upgradable -eq "true"})){
	$apUpgradeable ++
}

$userCount = 0
Foreach ($entry in ($jsonresultat.data | where-object { $_.type -like "uap"})){
	$userCount += $entry.'num_sta'
}

$guestCount = 0
Foreach ($entry in ($jsonresultat.data | where-object { $_.type -like "uap"})){
	$guestCount += $entry.'guest-num_sta'
}

# Now get data about connected clients

$invalidClientIPs = 0
Foreach ($entry in ($jsonresultat2.data | where-object { $_.ip -notlike $validIPRegex})){
	$invalidClientIPs ++
}

$validClientIPs = 0
Foreach ($entry in ($jsonresultat2.data | where-object { $_.ip -like $validIPRegex})){
	$validClientIPs ++
}

$invalidClientHostnames = ''
Foreach ($entry in ($jsonresultat2.data | where-object { $_.ip -notlike $validIPRegex})){
	if ($entry.hostname -ne $null){
		$invalidClientHostnames += $entry.'hostname' + ', '
	}
	else {
		$invalidClientHostnames += $entry.'mac' + ', '
	}
}

# Now get controller information
$controllerVersion = [version]$jsonresultat3.data.'version'
$controllerUpdateAvailable = $jsonresultat3.data.'update_available'
$controllerUpdateDownloaded = $jsonresultat3.data.'update_downloaded'

#Write Results

Write-Host "<prtg>"

# APs

Write-Host "<result>"
Write-Host "<channel>Access Points Connected</channel>"
Write-Host "<value>$($apCount)</value>"
Write-Host "</result>"

Write-Host "<result>"
Write-Host "<channel>Access Points Upgradeable</channel>"
Write-Host "<value>$($apUpgradeable)</value>"
Write-Host "</result>"

Write-Host "<result>"
Write-Host "<channel>Clients (Total)</channel>"
Write-Host "<value>$($userCount)</value>"
Write-Host "</result>"

Write-Host "<result>"
Write-Host "<channel>Guests</channel>"
Write-Host "<value>$($guestCount)</value>"
Write-Host "</result>"

# Clients

Write-Host "<result>"
Write-Host "<channel>Devices with Invalid Client IPs</channel>"
Write-Host "<LimitMode>1</LimitMode>"
Write-Host "<LimitMaxWarning>3</LimitMaxWarning>"
Write-Host "<LimitMaxError>4</LimitMaxError>"
Write-Host "<value>$($invalidClientIPs)</value>"
Write-Host "<LimitWarningMsg>$($invalidClientHostnames)</LimitWarningMsg>"
Write-Host "<LimitErrorMsg>$($invalidClientHostnames)</LimitErrorMsg>"
Write-Host "</result>"

Write-Host "<result>"
Write-Host "<channel>Devices with Valid Client IPs</channel>"
Write-Host "<value>$($validClientIPs)</value>"
Write-Host "</result>"

# Controller

Write-Host "<result>"
Write-Host "<channel>UniFi Controller Version</channel>"
Write-Host "<value>$($controllerVersion.Major)$($controllerVersion.Minor)$($controllerVersion.Build)</value>"
Write-Host "</result>"

Write-Host "<result>"
Write-Host "<channel>UniFi Controller Update Available</channel>"
Write-Host "<value>$($controllerUpdateAvailable)</value>"
Write-Host "</result>"

Write-Host "<result>"
Write-Host "<channel>UniFi Controller Update Downloaded</channel>"
Write-Host "<value>$($controllerUpdateDownloaded)</value>"
Write-Host "</result>"

Write-Host "<result>"
Write-Host "<channel>Response Time</channel>"
Write-Host "<value>$($queryMeasurement.ElapsedMilliseconds)</value>"
Write-Host "<CustomUnit>msecs</CustomUnit>"
Write-Host "</result>"

Write-Host "<Text>Controller: $($controllerVersion.ToString()). Invalid hostnames: $($invalidClientHostnames)</Text>"

Write-Host "</prtg>"

# Write JSON file to disk when -debug is set. For troubleshooting only.
if ($debug){
	[string]$logPath = ((Get-ItemProperty -Path "hklm:SOFTWARE\Wow6432Node\Paessler\PRTG Network Monitor\Server\Core" -Name "Datapath").DataPath) + "Logs (Sensors)\"
	$timeStamp = (Get-Date -format yyyy-dd-MM-hh-mm-ss)

	$json = $jsonresultat | ConvertTo-Json
	$json | Out-File $logPath"unifi_sensor$($timeStamp)_log.json"
}