diff --git a/.assets/prtg_channelview.png b/.assets/prtg_channelview.png new file mode 100644 index 0000000..0d32239 Binary files /dev/null and b/.assets/prtg_channelview.png differ diff --git a/.assets/prtg_sensorsettings.png b/.assets/prtg_sensorsettings.png new file mode 100644 index 0000000..a3dd944 Binary files /dev/null and b/.assets/prtg_sensorsettings.png differ diff --git a/README.md b/README.md index ffe6696..f25cb55 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,60 @@ -# template_repository +# PRTG Sensor: Netgear LM1200 – Datenvolumen & Funkwerte +Ein PRTG **Programm/Script (Erweitert)** Sensor, der sich am **Netgear LM1200** anmeldet, den monatlichen Datenverbrauch sowie Funkkennzahlen abfragt und als Kanäle an PRTG zurückgibt. +## Features +- Monatswerte: Limit, Verfuegbar, Verbrauch (GiB) und Verbrauch in Prozent +- Sitzung: Empfangene, Gesendete und Gesamt-Daten (MiB) +- Funkwerte: RSRP (Signalpegel), RSRQ (Qualitaet), SINR (Stoerabstand) +- Sensortext mit IP, Funktyp und Band sowie Anbieter +- Stabile Kanalreihenfolge durch fuehrende Nummern (01…11) -Wichtig: Link für Lizenz anpassen. +## Ablauf PRTG (Installation) +1. Datei ablegen unter: + `C:\Program Files (x86)\PRTG Network Monitor\Custom Sensors\EXEXML\lm1200-prtg-usage.v1.ps1` +2. In PRTG einen neuen Sensor anlegen: + Sensor-Typ: `Programm/Script (Erweitert)` + In den Spezifischen Sensoreinstellungen das Script `lm1200-prtg-usage.v1.ps1` auswaehlen. +3. Parameter eintragen (nur diese beiden sind noetig): + `-LmIp IP_ADRESSE -LmPass MEIN_PASSWORD` +Beispiel: + `-LmIp 192.168.178.1 -LmPass MeinSuperPasswort` + +![Einstellungen Sensor](.assets/prtg_sensorsettings.png) + +## Getestete Umgebung +- Hardware: Netgear LM1200 +- PRTG: 25.3.110.1313 + +## Ausgegebenen Kanaele (Beispiel) +01 Datenvolumen Limit (GiB) +02 Datenvolumen Verfuegbar (GiB) +03 Datenvolumen Verbrauch aktueller Monat (GiB) +04 Datenvolumen Verbrauch aktueller Monat (%) +05 Tage bis Abrechnungszyklus +06 Empfangene Daten (Sitzung, MiB) +07 Gesendete Daten (Sitzung, MiB) +08 Daten gesamt (Sitzung, MiB) +09 RSRP (Signalpegel) +10 RSRQ (Qualitaet) +11 SINR (Stoerabstand) + +![Übersicht Kanaele](.assets/prtg_channelview.png) + +## Hinweise +- Das Script nutzt den LM1200-Login-Flow (secToken + Session) und liest api/model.json. +- Einheiten im Namen: GiB/MiB (binary). Alarme komfortabel ueber Channel-Limits in PRTG setzen (z. B. Kanal 02: Warning 5 GiB, Error 2 GiB). +- Wenn Kanalnamen geaendert werden, am einfachsten den Sensor einmal loeschen und neu anlegen, damit keine Alt-Kanaele bleiben. + +## Lizenz +MIT + +## 💬 Support & Community +Du hast Fragen, brauchst Unterstützung bei der Einrichtung oder möchtest dich einfach mit anderen austauschen, die ähnliche Projekte betreiben? Dann schau gerne in unserer Techniverse Community vorbei: + +👉 **Matrix-Raum:** [#community:techniverse.net](https://matrix.to/#/#community:techniverse.net) +Wir freuen uns auf deinen Besuch und helfen dir gerne weiter!

@@ -11,5 +62,5 @@ Wichtig: Link für Lizenz anpassen.

-License License | Matrix Matrix | Matrix Mastodon +License License | Matrix Matrix | Matrix Mastodon

