mirror of
https://github.com/henrygd/beszel.git
synced 2026-09-26 01:44:26 +00:00
store compact RSSI stats and skip real-time collection
Stats.WiFi is now map[string]int8 (json "wf") holding only available RSSI readings; SSID and unavailable signals stay in Info.WiFi. Averages are rounded to whole dBm. Wi-Fi is collected only on the default 60s interval; real-time requests reuse the last snapshot to avoid spawning osascript / dumping the BSS cache every second.
This commit is contained in:
+7
-2
@@ -268,7 +268,13 @@ func (a *Agent) getSystemStats(cacheTimeMs uint16) system.Stats {
|
||||
}
|
||||
}
|
||||
|
||||
systemStats.WiFi = wifi.Collect()
|
||||
// Wi-Fi collection spawns a process on macOS and dumps the BSS cache on
|
||||
// Linux, so only refresh on the default interval. Real-time requests reuse
|
||||
// the last snapshot.
|
||||
if cacheTimeMs == defaultDataCacheTimeMs {
|
||||
a.systemInfo.WiFi = wifi.Collect()
|
||||
}
|
||||
systemStats.WiFi = wifi.Signals(a.systemInfo.WiFi)
|
||||
|
||||
// update system info
|
||||
a.systemInfo.ConnectionType = a.connectionManager.ConnectionType
|
||||
@@ -277,7 +283,6 @@ func (a *Agent) getSystemStats(cacheTimeMs uint16) system.Stats {
|
||||
a.systemInfo.MemPct = systemStats.MemPct
|
||||
a.systemInfo.DiskPct = systemStats.DiskPct
|
||||
a.systemInfo.Battery = systemStats.Battery
|
||||
a.systemInfo.WiFi = systemStats.WiFi
|
||||
a.systemInfo.Uptime, _ = getUptime()
|
||||
a.systemInfo.BandwidthBytes = systemStats.Bandwidth[0] + systemStats.Bandwidth[1]
|
||||
a.systemInfo.Threads = a.systemDetails.Threads
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
# Connected Wi-Fi signal
|
||||
|
||||
Each fresh stats poll reports a snapshot of connected station interfaces in
|
||||
`stats.wifi` and `info.wifi`. Map keys identify interfaces, not networks. `s` is
|
||||
optional SSID metadata; `r` is nullable native RSSI in dBm. Quality percentages
|
||||
are never converted to dBm. An associated interface without an accessible RSSI
|
||||
still appears with an unavailable signal. No scans or network changes occur.
|
||||
Each default-interval poll reports a snapshot of connected station interfaces
|
||||
in `info.wifi`. Map keys identify interfaces, not networks. `s` is optional SSID
|
||||
metadata; `r` is nullable native RSSI in dBm. Quality percentages are never
|
||||
converted to dBm. An associated interface without an accessible RSSI still
|
||||
appears with an unavailable signal. No scans or network changes occur.
|
||||
`stats.wf` stores only available RSSI values (integer dBm) keyed by interface.
|
||||
Real-time requests reuse the last snapshot instead of collecting again.
|
||||
|
||||
The hub panel gates exclusively on current `systems.info.wifi` and system `up`
|
||||
status, independently of the selected historical period. Empty/null snapshots
|
||||
clear it. Historical averages use only available readings per interface; gaps
|
||||
are not zero signal. Interface colors and keys remain stable on reconnect.
|
||||
SSID in an aggregate is the latest observed metadata, not a separate series.
|
||||
|
||||
## Platforms
|
||||
|
||||
@@ -39,7 +40,7 @@ SSID in an aggregate is the latest observed metadata, not a separate series.
|
||||
- FreeBSD and other platforms: unsupported, empty snapshot. No approximation
|
||||
from ifconfig quality and no stale data retained.
|
||||
|
||||
Collectors retry each fresh poll, allowing interfaces and capabilities to appear
|
||||
Collectors retry each default-interval poll, allowing interfaces and capabilities to appear
|
||||
without an agent restart. Standard agent response caching still applies. Existing
|
||||
hub record JSON storage requires no database schema migration. Older agents
|
||||
without the field keep the panel hidden. Native macOS/Windows runtime checks and
|
||||
|
||||
@@ -4,6 +4,7 @@ package wifi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math"
|
||||
"os"
|
||||
"os/exec"
|
||||
"time"
|
||||
@@ -38,3 +39,19 @@ func Collect() map[string]system.WiFi {
|
||||
defer cancel()
|
||||
return collect(ctx)
|
||||
}
|
||||
|
||||
// Signals reduces a snapshot to the RSSI values stored in stats history.
|
||||
// Interfaces without an available reading are omitted.
|
||||
func Signals(snapshot map[string]system.WiFi) map[string]int8 {
|
||||
var signals map[string]int8
|
||||
for id, reading := range snapshot {
|
||||
if reading.Signal == nil {
|
||||
continue
|
||||
}
|
||||
if signals == nil {
|
||||
signals = make(map[string]int8, len(snapshot))
|
||||
}
|
||||
signals[id] = int8(max(math.Round(*reading.Signal), math.MinInt8))
|
||||
}
|
||||
return signals
|
||||
}
|
||||
|
||||
@@ -33,3 +33,25 @@ func TestSSIDWireSafety(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignals(t *testing.T) {
|
||||
strong, weak, rounded := -40.0, -200.0, -52.6
|
||||
got := Signals(map[string]system.WiFi{
|
||||
"wlan0": {SSID: "home", Signal: &strong},
|
||||
"wlan1": {Signal: &weak},
|
||||
"wlan2": {Signal: &rounded},
|
||||
"wlan3": {SSID: "no rssi"},
|
||||
})
|
||||
want := map[string]int8{"wlan0": -40, "wlan1": -128, "wlan2": -53}
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("got %v, want %v", got, want)
|
||||
}
|
||||
for id, signal := range want {
|
||||
if got[id] != signal {
|
||||
t.Fatalf("got %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
if Signals(map[string]system.WiFi{"wlan0": {}}) != nil || Signals(nil) != nil {
|
||||
t.Fatal("expected nil without available readings")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ type WiFi struct {
|
||||
}
|
||||
|
||||
type Stats struct {
|
||||
WiFi map[string]WiFi `json:"wifi,omitempty" cbor:"40,keyasint,omitempty"`
|
||||
Cpu float64 `json:"cpu" cbor:"0,keyasint"`
|
||||
MaxCpu float64 `json:"cpum,omitempty" cbor:"-"`
|
||||
Mem float64 `json:"m" cbor:"2,keyasint"`
|
||||
@@ -64,6 +63,7 @@ type Stats struct {
|
||||
Batteries map[string]uint8 `json:"bats,omitempty" cbor:"37,keyasint,omitempty"`
|
||||
ZfsPools map[string]*ZfsPool `json:"z,omitempty" cbor:"39,keyasint,omitempty"` // ZFS pool metrics, keyed by pool name
|
||||
DiskIOTotal [2]uint64 `json:"diot,omitzero" cbor:"38,keyasint,omitzero"` // [total read bytes, total write bytes] cumulative device counters
|
||||
WiFi map[string]int8 `json:"wf,omitempty" cbor:"40,keyasint,omitempty"` // RSSI dBm keyed by interface; unavailable readings omitted
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,10 @@ import (
|
||||
func TestWiFiWireSnapshot(t *testing.T) {
|
||||
signal := -55.0
|
||||
for _, wifi := range []map[string]WiFi{nil, {}, {"wlan0": {SSID: "home", Signal: &signal}, "wlan1": {}}} {
|
||||
original := CombinedData{Info: Info{WiFi: wifi}, Stats: Stats{WiFi: wifi}}
|
||||
original := CombinedData{Info: Info{WiFi: wifi}, Stats: Stats{WiFi: make(map[string]int8, len(wifi))}}
|
||||
for id := range wifi {
|
||||
original.Stats.WiFi[id] = -55
|
||||
}
|
||||
encoded, err := cbor.Marshal(original)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -17,7 +17,7 @@ func TestCreateRecordsWiFiDisconnectReconnect(t *testing.T) {
|
||||
{}, nil,
|
||||
{"wlan0": {SSID: "new", Signal: &signal}},
|
||||
} {
|
||||
_, err := sys.createRecords(&system.CombinedData{Info: system.Info{WiFi: snapshot}, Stats: system.Stats{WiFi: snapshot}})
|
||||
_, err := sys.createRecords(&system.CombinedData{Info: system.Info{WiFi: snapshot}})
|
||||
require.NoError(t, err)
|
||||
record, err := app.FindRecordById("systems", sys.Id)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -16,13 +16,21 @@ func TestWiFiSequentialResponseSnapshots(t *testing.T) {
|
||||
{"wlan0": {SSID: "home"}}, {}, nil,
|
||||
{"wlan1": {SSID: "new", Signal: &signal}},
|
||||
} {
|
||||
payload, err := cbor.Marshal(system.CombinedData{Info: system.Info{WiFi: snapshot}, Stats: system.Stats{WiFi: snapshot}})
|
||||
signals := make(map[string]int8)
|
||||
for id, reading := range snapshot {
|
||||
if reading.Signal != nil {
|
||||
signals[id] = int8(*reading.Signal)
|
||||
}
|
||||
}
|
||||
payload, err := cbor.Marshal(system.CombinedData{Info: system.Info{WiFi: snapshot}, Stats: system.Stats{WiFi: signals}})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, UnmarshalResponse(common.AgentResponse{Data: payload}, common.GetData, &decoded))
|
||||
require.Len(t, decoded.Info.WiFi, len(snapshot))
|
||||
require.Len(t, decoded.Stats.WiFi, len(snapshot))
|
||||
require.Len(t, decoded.Stats.WiFi, len(signals))
|
||||
for id, want := range snapshot {
|
||||
require.Equal(t, want, decoded.Info.WiFi[id])
|
||||
}
|
||||
for id, want := range signals {
|
||||
require.Equal(t, want, decoded.Stats.WiFi[id])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,8 +267,7 @@ func AverageSystemStatsSlice(records []system.Stats) system.Stats {
|
||||
return sum
|
||||
}
|
||||
|
||||
// RSSI averages exclude absent and unavailable samples.
|
||||
wifiSums := make(map[string]float64)
|
||||
wifiSums := make(map[string]int)
|
||||
wifiCounts := make(map[string]int)
|
||||
// necessary because uint8 is not big enough for the sum
|
||||
batterySum := 0
|
||||
@@ -288,15 +287,9 @@ func AverageSystemStatsSlice(records []system.Stats) system.Stats {
|
||||
// Accumulate totals
|
||||
for i := range records {
|
||||
stats := &records[i]
|
||||
for id, reading := range stats.WiFi {
|
||||
if sum.WiFi == nil {
|
||||
sum.WiFi = make(map[string]system.WiFi)
|
||||
}
|
||||
sum.WiFi[id] = system.WiFi{SSID: reading.SSID}
|
||||
if reading.Signal != nil {
|
||||
wifiSums[id] += *reading.Signal
|
||||
wifiCounts[id]++
|
||||
}
|
||||
for id, signal := range stats.WiFi {
|
||||
wifiSums[id] += int(signal)
|
||||
wifiCounts[id]++
|
||||
}
|
||||
|
||||
sum.Cpu += stats.Cpu
|
||||
@@ -627,11 +620,11 @@ func AverageSystemStatsSlice(records []system.Stats) system.Stats {
|
||||
sum.CpuBreakdown = avg
|
||||
}
|
||||
|
||||
for id, reading := range sum.WiFi {
|
||||
if wifiCounts[id] > 0 {
|
||||
average := wifiSums[id] / float64(wifiCounts[id])
|
||||
reading.Signal = &average
|
||||
sum.WiFi[id] = reading
|
||||
// RSSI averages exclude records where the interface was absent.
|
||||
if len(wifiSums) > 0 {
|
||||
sum.WiFi = make(map[string]int8, len(wifiSums))
|
||||
for id, total := range wifiSums {
|
||||
sum.WiFi[id] = int8(math.Round(float64(total) / float64(wifiCounts[id])))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,17 +7,16 @@ import (
|
||||
)
|
||||
|
||||
func TestWiFiAverageAvailableSamples(t *testing.T) {
|
||||
a, b, c := -40.0, -60.0, -80.0
|
||||
input := []system.Stats{
|
||||
{WiFi: map[string]system.WiFi{"wlan0": {SSID: "old", Signal: &a}}},
|
||||
{WiFi: map[string]int8{"wlan0": -40}},
|
||||
{},
|
||||
{WiFi: map[string]system.WiFi{"wlan0": {SSID: "new", Signal: &b}, "wlan1": {Signal: &c}, "unknown": {}}},
|
||||
{WiFi: map[string]int8{"wlan0": -61, "wlan1": -80}},
|
||||
}
|
||||
result := AverageSystemStatsSlice(input)
|
||||
if len(result.WiFi) != 3 || *result.WiFi["wlan0"].Signal != -50 || *result.WiFi["wlan1"].Signal != -80 || result.WiFi["unknown"].Signal != nil || result.WiFi["wlan0"].SSID != "new" {
|
||||
if len(result.WiFi) != 2 || result.WiFi["wlan0"] != -51 || result.WiFi["wlan1"] != -80 {
|
||||
t.Fatalf("%#v", result.WiFi)
|
||||
}
|
||||
if *input[0].WiFi["wlan0"].Signal != -40 {
|
||||
if input[0].WiFi["wlan0"] != -40 {
|
||||
t.Fatal("mutated input")
|
||||
}
|
||||
if len(AverageSystemStatsSlice([]system.Stats{{}, {}}).WiFi) != 0 {
|
||||
|
||||
@@ -20,7 +20,7 @@ export function WiFiChart({
|
||||
const dataPoints = interfaces.map(([id, current]) => ({
|
||||
label: current.s ? `${id} (${current.s})` : id,
|
||||
color: wifiColor(id),
|
||||
dataKey: ({ stats }: SystemStatsRecord) => stats?.wifi?.[id]?.r,
|
||||
dataKey: ({ stats }: SystemStatsRecord) => stats?.wf?.[id],
|
||||
}))
|
||||
return (
|
||||
<ChartCard
|
||||
|
||||
Vendored
+2
-1
@@ -91,7 +91,6 @@ export interface SystemInfo {
|
||||
}
|
||||
|
||||
export interface SystemStats {
|
||||
wifi?: Record<string, WiFi>
|
||||
/** cpu percent */
|
||||
cpu: number
|
||||
/** peak cpu */
|
||||
@@ -168,6 +167,8 @@ export interface SystemStats {
|
||||
bat?: [number, BatteryState]
|
||||
/** battery percentages by device name */
|
||||
bats?: Record<string, number>
|
||||
/** Wi-Fi RSSI (dBm) by interface */
|
||||
wf?: Record<string, number>
|
||||
/** network interfaces [upload bytes, download bytes, total upload bytes, total download bytes] */
|
||||
ni?: Record<string, [number, number, number, number]>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user