Files
the-other-dude/poller/internal/device/wireless.go
Jason Staack b840047e19 feat: The Other Dude v9.0.1 — full-featured email system
ci: add GitHub Pages deployment workflow for docs site

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 19:30:44 -05:00

146 lines
4.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package device
import (
"log/slog"
"strconv"
routeros "github.com/go-routeros/routeros/v3"
)
// WirelessStats holds aggregated wireless metrics for a single wireless interface.
// Metrics are aggregated across all registered clients on that interface.
type WirelessStats struct {
Interface string `json:"interface"`
ClientCount int `json:"client_count"`
AvgSignal int `json:"avg_signal"` // dBm (negative), e.g. -67
CCQ int `json:"ccq"` // 0100 percentage; 0 if not available (v7)
Frequency int `json:"frequency"` // MHz
}
// CollectWireless queries the RouterOS device for wireless registration-table
// entries and aggregates them per interface.
//
// Version routing:
// - majorVersion >= 7: tries /interface/wifi/registration-table/print first;
// falls back to /interface/wireless/registration-table/print if that fails.
// - majorVersion < 7 (including 0 for unknown): uses the classic wireless path.
//
// Returns an empty slice (not an error) when the device has no wireless interfaces.
func CollectWireless(client *routeros.Client, majorVersion int) ([]WirelessStats, error) {
var registrations []map[string]string
var useV7WiFi bool
if majorVersion >= 7 {
// Try the v7 WiFi API first.
regReply, err := client.Run("/interface/wifi/registration-table/print")
if err == nil {
useV7WiFi = true
for _, s := range regReply.Re {
registrations = append(registrations, s.Map)
}
} else {
slog.Debug("v7 wifi registration-table not available, falling back to wireless", "error", err)
// Fall back to classic wireless path.
regReply, err = client.Run("/interface/wireless/registration-table/print")
if err != nil {
slog.Debug("device has no wireless interfaces", "error", err)
return nil, nil
}
for _, s := range regReply.Re {
registrations = append(registrations, s.Map)
}
}
} else {
regReply, err := client.Run("/interface/wireless/registration-table/print")
if err != nil {
slog.Debug("device has no wireless interfaces", "error", err)
return nil, nil
}
for _, s := range regReply.Re {
registrations = append(registrations, s.Map)
}
}
if len(registrations) == 0 {
return nil, nil
}
// Collect frequency per interface so we can include it in the stats.
frequencies := collectWirelessFrequencies(client, majorVersion, useV7WiFi)
// Aggregate registration-table rows per interface.
type ifaceAgg struct {
count int
signal int
ccq int
}
agg := make(map[string]*ifaceAgg)
for _, r := range registrations {
iface := r["interface"]
if iface == "" {
continue
}
if _, ok := agg[iface]; !ok {
agg[iface] = &ifaceAgg{}
}
a := agg[iface]
a.count++
if sig, err := strconv.Atoi(r["signal-strength"]); err == nil {
a.signal += sig
}
if ccq, err := strconv.Atoi(r["tx-ccq"]); err == nil {
a.ccq += ccq
}
}
result := make([]WirelessStats, 0, len(agg))
for iface, a := range agg {
avgSignal := 0
avgCCQ := 0
if a.count > 0 {
avgSignal = a.signal / a.count
avgCCQ = a.ccq / a.count
}
result = append(result, WirelessStats{
Interface: iface,
ClientCount: a.count,
AvgSignal: avgSignal,
CCQ: avgCCQ,
Frequency: frequencies[iface],
})
}
return result, nil
}
// collectWirelessFrequencies returns a map of interface name → frequency (MHz).
// Uses the v7 WiFi API or the classic wireless API based on the useV7WiFi flag.
func collectWirelessFrequencies(client *routeros.Client, majorVersion int, useV7WiFi bool) map[string]int {
freqs := make(map[string]int)
var cmd string
if useV7WiFi {
cmd = "/interface/wifi/print"
} else {
cmd = "/interface/wireless/print"
}
reply, err := client.Run(cmd, "=.proplist=name,frequency")
if err != nil {
slog.Debug("could not collect wireless frequencies", "command", cmd, "error", err)
return freqs
}
for _, s := range reply.Re {
m := s.Map
name := m["name"]
if freq, err := strconv.Atoi(m["frequency"]); err == nil {
freqs[name] = freq
}
}
return freqs
}