diff --git a/pkg/datadogunifi/datadog.go b/pkg/datadogunifi/datadog.go index 3eb511db..8d7eb94f 100644 --- a/pkg/datadogunifi/datadog.go +++ b/pkg/datadogunifi/datadog.go @@ -343,6 +343,8 @@ func (u *DatadogUnifi) switchExport(r report, v any) { //nolint:cyclop u.batchUBB(r, v) case *unifi.UCI: u.batchUCI(r, v) + case *unifi.UDB: + u.batchUDB(r, v) case *unifi.Site: u.reportSite(r, v) case *unifi.Client: diff --git a/pkg/datadogunifi/integration_test_expectations.yaml b/pkg/datadogunifi/integration_test_expectations.yaml index 343b1a53..99e36179 100644 --- a/pkg/datadogunifi/integration_test_expectations.yaml +++ b/pkg/datadogunifi/integration_test_expectations.yaml @@ -507,6 +507,51 @@ gauges: - unifi.uci.stat_rx_packets - unifi.uci.stat_tx_dropped - unifi.uci.state + - unifi.udb.bytes + - unifi.udb.cpu + - unifi.udb.fan_level + - unifi.udb.general_temperature + - unifi.udb.guest_num_sta + - unifi.udb.guest_wlan_num_sta + - unifi.udb.last_seen + - unifi.udb.loadavg_1 + - unifi.udb.loadavg_5 + - unifi.udb.loadavg_15 + - unifi.udb.mem + - unifi.udb.mem_buffer + - unifi.udb.mem_total + - unifi.udb.mem_used + - unifi.udb.memory + - unifi.udb.network + - unifi.udb.num_sta + - unifi.udb.probe + - unifi.udb.rx_bytes + - unifi.udb.satisfaction + - unifi.udb.stat_bytes + - unifi.udb.stat_rx_bytes + - unifi.udb.stat_rx_crypts + - unifi.udb.stat_rx_dropped + - unifi.udb.stat_rx_errors + - unifi.udb.stat_rx_frags + - unifi.udb.stat_rx_packets + - unifi.udb.stat_tx_bytes + - unifi.udb.stat_tx_dropped + - unifi.udb.stat_tx_errors + - unifi.udb.stat_tx_packets + - unifi.udb.stat_tx_retries + - unifi.udb.state + - unifi.udb.sys + - unifi.udb.system_uptime + - unifi.udb.total_max_power + - unifi.udb.tx_bytes + - unifi.udb.upgradeable + - unifi.udb.uplink_latency + - unifi.udb.uplink_max_speed + - unifi.udb.uplink_speed + - unifi.udb.uplink_uptime + - unifi.udb.uptime + - unifi.udb.user_num_sta + - unifi.udb.user_wlan_num_sta counts: - unifi.collector.num_devices - unifi.collector.num_errors diff --git a/pkg/datadogunifi/udb.go b/pkg/datadogunifi/udb.go new file mode 100644 index 00000000..f39f9be2 --- /dev/null +++ b/pkg/datadogunifi/udb.go @@ -0,0 +1,68 @@ +package datadogunifi + +import "github.com/unpoller/unifi/v5" + +// udbT is used as a name for printed/logged counters. +const udbT = item("UDB") + +// batchUDB generates datapoints for UDB (UniFi Device Bridge) devices. +// UDB-Switch is a hybrid device combining switch ports with WiFi 7 +// wireless bridge capability. +func (u *DatadogUnifi) batchUDB(r report, s *unifi.UDB) { + if !s.Adopted.Val || s.Locating.Val { + return + } + + 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, + }) + + data := CombineFloat64( + u.batchUSWstat(s.Stat.Sw), + u.batchSysStats(s.SysStats, s.SystemStats), + map[string]float64{ + "guest_num_sta": s.GuestNumSta.Val, + "bytes": s.Bytes.Val, + "fan_level": s.FanLevel.Val, + "general_temperature": s.GeneralTemperature.Val, + "last_seen": s.LastSeen.Val, + "rx_bytes": s.RxBytes.Val, + "tx_bytes": s.TxBytes.Val, + "uptime": s.Uptime.Val, + "state": s.State.Val, + "user_num_sta": s.UserNumSta.Val, + "num_sta": s.NumSta.Val, + "upgradeable": boolToFloat64(s.Upgradable.Val), + "guest_wlan_num_sta": s.GuestWlanNumSta.Val, + "user_wlan_num_sta": s.UserWlanNumSta.Val, + "satisfaction": s.Satisfaction.Val, + "total_max_power": s.TotalMaxPower.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, + }) + + r.addCount(udbT) + + metricName := metricNamespace("udb") + + reportGaugeForFloat64Map(r, metricName, data, tags) + + // Port table (reuse USW function) + u.batchPortTable(r, tags, s.PortTable) + + // Radio table (reuse UAP functions) + u.processRadTable(r, tags, s.RadioTable, s.RadioTableStats) + + // VAP table (reuse UAP function) + u.processVAPTable(r, tags, s.VapTable) +} diff --git a/pkg/influxunifi/influxdb.go b/pkg/influxunifi/influxdb.go index f99c1803..38238010 100644 --- a/pkg/influxunifi/influxdb.go +++ b/pkg/influxunifi/influxdb.go @@ -454,6 +454,8 @@ func (u *InfluxUnifi) switchExport(r report, v any) { //nolint:cyclop u.batchUBB(r, v) case *unifi.UCI: u.batchUCI(r, v) + case *unifi.UDB: + u.batchUDB(r, v) case *unifi.UDM: u.batchUDM(r, v) case *unifi.Site: diff --git a/pkg/influxunifi/integration_test_expectations.yaml b/pkg/influxunifi/integration_test_expectations.yaml index 901791fd..b1cf78e5 100644 --- a/pkg/influxunifi/integration_test_expectations.yaml +++ b/pkg/influxunifi/integration_test_expectations.yaml @@ -581,6 +581,64 @@ points: tx_bytes: float uptime: float version: string + udb: + tags: + - mac + - model + - name + - serial + - site_name + - source + - type + - version + fields: + bytes: float + cpu: float + fan_level: float + general_temperature: float + guest-num_sta: float + guest-wlan-num_sta: float + ip: string + last_seen: float + loadavg_1: float + loadavg_5: float + loadavg_15: float + mem: float + mem_buffer: float + mem_total: float + mem_used: float + num_sta: float + rx_bytes: float + satisfaction: float + stat_bytes: float + stat_rx_bytes: float + stat_rx_crypts: float + stat_rx_dropped: float + stat_rx_errors: float + stat_rx_frags: float + stat_rx_packets: float + stat_tx_bytes: float + stat_tx_dropped: float + stat_tx_errors: float + stat_tx_packets: float + stat_tx_retries: float + state: float + system_uptime: float + temp_cpu: float + temp_memory: float + temp_network: float + temp_probe: float + temp_sys: float + total_max_power: float + tx_bytes: float + upgradeable: bool + uplink_latency: float + uplink_max_speed: float + uplink_speed: float + uplink_uptime: float + uptime: float + user-num_sta: float + user-wlan-num_sta: float unifi_alarm: tags: - action diff --git a/pkg/influxunifi/udb.go b/pkg/influxunifi/udb.go new file mode 100644 index 00000000..364c288f --- /dev/null +++ b/pkg/influxunifi/udb.go @@ -0,0 +1,65 @@ +package influxunifi + +import "github.com/unpoller/unifi/v5" + +// udbT is used as a name for printed/logged counters. +const udbT = item("UDB") + +// batchUDB generates datapoints for UDB (UniFi Device Bridge) devices. +// UDB-Switch is a hybrid device combining switch ports with WiFi 7 +// wireless bridge capability. +func (u *InfluxUnifi) batchUDB(r report, s *unifi.UDB) { + if !s.Adopted.Val || s.Locating.Val { + return + } + + 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.batchUSWstat(s.Stat.Sw), + u.batchSysStats(s.SysStats, s.SystemStats), + map[string]any{ + "guest-num_sta": s.GuestNumSta.Val, + "ip": s.IP, + "bytes": s.Bytes.Val, + "fan_level": s.FanLevel.Val, + "general_temperature": s.GeneralTemperature.Val, + "last_seen": s.LastSeen.Val, + "rx_bytes": s.RxBytes.Val, + "tx_bytes": s.TxBytes.Val, + "uptime": s.Uptime.Val, + "state": s.State.Val, + "user-num_sta": s.UserNumSta.Val, + "num_sta": s.NumSta.Val, + "upgradeable": s.Upgradable.Val, + "guest-wlan-num_sta": s.GuestWlanNumSta.Val, + "user-wlan-num_sta": s.UserWlanNumSta.Val, + "satisfaction": s.Satisfaction.Val, + "total_max_power": s.TotalMaxPower.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, + }) + + r.addCount(udbT) + r.send(&metric{Table: "udb", Tags: tags, Fields: fields}) + + // Port table (reuse USW function) + u.batchPortTable(r, tags, s.PortTable) + + // Radio table (reuse UAP functions) + u.processRadTable(r, tags, s.RadioTable, s.RadioTableStats) + + // VAP table (reuse UAP function) + u.processVAPTable(r, tags, s.VapTable) +} diff --git a/pkg/inputunifi/collector.go b/pkg/inputunifi/collector.go index f82441e8..f5f8eb56 100644 --- a/pkg/inputunifi/collector.go +++ b/pkg/inputunifi/collector.go @@ -174,10 +174,10 @@ func (u *InputUnifi) pollController(c *Controller) (*poller.Metrics, error) { return nil, fmt.Errorf("unifi.GetDevices(%s): %w", c.URL, err) } - u.LogDebugf("Found %d UBB, %d UXG, %d PDU, %d UCI, %d UAP %d USG %d USW %d UDM devices", + u.LogDebugf("Found %d UBB, %d UXG, %d PDU, %d UCI, %d UDB, %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.UAPs), len(m.Devices.USGs), + len(m.Devices.UDBs), len(m.Devices.UAPs), len(m.Devices.USGs), len(m.Devices.USWs), len(m.Devices.UDMs)) // Get speed test results for all WANs @@ -486,6 +486,10 @@ func applySiteNameOverride(m *poller.Metrics, overrideName string) { if isDefaultSiteName(d.SiteName) { d.SiteName = overrideName } + case *unifi.UDB: + if isDefaultSiteName(d.SiteName) { + d.SiteName = overrideName + } case *unifi.PDU: if isDefaultSiteName(d.SiteName) { d.SiteName = overrideName @@ -595,6 +599,15 @@ func extractDevices(metrics *Metrics) (*poller.Metrics, map[string]string, map[s m.Devices = append(m.Devices, r) } + for _, r := range metrics.Devices.UDBs { + devices[r.Mac] = r.Name + m.Devices = append(m.Devices, r) + + for _, v := range r.VapTable { + bssdIDs[v.Bssid] = fmt.Sprintf("%s %s %s:", r.Name, v.Radio, v.RadioName) + } + } + for _, r := range metrics.Devices.PDUs { devices[r.Mac] = r.Name m.Devices = append(m.Devices, r) diff --git a/pkg/promunifi/collector.go b/pkg/promunifi/collector.go index efe06a16..8e6a203f 100644 --- a/pkg/promunifi/collector.go +++ b/pkg/promunifi/collector.go @@ -101,6 +101,7 @@ type Report struct { UXG int // Total count of UXG devices. UBB int // Total count of UBB devices. UCI int // Total count of UCI devices. + UDB int // Total count of UDB devices. Metrics *poller.Metrics // Metrics collected and recorded. Elapsed time.Duration // Duration elapsed collecting and exporting. Fetch time.Duration // Duration elapsed making controller requests. @@ -479,6 +480,9 @@ func (u *promUnifi) switchExport(r report, v any) { case *unifi.UCI: r.addUCI() u.exportUCI(r, v) + case *unifi.UDB: + r.addUDB() + u.exportUDB(r, v) case *unifi.UDM: r.addUDM() u.exportUDM(r, v) diff --git a/pkg/promunifi/report.go b/pkg/promunifi/report.go index 52b879a6..fff6f13e 100644 --- a/pkg/promunifi/report.go +++ b/pkg/promunifi/report.go @@ -23,6 +23,7 @@ type report interface { addUXG() addUBB() addUCI() + addUDB() addUSG() addUAP() addUSW() @@ -111,13 +112,17 @@ func (r *Report) addUXG() { } func (r *Report) addUBB() { - r.UCI++ + r.UBB++ } func (r *Report) addUCI() { r.UCI++ } +func (r *Report) addUDB() { + r.UDB++ +} + // close is not part of the interface. func (r *Report) close() { r.wg.Wait() diff --git a/pkg/promunifi/udb.go b/pkg/promunifi/udb.go new file mode 100644 index 00000000..34ee8522 --- /dev/null +++ b/pkg/promunifi/udb.go @@ -0,0 +1,53 @@ +package promunifi + +import "github.com/unpoller/unifi/v5" + +// exportUDB exports metrics for UDB (UniFi Device Bridge) devices. +// The UDB range includes UDB-Switch, UDB-Pro, UDB-Pro-Sector. +// UDB-Switch is a hybrid device combining switch ports (8 PoE ports) +// with WiFi 7 wireless bridge capability (5GHz + 6GHz radios). +func (u *promUnifi) exportUDB(r report, d *unifi.UDB) { + if !d.Adopted.Val || d.Locating.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(baseLabels, tag) + infoLabels := append(baseInfoLabels, tag) + + // Export switch stats (reuse USW functions) + u.exportUSWstats(r, labels, d.Stat.Sw) + u.exportPRTtable(r, labels, d.PortTable) + + // Export wireless stats (reuse UAP functions) + u.exportVAPtable(r, labels, d.VapTable) + u.exportRADtable(r, labels, d.RadioTable, d.RadioTableStats) + + // Common device stats + u.exportBYTstats(r, labels, d.TxBytes, d.RxBytes) + u.exportSYSstats(r, labels, d.SysStats, d.SystemStats) + u.exportSTAcount(r, labels, d.UserNumSta, d.GuestNumSta) + + r.send([]*metric{ + {u.Device.Info, gauge, 1.0, append(baseLabels, infoLabels...)}, + {u.Device.Uptime, gauge, d.Uptime, labels}, + {u.Device.Upgradeable, gauge, d.Upgradable.Val, labels}, + }) + + if d.HasTemperature.Val { + r.send([]*metric{{u.Device.Temperature, gauge, d.GeneralTemperature, append(labels, "general", "board")}}) + } + + if d.HasFan.Val { + r.send([]*metric{{u.Device.FanLevel, gauge, d.FanLevel, labels}}) + } + + if d.TotalMaxPower.Txt != "" { + r.send([]*metric{{u.Device.TotalMaxPower, gauge, d.TotalMaxPower, labels}}) + } + }) +}