mirror of
https://github.com/unpoller/unpoller.git
synced 2026-10-06 14:31:32 +02:00
Merge pull request #1102 from tomwilkie/feat/umbb
feat: export UMBB (U5G Max) cellular modem metrics
This commit is contained in:
@@ -13,7 +13,7 @@ require (
|
||||
github.com/prometheus/common v0.71.0
|
||||
github.com/spf13/pflag v1.0.10
|
||||
github.com/stretchr/testify v1.12.1
|
||||
github.com/unpoller/unifi/v6 v6.2.0
|
||||
github.com/unpoller/unifi/v6 v6.3.0
|
||||
go.opentelemetry.io/otel v1.46.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.46.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.46.0
|
||||
|
||||
@@ -121,8 +121,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
|
||||
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
|
||||
github.com/unpoller/unifi/v6 v6.2.0 h1:5P3C1KkbVRhX11/aQIcgKex/aotroUEXtXLQLLfanv4=
|
||||
github.com/unpoller/unifi/v6 v6.2.0/go.mod h1:fJ1lU/mpKrQ8Jv3b2yq399ATlrUuiOZAMbKWcw/1GaU=
|
||||
github.com/unpoller/unifi/v6 v6.3.0 h1:J7etDpdkarEG9sOiSORtzZB93RDyzXu9KqZ4cWoBYcQ=
|
||||
github.com/unpoller/unifi/v6 v6.3.0/go.mod h1:fJ1lU/mpKrQ8Jv3b2yq399ATlrUuiOZAMbKWcw/1GaU=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
|
||||
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
||||
|
||||
@@ -453,6 +453,8 @@ func (u *DatadogUnifi) switchExport(r report, v any) { //nolint:cyclop
|
||||
u.batchUCI(r, v)
|
||||
case *unifi.UDB:
|
||||
u.batchUDB(r, v)
|
||||
case *unifi.UMBB:
|
||||
u.batchUMBB(r, v)
|
||||
case *unifi.Site:
|
||||
u.reportSite(r, v)
|
||||
case *unifi.Client:
|
||||
|
||||
@@ -566,6 +566,58 @@ gauges:
|
||||
- unifi.udb.uptime
|
||||
- unifi.udb.user_num_sta
|
||||
- unifi.udb.user_wlan_num_sta
|
||||
- unifi.umbb_sim.active
|
||||
- unifi.umbb_sim.card_present
|
||||
- unifi.umbb_sim.data_limited
|
||||
- unifi.umbb_sim.data_warning
|
||||
- unifi.umbb_sim.display_state_elapsed
|
||||
- unifi.umbb_sim.pin_blocked
|
||||
- unifi.umbb_sim.rx_bytes
|
||||
- unifi.umbb_sim.tx_bytes
|
||||
- unifi.umbb.cpu
|
||||
- unifi.umbb.current_slot
|
||||
- unifi.umbb.dl_bandwidth_mhz
|
||||
- unifi.umbb.has_coverage
|
||||
- unifi.umbb.internet
|
||||
- unifi.umbb.last_seen
|
||||
- unifi.umbb.loadavg_1
|
||||
- unifi.umbb.loadavg_15
|
||||
- unifi.umbb.loadavg_5
|
||||
- unifi.umbb.lte_carriers
|
||||
- unifi.umbb.lte_max_bitrate_dl
|
||||
- unifi.umbb.lte_max_bitrate_ul
|
||||
- unifi.umbb.mem
|
||||
- unifi.umbb.mem_buffer
|
||||
- unifi.umbb.mem_total
|
||||
- unifi.umbb.mem_used
|
||||
- unifi.umbb.memory
|
||||
- unifi.umbb.network
|
||||
- unifi.umbb.nr_carriers
|
||||
- unifi.umbb.nr_max_bitrate_dl
|
||||
- unifi.umbb.nr_max_bitrate_ul
|
||||
- unifi.umbb.nr_rsrp
|
||||
- unifi.umbb.nr_rsrq
|
||||
- unifi.umbb.nr_snr
|
||||
- unifi.umbb.primary_slot
|
||||
- unifi.umbb.probe
|
||||
- unifi.umbb.registration_state
|
||||
- unifi.umbb.roaming
|
||||
- unifi.umbb.rsrp
|
||||
- unifi.umbb.rsrq
|
||||
- unifi.umbb.sa_mode
|
||||
- unifi.umbb.signal
|
||||
- unifi.umbb.signal_percent
|
||||
- unifi.umbb.snr
|
||||
- unifi.umbb.state
|
||||
- unifi.umbb.sys
|
||||
- unifi.umbb.system_uptime
|
||||
- unifi.umbb.ul_bandwidth_mhz
|
||||
- unifi.umbb.upgradeable
|
||||
- unifi.umbb.uplink_latency
|
||||
- unifi.umbb.uplink_max_speed
|
||||
- unifi.umbb.uplink_speed
|
||||
- unifi.umbb.uplink_uptime
|
||||
- unifi.umbb.uptime
|
||||
counts:
|
||||
- unifi.collector.num_devices
|
||||
- unifi.collector.num_errors
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
package datadogunifi
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strconv"
|
||||
|
||||
"github.com/unpoller/unifi/v6"
|
||||
)
|
||||
|
||||
// umbbT is used as a name for printed/logged counters.
|
||||
const umbbT = item("UMBB")
|
||||
|
||||
// batchUMBB generates datapoints for UMBB (mobile broadband) devices, such as the U5G Max.
|
||||
// The device itself goes to "umbb" with its cellular radio state; each SIM slot goes to "umbb_sim".
|
||||
func (u *DatadogUnifi) batchUMBB(r report, s *unifi.UMBB) {
|
||||
if !s.Adopted.Val {
|
||||
return
|
||||
}
|
||||
|
||||
rad := s.Mbb.Radio
|
||||
dl, ul := mbbBandwidth(rad)
|
||||
|
||||
tags := cleanTags(map[string]string{
|
||||
"mac": s.Mac,
|
||||
"site_name": s.SiteName,
|
||||
"source": s.SourceName,
|
||||
"name": s.Name,
|
||||
"version": s.Version,
|
||||
"model": s.Model,
|
||||
"serial": s.Serial,
|
||||
"type": s.Type,
|
||||
"ip": s.IP,
|
||||
"mbb_state": s.Mbb.State,
|
||||
"failover_mode": s.Mbb.Mode,
|
||||
"rat": rad.Rat,
|
||||
"band": rad.Band,
|
||||
"operator": rad.NetworkOperator,
|
||||
})
|
||||
|
||||
data := CombineFloat64(
|
||||
u.batchSysStats(s.SysStats, s.SystemStats),
|
||||
map[string]float64{
|
||||
"last_seen": s.LastSeen.Val,
|
||||
"uptime": s.Uptime.Val,
|
||||
"state": s.State.Val,
|
||||
"upgradeable": boolToFloat64(s.Upgradable.Val),
|
||||
"uplink_speed": s.Uplink.Speed.Val,
|
||||
"uplink_max_speed": s.Uplink.MaxSpeed.Val,
|
||||
"uplink_latency": s.Uplink.Latency.Val,
|
||||
"uplink_uptime": s.Uplink.Uptime.Val,
|
||||
"sa_mode": rad.SA5GMode.Float64(),
|
||||
"current_slot": rad.CurrentSlot.Val,
|
||||
"primary_slot": s.MbbOverrides.PrimarySlot.Val,
|
||||
"internet": s.Internet.Float64(),
|
||||
"has_coverage": rad.HasCoverage.Float64(),
|
||||
"roaming": rad.Roaming.Float64(),
|
||||
"registration_state": rad.RegistrationState.Val,
|
||||
"nr_carriers": float64(len(rad.CaNr)),
|
||||
"lte_carriers": float64(len(rad.CaLte)),
|
||||
"dl_bandwidth_mhz": dl,
|
||||
"ul_bandwidth_mhz": ul,
|
||||
},
|
||||
mbbOptional(rad))
|
||||
|
||||
r.addCount(umbbT)
|
||||
reportGaugeForFloat64Map(r, metricNamespace("umbb"), data, tags)
|
||||
|
||||
for _, sim := range s.Mbb.Sim {
|
||||
simTags := cleanTags(map[string]string{
|
||||
"mac": s.Mac,
|
||||
"site_name": s.SiteName,
|
||||
"source": s.SourceName,
|
||||
"name": s.Name,
|
||||
"model": s.Model,
|
||||
"type": s.Type,
|
||||
"slot": sim.Slot.Txt,
|
||||
"esim": strconv.FormatBool(sim.Esim.Val),
|
||||
"spn": sim.Spn,
|
||||
"display_state": sim.DisplayState,
|
||||
"has_carrier": strconv.FormatBool(sim.HasCarrier.Val),
|
||||
"metered": strconv.FormatBool(sim.Metered.Val),
|
||||
"network_reject_type": sim.NetworkRejectType,
|
||||
"network_reject_text": sim.NetworkRejectText,
|
||||
})
|
||||
simData := map[string]float64{
|
||||
"active": sim.Active.Float64(),
|
||||
"card_present": sim.CardPresent.Float64(),
|
||||
"pin_blocked": sim.PinBlocked.Float64(),
|
||||
"data_warning": sim.DataWarning.Float64(),
|
||||
"data_limited": sim.DataLimited.Float64(),
|
||||
"display_state_elapsed": sim.DisplayStateElapsed.Val,
|
||||
"rx_bytes": sim.RxBytes.Val,
|
||||
"tx_bytes": sim.TxBytes.Val,
|
||||
}
|
||||
|
||||
reportGaugeForFloat64Map(r, metricNamespace("umbb_sim"), simData, simTags)
|
||||
}
|
||||
}
|
||||
|
||||
// mbbOptional returns the signal readings and bitrates the modem reported, keyed by field name.
|
||||
// Missing readings are left out, so they are not mistaken for 0 dBm.
|
||||
func mbbOptional(rad unifi.MBBRadio) map[string]float64 {
|
||||
out := map[string]float64{}
|
||||
|
||||
for k, v := range map[string]unifi.FlexInt{
|
||||
"rsrp": rad.Rsrp,
|
||||
"rsrq": rad.Rsrq,
|
||||
"snr": rad.Snr,
|
||||
"nr_rsrp": rad.RsrpNr,
|
||||
"nr_rsrq": rad.RsrqNr,
|
||||
"nr_snr": rad.SnrNr,
|
||||
"signal": rad.Signal,
|
||||
"signal_percent": rad.SignalPercent,
|
||||
"lte_max_bitrate_dl": rad.MaxBitrateDl,
|
||||
"lte_max_bitrate_ul": rad.MaxBitrateUl,
|
||||
"nr_max_bitrate_dl": rad.MaxBitrateDlNr,
|
||||
"nr_max_bitrate_ul": rad.MaxBitrateUlNr,
|
||||
} {
|
||||
if v.Txt != "" {
|
||||
out[k] = v.Val
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// mbbBandwidth sums the downlink and uplink bandwidth of every aggregated carrier, NR and LTE.
|
||||
// A carrier with no uplink reports a null bandwidth, which decodes as zero.
|
||||
// A sum that overflows is reported as zero: InfluxDB rejects a whole point with an infinite field.
|
||||
func mbbBandwidth(rad unifi.MBBRadio) (dl, ul float64) {
|
||||
for _, c := range rad.CaNr {
|
||||
dl += c.DlBwMhz.Val
|
||||
ul += c.UlBwMhz.Val
|
||||
}
|
||||
|
||||
for _, c := range rad.CaLte {
|
||||
dl += c.DlBwMhz.Val
|
||||
ul += c.UlBwMhz.Val
|
||||
}
|
||||
|
||||
return finiteOrZero(dl), finiteOrZero(ul)
|
||||
}
|
||||
|
||||
func finiteOrZero(v float64) float64 {
|
||||
if math.IsInf(v, 0) || math.IsNaN(v) {
|
||||
return 0
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
@@ -718,6 +718,8 @@ func (u *InfluxUnifi) switchExport(r report, v any) { //nolint:cyclop
|
||||
u.batchUCI(r, v)
|
||||
case *unifi.UDB:
|
||||
u.batchUDB(r, v)
|
||||
case *unifi.UMBB:
|
||||
u.batchUMBB(r, v)
|
||||
case *unifi.UDM:
|
||||
u.batchUDM(r, v)
|
||||
case *unifi.Site:
|
||||
|
||||
@@ -671,6 +671,98 @@ points:
|
||||
uptime: float
|
||||
user-num_sta: float
|
||||
user-wlan-num_sta: float
|
||||
umbb:
|
||||
tags:
|
||||
- mac
|
||||
- model
|
||||
- name
|
||||
- serial
|
||||
- site_name
|
||||
- source
|
||||
- type
|
||||
- version
|
||||
fields:
|
||||
band: string
|
||||
cell_id: float
|
||||
channel: float
|
||||
cpu: float
|
||||
current_slot: float
|
||||
dl_bandwidth_mhz: float
|
||||
failover_mode: string
|
||||
has_coverage: bool
|
||||
internet: bool
|
||||
ip: string
|
||||
last_seen: float
|
||||
loadavg_1: float
|
||||
loadavg_5: float
|
||||
loadavg_15: float
|
||||
lte_carriers: float
|
||||
lte_max_bitrate_dl: float
|
||||
lte_max_bitrate_ul: float
|
||||
mbb_state: string
|
||||
mcc: float
|
||||
mem: float
|
||||
mem_buffer: float
|
||||
mem_total: float
|
||||
mem_used: float
|
||||
mnc: float
|
||||
nr_carriers: float
|
||||
nr_max_bitrate_dl: float
|
||||
nr_max_bitrate_ul: float
|
||||
nr_rsrp: float
|
||||
nr_rsrq: float
|
||||
nr_snr: float
|
||||
operator: string
|
||||
pci: float
|
||||
primary_slot: float
|
||||
rat: string
|
||||
registration_state: float
|
||||
roaming: bool
|
||||
rsrp: float
|
||||
rsrq: float
|
||||
sa_mode: bool
|
||||
signal: float
|
||||
signal_percent: float
|
||||
snr: float
|
||||
state: float
|
||||
system_uptime: float
|
||||
temp_cpu: float
|
||||
temp_memory: float
|
||||
temp_network: float
|
||||
temp_probe: float
|
||||
temp_sys: float
|
||||
ul_bandwidth_mhz: float
|
||||
upgradeable: bool
|
||||
uplink_latency: float
|
||||
uplink_max_speed: float
|
||||
uplink_speed: float
|
||||
uplink_uptime: float
|
||||
uptime: float
|
||||
umbb_sim:
|
||||
tags:
|
||||
- esim
|
||||
- mac
|
||||
- model
|
||||
- name
|
||||
- site_name
|
||||
- slot
|
||||
- source
|
||||
- spn
|
||||
- type
|
||||
fields:
|
||||
active: bool
|
||||
card_present: bool
|
||||
data_limited: bool
|
||||
data_warning: bool
|
||||
display_state: string
|
||||
display_state_elapsed: float
|
||||
has_carrier: bool
|
||||
metered: bool
|
||||
network_reject_text: string
|
||||
network_reject_type: string
|
||||
pin_blocked: bool
|
||||
rx_bytes: float
|
||||
tx_bytes: float
|
||||
unifi_alarm:
|
||||
tags:
|
||||
- action
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
package influxunifi
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strconv"
|
||||
|
||||
"github.com/unpoller/unifi/v6"
|
||||
)
|
||||
|
||||
// umbbT is used as a name for printed/logged counters.
|
||||
const umbbT = item("UMBB")
|
||||
|
||||
// batchUMBB generates datapoints for UMBB (mobile broadband) devices, such as the U5G Max.
|
||||
// The device itself goes to "umbb" with its cellular radio state; each SIM slot goes to "umbb_sim".
|
||||
func (u *InfluxUnifi) batchUMBB(r report, s *unifi.UMBB) {
|
||||
if !s.Adopted.Val {
|
||||
return
|
||||
}
|
||||
|
||||
rad := s.Mbb.Radio
|
||||
dl, ul := mbbBandwidth(rad)
|
||||
|
||||
tags := map[string]string{
|
||||
"mac": s.Mac,
|
||||
"site_name": s.SiteName,
|
||||
"source": s.SourceName,
|
||||
"name": s.Name,
|
||||
"version": s.Version,
|
||||
"model": s.Model,
|
||||
"serial": s.Serial,
|
||||
"type": s.Type,
|
||||
}
|
||||
|
||||
fields := Combine(
|
||||
u.batchSysStats(s.SysStats, s.SystemStats),
|
||||
map[string]any{
|
||||
"ip": s.IP,
|
||||
"last_seen": s.LastSeen.Val,
|
||||
"uptime": s.Uptime.Val,
|
||||
"state": s.State.Val,
|
||||
"upgradeable": s.Upgradable.Val,
|
||||
"uplink_speed": s.Uplink.Speed.Val,
|
||||
"uplink_max_speed": s.Uplink.MaxSpeed.Val,
|
||||
"uplink_latency": s.Uplink.Latency.Val,
|
||||
"uplink_uptime": s.Uplink.Uptime.Val,
|
||||
"internet": s.Internet.Val,
|
||||
"mbb_state": s.Mbb.State,
|
||||
"failover_mode": s.Mbb.Mode,
|
||||
"rat": rad.Rat,
|
||||
"band": rad.Band,
|
||||
"operator": rad.NetworkOperator,
|
||||
"sa_mode": rad.SA5GMode.Val,
|
||||
"current_slot": rad.CurrentSlot.Val,
|
||||
"primary_slot": s.MbbOverrides.PrimarySlot.Val,
|
||||
"mcc": rad.Mcc.Val,
|
||||
"mnc": rad.Mnc.Val,
|
||||
"cell_id": rad.CellID.Val,
|
||||
"pci": rad.Pci.Val,
|
||||
"channel": rad.Channel.Val,
|
||||
"has_coverage": rad.HasCoverage.Val,
|
||||
"roaming": rad.Roaming.Val,
|
||||
"registration_state": rad.RegistrationState.Val,
|
||||
"nr_carriers": float64(len(rad.CaNr)),
|
||||
"lte_carriers": float64(len(rad.CaLte)),
|
||||
"dl_bandwidth_mhz": dl,
|
||||
"ul_bandwidth_mhz": ul,
|
||||
})
|
||||
|
||||
for k, v := range mbbOptional(rad) {
|
||||
fields[k] = v
|
||||
}
|
||||
|
||||
r.addCount(umbbT)
|
||||
r.send(&metric{Table: "umbb", Tags: tags, Fields: fields})
|
||||
|
||||
for _, sim := range s.Mbb.Sim {
|
||||
r.send(&metric{
|
||||
Table: "umbb_sim",
|
||||
Tags: map[string]string{
|
||||
"mac": s.Mac,
|
||||
"site_name": s.SiteName,
|
||||
"source": s.SourceName,
|
||||
"name": s.Name,
|
||||
"model": s.Model,
|
||||
"type": s.Type,
|
||||
"slot": sim.Slot.Txt,
|
||||
"esim": strconv.FormatBool(sim.Esim.Val),
|
||||
"spn": sim.Spn,
|
||||
},
|
||||
Fields: map[string]any{
|
||||
"active": sim.Active.Val,
|
||||
"card_present": sim.CardPresent.Val,
|
||||
"pin_blocked": sim.PinBlocked.Val,
|
||||
"has_carrier": sim.HasCarrier.Val,
|
||||
"metered": sim.Metered.Val,
|
||||
"data_warning": sim.DataWarning.Val,
|
||||
"data_limited": sim.DataLimited.Val,
|
||||
"display_state": sim.DisplayState,
|
||||
"display_state_elapsed": sim.DisplayStateElapsed.Val,
|
||||
"network_reject_type": sim.NetworkRejectType,
|
||||
"network_reject_text": sim.NetworkRejectText,
|
||||
"rx_bytes": sim.RxBytes.Val,
|
||||
"tx_bytes": sim.TxBytes.Val,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// mbbOptional returns the signal readings and bitrates the modem reported, keyed by field name.
|
||||
// Missing readings are left out, so they are not mistaken for 0 dBm.
|
||||
func mbbOptional(rad unifi.MBBRadio) map[string]float64 {
|
||||
out := map[string]float64{}
|
||||
|
||||
for k, v := range map[string]unifi.FlexInt{
|
||||
"rsrp": rad.Rsrp,
|
||||
"rsrq": rad.Rsrq,
|
||||
"snr": rad.Snr,
|
||||
"nr_rsrp": rad.RsrpNr,
|
||||
"nr_rsrq": rad.RsrqNr,
|
||||
"nr_snr": rad.SnrNr,
|
||||
"signal": rad.Signal,
|
||||
"signal_percent": rad.SignalPercent,
|
||||
"lte_max_bitrate_dl": rad.MaxBitrateDl,
|
||||
"lte_max_bitrate_ul": rad.MaxBitrateUl,
|
||||
"nr_max_bitrate_dl": rad.MaxBitrateDlNr,
|
||||
"nr_max_bitrate_ul": rad.MaxBitrateUlNr,
|
||||
} {
|
||||
if v.Txt != "" {
|
||||
out[k] = v.Val
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// mbbBandwidth sums the downlink and uplink bandwidth of every aggregated carrier, NR and LTE.
|
||||
// A carrier with no uplink reports a null bandwidth, which decodes as zero.
|
||||
// A sum that overflows is reported as zero: InfluxDB rejects a whole point with an infinite field.
|
||||
func mbbBandwidth(rad unifi.MBBRadio) (dl, ul float64) {
|
||||
for _, c := range rad.CaNr {
|
||||
dl += c.DlBwMhz.Val
|
||||
ul += c.UlBwMhz.Val
|
||||
}
|
||||
|
||||
for _, c := range rad.CaLte {
|
||||
dl += c.DlBwMhz.Val
|
||||
ul += c.UlBwMhz.Val
|
||||
}
|
||||
|
||||
return finiteOrZero(dl), finiteOrZero(ul)
|
||||
}
|
||||
|
||||
func finiteOrZero(v float64) float64 {
|
||||
if math.IsInf(v, 0) || math.IsNaN(v) {
|
||||
return 0
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
@@ -149,6 +149,12 @@ func (u *InputUnifi) collectAlarms(logs []any, sites []*unifi.Site, c *Controlle
|
||||
}
|
||||
}
|
||||
|
||||
for _, d := range devices.UMBBs {
|
||||
if d.Mac != "" && d.Name != "" {
|
||||
macToName[strings.ToLower(d.Mac)] = d.Name
|
||||
}
|
||||
}
|
||||
|
||||
for _, s := range sites {
|
||||
events, err := c.Unifi.GetAlarmsSite(s)
|
||||
if errors.Is(err, unifi.ErrEndpointNotFound) {
|
||||
|
||||
@@ -224,10 +224,10 @@ func (u *InputUnifi) pollNetwork(c *Controller, sites []*unifi.Site, m *Metrics)
|
||||
return fmt.Errorf("unifi.GetDevices(%s): %w", c.URL, err)
|
||||
}
|
||||
|
||||
u.LogDebugf("Found %d UBB, %d UXG, %d PDU, %d UCI, %d UDB, %d UAP %d USG %d USW %d UDM devices",
|
||||
u.LogDebugf("Found %d UBB, %d UXG, %d PDU, %d UCI, %d UDB, %d UMBB, %d UAP %d USG %d USW %d UDM devices",
|
||||
len(m.Devices.UBBs), len(m.Devices.UXGs),
|
||||
len(m.Devices.PDUs), len(m.Devices.UCIs),
|
||||
len(m.Devices.UDBs), len(m.Devices.UAPs), len(m.Devices.USGs),
|
||||
len(m.Devices.UDBs), len(m.Devices.UMBBs), len(m.Devices.UAPs), len(m.Devices.USGs),
|
||||
len(m.Devices.USWs), len(m.Devices.UDMs))
|
||||
|
||||
// Get speed test results for all WANs
|
||||
@@ -1107,6 +1107,10 @@ func applySiteNameOverride(m *poller.Metrics, overrideName string) {
|
||||
if isDefaultSiteName(d.SiteName) {
|
||||
d.SiteName = overrideName
|
||||
}
|
||||
case *unifi.UMBB:
|
||||
if isDefaultSiteName(d.SiteName) {
|
||||
d.SiteName = overrideName
|
||||
}
|
||||
case *unifi.PDU:
|
||||
if isDefaultSiteName(d.SiteName) {
|
||||
d.SiteName = overrideName
|
||||
@@ -1362,6 +1366,11 @@ func extractDevices(metrics *Metrics) (*poller.Metrics, map[string]string, map[s
|
||||
}
|
||||
}
|
||||
|
||||
for _, r := range metrics.Devices.UMBBs {
|
||||
devices[r.Mac] = r.Name
|
||||
m.Devices = append(m.Devices, r)
|
||||
}
|
||||
|
||||
for _, r := range metrics.Devices.PDUs {
|
||||
devices[r.Mac] = r.Name
|
||||
m.Devices = append(m.Devices, r)
|
||||
|
||||
@@ -24,6 +24,7 @@ type Report struct {
|
||||
USG int // Total count of USG devices exported.
|
||||
UDM int // Total count of UDM devices exported.
|
||||
UXG int // Total count of UXG devices exported.
|
||||
UMBB int // Total count of UMBB devices exported.
|
||||
Elapsed time.Duration // Duration elapsed collecting and exporting.
|
||||
}
|
||||
|
||||
@@ -197,6 +198,10 @@ func (u *OtelOutput) exportDevices(ctx context.Context, meter metric.Meter, m *p
|
||||
r.UXG++
|
||||
u.exportUXG(ctx, meter, r, d)
|
||||
|
||||
case *unifi.UMBB:
|
||||
r.UMBB++
|
||||
u.exportUMBB(ctx, meter, r, d)
|
||||
|
||||
default:
|
||||
if u.Collector.Poller().LogUnknownTypes {
|
||||
u.LogDebugf("otel: unknown device type: %T", item)
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
package otelunifi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math"
|
||||
"strconv"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
|
||||
"github.com/unpoller/unifi/v6"
|
||||
)
|
||||
|
||||
// exportUMBB emits metrics for a mobile broadband (cellular) device, such as the U5G Max.
|
||||
func (u *OtelOutput) exportUMBB(ctx context.Context, meter metric.Meter, r *Report, s *unifi.UMBB) {
|
||||
if !s.Adopted.Val {
|
||||
return
|
||||
}
|
||||
|
||||
rad := s.Mbb.Radio
|
||||
attrs := attribute.NewSet(
|
||||
attribute.String("mac", s.Mac),
|
||||
attribute.String("site_name", s.SiteName),
|
||||
attribute.String("source", s.SourceName),
|
||||
attribute.String("name", s.Name),
|
||||
attribute.String("model", s.Model),
|
||||
attribute.String("version", s.Version),
|
||||
attribute.String("type", s.Type),
|
||||
attribute.String("ip", s.IP),
|
||||
attribute.String("mbb_state", s.Mbb.State),
|
||||
attribute.String("failover_mode", s.Mbb.Mode),
|
||||
attribute.String("rat", rad.Rat),
|
||||
attribute.String("band", rad.Band),
|
||||
attribute.String("operator", rad.NetworkOperator),
|
||||
)
|
||||
|
||||
up := 0.0
|
||||
if s.State.Val == 1 {
|
||||
up = 1.0
|
||||
}
|
||||
|
||||
u.recordGauge(ctx, meter, r, "unifi_device_umbb_up",
|
||||
"Whether UMBB is up (1) or down (0)", up, attrs)
|
||||
u.recordGauge(ctx, meter, r, "unifi_device_umbb_uptime_seconds",
|
||||
"UMBB uptime in seconds", s.Uptime.Val, attrs)
|
||||
u.recordGauge(ctx, meter, r, "unifi_device_umbb_cpu_utilization",
|
||||
"UMBB CPU utilization percentage", s.SystemStats.CPU.Val, attrs)
|
||||
u.recordGauge(ctx, meter, r, "unifi_device_umbb_mem_utilization",
|
||||
"UMBB memory utilization percentage", s.SystemStats.Mem.Val, attrs)
|
||||
u.recordGauge(ctx, meter, r, "unifi_device_umbb_internet",
|
||||
"Modem reports internet connectivity (1) or not (0)", s.Internet.Float64(), attrs)
|
||||
|
||||
// Signal readings and bitrates are left out when the modem does not report them,
|
||||
// so a missing reading is not mistaken for 0 dBm.
|
||||
for _, g := range []struct {
|
||||
name, desc string
|
||||
val unifi.FlexInt
|
||||
}{
|
||||
{"unifi_device_umbb_rsrp_dbm", "Cellular RSRP of the serving cell in dBm. LTE anchor in 5G NSA mode", rad.Rsrp},
|
||||
{"unifi_device_umbb_rsrq_db", "Cellular RSRQ of the serving cell in dB. LTE anchor in 5G NSA mode", rad.Rsrq},
|
||||
{"unifi_device_umbb_snr_db", "Cellular signal-to-noise ratio of the serving cell in dB. LTE anchor in 5G NSA mode", rad.Snr},
|
||||
{"unifi_device_umbb_nr_rsrp_dbm", "5G NR RSRP in dBm", rad.RsrpNr},
|
||||
{"unifi_device_umbb_nr_rsrq_db", "5G NR RSRQ in dB", rad.RsrqNr},
|
||||
{"unifi_device_umbb_nr_snr_db", "5G NR signal-to-noise ratio in dB", rad.SnrNr},
|
||||
{"unifi_device_umbb_signal_bars", "Cellular signal strength in bars, as shown by the controller", rad.Signal},
|
||||
{"unifi_device_umbb_signal_percent", "Cellular signal strength percentage (0-100)", rad.SignalPercent},
|
||||
{"unifi_device_umbb_lte_max_bitrate_dl_bps", "Maximum LTE downlink bitrate in bits per second", rad.MaxBitrateDl},
|
||||
{"unifi_device_umbb_lte_max_bitrate_ul_bps", "Maximum LTE uplink bitrate in bits per second", rad.MaxBitrateUl},
|
||||
{"unifi_device_umbb_nr_max_bitrate_dl_bps", "Maximum 5G NR downlink bitrate in bits per second", rad.MaxBitrateDlNr},
|
||||
{"unifi_device_umbb_nr_max_bitrate_ul_bps", "Maximum 5G NR uplink bitrate in bits per second", rad.MaxBitrateUlNr},
|
||||
} {
|
||||
if g.val.Txt != "" {
|
||||
u.recordGauge(ctx, meter, r, g.name, g.desc, g.val.Val, attrs)
|
||||
}
|
||||
}
|
||||
|
||||
u.recordGauge(ctx, meter, r, "unifi_device_umbb_coverage",
|
||||
"Modem has cellular coverage (1) or not (0)", rad.HasCoverage.Float64(), attrs)
|
||||
u.recordGauge(ctx, meter, r, "unifi_device_umbb_roaming",
|
||||
"Modem is roaming (1) or on its home network (0)", rad.Roaming.Float64(), attrs)
|
||||
u.recordGauge(ctx, meter, r, "unifi_device_umbb_registration_state",
|
||||
"Cellular network registration state code (undocumented; likely 0 not registered, 1 home, "+
|
||||
"2 searching, 3 denied, 4 unknown, 5 roaming)", rad.RegistrationState.Val, attrs)
|
||||
u.recordGauge(ctx, meter, r, "unifi_device_umbb_nr_carriers",
|
||||
"Aggregated 5G NR component carriers in use", float64(len(rad.CaNr)), attrs)
|
||||
u.recordGauge(ctx, meter, r, "unifi_device_umbb_lte_carriers",
|
||||
"Aggregated LTE component carriers in use", float64(len(rad.CaLte)), attrs)
|
||||
|
||||
dl, ul := mbbBandwidth(rad)
|
||||
|
||||
u.recordGauge(ctx, meter, r, "unifi_device_umbb_dl_bandwidth_mhz",
|
||||
"Total aggregated downlink carrier bandwidth in MHz", dl, attrs)
|
||||
u.recordGauge(ctx, meter, r, "unifi_device_umbb_ul_bandwidth_mhz",
|
||||
"Total aggregated uplink carrier bandwidth in MHz", ul, attrs)
|
||||
|
||||
for _, sim := range s.Mbb.Sim {
|
||||
simAttrs := attribute.NewSet(
|
||||
attribute.String("mac", s.Mac),
|
||||
attribute.String("site_name", s.SiteName),
|
||||
attribute.String("source", s.SourceName),
|
||||
attribute.String("name", s.Name),
|
||||
attribute.String("slot", sim.Slot.Txt),
|
||||
attribute.String("esim", strconv.FormatBool(sim.Esim.Val)),
|
||||
attribute.String("spn", sim.Spn),
|
||||
attribute.String("display_state", sim.DisplayState),
|
||||
attribute.Bool("has_carrier", sim.HasCarrier.Val),
|
||||
attribute.Bool("metered", sim.Metered.Val),
|
||||
attribute.String("network_reject_type", sim.NetworkRejectType),
|
||||
attribute.String("network_reject_text", sim.NetworkRejectText),
|
||||
)
|
||||
|
||||
u.recordGauge(ctx, meter, r, "unifi_device_umbb_sim_active",
|
||||
"SIM slot is the active data SIM (1) or not (0)", sim.Active.Float64(), simAttrs)
|
||||
u.recordGauge(ctx, meter, r, "unifi_device_umbb_sim_state_seconds",
|
||||
"Seconds the SIM has been in its current display state", sim.DisplayStateElapsed.Val, simAttrs)
|
||||
u.recordGauge(ctx, meter, r, "unifi_device_umbb_sim_card_present",
|
||||
"SIM card or eSIM profile is present (1) or not (0)", sim.CardPresent.Float64(), simAttrs)
|
||||
u.recordGauge(ctx, meter, r, "unifi_device_umbb_sim_pin_blocked",
|
||||
"SIM is PIN blocked (1) or not (0)", sim.PinBlocked.Float64(), simAttrs)
|
||||
u.recordGauge(ctx, meter, r, "unifi_device_umbb_sim_data_warning",
|
||||
"SIM has passed its data usage warning threshold (1) or not (0)", sim.DataWarning.Float64(), simAttrs)
|
||||
u.recordGauge(ctx, meter, r, "unifi_device_umbb_sim_data_limited",
|
||||
"SIM has reached its data limit (1) or not (0)", sim.DataLimited.Float64(), simAttrs)
|
||||
u.recordGauge(ctx, meter, r, "unifi_device_umbb_sim_receive_bytes_total",
|
||||
"Bytes received over this SIM. Cumulative; not reset when the modem reboots", sim.RxBytes.Val, simAttrs)
|
||||
u.recordGauge(ctx, meter, r, "unifi_device_umbb_sim_transmit_bytes_total",
|
||||
"Bytes transmitted over this SIM. Cumulative; not reset when the modem reboots", sim.TxBytes.Val, simAttrs)
|
||||
}
|
||||
}
|
||||
|
||||
// mbbBandwidth sums the downlink and uplink bandwidth of every aggregated carrier, NR and LTE.
|
||||
// A carrier with no uplink reports a null bandwidth, which decodes as zero.
|
||||
// A sum that overflows is reported as zero: InfluxDB rejects a whole point with an infinite field.
|
||||
func mbbBandwidth(rad unifi.MBBRadio) (dl, ul float64) {
|
||||
for _, c := range rad.CaNr {
|
||||
dl += c.DlBwMhz.Val
|
||||
ul += c.UlBwMhz.Val
|
||||
}
|
||||
|
||||
for _, c := range rad.CaLte {
|
||||
dl += c.DlBwMhz.Val
|
||||
ul += c.UlBwMhz.Val
|
||||
}
|
||||
|
||||
return finiteOrZero(dl), finiteOrZero(ul)
|
||||
}
|
||||
|
||||
func finiteOrZero(v float64) float64 {
|
||||
if math.IsInf(v, 0) || math.IsNaN(v) {
|
||||
return 0
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
@@ -53,6 +53,7 @@ type promUnifi struct {
|
||||
USG *usg
|
||||
USW *usw
|
||||
PDU *pdu
|
||||
UMBB *umbb
|
||||
Site *site
|
||||
RogueAP *rogueap
|
||||
SpeedTest *speedtest
|
||||
@@ -198,6 +199,7 @@ type Report struct {
|
||||
UBB int // Total count of UBB devices.
|
||||
UCI int // Total count of UCI devices.
|
||||
UDB int // Total count of UDB devices.
|
||||
UMBB int // Total count of UMBB devices.
|
||||
Metrics *poller.Metrics // Metrics collected and recorded.
|
||||
Elapsed time.Duration // Duration elapsed collecting and exporting.
|
||||
Fetch time.Duration // Duration elapsed making controller requests.
|
||||
@@ -306,6 +308,7 @@ func (u *promUnifi) Run(c poller.Collect) error {
|
||||
u.USG = descUSG(u.Namespace + "_device_")
|
||||
u.USW = descUSW(u.Namespace + "_device_")
|
||||
u.PDU = descPDU(u.Namespace + "_device_")
|
||||
u.UMBB = descUMBB(u.Namespace + "_device_")
|
||||
u.Site = descSite(u.Namespace + "_site_")
|
||||
u.RogueAP = descRogueAP(u.Namespace + "_rogueap_")
|
||||
u.SpeedTest = descSpeedTest(u.Namespace + "_speedtest_")
|
||||
@@ -1025,6 +1028,9 @@ func (u *promUnifi) switchExport(r report, v any) {
|
||||
case *unifi.UDB:
|
||||
r.addUDB()
|
||||
u.exportUDB(r, v)
|
||||
case *unifi.UMBB:
|
||||
r.addUMBB()
|
||||
u.exportUMBB(r, v)
|
||||
case *unifi.UDM:
|
||||
r.addUDM()
|
||||
u.exportUDM(r, v)
|
||||
|
||||
@@ -24,6 +24,7 @@ type report interface {
|
||||
addUBB()
|
||||
addUCI()
|
||||
addUDB()
|
||||
addUMBB()
|
||||
addUSG()
|
||||
addUAP()
|
||||
addUSW()
|
||||
@@ -51,9 +52,9 @@ func (r *Report) report(c poller.Logger, descs map[*prometheus.Desc]bool) {
|
||||
m := r.Metrics
|
||||
|
||||
c.Logf("UniFi Measurements Exported. Site: %d, Client: %d, "+
|
||||
"UAP: %d, USG/UDM: %d, USW: %d, DPI Site/Client: %d/%d, Countries: %d, Desc: %d, "+
|
||||
"UAP: %d, USG/UDM: %d, USW: %d, UMBB: %d, DPI Site/Client: %d/%d, Countries: %d, Desc: %d, "+
|
||||
"Metric: %d, Bytes: %d, Err: %d, 0s: %d, Req/Total: %v / %v",
|
||||
len(m.Sites), len(m.Clients), r.UAP, r.UDM+r.USG+r.UXG, r.USW, len(m.SitesDPI),
|
||||
len(m.Sites), len(m.Clients), r.UAP, r.UDM+r.USG+r.UXG, r.USW, r.UMBB, len(m.SitesDPI),
|
||||
len(m.ClientsDPI), len(m.CountryTraffic), len(descs), r.Total, r.Bytes, r.Errors, r.Zeros,
|
||||
r.Fetch.Round(time.Millisecond/oneDecimalPoint),
|
||||
r.Elapsed.Round(time.Millisecond/oneDecimalPoint))
|
||||
@@ -124,6 +125,10 @@ func (r *Report) addUDB() {
|
||||
r.UDB++
|
||||
}
|
||||
|
||||
func (r *Report) addUMBB() {
|
||||
r.UMBB++
|
||||
}
|
||||
|
||||
// close is not part of the interface.
|
||||
func (r *Report) close() {
|
||||
r.wg.Wait()
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
package promunifi
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strconv"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/unpoller/unifi/v6"
|
||||
)
|
||||
|
||||
// umbb holds the cellular modem descriptors for UMBB (mobile broadband) devices, such as the U5G Max.
|
||||
type umbb struct {
|
||||
Internet *prometheus.Desc
|
||||
Rsrp *prometheus.Desc
|
||||
Rsrq *prometheus.Desc
|
||||
Snr *prometheus.Desc
|
||||
RsrpNr *prometheus.Desc
|
||||
RsrqNr *prometheus.Desc
|
||||
SnrNr *prometheus.Desc
|
||||
SignalBars *prometheus.Desc
|
||||
SignalPercent *prometheus.Desc
|
||||
Coverage *prometheus.Desc
|
||||
Roaming *prometheus.Desc
|
||||
RegistrationState *prometheus.Desc
|
||||
Carriers *prometheus.Desc
|
||||
BandwidthMhz *prometheus.Desc
|
||||
MaxBitrate *prometheus.Desc
|
||||
Info *prometheus.Desc
|
||||
SimActive *prometheus.Desc
|
||||
SimCardPresent *prometheus.Desc
|
||||
SimPinBlocked *prometheus.Desc
|
||||
SimDataWarning *prometheus.Desc
|
||||
SimDataLimited *prometheus.Desc
|
||||
SimRxBytes *prometheus.Desc
|
||||
SimTxBytes *prometheus.Desc
|
||||
SimStateSeconds *prometheus.Desc
|
||||
SimInfo *prometheus.Desc
|
||||
}
|
||||
|
||||
func descUMBB(ns string) *umbb {
|
||||
labels := []string{"type", "site_name", "name", "source", "tag"}
|
||||
labelS := append(append([]string{}, labels...), "slot", "esim", "spn")
|
||||
nd := prometheus.NewDesc
|
||||
|
||||
return &umbb{
|
||||
Internet: nd(ns+"mbb_internet", "Modem reports internet connectivity (1) or not (0).", labels, nil),
|
||||
Rsrp: nd(ns+"mbb_rsrp_dbm", "Cellular RSRP of the serving cell in dBm. LTE anchor in 5G NSA mode.", labels, nil),
|
||||
Rsrq: nd(ns+"mbb_rsrq_db", "Cellular RSRQ of the serving cell in dB. LTE anchor in 5G NSA mode.", labels, nil),
|
||||
Snr: nd(ns+"mbb_snr_db", "Cellular signal-to-noise ratio of the serving cell in dB. LTE anchor in 5G NSA mode.", labels, nil),
|
||||
RsrpNr: nd(ns+"mbb_nr_rsrp_dbm", "5G NR RSRP in dBm. Same as mbb_rsrp_dbm in 5G SA mode.", labels, nil),
|
||||
RsrqNr: nd(ns+"mbb_nr_rsrq_db", "5G NR RSRQ in dB. Same as mbb_rsrq_db in 5G SA mode.", labels, nil),
|
||||
SnrNr: nd(ns+"mbb_nr_snr_db", "5G NR signal-to-noise ratio in dB. Same as mbb_snr_db in 5G SA mode.", labels, nil),
|
||||
SignalBars: nd(ns+"mbb_signal_bars",
|
||||
"Cellular signal strength in bars, as shown by the controller.", labels, nil),
|
||||
SignalPercent: nd(ns+"mbb_signal_percent",
|
||||
"Cellular signal strength percentage (0-100), as computed by the controller.", labels, nil),
|
||||
Coverage: nd(ns+"mbb_coverage", "Modem has cellular coverage (1) or not (0).", labels, nil),
|
||||
Roaming: nd(ns+"mbb_roaming", "Modem is roaming (1) or on its home network (0).", labels, nil),
|
||||
RegistrationState: nd(ns+"mbb_registration_state",
|
||||
"Cellular network registration state code. Undocumented; likely 0 not registered, 1 registered home, "+
|
||||
"2 searching, 3 denied, 4 unknown, 5 registered roaming.", labels, nil),
|
||||
Carriers: nd(ns+"mbb_carriers", "Aggregated component carriers in use.", append(labels, "carrier_type"), nil),
|
||||
BandwidthMhz: nd(ns+"mbb_bandwidth_mhz", "Total aggregated carrier bandwidth in MHz.",
|
||||
append(labels, "direction"), nil),
|
||||
MaxBitrate: nd(ns+"mbb_max_bitrate_bps", "Maximum bitrate negotiated with the network, in bits per second.",
|
||||
append(labels, "carrier_type", "direction"), nil),
|
||||
Info: nd(ns+"mbb_info", "Cellular modem state. Always 1; state and network details are labels.",
|
||||
append(labels, "mbb_state", "failover_mode", "rat", "band", "operator", "sa_mode", "current_slot",
|
||||
"primary_slot", "mcc", "mnc", "cell_id", "pci", "channel"), nil),
|
||||
SimActive: nd(ns+"mbb_sim_active", "SIM slot is the active data SIM (1) or not (0).", labelS, nil),
|
||||
SimCardPresent: nd(ns+"mbb_sim_card_present", "SIM card or eSIM profile is present (1) or not (0).", labelS, nil),
|
||||
SimPinBlocked: nd(ns+"mbb_sim_pin_blocked", "SIM is PIN blocked (1) or not (0).", labelS, nil),
|
||||
SimDataWarning: nd(ns+"mbb_sim_data_warning", "SIM has passed its data usage warning threshold (1) or not (0).",
|
||||
labelS, nil),
|
||||
SimDataLimited: nd(ns+"mbb_sim_data_limited", "SIM has reached its data limit (1) or not (0).", labelS, nil),
|
||||
SimRxBytes: nd(ns+"mbb_sim_receive_bytes_total",
|
||||
"Bytes received over this SIM. Cumulative; not reset when the modem reboots.", labelS, nil),
|
||||
SimTxBytes: nd(ns+"mbb_sim_transmit_bytes_total",
|
||||
"Bytes transmitted over this SIM. Cumulative; not reset when the modem reboots.", labelS, nil),
|
||||
SimStateSeconds: nd(ns+"mbb_sim_state_seconds", "Seconds the SIM has been in its current display state.",
|
||||
labelS, nil),
|
||||
SimInfo: nd(ns+"mbb_sim_info", "SIM state. Always 1; display state and last network reject are labels.",
|
||||
append(labelS, "display_state", "has_carrier", "metered", "network_reject_type", "network_reject_text"), nil),
|
||||
}
|
||||
}
|
||||
|
||||
// exportUMBB exports metrics for UMBB (mobile broadband) devices, such as the U5G Max.
|
||||
func (u *promUnifi) exportUMBB(r report, d *unifi.UMBB) {
|
||||
if !d.Adopted.Val {
|
||||
return
|
||||
}
|
||||
|
||||
baseLabels := []string{d.Type, d.SiteName, d.Name, d.SourceName}
|
||||
baseInfoLabels := []string{d.Version, d.Model, d.Serial, d.Mac, d.IP, d.ID}
|
||||
|
||||
u.exportWithTags(r, d.Tags, func(tagLabels []string) {
|
||||
tag := tagLabels[0]
|
||||
labels := append(append([]string{}, baseLabels...), tag)
|
||||
infoLabels := append(append([]string{}, baseInfoLabels...), tag)
|
||||
|
||||
u.exportSYSstats(r, labels, d.SysStats, d.SystemStats)
|
||||
u.exportDeviceUplink(r, labels, d.Uplink)
|
||||
u.exportMBB(r, labels, d)
|
||||
|
||||
r.send([]*metric{
|
||||
{u.Device.Info, gauge, 1.0, append(append([]string{}, baseLabels...), infoLabels...)},
|
||||
{u.Device.Uptime, gauge, d.Uptime, labels},
|
||||
{u.Device.Upgradeable, gauge, d.Upgradable.Val, labels},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (u *promUnifi) exportMBB(r report, labels []string, d *unifi.UMBB) {
|
||||
m := d.Mbb
|
||||
rad := m.Radio
|
||||
dl, ul := mbbBandwidth(rad)
|
||||
info := append(append([]string{}, labels...),
|
||||
m.State, m.Mode, rad.Rat, rad.Band, rad.NetworkOperator,
|
||||
strconv.FormatBool(rad.SA5GMode.Val), rad.CurrentSlot.Txt, d.MbbOverrides.PrimarySlot.Txt,
|
||||
rad.Mcc.Txt, rad.Mnc.Txt, rad.CellID.Txt, rad.Pci.Txt, rad.Channel.Txt)
|
||||
|
||||
r.send([]*metric{
|
||||
{u.UMBB.Internet, gauge, d.Internet.Val, labels},
|
||||
{u.UMBB.Coverage, gauge, rad.HasCoverage.Val, labels},
|
||||
{u.UMBB.Roaming, gauge, rad.Roaming.Val, labels},
|
||||
{u.UMBB.RegistrationState, gauge, rad.RegistrationState, labels},
|
||||
{u.UMBB.Carriers, gauge, float64(len(rad.CaNr)), append(append([]string{}, labels...), "nr")},
|
||||
{u.UMBB.Carriers, gauge, float64(len(rad.CaLte)), append(append([]string{}, labels...), "lte")},
|
||||
{u.UMBB.BandwidthMhz, gauge, dl, append(append([]string{}, labels...), "dl")},
|
||||
{u.UMBB.BandwidthMhz, gauge, ul, append(append([]string{}, labels...), "ul")},
|
||||
{u.UMBB.Info, gauge, 1.0, info},
|
||||
})
|
||||
|
||||
// Signal readings and bitrates are left out when the modem does not report them,
|
||||
// so a missing reading is not mistaken for 0 dBm.
|
||||
for _, s := range []struct {
|
||||
desc *prometheus.Desc
|
||||
val unifi.FlexInt
|
||||
dims []string
|
||||
}{
|
||||
{u.UMBB.Rsrp, rad.Rsrp, nil},
|
||||
{u.UMBB.Rsrq, rad.Rsrq, nil},
|
||||
{u.UMBB.Snr, rad.Snr, nil},
|
||||
{u.UMBB.RsrpNr, rad.RsrpNr, nil},
|
||||
{u.UMBB.RsrqNr, rad.RsrqNr, nil},
|
||||
{u.UMBB.SnrNr, rad.SnrNr, nil},
|
||||
{u.UMBB.SignalBars, rad.Signal, nil},
|
||||
{u.UMBB.SignalPercent, rad.SignalPercent, nil},
|
||||
{u.UMBB.MaxBitrate, rad.MaxBitrateDl, []string{"lte", "dl"}},
|
||||
{u.UMBB.MaxBitrate, rad.MaxBitrateUl, []string{"lte", "ul"}},
|
||||
{u.UMBB.MaxBitrate, rad.MaxBitrateDlNr, []string{"nr", "dl"}},
|
||||
{u.UMBB.MaxBitrate, rad.MaxBitrateUlNr, []string{"nr", "ul"}},
|
||||
} {
|
||||
if s.val.Txt != "" {
|
||||
r.send([]*metric{{s.desc, gauge, s.val.Val, append(append([]string{}, labels...), s.dims...)}})
|
||||
}
|
||||
}
|
||||
|
||||
for _, sim := range m.Sim {
|
||||
simLabels := append(append([]string{}, labels...),
|
||||
sim.Slot.Txt, strconv.FormatBool(sim.Esim.Val), sim.Spn)
|
||||
simInfo := append(append([]string{}, simLabels...),
|
||||
sim.DisplayState, strconv.FormatBool(sim.HasCarrier.Val), strconv.FormatBool(sim.Metered.Val),
|
||||
sim.NetworkRejectType, sim.NetworkRejectText)
|
||||
|
||||
r.send([]*metric{
|
||||
{u.UMBB.SimActive, gauge, sim.Active.Val, simLabels},
|
||||
{u.UMBB.SimCardPresent, gauge, sim.CardPresent.Val, simLabels},
|
||||
{u.UMBB.SimPinBlocked, gauge, sim.PinBlocked.Val, simLabels},
|
||||
{u.UMBB.SimDataWarning, gauge, sim.DataWarning.Val, simLabels},
|
||||
{u.UMBB.SimDataLimited, gauge, sim.DataLimited.Val, simLabels},
|
||||
{u.UMBB.SimRxBytes, counter, sim.RxBytes, simLabels},
|
||||
{u.UMBB.SimTxBytes, counter, sim.TxBytes, simLabels},
|
||||
{u.UMBB.SimStateSeconds, gauge, sim.DisplayStateElapsed, simLabels},
|
||||
{u.UMBB.SimInfo, gauge, 1.0, simInfo},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// mbbBandwidth sums the downlink and uplink bandwidth of every aggregated carrier, NR and LTE.
|
||||
// A carrier with no uplink reports a null bandwidth, which decodes as zero.
|
||||
// A sum that overflows is reported as zero: InfluxDB rejects a whole point with an infinite field.
|
||||
func mbbBandwidth(rad unifi.MBBRadio) (dl, ul float64) {
|
||||
for _, c := range rad.CaNr {
|
||||
dl += c.DlBwMhz.Val
|
||||
ul += c.UlBwMhz.Val
|
||||
}
|
||||
|
||||
for _, c := range rad.CaLte {
|
||||
dl += c.DlBwMhz.Val
|
||||
ul += c.UlBwMhz.Val
|
||||
}
|
||||
|
||||
return finiteOrZero(dl), finiteOrZero(ul)
|
||||
}
|
||||
|
||||
func finiteOrZero(v float64) float64 {
|
||||
if math.IsInf(v, 0) || math.IsNaN(v) {
|
||||
return 0
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
//nolint:testpackage // white-box: exercises the unexported descriptors and export path.
|
||||
package promunifi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/unpoller/unifi/v6"
|
||||
)
|
||||
|
||||
// Trimmed from a U5G Max (UMBBE630) on 5G SA. Identifiers and addresses are omitted.
|
||||
const u5gMaxJSON = `{
|
||||
"adopted": true,
|
||||
"type": "umbb",
|
||||
"model": "UMBBE630",
|
||||
"name": "U5G Max",
|
||||
"mac": "00:00:5e:00:53:02",
|
||||
"version": "7.5.3.19533",
|
||||
"uptime": 5264387,
|
||||
"internet": true,
|
||||
"mbb_overrides": {"primary_slot": 1},
|
||||
"mbb": {
|
||||
"mode": "failover",
|
||||
"state": "ready",
|
||||
"sim": [
|
||||
{"active": false, "slot": 1, "esim": false, "card_present": false, "display_state": "no-sim", "display_state_elapsed": 5264323},
|
||||
{
|
||||
"active": true, "slot": 2, "esim": true, "spn": "EE", "card_present": true, "has_carrier": true,
|
||||
"metered": false, "pin_blocked": false, "data_warning": true, "data_limited": false,
|
||||
"display_state": "operational", "display_state_elapsed": 5230,
|
||||
"network_reject_type": "wds", "network_reject_text": "Carrier rejected data", "network_reject_age": 45,
|
||||
"rxbytes": "3013150278176", "txbytes": "725650860811"
|
||||
}
|
||||
],
|
||||
"radio": {
|
||||
"5g_sa_mode": true, "band": "n78", "rat": "5G", "networkoperator": "EE", "current_slot": 2,
|
||||
"has_coverage": true, "roaming": false, "registration_state": 1,
|
||||
"mcc": 234, "mnc": 30, "cell_id": 2719245, "pci": 216, "channel": 646080,
|
||||
"max_bitrate_dl": 0, "max_bitrate_ul": 0, "max_bitrate_dl_nr": 640000000, "max_bitrate_ul_nr": 224000000,
|
||||
"rsrp_nr": -96, "rsrq_nr": -13, "snr_nr": 17.600000381469727,
|
||||
"rsrp": -96, "rsrq": -13, "snr": 17.600000381469727, "signal": 3, "signal_percent": 95,
|
||||
"ca_lte": [],
|
||||
"ca_nr": [
|
||||
{"band": 78, "dl_arfcn": 637334, "dl_bw_mhz": 40.0, "primary": true, "ul_arfcn": 637334, "ul_bw_mhz": 40.0},
|
||||
{"band": 78, "dl_arfcn": 646666, "dl_bw_mhz": 40.0, "primary": false, "ul_arfcn": 0, "ul_bw_mhz": null}
|
||||
]
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
func TestExportUMBB(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var d unifi.UMBB
|
||||
|
||||
require.NoError(t, json.Unmarshal([]byte(u5gMaxJSON), &d))
|
||||
|
||||
d.SiteName = "Default"
|
||||
d.SourceName = "https://udm.example"
|
||||
|
||||
r := &fakeReport{}
|
||||
u := testProm()
|
||||
u.UMBB = descUMBB("unifi_")
|
||||
u.exportUMBB(r, &d)
|
||||
|
||||
a := assert.New(t)
|
||||
labels := []string{"umbb", "Default", "U5G Max", "https://udm.example", ""}
|
||||
|
||||
a.InDelta(-96, findMetric(t, r.sent, "unifi_mbb_rsrp_dbm", labels).value, 0.001)
|
||||
a.InDelta(-13, findMetric(t, r.sent, "unifi_mbb_rsrq_db", labels).value, 0.001)
|
||||
a.InDelta(17.6, findMetric(t, r.sent, "unifi_mbb_snr_db", labels).value, 0.001)
|
||||
a.InDelta(3, findMetric(t, r.sent, "unifi_mbb_signal_bars", labels).value, 0.001)
|
||||
a.InDelta(95, findMetric(t, r.sent, "unifi_mbb_signal_percent", labels).value, 0.001)
|
||||
a.InDelta(1, findMetric(t, r.sent, "unifi_mbb_coverage", labels).value, 0.001)
|
||||
a.InDelta(2, findMetric(t, r.sent, "unifi_mbb_carriers", append(labels, "nr")).value, 0.001)
|
||||
a.InDelta(0, findMetric(t, r.sent, "unifi_mbb_carriers", append(labels, "lte")).value, 0.001)
|
||||
a.InDelta(80, findMetric(t, r.sent, "unifi_mbb_bandwidth_mhz", append(labels, "dl")).value, 0.001)
|
||||
a.InDelta(40, findMetric(t, r.sent, "unifi_mbb_bandwidth_mhz", append(labels, "ul")).value, 0.001,
|
||||
"a null uplink bandwidth adds nothing")
|
||||
a.InDelta(-96, findMetric(t, r.sent, "unifi_mbb_nr_rsrp_dbm", labels).value, 0.001)
|
||||
a.InDelta(-13, findMetric(t, r.sent, "unifi_mbb_nr_rsrq_db", labels).value, 0.001)
|
||||
a.InDelta(17.6, findMetric(t, r.sent, "unifi_mbb_nr_snr_db", labels).value, 0.001)
|
||||
a.InDelta(1, findMetric(t, r.sent, "unifi_mbb_internet", labels).value, 0.001)
|
||||
a.InDelta(640000000, findMetric(t, r.sent, "unifi_mbb_max_bitrate_bps", append(labels, "nr", "dl")).value, 1)
|
||||
a.InDelta(224000000, findMetric(t, r.sent, "unifi_mbb_max_bitrate_bps", append(labels, "nr", "ul")).value, 1)
|
||||
a.InDelta(0, findMetric(t, r.sent, "unifi_mbb_max_bitrate_bps", append(labels, "lte", "dl")).value, 1)
|
||||
a.InDelta(1, findMetric(t, r.sent, "unifi_mbb_info",
|
||||
append(labels, "ready", "failover", "5G", "n78", "EE", "true", "2", "1",
|
||||
"234", "30", "2719245", "216", "646080")).value, 0.001)
|
||||
|
||||
noSim := append(labels, "1", "false", "")
|
||||
a.InDelta(0, findMetric(t, r.sent, "unifi_mbb_sim_active", noSim).value, 0.001)
|
||||
a.InDelta(0, findMetric(t, r.sent, "unifi_mbb_sim_card_present", noSim).value, 0.001)
|
||||
a.InDelta(1, findMetric(t, r.sent, "unifi_mbb_sim_info",
|
||||
append(noSim, "no-sim", "false", "false", "", "")).value, 0.001)
|
||||
|
||||
esim := append(labels, "2", "true", "EE")
|
||||
a.InDelta(1, findMetric(t, r.sent, "unifi_mbb_sim_active", esim).value, 0.001)
|
||||
a.InDelta(3013150278176, findMetric(t, r.sent, "unifi_mbb_sim_receive_bytes_total", esim).value, 1)
|
||||
a.InDelta(725650860811, findMetric(t, r.sent, "unifi_mbb_sim_transmit_bytes_total", esim).value, 1)
|
||||
a.InDelta(5230, findMetric(t, r.sent, "unifi_mbb_sim_state_seconds", esim).value, 0.001)
|
||||
a.InDelta(1, findMetric(t, r.sent, "unifi_mbb_sim_card_present", esim).value, 0.001)
|
||||
a.InDelta(0, findMetric(t, r.sent, "unifi_mbb_sim_pin_blocked", esim).value, 0.001)
|
||||
a.InDelta(1, findMetric(t, r.sent, "unifi_mbb_sim_data_warning", esim).value, 0.001)
|
||||
a.InDelta(0, findMetric(t, r.sent, "unifi_mbb_sim_data_limited", esim).value, 0.001)
|
||||
a.InDelta(1, findMetric(t, r.sent, "unifi_mbb_sim_info",
|
||||
append(esim, "operational", "true", "false", "wds", "Carrier rejected data")).value, 0.001)
|
||||
}
|
||||
|
||||
func TestExportUMBBMissingSignalIsSkipped(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var d unifi.UMBB
|
||||
|
||||
require.NoError(t, json.Unmarshal([]byte(`{"adopted": true, "type": "umbb", "name": "U5G Max",
|
||||
"mbb": {"state": "searching", "radio": {"has_coverage": false}}}`), &d))
|
||||
|
||||
r := &fakeReport{}
|
||||
u := testProm()
|
||||
u.UMBB = descUMBB("unifi_")
|
||||
u.exportUMBB(r, &d)
|
||||
|
||||
for _, m := range r.sent {
|
||||
name := prometheusDescName(t, m.Desc)
|
||||
assert.NotContains(t, []string{
|
||||
"unifi_mbb_rsrp_dbm", "unifi_mbb_rsrq_db", "unifi_mbb_snr_db",
|
||||
"unifi_mbb_nr_rsrp_dbm", "unifi_mbb_nr_rsrq_db", "unifi_mbb_nr_snr_db",
|
||||
"unifi_mbb_signal_bars", "unifi_mbb_signal_percent", "unifi_mbb_max_bitrate_bps",
|
||||
}, name, "a reading the modem did not report must not be exported as 0")
|
||||
}
|
||||
|
||||
labels := []string{"umbb", "", "U5G Max", "", ""}
|
||||
assert.InDelta(t, 0, findMetric(t, r.sent, "unifi_mbb_coverage", labels).value, 0.001)
|
||||
}
|
||||
|
||||
func TestExportUMBBUnadoptedIsSkipped(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
r := &fakeReport{}
|
||||
u := testProm()
|
||||
u.UMBB = descUMBB("unifi_")
|
||||
u.exportUMBB(r, &unifi.UMBB{Name: "U5G Max"})
|
||||
|
||||
assert.Empty(t, r.sent)
|
||||
}
|
||||
|
||||
// findMetric returns the one series with this name and exact label values.
|
||||
func findMetric(t *testing.T, sent []*metric, name string, labels []string) recordedMetric {
|
||||
t.Helper()
|
||||
|
||||
for _, m := range sent {
|
||||
if prometheusDescName(t, m.Desc) == name && slices.Equal(m.Labels, labels) {
|
||||
return recordedMetric{value: metricFloat(t, m), labels: m.Labels}
|
||||
}
|
||||
}
|
||||
|
||||
t.Fatalf("no %s series with labels %q", name, labels)
|
||||
|
||||
return recordedMetric{}
|
||||
}
|
||||
Reference in New Issue
Block a user