diff --git a/pkg/datadogunifi/integration_test_expectations.yaml b/pkg/datadogunifi/integration_test_expectations.yaml index c65aeda5..10f5e068 100644 --- a/pkg/datadogunifi/integration_test_expectations.yaml +++ b/pkg/datadogunifi/integration_test_expectations.yaml @@ -567,19 +567,25 @@ gauges: - 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.network_reject_age + - 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 @@ -587,6 +593,12 @@ gauges: - 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 diff --git a/pkg/datadogunifi/umbb.go b/pkg/datadogunifi/umbb.go index 14c638e4..49c86817 100644 --- a/pkg/datadogunifi/umbb.go +++ b/pkg/datadogunifi/umbb.go @@ -21,20 +21,20 @@ func (u *DatadogUnifi) batchUMBB(r report, s *unifi.UMBB) { 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, - "mbb_mode": s.Mbb.Mode, - "rat": rad.Rat, - "band": rad.Band, - "operator": rad.NetworkOperator, + "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( @@ -50,11 +50,8 @@ func (u *DatadogUnifi) batchUMBB(r report, s *unifi.UMBB) { "uplink_uptime": s.Uplink.Uptime.Val, "sa_mode": rad.SA5GMode.Float64(), "current_slot": rad.CurrentSlot.Val, - "rsrp": rad.Rsrp.Val, - "rsrq": rad.Rsrq.Val, - "snr": rad.Snr.Val, - "signal": rad.Signal.Val, - "signal_percent": rad.SignalPercent.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, @@ -62,7 +59,8 @@ func (u *DatadogUnifi) batchUMBB(r report, s *unifi.UMBB) { "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) @@ -79,13 +77,18 @@ func (u *DatadogUnifi) batchUMBB(r report, s *unifi.UMBB) { "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, - "network_reject_age": sim.NetworkRejectAge.Val, "rx_bytes": sim.RxBytes.Val, "tx_bytes": sim.TxBytes.Val, } @@ -94,6 +97,33 @@ func (u *DatadogUnifi) batchUMBB(r report, s *unifi.UMBB) { } } +// 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. diff --git a/pkg/influxunifi/integration_test_expectations.yaml b/pkg/influxunifi/integration_test_expectations.yaml index 2275345e..0b713e20 100644 --- a/pkg/influxunifi/integration_test_expectations.yaml +++ b/pkg/influxunifi/integration_test_expectations.yaml @@ -683,24 +683,38 @@ points: - 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 - mbb_mode: string + 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 @@ -737,11 +751,16 @@ points: - type fields: active: bool + card_present: bool + data_limited: bool + data_warning: bool display_state: string display_state_elapsed: float - network_reject_age: 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: diff --git a/pkg/influxunifi/umbb.go b/pkg/influxunifi/umbb.go index 5ee0fcc3..408fc341 100644 --- a/pkg/influxunifi/umbb.go +++ b/pkg/influxunifi/umbb.go @@ -43,18 +43,20 @@ func (u *InfluxUnifi) batchUMBB(r report, s *unifi.UMBB) { "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, - "mbb_mode": s.Mbb.Mode, + "failover_mode": s.Mbb.Mode, "rat": rad.Rat, "band": rad.Band, "operator": rad.NetworkOperator, "sa_mode": rad.SA5GMode.Val, "current_slot": rad.CurrentSlot.Val, - "rsrp": rad.Rsrp.Val, - "rsrq": rad.Rsrq.Val, - "snr": rad.Snr.Val, - "signal": rad.Signal.Val, - "signal_percent": rad.SignalPercent.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, @@ -64,6 +66,10 @@ func (u *InfluxUnifi) batchUMBB(r report, s *unifi.UMBB) { "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}) @@ -83,11 +89,16 @@ func (u *InfluxUnifi) batchUMBB(r report, s *unifi.UMBB) { }, 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, - "network_reject_age": sim.NetworkRejectAge.Val, "rx_bytes": sim.RxBytes.Val, "tx_bytes": sim.TxBytes.Val, }, @@ -95,6 +106,33 @@ func (u *InfluxUnifi) batchUMBB(r report, s *unifi.UMBB) { } } +// 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. diff --git a/pkg/otelunifi/umbb.go b/pkg/otelunifi/umbb.go index 0c331c01..30193651 100644 --- a/pkg/otelunifi/umbb.go +++ b/pkg/otelunifi/umbb.go @@ -28,7 +28,7 @@ func (u *OtelOutput) exportUMBB(ctx context.Context, meter metric.Meter, r *Repo attribute.String("type", s.Type), attribute.String("ip", s.IP), attribute.String("mbb_state", s.Mbb.State), - attribute.String("mbb_mode", s.Mbb.Mode), + attribute.String("failover_mode", s.Mbb.Mode), attribute.String("rat", rad.Rat), attribute.String("band", rad.Band), attribute.String("operator", rad.NetworkOperator), @@ -47,22 +47,40 @@ func (u *OtelOutput) exportUMBB(ctx context.Context, meter metric.Meter, r *Repo "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_rsrp_dbm", - "Cellular RSRP in dBm", rad.Rsrp.Val, attrs) - u.recordGauge(ctx, meter, r, "unifi_device_umbb_rsrq_db", - "Cellular RSRQ in dB", rad.Rsrq.Val, attrs) - u.recordGauge(ctx, meter, r, "unifi_device_umbb_snr_db", - "Cellular signal-to-noise ratio in dB", rad.Snr.Val, attrs) - u.recordGauge(ctx, meter, r, "unifi_device_umbb_signal_bars", - "Cellular signal strength in bars (0-5)", rad.Signal.Val, attrs) - u.recordGauge(ctx, meter, r, "unifi_device_umbb_signal_percent", - "Cellular signal strength percentage", rad.SignalPercent.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", rad.RegistrationState.Val, attrs) + "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", @@ -85,6 +103,8 @@ func (u *OtelOutput) exportUMBB(ctx context.Context, meter metric.Meter, r *Repo 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), ) @@ -93,12 +113,18 @@ func (u *OtelOutput) exportUMBB(ctx context.Context, meter metric.Meter, r *Repo "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_network_reject_age", - "Age of the last carrier network reject, as reported by the controller", sim.NetworkRejectAge.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", sim.RxBytes.Val, simAttrs) + "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", sim.TxBytes.Val, simAttrs) + "Bytes transmitted over this SIM. Cumulative; not reset when the modem reboots", sim.TxBytes.Val, simAttrs) } } diff --git a/pkg/promunifi/report.go b/pkg/promunifi/report.go index da9520d4..9485d4ac 100644 --- a/pkg/promunifi/report.go +++ b/pkg/promunifi/report.go @@ -52,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)) diff --git a/pkg/promunifi/umbb.go b/pkg/promunifi/umbb.go index eed12e2e..a847e336 100644 --- a/pkg/promunifi/umbb.go +++ b/pkg/promunifi/umbb.go @@ -10,9 +10,13 @@ import ( // 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 @@ -20,12 +24,16 @@ type umbb struct { 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 - SimRejectAge *prometheus.Desc SimInfo *prometheus.Desc } @@ -35,26 +43,44 @@ func descUMBB(ns string) *umbb { nd := prometheus.NewDesc return &umbb{ - Rsrp: nd(ns+"mbb_rsrp_dbm", "Cellular RSRP in dBm.", labels, nil), - Rsrq: nd(ns+"mbb_rsrq_db", "Cellular RSRQ in dB.", labels, nil), - Snr: nd(ns+"mbb_snr_db", "Cellular signal-to-noise ratio in dB.", labels, nil), - SignalBars: nd(ns+"mbb_signal_bars", "Cellular signal strength in bars (0-5).", labels, nil), - SignalPercent: nd(ns+"mbb_signal_percent", "Cellular signal strength percentage.", 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.", 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), + 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", "mbb_mode", "rat", "band", "operator", "sa_mode", "current_slot"), nil), - SimActive: nd(ns+"mbb_sim_active", "SIM slot is the active data SIM (1) or not (0).", labelS, nil), - SimRxBytes: nd(ns+"mbb_sim_receive_bytes_total", "Bytes received over this SIM.", labelS, nil), - SimTxBytes: nd(ns+"mbb_sim_transmit_bytes_total", "Bytes transmitted over this SIM.", labelS, nil), - SimStateSeconds: nd(ns+"mbb_sim_state_seconds", "Seconds the SIM has been in its current display state.", labelS, nil), - SimRejectAge: nd(ns+"mbb_sim_network_reject_age", - "Age of the last carrier network reject, as reported by the controller.", labelS, nil), + 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", "network_reject_type", "network_reject_text"), nil), + append(labelS, "display_state", "has_carrier", "metered", "network_reject_type", "network_reject_text"), nil), } } @@ -74,7 +100,7 @@ func (u *promUnifi) exportUMBB(r report, d *unifi.UMBB) { u.exportSYSstats(r, labels, d.SysStats, d.SystemStats) u.exportDeviceUplink(r, labels, d.Uplink) - u.exportMBB(r, labels, d.Mbb) + u.exportMBB(r, labels, d) r.send([]*metric{ {u.Device.Info, gauge, 1.0, append(append([]string{}, baseLabels...), infoLabels...)}, @@ -84,19 +110,17 @@ func (u *promUnifi) exportUMBB(r report, d *unifi.UMBB) { }) } -func (u *promUnifi) exportMBB(r report, labels []string, m unifi.MBB) { +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) + 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.Rsrp, gauge, rad.Rsrp, labels}, - {u.UMBB.Rsrq, gauge, rad.Rsrq, labels}, - {u.UMBB.Snr, gauge, rad.Snr, labels}, - {u.UMBB.SignalBars, gauge, rad.Signal, labels}, - {u.UMBB.SignalPercent, gauge, rad.SignalPercent, labels}, + {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}, @@ -107,18 +131,47 @@ func (u *promUnifi) exportMBB(r report, labels []string, m unifi.MBB) { {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, sim.NetworkRejectType, sim.NetworkRejectText) + 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.SimRejectAge, gauge, sim.NetworkRejectAge, simLabels}, {u.UMBB.SimInfo, gauge, 1.0, simInfo}, }) } diff --git a/pkg/promunifi/umbb_test.go b/pkg/promunifi/umbb_test.go index bce0a05f..889f8916 100644 --- a/pkg/promunifi/umbb_test.go +++ b/pkg/promunifi/umbb_test.go @@ -20,13 +20,16 @@ const u5gMaxJSON = `{ "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, "display_state": "no-sim", "display_state_elapsed": 5264323}, + {"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", + "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" @@ -35,6 +38,9 @@ const u5gMaxJSON = `{ "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": [ @@ -74,21 +80,60 @@ func TestExportUMBB(t *testing.T) { 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")).value, 0.001) + 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(1, findMetric(t, r.sent, "unifi_mbb_sim_info", append(noSim, "no-sim", "", "")).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(45, findMetric(t, r.sent, "unifi_mbb_sim_network_reject_age", 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", "wds", "Carrier rejected data")).value, 0.001) + 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) {