\ No newline at end of file diff --git a/lm1200-prtg-usage.v1.ps1 b/lm1200-prtg-usage.v1.ps1 new file mode 100644 index 0000000..1b636d1 --- /dev/null +++ b/lm1200-prtg-usage.v1.ps1 @@ -0,0 +1,153 @@ +# Script Name: lm1200-prtg-usage.v1.ps1 +# Beschreibung: Überwacht einen Netgear LM1200 auf Traffic und Netzwerkstatus +# Aufruf: powershell -NoProfile -ExecutionPolicy Bypass -File .\lm1200-prtg-usage.v1.ps1 -LmIp IP_ADRESSE -LmPass "MEIN_PASSWORD" +# Autor: Patrick Asmus +# Web: https://www.cleveradmin.de +# Git-Reposit.: https://git.techniverse.net/scriptos/lm1200-prtg-datausage-sensor.git +# Version: 1.0 +# Datum: 23.09.2025 +# Modifikation: Release Version 1 +##################################################### + + +param( + [Parameter(Mandatory = $true)][string]$LmIp, + [ValidateSet("http","https")][string]$LmProto = "http", + [Parameter(Mandatory = $true)][string]$LmPass, + [int]$TimeoutSec = 10, + [switch]$InsecureTls, + [double]$MinRemainingGiB = [double]::NaN, + [double]$MaxPercentUsed = [double]::NaN, + [switch]$TripSensorOnThreshold +) + +$ErrorActionPreference = 'Stop' +$ProgressPreference = 'SilentlyContinue' +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 +[System.Threading.Thread]::CurrentThread.CurrentCulture = [System.Globalization.CultureInfo]::InvariantCulture +[System.Threading.Thread]::CurrentThread.CurrentUICulture = [System.Globalization.CultureInfo]::InvariantCulture + +$Base = "${LmProto}://${LmIp}" +$Sess = New-Object Microsoft.PowerShell.Commands.WebRequestSession +$Headers = @{ "Accept"="application/json"; "X-Requested-With"="XMLHttpRequest" } +$HasBasic = $PSVersionTable.PSVersion.Major -lt 6 + +if ($LmProto -eq "https" -and $InsecureTls) { + Add-Type @" +using System.Net; +using System.Security.Cryptography.X509Certificates; +public class TrustAllCertsPolicy : ICertificatePolicy { + public bool CheckValidationResult(ServicePoint srvPoint, X509Certificate certificate, WebRequest request, int certificateProblem) { return true; } +} +"@ | Out-Null + [System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy +} + +function Invoke-Json([string]$Uri) { + if ($HasBasic) { Invoke-RestMethod -UseBasicParsing -Uri $Uri -Headers $Headers -WebSession $Sess -TimeoutSec $TimeoutSec -MaximumRedirection 5 } + else { Invoke-RestMethod -Uri $Uri -Headers $Headers -WebSession $Sess -TimeoutSec $TimeoutSec -MaximumRedirection 5 } +} +function Invoke-Post([string]$Uri, [hashtable]$Form) { + if ($HasBasic) { Invoke-WebRequest -UseBasicParsing -Uri $Uri -Method Post -Body $Form -ContentType "application/x-www-form-urlencoded" -Headers @{'X-Requested-With'='XMLHttpRequest'} -WebSession $Sess -TimeoutSec $TimeoutSec | Out-Null } + else { Invoke-WebRequest -Uri $Uri -Method Post -Body $Form -ContentType "application/x-www-form-urlencoded" -Headers @{'X-Requested-With'='XMLHttpRequest'} -WebSession $Sess -TimeoutSec $TimeoutSec | Out-Null } +} + +function XmlEsc([string]$s){ + if($null -eq $s){ return "" } + ($s -replace '&','&') -replace '<','<' -replace '>','>' +} + +function Num([object]$x){ if($null -eq $x){[double]0}else{ try{[double]$x}catch{[double]0}} } + +function ChannelXml([string]$name,[double]$value,[string]$unit,[string]$customUnit,[bool]$isFloat){ + $valStr = $value.ToString([System.Globalization.CultureInfo]::InvariantCulture) + $n = XmlEsc $name + $cu = if($customUnit){ XmlEsc $customUnit } else { "" } + $sb = New-Object System.Text.StringBuilder + [void]$sb.Append("$n$valStr") + if($unit){ [void]$sb.Append("$unit") } + if($customUnit){ [void]$sb.Append("$cu") } + if($isFloat){ [void]$sb.Append("12") } + [void]$sb.Append("") + $sb.ToString() +} + +function Write-PrtgError([string]$msg){ "1$(XmlEsc $msg)" } +function Write-PrtgOk([System.Collections.Generic.List[string]]$results,[string]$text="OK",[bool]$trip=$false,[string]$tripText=""){ + $sb=New-Object System.Text.StringBuilder + [void]$sb.Append("") + foreach($r in $results){ [void]$sb.Append($r) } + $t = if($trip -and $tripText){ $tripText } else { $text } + [void]$sb.Append("$(XmlEsc $t)") + if($trip){ [void]$sb.Append("1") } + [void]$sb.Append("") + $sb.ToString() +} + +function Get-DaysToBilling([int]$billingDay){ + if($billingDay -lt 1 -or $billingDay -gt 31){ return [double]::NaN } + $now = [DateTime]::Now.Date + $daysInThis = [DateTime]::DaysInMonth($now.Year, $now.Month) + $targetThis = [DateTime]::new($now.Year, $now.Month, [Math]::Min($billingDay,$daysInThis)) + if($now -lt $targetThis){ return [double]([int]($targetThis - $now).TotalDays) } + $next = $now.AddMonths(1) + $daysInNext = [DateTime]::DaysInMonth($next.Year, $next.Month) + $targetNext = [DateTime]::new($next.Year, $next.Month, [Math]::Min($billingDay,$daysInNext)) + return [double]([int]($targetNext - $now).TotalDays) +} + +try{ + $model1 = Invoke-Json "$Base/api/model.json" + $token = $model1.session.secToken + if(-not $token){ throw "secToken nicht gefunden (Login-Flow geaendert?)" } + + Invoke-Post "$Base/Forms/config" @{ + token = $token; err_redirect='/index.html?loginfailed'; ok_redirect='/index.html'; 'session.password'=$LmPass.Trim() + } + + $m = Invoke-Json "$Base/api/model.json" + + $GiB=[double][Math]::Pow(2,30); $MiB=[double][Math]::Pow(2,20) + $usedBytes = Num $m.wwan.dataUsage.generic.dataTransferred + $limitBytes = Num $m.wwan.dataUsage.generic.billingCycleLimit + $billDayRaw = [int](Num $m.wwan.dataUsage.generic.billingDay) + $limitKnown = $limitBytes -gt 0 + $remainBytes = if($limitKnown){$limitBytes-$usedBytes}else{[double]::NaN} + $usedGiB = [Math]::Round(($usedBytes/$GiB), 2) + $limitGiB = if($limitKnown){[Math]::Round(($limitBytes/$GiB), 2)}else{[double]::NaN} + $remainGiB = if($limitKnown){[Math]::Round(($remainBytes/$GiB), 2)}else{[double]::NaN} + $percentUsed = if($limitKnown){[Math]::Round((($usedBytes*100.0)/$limitBytes), 2)}else{[double]::NaN} + $rxMiB=[Math]::Round((Num $m.wwan.dataTransferred.rxb)/$MiB,2) + $txMiB=[Math]::Round((Num $m.wwan.dataTransferred.txb)/$MiB,2) + $totMiB=[Math]::Round((Num $m.wwan.dataTransferred.totalb)/$MiB,2) + $daysToBill = if($limitKnown -and $billDayRaw -gt 0){ Get-DaysToBilling $billDayRaw } else { [double]::NaN } + + $provider = $null + try{ $defId = $m.wwan.profile.default; $provider = ($m.wwan.profileList | Where-Object { $_.id -eq $defId } | Select-Object -First 1).name }catch{} + if([string]::IsNullOrWhiteSpace($provider)){ $provider = $m.wwan.registerNetworkDisplay } + if([string]::IsNullOrWhiteSpace($provider)){ $mcc=$m.wwanadv.MCC; $mnc=$m.wwanadv.MNC; if($mcc -and $mnc){ $provider="$mcc-$mnc" } } + $radio = $m.wwan.connectionText + $band = $m.wwanadv.curBand + $ip = $m.wwan.IP + + $res=New-Object 'System.Collections.Generic.List[string]' + function N($i){ $i.ToString().PadLeft(2,'0') } + $res.Add( (ChannelXml "$(N 1) Datenvolumen Limit (GiB)" $limitGiB "Custom" "GiB" $true) ) + $res.Add( (ChannelXml "$(N 2) Datenvolumen Verfuegbar (GiB)" $remainGiB "Custom" "GiB" $true) ) + $res.Add( (ChannelXml "$(N 3) Datenvolumen Verbrauch aktueller Monat (GiB)" $usedGiB "Custom" "GiB" $true) ) + $res.Add( (ChannelXml "$(N 4) Datenvolumen Verbrauch aktueller Monat (%)" $percentUsed "Percent" "" $true) ) + $res.Add( (ChannelXml "$(N 5) Tage bis zum neuen Abrechnungszyklus" $daysToBill "Custom" "Tage" $false) ) + $res.Add( (ChannelXml "$(N 6) Empfangene Daten (Sitzung, MiB)" $rxMiB "Custom" "MiB" $true) ) + $res.Add( (ChannelXml "$(N 7) Gesendete Daten (Sitzung, MiB)" $txMiB "Custom" "MiB" $true) ) + $res.Add( (ChannelXml "$(N 8) Daten gesamt (Sitzung, MiB)" $totMiB "Custom" "MiB" $true) ) + $res.Add( (ChannelXml "$(N 9) RSRP (Signalpegel)" (Num $m.wwan.signalStrength.rsrp) "Custom" "dBm" $false) ) + $res.Add( (ChannelXml "$(N 10) RSRQ (Qualitaet)" (Num $m.wwan.signalStrength.rsrq) "Custom" "dB" $false) ) + $res.Add( (ChannelXml "$(N 11) SINR (Stoerabstand)" (Num $m.wwan.signalStrength.sinr) "Custom" "dB" $false) ) + + $title = "IP $ip | Funk $radio | $band | Anbieter $provider" + [Console]::Out.Write( (Write-PrtgOk -results $res -text $title -trip ($TripSensorOnThreshold -and $limitKnown -and ((-not [double]::IsNaN($MinRemainingGiB) -and $remainGiB -le $MinRemainingGiB) -or (-not [double]::IsNaN($MaxPercentUsed) -and $percentUsed -ge $MaxPercentUsed)))) ) +} +catch{ + [Console]::Out.Write( (Write-PrtgError $_.Exception.Message) ) + exit 1 +}