Merge pull request #1102 from tomwilkie/feat/umbb

feat: export UMBB (U5G Max) cellular modem metrics
This commit is contained in:
Cody Lee
2026-10-01 16:59:22 -04:00
committed by GitHub
16 changed files with 1015 additions and 7 deletions
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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=
+2
View File
@@ -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
+150
View File
@@ -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
}
+2
View File
@@ -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
+159
View File
@@ -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
}
+6
View File
@@ -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) {
+11 -2
View File
@@ -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)
+5
View File
@@ -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)
+154
View File
@@ -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
}
+6
View File
@@ -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)
+7 -2
View File
@@ -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()
+203
View File
@@ -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
}
+163
View File
@@ -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{}
}