├── LICENSE
├── install.ps1
├── README.md
└── unifidash.ps1
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 fawn
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/install.ps1:
--------------------------------------------------------------------------------
1 | param(
2 | [string]$InstallPath = "C:\unifidash",
3 | [switch]$Uninstall
4 | )
5 |
6 | if ($Uninstall) {
7 | $currentPath = [Environment]::GetEnvironmentVariable("PATH", "Machine")
8 | $pathArray = $currentPath -split ";" | Where-Object { $_ -ne $InstallPath }
9 | $newPath = $pathArray -join ";"
10 | [Environment]::SetEnvironmentVariable("PATH", $newPath, "Machine")
11 | Remove-Item $InstallPath -Recurse -Force -ErrorAction SilentlyContinue
12 | Write-Host "Uninstalled"
13 | exit
14 | }
15 |
16 | if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
17 | Write-Host "Run as Administrator"
18 | exit
19 | }
20 |
21 | New-Item -ItemType Directory -Path $InstallPath -Force | Out-Null
22 | Copy-Item "unifidash.ps1" (Join-Path $InstallPath "unifidash.ps1") -Force
23 |
24 | $batchContent = "@echo off`npowershell.exe -ExecutionPolicy Bypass -File `"$(Join-Path $InstallPath "unifidash.ps1")`" %*"
25 | $batchContent | Out-File -FilePath (Join-Path $InstallPath "unifidash.bat") -Encoding ASCII -Force
26 |
27 | $currentPath = [Environment]::GetEnvironmentVariable("PATH", "Machine")
28 | if ($currentPath -split ";" -notcontains $InstallPath) {
29 | [Environment]::SetEnvironmentVariable("PATH", $currentPath + ";" + $InstallPath, "Machine")
30 | }
31 |
32 | Write-Host "Installed. Restart terminal and type: unifidash"
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # unifidash
4 |
5 | 
6 | 
7 | 
8 | 
9 |
10 | **Real-time network monitoring dashboard leveraging UniFi Controller's private undocumented REST API endpoints for comprehensive telemetry aggregation and performance analytics.**
11 |
12 |
13 |
14 |
15 | ---
16 |
17 | ## Architecture
18 |
19 | *wags tail* Built this because I wanted better visibility into my network infrastructure. This interfaces directly with Ubiquiti's undocumented controller API, bypassing the traditional web interface to extract raw telemetry data from multiple subsystems. The application implements parallel data collection across authentication, system statistics, device enumeration, client tracking, DPI analytics, and health monitoring endpoints.
20 |
21 | Core data flows include authenticated session management with CSRF token handling, JSON deserialization pipelines, and real-time bandwidth calculation w/ asynchronous coroutines. The system aggregates monthly usage statistics through direct DPI subsystem queries (I didn't manage to find a better endpoint for this, sorry) with refresh rates via ws-like persistent connections. Had to implement custom HMAC validation and session fingerprinting >< woof
22 |
23 | ## Implementation
24 |
25 | Sorry for linux users, this leverages pwsh's native HTTP client with custom session persistence, connection pooling, and SSL certificate validation bypass (since unifi uses a self cert locally).
26 |
27 | Tried optimizing performance (if like me you actually use this script with a cron job for other tasks) with concurrent API calls with exponential backoff retry logic, intelligent data caching using LRU eviction policies to minimize controller load, and streamlined object creation with memory pooling for large client datasets. *proud puppy noises* Did my absolute best to make it enterprise-grade performant~ wwwrf,,
28 |
29 | ## Installation
30 |
31 | Run the installer with administrator privileges:
32 | Run this inside an elevated shell if you don't have sudo installed.
33 |
34 | ```powershell
35 | sudo .\install.ps1
36 | ```
37 |
38 |
39 | This creates a system-wide installation and adds it to your PATH and registry. Since this is a powershell script we use a command wrapper using PowerShell manifest.
40 | Uninstall with `.\install.ps1 -Uninstall` if needed :(
41 |
42 | ## Configuration
43 |
44 | Edit the configuration hash table in the installed script:
45 |
46 | ```powershell
47 | $Config = @{
48 | IP = "192.168.1.1"
49 | Username = "admin"
50 | Password = "password"
51 | }
52 | ```
53 |
54 |
55 |
56 | Remember to feed it your actual credentials. Create a local user on your unifi dashboard and use those creds. Don't use your unifi SSO credentials.
57 |
58 | ## Usage
59 |
60 | Execute from any terminal:
61 |
62 | ```powershell
63 | unifidash
64 | ```
65 |
66 |
67 |
68 |

69 |
70 |
71 |
72 | The application will authenticate against the controller using secure token exchange protocols, enumerate all network devices with parallel topology discovery, collect real-time statistics via optimized polling algorithms, and render a comprehensive dashboard. *wags tail*
73 |
74 | ## API Coverage
75 |
76 |
77 |
78 | | Feature | Description |
79 | |---------|-------------|
80 | | 🔐 **Authentication** | Session management with cryptographic validation |
81 | | 📊 **System Info** | Hardware statistics with thermal monitoring |
82 | | 🌐 **Device Enum** | Status monitoring using SNMP-like protocols |
83 | | 📈 **Client Tracking** | Real-time throughput calculation and QoS analysis |
84 | | 🔍 **DPI Analytics** | Machine learning application classification |
85 | | ❤️ **Health Monitor** | WAN, LAN, wireless subsystems with anomaly detection |
86 | | 📉 **Usage Analytics** | Time-series analysis and trend prediction |
87 | | ⚡ **Speed Tests** | Jitter analysis and performance benchmarking |
88 |
89 |
90 |
91 | ## Requirements
92 |
93 | **PowerShell 5.1+** with network access to UniFi Express controller and elevated execution policies for enterprise security compliance.
94 |
95 | Administrator privileges required for system installation and registry modifications.
96 |
97 | Pwease be gentle with your network access, this dashboard is very sensitive and needs lots of pets :3
98 |
99 | ---
100 |
101 |
102 |
103 | Made with ❤️ and on a lot of drugs
104 |
105 |
106 |
--------------------------------------------------------------------------------
/unifidash.ps1:
--------------------------------------------------------------------------------
1 | # ==============================================================================
2 | # CONFIGURATION
3 | # ==============================================================================
4 |
5 | $Config = @{
6 | # UniFi Express Connection Settings
7 | IP = "your unifi ip"
8 | Username = "your unifi username"
9 | Password = "your unifi password"
10 |
11 | # Display Settings
12 | ShowTopUsers = 5 # Number of top bandwidth users to show
13 | ShowTopApps = 8 # Number of top applications to show
14 | ShowRecentEvents = 5 # Number of recent device events to show
15 | MinAppSizeMB = 1 # Minimum app usage to display (in MB)
16 |
17 | # Feature Toggles
18 | EnableSpeedTest = $true # Show speed test results
19 | EnableWiFiList = $true # Show WiFi networks
20 | EnableAppBreakdown = $true # Show application usage breakdown
21 | EnableDeviceEvents = $true # Show device adoption history
22 | EnableAllClients = $true # Show detailed client list
23 | }
24 |
25 | # ==============================================================================
26 | # HELPER FUNCTIONS
27 | # ==============================================================================
28 |
29 | function Write-Banner {
30 | param($Title, $Color = "Cyan")
31 |
32 | $border = "=" * ($Title.Length + 4)
33 | Write-Host ""
34 | Write-Host "+$border+" -ForegroundColor $Color
35 | Write-Host "| $Title |" -ForegroundColor $Color
36 | Write-Host "+$border+" -ForegroundColor $Color
37 | Write-Host ""
38 | }
39 |
40 | function Write-Section {
41 | param($Title, $Color = "Yellow")
42 |
43 | Write-Host ""
44 | Write-Host "--- $Title " -ForegroundColor $Color -NoNewline
45 | Write-Host ("-" * (60 - $Title.Length - 4)) -ForegroundColor $Color
46 | Write-Host ""
47 | }
48 |
49 | function Write-Status {
50 | param($Message, $Status = "Info", $NoNewline = $false)
51 |
52 | switch ($Status) {
53 | "Success" { $icon = "[OK]"; $color = "Green" }
54 | "Error" { $icon = "[ERR]"; $color = "Red" }
55 | "Warning" { $icon = "[WARN]"; $color = "Yellow" }
56 | "Info" { $icon = "[INFO]"; $color = "Cyan" }
57 | "Loading" { $icon = "[LOAD]"; $color = "Magenta" }
58 | default { $icon = "[*]"; $color = "White" }
59 | }
60 |
61 | if ($NoNewline) {
62 | Write-Host " $icon " -ForegroundColor $color -NoNewline
63 | Write-Host $Message -NoNewline
64 | } else {
65 | Write-Host " $icon " -ForegroundColor $color -NoNewline
66 | Write-Host $Message
67 | }
68 | }
69 |
70 | function Write-DataItem {
71 | param($Label, $Value, $Unit = "", $Color = "White")
72 |
73 | $formattedLabel = $Label.PadRight(20, '.')
74 | Write-Host " $formattedLabel " -NoNewline -ForegroundColor Gray
75 | Write-Host "$Value$Unit" -ForegroundColor $Color
76 | }
77 |
78 | function Write-ProgressDots {
79 | param($Count = 3)
80 |
81 | for ($i = 0; $i -lt $Count; $i++) {
82 | Start-Sleep -Milliseconds 300
83 | Write-Host "." -NoNewline -ForegroundColor Magenta
84 | }
85 | Write-Host ""
86 | }
87 |
88 | # ==============================================================================
89 | # MAIN SCRIPT
90 | # ==============================================================================
91 |
92 | Clear-Host
93 |
94 | Write-Host "==============================================================================" -ForegroundColor Cyan
95 | Write-Host " unifidash " -ForegroundColor Cyan
96 | Write-Host " UniFi System Dashboard " -ForegroundColor White
97 | Write-Host " made by fawn / github.com/57 " -ForegroundColor Gray
98 | Write-Host "==============================================================================" -ForegroundColor Cyan
99 |
100 | Write-Status "Initializing unifidash..." "Loading"
101 |
102 | # Skip SSL certificate validation for self-signed certs
103 | [System.Net.ServicePointManager]::ServerCertificateValidationCallback = {$true}
104 |
105 | Write-Section "Authentication" "Magenta"
106 |
107 | Write-Status "Connecting to UniFi Gateway at $($Config.IP)" "Loading" -NoNewline $true
108 | Write-ProgressDots
109 |
110 | try {
111 | $loginBody = @{
112 | username = $Config.Username
113 | password = $Config.Password
114 | } | ConvertTo-Json
115 |
116 | $loginResponse = Invoke-WebRequest -Uri "https://$($Config.IP)/api/auth/login" -Method POST -Headers @{"Content-Type"="application/json"} -Body $loginBody -SessionVariable 'unifiSession'
117 | Write-Status "Authentication successful!" "Success"
118 | } catch {
119 | Write-Status "Authentication failed: $($_.Exception.Message)" "Error"
120 | Write-Host ""
121 | exit 1
122 | }
123 |
124 | Write-Section "Data Collection" "Blue"
125 |
126 | Write-Status "Fetching system information" "Loading" -NoNewline $true
127 | Write-ProgressDots
128 | try {
129 | $systemResponse = Invoke-WebRequest -Uri "https://$($Config.IP)/api/system" -Method GET -WebSession $unifiSession
130 | $systemData = $systemResponse.Content | ConvertFrom-Json
131 | $sizeKB = [math]::Round($systemResponse.Content.Length / 1KB, 1)
132 | Write-Status "System data retrieved ($sizeKB KB)" "Success"
133 | } catch {
134 | Write-Status "Failed to get system data: $($_.Exception.Message)" "Error"
135 | exit 1
136 | }
137 |
138 | Write-Status "Fetching network devices" "Loading" -NoNewline $true
139 | Write-ProgressDots
140 | try {
141 | $deviceResponse = Invoke-WebRequest -Uri "https://$($Config.IP)/proxy/network/api/s/default/stat/device" -Method GET -WebSession $unifiSession
142 | $deviceData = $deviceResponse.Content | ConvertFrom-Json
143 | Write-Status "Device data retrieved ($($deviceData.data.Count) devices)" "Success"
144 | } catch {
145 | Write-Status "Failed to get device data" "Warning"
146 | }
147 |
148 | Write-Status "Fetching connected clients" "Loading" -NoNewline $true
149 | Write-ProgressDots
150 | try {
151 | $clientResponse = Invoke-WebRequest -Uri "https://$($Config.IP)/proxy/network/api/s/default/stat/sta" -Method GET -WebSession $unifiSession
152 | $clientData = $clientResponse.Content | ConvertFrom-Json
153 | Write-Status "Client data retrieved ($($clientData.data.Count) clients)" "Success"
154 | } catch {
155 | Write-Status "Failed to get client data" "Warning"
156 | }
157 |
158 | Write-Status "Fetching usage analytics" "Loading" -NoNewline $true
159 | Write-ProgressDots
160 | try {
161 | $monthlyDpiResponse = Invoke-WebRequest -Uri "https://$($Config.IP)/proxy/network/api/s/default/stat/dpi/monthly" -Method GET -WebSession $unifiSession
162 | $monthlyDpiData = $monthlyDpiResponse.Content | ConvertFrom-Json
163 | Write-Status "Monthly usage data retrieved" "Success"
164 | } catch {
165 | Write-Status "Usage analytics unavailable" "Warning"
166 | }
167 |
168 | Write-Status "Fetching health metrics" "Loading" -NoNewline $true
169 | Write-ProgressDots
170 | try {
171 | $healthResponse = Invoke-WebRequest -Uri "https://$($Config.IP)/proxy/network/api/s/default/stat/health" -Method GET -WebSession $unifiSession
172 | $healthData = $healthResponse.Content | ConvertFrom-Json
173 | Write-Status "Health metrics retrieved" "Success"
174 | } catch {
175 | Write-Status "Health metrics unavailable" "Warning"
176 | }
177 |
178 | # Fetch additional optional data
179 | $additionalData = @{}
180 |
181 | if ($Config.EnableWiFiList) {
182 | try {
183 | $wifiResponse = Invoke-WebRequest -Uri "https://$($Config.IP)/proxy/network/api/s/default/rest/wlanconf" -Method GET -WebSession $unifiSession
184 | $additionalData.WiFiNetworks = ($wifiResponse.Content | ConvertFrom-Json)
185 | } catch { }
186 | }
187 |
188 | if ($Config.EnableSpeedTest) {
189 | try {
190 | $speedTestResponse = Invoke-WebRequest -Uri "https://$($Config.IP)/proxy/network/api/s/default/stat/speedtest" -Method GET -WebSession $unifiSession
191 | $additionalData.SpeedTest = ($speedTestResponse.Content | ConvertFrom-Json)
192 | } catch { }
193 | }
194 |
195 | if ($Config.EnableAppBreakdown) {
196 | try {
197 | $dpiResponse = Invoke-WebRequest -Uri "https://$($Config.IP)/proxy/network/api/s/default/stat/dpi" -Method GET -WebSession $unifiSession
198 | $additionalData.DPI = ($dpiResponse.Content | ConvertFrom-Json)
199 | } catch { }
200 | }
201 |
202 | if ($Config.EnableDeviceEvents) {
203 | try {
204 | $eventsResponse = Invoke-WebRequest -Uri "https://$($Config.IP)/proxy/network/api/s/default/stat/event" -Method GET -WebSession $unifiSession
205 | $additionalData.Events = ($eventsResponse.Content | ConvertFrom-Json)
206 | } catch { }
207 | }
208 |
209 | Write-Status "Data collection complete! Processing results..." "Success"
210 |
211 | # ==============================================================================
212 | # DATA PROCESSING
213 | # ==============================================================================
214 |
215 | # Calculate uptime
216 | $uptimeSeconds = $systemData.uptime
217 | $uptimeDays = [math]::Floor($uptimeSeconds / 86400)
218 | $uptimeHours = [math]::Floor(($uptimeSeconds % 86400) / 3600)
219 | $uptimeMinutes = [math]::Floor(($uptimeSeconds % 3600) / 60)
220 |
221 | # Calculate monthly data usage
222 | $monthlyDataUsage = "N/A"
223 | if ($monthlyDpiData -and $monthlyDpiData.data -and $monthlyDpiData.data.by_cat) {
224 | $totalRx = 0
225 | $totalTx = 0
226 | foreach ($category in $monthlyDpiData.data.by_cat) {
227 | $totalRx += $category.rx_bytes
228 | $totalTx += $category.tx_bytes
229 | }
230 | $totalBytes = $totalRx + $totalTx
231 | if ($totalBytes -gt 0) {
232 | $totalGB = [math]::Round($totalBytes / 1GB, 2)
233 | if ($totalGB -gt 1000) {
234 | $monthlyDataUsage = "$([math]::Round($totalGB / 1000, 2)) TB"
235 | } else {
236 | $monthlyDataUsage = "$totalGB GB"
237 | }
238 | }
239 | }
240 |
241 | # Get current activity and speed test data
242 | $currentDownload = "N/A"
243 | $currentUpload = "N/A"
244 | $downloadSpeed = "N/A"
245 | $uploadSpeed = "N/A"
246 |
247 | if ($healthData -and $healthData.data) {
248 | foreach ($health in $healthData.data) {
249 | if ($health.subsystem -eq "wan") {
250 | if ($health.'tx_bytes-r' -and $health.'rx_bytes-r') {
251 | $currentDownload = "$([math]::Round($health.'rx_bytes-r' * 8 / 1000, 1)) Kbps"
252 | $currentUpload = "$([math]::Round($health.'tx_bytes-r' * 8 / 1000, 1)) Kbps"
253 | }
254 | }
255 | if ($health.subsystem -eq "www") {
256 | if ($health.speedtest_ping -and $health.speedtest_ping -gt 0) {
257 | if ($health.xput_down) { $downloadSpeed = "$([math]::Round($health.xput_down, 1)) Mbps" }
258 | if ($health.xput_up) { $uploadSpeed = "$([math]::Round($health.xput_up, 1)) Mbps" }
259 | }
260 | }
261 | }
262 | }
263 |
264 | # ==============================================================================
265 | # DASHBOARD DISPLAY
266 | # ==============================================================================
267 |
268 | Write-Banner "SYSTEM OVERVIEW" "Green"
269 |
270 | Write-DataItem "Site Name" $systemData.name "" "Cyan"
271 | Write-DataItem "Device Model" $systemData.hardware.name "" "White"
272 | Write-DataItem "Firmware" $systemData.hardware.firmwareVersion "" "Yellow"
273 | Write-DataItem "Hostname" $systemData.hostname "" "White"
274 |
275 | Write-Section "Network Information" "Cyan"
276 |
277 | Write-DataItem "Gateway IP" $systemData.network.interfaces.br0[0].address "" "Green"
278 | Write-DataItem "WAN IP" $systemData.ip "" "Green"
279 | Write-DataItem "ISP Provider" $systemData.ispInfo.name "" "White"
280 | Write-DataItem "Location" $systemData.location.text "" "Gray"
281 |
282 | Write-Section "System Status" "Green"
283 |
284 | Write-DataItem "Uptime" "$uptimeDays days, $uptimeHours hours, $uptimeMinutes minutes" "" "Green"
285 | $internetStatus = if($systemData.hasInternet){"Connected"}else{"Disconnected"}
286 | Write-DataItem "Internet Status" $internetStatus "" "White"
287 | Write-DataItem "Health Score" $systemData.apps.controllers[0].info.health.label "" "Green"
288 |
289 | Write-Section "Connected Devices" "Blue"
290 |
291 | Write-DataItem "Total Clients" $clientData.data.Count "" "Cyan"
292 | Write-DataItem "Wired Devices" $systemData.apps.controllers[0].info.wiredClients "" "Green"
293 | Write-DataItem "Wireless Devices" $systemData.apps.controllers[0].info.wirelessClients "" "Blue"
294 | Write-DataItem "Guest Devices" $systemData.apps.controllers[0].info.guestClients "" "Yellow"
295 |
296 | Write-Section "Data Usage" "Magenta"
297 |
298 | Write-DataItem "Monthly Total" $monthlyDataUsage "" "Magenta"
299 | Write-DataItem "Current Download" $currentDownload "" "Green"
300 | Write-DataItem "Current Upload" $currentUpload "" "Red"
301 | Write-DataItem "WiFi Experience" "$($systemData.apps.controllers[0].info.wifiExperienceScore)/100" "" "Yellow"
302 |
303 | # WiFi Networks
304 | if ($Config.EnableWiFiList -and $additionalData.WiFiNetworks -and $additionalData.WiFiNetworks.data) {
305 | Write-Section "WiFi Networks" "Yellow"
306 | foreach ($network in $additionalData.WiFiNetworks.data) {
307 | if ($network.enabled) {
308 | $security = if ($network.security) { $network.security } else { "Open" }
309 | Write-Host " [WiFi] " -NoNewline -ForegroundColor Yellow
310 | Write-Host "$($network.name)" -NoNewline -ForegroundColor White
311 | Write-Host " ($security, Channel $($network.channel))" -ForegroundColor Gray
312 | }
313 | }
314 | }
315 |
316 | # Speed Test Results
317 | if ($Config.EnableSpeedTest -and $additionalData.SpeedTest -and $additionalData.SpeedTest.data -and $additionalData.SpeedTest.data.Count -gt 0) {
318 | $latestTest = $additionalData.SpeedTest.data | Sort-Object time -Descending | Select-Object -First 1
319 | if ($latestTest.download -and $latestTest.upload) {
320 | $testDate = [DateTimeOffset]::FromUnixTimeMilliseconds($latestTest.time).ToString("yyyy-MM-dd HH:mm")
321 | Write-Section "Latest Speed Test ($testDate)" "Cyan"
322 | Write-DataItem "Download Speed" "$([math]::Round($latestTest.download / 1000000, 1))" "Mbps" "Green"
323 | Write-DataItem "Upload Speed" "$([math]::Round($latestTest.upload / 1000000, 1))" "Mbps" "Red"
324 | Write-DataItem "Ping" "$($latestTest.ping)" "ms" "Yellow"
325 | }
326 | }
327 |
328 | # Device Performance
329 | Write-Section "Device Performance" "Red"
330 |
331 | $memoryUsage = [math]::Round(($systemData.memory.total - $systemData.memory.available) / $systemData.memory.total * 100, 1)
332 | Write-DataItem "Memory Usage" "$memoryUsage" "%" "Yellow"
333 |
334 | $storageUsage = [math]::Round($systemData.storage[0].used / $systemData.storage[0].size * 100, 1)
335 | Write-DataItem "Storage Used" "$storageUsage" "%" "Red"
336 |
337 | $tempStatus = if($systemData.temperature){"$($systemData.temperature)C"}else{"Not available"}
338 | Write-DataItem "Temperature" $tempStatus "" "White"
339 |
340 | # Top Bandwidth Users
341 | if ($Config.ShowTopUsers -gt 0) {
342 | Write-Section "Top Bandwidth Users" "Magenta"
343 | $topUsers = $clientData.data | Where-Object { $_.rx_bytes -or $_.tx_bytes } | ForEach-Object {
344 | $clientName = if ($_.hostname) { $_.hostname } elseif ($_.name) { $_.name } else { "Unknown Device" }
345 | $totalBytes = ($_.rx_bytes + $_.tx_bytes)
346 | [PSCustomObject]@{
347 | Name = $clientName
348 | IP = if ($_.ip) { $_.ip } else { "No IP" }
349 | TotalMB = [math]::Round($totalBytes / 1MB, 1)
350 | DownloadMB = [math]::Round($_.rx_bytes / 1MB, 1)
351 | UploadMB = [math]::Round($_.tx_bytes / 1MB, 1)
352 | Type = if ($_.is_wired) { "Wired" } else { "WiFi" }
353 | }
354 | } | Sort-Object TotalMB -Descending | Select-Object -First $Config.ShowTopUsers
355 |
356 | foreach ($user in $topUsers) {
357 | $typeIcon = if ($user.Type -eq "Wired") { "[WIRE]" } else { "[WIFI]" }
358 | Write-Host " [TOP] " -NoNewline -ForegroundColor Yellow
359 | Write-Host "$($user.Name)" -NoNewline -ForegroundColor White
360 | Write-Host " ($($user.IP)) - $typeIcon" -ForegroundColor Gray
361 | Write-Host " Total: " -NoNewline -ForegroundColor Gray
362 | Write-Host "$($user.TotalMB) MB" -NoNewline -ForegroundColor Magenta
363 | Write-Host " (Down $($user.DownloadMB) MB Up $($user.UploadMB) MB)" -ForegroundColor Gray
364 | }
365 | }
366 |
367 | # Application Breakdown
368 | if ($Config.EnableAppBreakdown -and $additionalData.DPI -and $additionalData.DPI.data -and $additionalData.DPI.data.by_app) {
369 | Write-Section "Application Usage" "Green"
370 |
371 | $appNames = @{
372 | 1 = "[Web] Browsing"; 3 = "[Mail] Email"; 5 = "[DNS] DNS"; 9 = "[FTP] FTP"; 10 = "[Web] HTTP/HTTPS"
373 | 15 = "[Video] YouTube"; 17 = "[Video] Netflix"; 27 = "[Sec] SSL/TLS"; 29 = "[Net] DHCP"; 32 = "[SSH] SSH"
374 | 33 = "[P2P] BitTorrent"; 41 = "[Mail] SMTP"; 63 = "[Game] Gaming"; 68 = "[VPN] VPN"; 69 = "[Social] Facebook"
375 | 84 = "[Social] Instagram"; 94 = "[Chat] WhatsApp"; 107 = "[Sec] Encrypted"; 111 = "[Video] Zoom"
376 | 112 = "[Video] Streaming"; 120 = "[Apple] Services"; 130 = "[Work] MS Teams"
377 | 134 = "[Music] Spotify"; 136 = "[Video] TikTok"; 150 = "[Chat] Discord"; 160 = "[Chat] Telegram"
378 | 172 = "[Social] Reddit"; 185 = "[Cloud] iCloud"; 186 = "[Cloud] Google Drive"; 189 = "[Cloud] Dropbox"
379 | 190 = "[Cloud] OneDrive"; 193 = "[Sys] Update"; 194 = "[Store] App Store"; 195 = "[Sys] Software Update"
380 | 197 = "[Cloud] Storage"; 199 = "[Social] Media"; 222 = "[News] News"; 234 = "[Shop] Shopping"
381 | 250 = "[Maps] Maps"; 263 = "[Social] Snapchat"; 294 = "[VoIP] VoIP"; 318 = "[File] Sharing"
382 | 65535 = "[Other] Unknown"
383 | }
384 |
385 | $topApps = $additionalData.DPI.data.by_app | ForEach-Object {
386 | $totalBytes = $_.rx_bytes + $_.tx_bytes
387 | $appName = if ($appNames[$_.app]) { $appNames[$_.app] } else { "[App] App $($_.app)" }
388 | [PSCustomObject]@{
389 | Name = $appName
390 | TotalMB = [math]::Round($totalBytes / 1MB, 1)
391 | DownloadMB = [math]::Round($_.rx_bytes / 1MB, 1)
392 | UploadMB = [math]::Round($_.tx_bytes / 1MB, 1)
393 | Clients = $_.known_clients
394 | }
395 | } | Sort-Object TotalMB -Descending | Select-Object -First $Config.ShowTopApps | Where-Object { $_.TotalMB -gt $Config.MinAppSizeMB }
396 |
397 | foreach ($app in $topApps) {
398 | Write-Host " $($app.Name)" -NoNewline -ForegroundColor White
399 | Write-Host " - $($app.TotalMB) MB" -NoNewline -ForegroundColor Cyan
400 | Write-Host " ($($app.Clients) devices)" -ForegroundColor Gray
401 | Write-Host " Down $($app.DownloadMB) MB Up $($app.UploadMB) MB" -ForegroundColor Gray
402 | }
403 | }
404 |
405 | # Device Events
406 | if ($Config.EnableDeviceEvents -and $additionalData.Events -and $additionalData.Events.data) {
407 | Write-Section "Recent Device Events" "Yellow"
408 | $deviceEvents = $additionalData.Events.data | Where-Object {
409 | $_.key -eq "EVT_AP_Connected" -or $_.key -eq "EVT_AP_Disconnected" -or
410 | $_.key -eq "EVT_SW_Connected" -or $_.key -eq "EVT_SW_Disconnected" -or
411 | $_.key -eq "EVT_GW_Connected" -or $_.key -eq "EVT_GW_Disconnected" -or
412 | $_.key -eq "EVT_AP_Adopted" -or $_.key -eq "EVT_SW_Adopted"
413 | } | Sort-Object time -Descending | Select-Object -First $Config.ShowRecentEvents
414 |
415 | foreach ($event in $deviceEvents) {
416 | $eventTime = [DateTimeOffset]::FromUnixTimeMilliseconds($event.time).ToString("MM-dd HH:mm")
417 | $deviceName = if ($event.ap_name) { $event.ap_name } elseif ($event.sw_name) { $event.sw_name } elseif ($event.gw_name) { $event.gw_name } else { "Unknown Device" }
418 | $eventInfo = switch ($event.key) {
419 | "EVT_AP_Connected" { "[AP] Connected" }
420 | "EVT_AP_Disconnected" { "[AP] Disconnected" }
421 | "EVT_SW_Connected" { "[Switch] Connected" }
422 | "EVT_SW_Disconnected" { "[Switch] Disconnected" }
423 | "EVT_GW_Connected" { "[Gateway] Connected" }
424 | "EVT_GW_Disconnected" { "[Gateway] Disconnected" }
425 | "EVT_AP_Adopted" { "[AP] Adopted" }
426 | "EVT_SW_Adopted" { "[Switch] Adopted" }
427 | default { "[Event] $($event.key)" }
428 | }
429 | Write-Host " [Time] $eventTime - " -NoNewline -ForegroundColor Gray
430 | Write-Host "$deviceName" -NoNewline -ForegroundColor White
431 | Write-Host " - $eventInfo" -ForegroundColor Yellow
432 | }
433 | }
434 |
435 | # Uptime Statistics
436 | Write-Section "Uptime Statistics" "Cyan"
437 |
438 | Write-DataItem "Current Uptime" "$uptimeDays days, $uptimeHours hours, $uptimeMinutes minutes" "" "Green"
439 |
440 | if ($healthData -and $healthData.data) {
441 | $wanHealth = $healthData.data | Where-Object { $_.subsystem -eq "wan" }
442 | if ($wanHealth -and $wanHealth.uptime_stats -and $wanHealth.uptime_stats.WAN) {
443 | $uptimeInfo = $wanHealth.uptime_stats.WAN
444 | if ($uptimeInfo.availability) {
445 | Write-DataItem "WAN Availability" "$($uptimeInfo.availability)" "%" "Green"
446 | }
447 | if ($uptimeInfo.latency_average) {
448 | Write-DataItem "Average Latency" "$($uptimeInfo.latency_average)" "ms" "Yellow"
449 | }
450 | }
451 | }
452 |
453 | # All Connected Clients
454 | if ($Config.EnableAllClients) {
455 | Write-Section "All Connected Clients" "Blue"
456 | $clientData.data | ForEach-Object {
457 | $clientName = if ($_.hostname) { $_.hostname } elseif ($_.name) { $_.name } else { "Unknown Device" }
458 | $clientIP = if ($_.ip) { $_.ip } else { "No IP" }
459 | $clientType = if ($_.is_wired) { "[WIRE]" } else { "[WIFI]" }
460 | $rxMB = if ($_.rx_bytes) { [math]::Round($_.rx_bytes / 1MB, 1) } else { 0 }
461 | $txMB = if ($_.tx_bytes) { [math]::Round($_.tx_bytes / 1MB, 1) } else { 0 }
462 |
463 | Write-Host " $clientType " -NoNewline
464 | Write-Host "$clientName" -NoNewline -ForegroundColor White
465 | Write-Host " ($clientIP)" -NoNewline -ForegroundColor Gray
466 | Write-Host " - Down $($rxMB)MB Up $($txMB)MB" -ForegroundColor Cyan
467 | }
468 | }
469 |
470 | Write-Banner "DASHBOARD COMPLETE" "Green"
471 |
472 | Write-Status "unifidash completed successfully!" "Success"
473 | Write-Host ""
--------------------------------------------------------------------------------