Iedereen kent het moment.
Teams call die hapert.
Download die blijft hangen op 2 MB/s.
Of die ene collega die zegt: “bij mij werkt het gewoon hoor…”
En dan begint het: speedtest.net openen, screenshot maken, weer vergeten.
Dus dacht ik: dit moet slimmer kunnen.
🚀 Waarom ik dit script heb gemaakt
In mijn HomeLab én bij klanten wilde ik niet alleen weten hoe snel de verbinding is, maar vooral:
- Welke adapter wordt gebruikt?
- Zit je op WiFi of bekabeld?
- Wat is je publieke IP?
- Welke DNS gebruik je?
- Waar staat die testserver eigenlijk?
En vooral: ik wil logging. Altijd.
Niet gokken, maar data.
Daarom heb ik een PowerShell script gebouwd dat:
✔ Speedtest draait via de officiële Ookla CLI
✔ Netwerkdata verzamelt
✔ Alles netjes wegschrijft naar JSON én CSV
✔ Logs bijhoudt (ook handig voor Intune / troubleshooting)
🧠 Wat maakt dit script anders?
Dit is geen simpele “run speedtest en klaar”.
Het script:
- Detecteert je actieve netwerkadapter
- Haalt publieke IP op via meerdere bronnen (fallback logic 💪)
- Probeert je SSID te achterhalen (best effort, want Windows…)
- Verrijkt data met geolocation
- Logt alles gestructureerd
- Draait ook volledig silent (handig voor automation / Intune)
Oftewel: dit is production-ready spul
📦 Het script
Hieronder de volledige versie:
[CmdletBinding(SupportsShouldProcess=$true)]
param(
[switch]$Silent,
[string]$LogName = "NetworkSpeedTest",
[string]$OutputFolder = "C:\log"
)
<#
.NOTES
===========================================================================
Created on: 2026-03-15
Created by: Vincent van Unen
Filename: sca-commandlinespeedtest.ps1
===========================================================================
.DESCRIPTION
PowerShell script to perform a network speed test using Ookla's Speedtest CLI, gather network information, and log results in structured JSON and CSV formats. The script includes robust error handling, logging, and is designed to run in both interactive and silent modes.
.SYNOPSIS
This script performs a network speed test using Ookla's Speedtest CLI, gathers detailed network information, and logs results in structured JSON and CSV formats. It includes robust error handling and can run in both interactive and silent modes.
.PARAMETER Silent
Run the script in silent mode (no console output, only logs).
#>
# ==============================
# Helper: Console + Log Writers
# ==============================
function Write-Console {
param(
[Parameter(Mandatory=$true)][string]$Message,
[ValidateSet("White","Yellow","Green","Cyan","Red","Gray")][string]$Color = "White"
)
if (-not $Silent) {
Write-Host $Message -ForegroundColor $Color
}
}
# ===================================
# Function: Get SSID (best effort)
# ===================================
function Get-CurrentSSID {
try {
# Gebruik netsh, match alleen "SSID" (niet "BSSID")
$w = netsh wlan show interfaces 2>$null | Select-String -Pattern "^\s*SSID\s*:"
if ($w) {
$raw = ($w -split ":",2)[1].Trim()
if ($raw -and $raw -ne "SSID") { return $raw }
}
} catch { }
try {
# Fallback via net connection profile
$cp = Get-NetConnectionProfile | Where-Object { $_.InterfaceAlias -like "*Wi-Fi*" -or $_.InterfaceAlias -like "*Wireless*" } | Select-Object -First 1
if ($cp) { return $cp.Name }
} catch { }
try {
# Legacy WMI fallback
$wifiAdapter = Get-WmiObject -Class Win32_NetworkAdapter | Where-Object { $_.Name -like "*Wi-Fi*" -or $_.Name -like "*Wireless*" } | Select-Object -First 1
if ($wifiAdapter) {
$wifiConfig = Get-WmiObject -Class Win32_NetworkAdapterConfiguration | Where-Object { $_.Index -eq $wifiAdapter.Index -and $_.IPEnabled -eq $true }
if ($wifiConfig) { return $wifiConfig.Description }
}
} catch { }
return $null
}
# ==============================
# Administrator Check
# ==============================
if (-NOT ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole(
[Security.Principal.WindowsBuiltInRole] "Administrator")) {
Write-Console "This script requires administrator privileges." "Yellow"
if (-not $Silent) { Read-Host "Press Enter to exit" }
exit 1
}
# ==============================
# Global/Script Configuration
# ==============================
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Set-ExecutionPolicy -ExecutionPolicy Unrestricted -Scope LocalMachine -Force -WarningAction Ignore -ErrorAction Ignore
Set-ExecutionPolicy -ExecutionPolicy Unrestricted -Scope Process -Force -WarningAction Ignore -ErrorAction Ignore
Set-ExecutionPolicy -ExecutionPolicy Unrestricted -Scope CurrentUser -Force -WarningAction Ignore -ErrorAction Ignore
$MaximumFunctionCount = 16384
$MaximumVariableCount = 16384
# Dirs
$XIPMyModuleDir1 = "C:\temp"
$XIPMyModuleDir2 = "C:\tmp"
$XIPMyModuleDir3 = "C:\log"
foreach ($dir in @($XIPMyModuleDir1,$XIPMyModuleDir2,$XIPMyModuleDir3,$OutputFolder)) {
if (!(Test-Path -Path $dir)) { New-Item -ItemType Directory -Path $dir | Out-Null }
}
# Script meta
$ScriptAuthor = "Vincent van Unen"
$ScripVersion = "1.0.1"
$ScriptChangeDate = (Get-Date -Format 'yyyy-MM-dd')
$ScriptChangeLog = "Fixed Get-CurrentSSID scope + math rounders"
$ScriptCurrentUser = $env:UserName
$ScriptRunningDevice = $env:COMPUTERNAME
$Getcurrentdate = Get-Date -Format 'yyyy-MM-dd'
$NowStamp = Get-Date -Format 'yyyy-MM-dd_HH-mm-ss'
# Speedtest config
$speedtestVersion = "1.2.0"
$speedtestUrl = "https://install.speedtest.net/app/cli/ookla-speedtest-$speedtestVersion-win64.zip"
$tempPath = Join-Path $env:TEMP "speedtest_cli_$NowStamp"
$zipFile = Join-Path $env:TEMP "speedtest_$NowStamp.zip"
# Logging file
$LogFile = Join-Path $XIPMyModuleDir3 ("{0} {1}.log" -f $LogName, $Getcurrentdate)
# ==============================
# Logger
# ==============================
function WriteToLogFile {
[CmdletBinding()]
Param(
[Parameter(Mandatory=$False)]
[ValidateSet("INFO","WARN","ERROR","FATAL","DEBUG")]
[String] $Level = "INFO",
[Parameter(Mandatory=$True)]
[string] $Message,
[Parameter(Mandatory=$False)]
[string] $logfile = $LogFile
)
if ($Message -eq " ") {
Add-Content $logfile -Value " " -ErrorAction SilentlyContinue
} else {
$Date = (Get-Date).ToUniversalTime().ToString('yyyy-MM-dd HH:mm:ss.fff')
Add-Content $logfile -Value "[$Date] [$Level] $Message" -ErrorAction SilentlyContinue
}
}
# ==============================
# Transcript
# ==============================
$TranscriptFile = Join-Path $XIPMyModuleDir3 ("{0}_{1}_Transcript.log" -f $Getcurrentdate, $LogName)
try { Start-Transcript -Path $TranscriptFile -ErrorAction SilentlyContinue | Out-Null } catch {}
# ==============================
# Initial Log Lines
# ==============================
WriteToLogFile -Message "Current Date = $Getcurrentdate"
WriteToLogFile -Message "Script Author = $ScriptAuthor"
WriteToLogFile -Message "Script Version = $ScripVersion"
WriteToLogFile -Message "Script ChangeDate = $ScriptChangeDate"
WriteToLogFile -Message "Current User = $ScriptCurrentUser"
WriteToLogFile -Message "Current Device = $ScriptRunningDevice"
# ===================================
# Function: Get Network Information
# ===================================
function Get-NetworkInfo {
Write-Console "`nGathering network information..." "Cyan"
WriteToLogFile -Level "INFO" -Message "Gathering network information"
# Active adapter (exclude loopback)
$activeAdapter = Get-NetAdapter | Where-Object { $_.Status -eq "Up" -and $_.InterfaceType -ne "Software Loopback" } | Select-Object -First 1
if (-not $activeAdapter) {
WriteToLogFile -Level "ERROR" -Message "No active network adapter found."
throw "No active network adapter found."
}
# IP config for active adapter
$ipConfig = Get-NetIPConfiguration | Where-Object { $_.NetAdapter -and $_.NetAdapter.Name -eq $activeAdapter.Name }
# DNS (IPv4)
$dnsServersObj = Get-DnsClientServerAddress | Where-Object { $_.InterfaceAlias -eq $activeAdapter.Name -and $_.AddressFamily -eq 2 }
$dnsList = @()
if ($dnsServersObj) {
foreach ($o in $dnsServersObj) {
if ($o.ServerAddresses) { $dnsList += $o.ServerAddresses }
}
}
# Public IP (try multiple sources)
$publicIp = $null
$publicIpSources = @(
"https://api.ipify.org",
"https://icanhazip.com",
"https://ipinfo.io/ip"
)
foreach ($source in $publicIpSources) {
try {
$r = Invoke-RestMethod -Uri $source -TimeoutSec 5
if ($r) {
$publicIp = $r.ToString().Trim()
break
}
} catch { continue }
}
# Compute local fields safely
$localIPv4 = $ipConfig.IPv4Address.IPAddress
$prefixLen = $ipConfig.IPv4Address.PrefixLength
$gw = $ipConfig.IPv4DefaultGateway.NextHop
return @{
ActiveAdapter = $activeAdapter
IPConfig = $ipConfig
DNSServers = $dnsList
PublicIP = $publicIp
DefaultGateway = $gw
LocalIP = $localIPv4
SubnetMask = $prefixLen
}
}
# ===================================
# Main
# ===================================
$ResultsObject = $null
$JsonOut = Join-Path $OutputFolder ("SpeedTest_{0}_{1}.json" -f $env:COMPUTERNAME,$NowStamp)
$CsvOut = Join-Path $OutputFolder ("SpeedTest_{0}_{1}.csv" -f $env:COMPUTERNAME,$NowStamp)
try {
# 1) Netwerk info
$networkInfo = Get-NetworkInfo
# Log public/private quick
try {
$GetpublicIP = $networkInfo.PublicIP
} catch { $GetpublicIP = $null }
try {
$GetprivateIP = (Get-NetIPAddress | Where-Object { $_.AddressState -eq "Preferred" -and $_.ValidLifetime -lt "24:00:00" -and $_.AddressFamily -eq 'IPv4' } | Select-Object -First 1).IPAddress
} catch { $GetprivateIP = $null }
WriteToLogFile -Message "Current Public IP = $GetpublicIP"
WriteToLogFile -Message "Current Private IP = $GetprivateIP"
WriteToLogFile -Message "Active Adapter = $($networkInfo.ActiveAdapter.Name) / $($networkInfo.ActiveAdapter.InterfaceDescription)"
# 2) Download Speedtest CLI
Write-Console "Downloading Speedtest CLI..." "Green"
WriteToLogFile -Message "Downloading Speedtest from $speedtestUrl"
Invoke-WebRequest -Uri $speedtestUrl -OutFile $zipFile -UseBasicParsing -ErrorAction Stop
# 3) Extract
Write-Console "Extracting files..." "Green"
WriteToLogFile -Message "Extracting to $tempPath"
Expand-Archive -Path $zipFile -DestinationPath $tempPath -Force -ErrorAction Stop
# Locate speedtest.exe (support nested folder structure)
$speedtestExe = Get-ChildItem -Path $tempPath -Filter "speedtest.exe" -Recurse | Select-Object -First 1
if (-not $speedtestExe) {
throw "speedtest.exe not found after extraction."
}
# 4) Run speed test
Write-Console "Running speed test..." "Green"
WriteToLogFile -Message "Starting speed test"
Push-Location $speedtestExe.Directory.FullName
$ResultsJson = & .\speedtest.exe --accept-license --accept-gdpr --format=json
Pop-Location
Write-Console "Speed test completed!" "Green"
WriteToLogFile -Message "Speed test completed"
if (-not $Silent) { Clear-Host }
# 5) Parse + compute metrics (FIXED rounding)
$results = $ResultsJson | ConvertFrom-Json
$downloadMbps = [math]::Round($results.download.bandwidth * 8 / 1MB, 2)
$uploadMbps = [math]::Round($results.upload.bandwidth * 8 / 1MB, 2)
$pingLatency = [math]::Round($results.ping.latency, 2)
$jitter = [math]::Round($results.ping.jitter, 2)
$packetLoss = $results.packetLoss
# 6) Geo info (best effort)
$geoInfo = $null
try {
if ($networkInfo.PublicIP) {
$geoInfo = Invoke-RestMethod -Uri ("http://ip-api.com/json/{0}" -f $networkInfo.PublicIP) -TimeoutSec 10
}
} catch {
WriteToLogFile -Level "WARN" -Message "Could not retrieve geolocation information: $($_.Exception.Message)"
}
# 7) SSID (function now defined!)
$SSID = Get-CurrentSSID
# 8) Compose single PSCustomObject
$ResultsObject = [PSCustomObject]@{
# Speed Test Results
SpeedTestDateTime = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
SSID = $SSID
DownloadSpeed_Mbps = $downloadMbps
UploadSpeed_Mbps = $uploadMbps
PingLatency_ms = $pingLatency
Jitter_ms = $jitter
PacketLoss_Percent = $packetLoss
ISP = $results.isp
TestServer = "$($results.server.name) - $($results.server.location), $($results.server.country)"
TestURL = $results.result.url
# Network Information
ActiveAdapter = $networkInfo.ActiveAdapter.Name
AdapterType = $networkInfo.ActiveAdapter.InterfaceDescription
LinkSpeed = $networkInfo.ActiveAdapter.LinkSpeed
MACAddress = $networkInfo.ActiveAdapter.MacAddress
LocalIP = ("{0}/{1}" -f $networkInfo.LocalIP, $networkInfo.SubnetMask)
DefaultGateway = $networkInfo.DefaultGateway
PublicIP = $networkInfo.PublicIP
DNSServers = ($networkInfo.DNSServers -join ", ")
ConnectionState = $networkInfo.ActiveAdapter.Status
# Geolocation Information
Country = if ($geoInfo -and $geoInfo.status -eq "success") { $geoInfo.country } else { "N/A" }
Region = if ($geoInfo -and $geoInfo.status -eq "success") { $geoInfo.regionName } else { "N/A" }
City = if ($geoInfo -and $geoInfo.status -eq "success") { $geoInfo.city } else { "N/A" }
ZipCode = if ($geoInfo -and $geoInfo.status -eq "success") { $geoInfo.zip } else { "N/A" }
Latitude = if ($geoInfo -and $geoInfo.status -eq "success") { $geoInfo.lat } else { "N/A" }
Longitude = if ($geoInfo -and $geoInfo.status -eq "success") { $geoInfo.lon } else { "N/A" }
Timezone = if ($geoInfo -and $geoInfo.status -eq "success") { $geoInfo.timezone } else { "N/A" }
GeoISP = if ($geoInfo -and $geoInfo.status -eq "success") { $geoInfo.isp } else { "N/A" }
# Script/Host context
ScriptAuthor = $ScriptAuthor
ScriptVersion = $ScripVersion
ScriptChangeDate = $ScriptChangeDate
RunUser = $ScriptCurrentUser
RunDevice = $ScriptRunningDevice
}
# 9) Log key metrics
WriteToLogFile -Message "Download: $downloadMbps Mbps | Upload: $uploadMbps Mbps | Ping: $pingLatency ms | Jitter: $jitter ms | Loss: $packetLoss"
WriteToLogFile -Message "Server: $($ResultsObject.TestServer) | ISP: $($ResultsObject.ISP)"
WriteToLogFile -Message "PublicIP: $($ResultsObject.PublicIP) | LocalIP: $($ResultsObject.LocalIP)"
# 10) Persist JSON + CSV
$ResultsObject | ConvertTo-Json -Depth 6 | Out-File -FilePath $JsonOut -Encoding utf8
$ResultsObject | Export-Csv -NoTypeInformation -Delimiter ';' -Encoding UTF8 -Path $CsvOut
WriteToLogFile -Message "Saved JSON to $JsonOut"
WriteToLogFile -Message "Saved CSV to $CsvOut"
} catch {
WriteToLogFile -Level "ERROR" -Message $_.Exception.Message
Write-Console "Error: $($_.Exception.Message)" "Red"
} finally {
# Cleanup
try { Set-Location $env:TEMP } catch {}
foreach ($p in @($zipFile,$tempPath)) {
try {
if (Test-Path $p) {
if ((Get-Item $p) -is [System.IO.DirectoryInfo]) {
Remove-Item $p -Recurse -Force -ErrorAction SilentlyContinue
} else {
Remove-Item $p -Force -ErrorAction SilentlyContinue
}
WriteToLogFile -Level "DEBUG" -Message "Cleaned up $p"
}
} catch {}
}
# Transcript stop & copy to IME logs
try { Stop-Transcript | Out-Null } catch {}
try {
$imeLogs = "C:\ProgramData\Microsoft\IntuneManagementExtension\Logs"
if (!(Test-Path $imeLogs)) { New-Item -ItemType Directory -Path $imeLogs | Out-Null }
Copy-Item -Path $TranscriptFile -Destination $imeLogs -Force -ErrorAction SilentlyContinue
WriteToLogFile -Message "Transcript copied to $imeLogs"
} catch {}
}
# Final output to pipeline for programmatic use
if ($ResultsObject) {
$ResultsObject
}
if (-not $Silent) {
Read-Host "Press Enter to exit"
}
👉 Tip: op je website kun je hier een “copy” knop bij zetten — werkt altijd lekker.
📊 Wat krijg je eruit?
Na het draaien van het script krijg je:
JSON output (perfect voor automation)
- Ideaal voor API’s, dashboards of logging pipelines
CSV output (lekker voor Excel)
- Snel analyseren
- Trends bekijken
Log file
- Volledige run geschiedenis
- Errors + debug info
🔍 Voorbeeld van data die je krijgt
- Download / upload snelheid (Mbps)
- Ping + jitter + packet loss
- ISP + testserver
- Adapter + MAC address
- DNS servers
- Public & private IP
- Locatie (land, stad, timezone)
Kort gezegd: alles wat je nodig hebt om een netwerkprobleem écht te begrijpen
🧰 Waar gebruik ik dit voor?
Een paar real-life use cases:
💼 Intune monitoring
Silent draaien op endpoints → resultaten verzamelen
🏠 HomeLab insights
WiFi vs bekabeld vergelijken (spoiler: WiFi verliest altijd 😄)
🧑💻 Troubleshooting bij klanten
Niet meer discussiëren — gewoon data laten zien
📈 Performance logging
Periodiek draaien → trends ontdekken
⚠️ Kleine kanttekeningen
- Admin rechten zijn verplicht
- Speedtest CLI wordt gedownload (internet nodig)
- Geolocation is “best effort” (API afhankelijk)
💡 Bonus tip
Wil je dit naar een hoger niveau tillen?
Combineer dit met:
- Task Scheduler (periodiek meten)
- Azure Log Analytics
- Power BI dashboard
En ineens heb je enterprise-level netwerk monitoring… met PowerShell.
🎯 Conclusie
Dit script is ontstaan uit frustratie, maar geëindigd als iets wat ik nu standaard gebruik.
Geen nattevingerwerk meer.
Geen “het zal wel aan WiFi liggen”.
Gewoon:
👉 meten
👉 loggen
👉 weten