commit
0913a89d85
|
|
@ -3,3 +3,5 @@
|
||||||
/*.1.gz
|
/*.1.gz
|
||||||
/*.1
|
/*.1
|
||||||
/vendor
|
/vendor
|
||||||
|
.DS_Store
|
||||||
|
*~
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,104 @@
|
||||||
# Unifi
|
# Unifi
|
||||||
|
|
||||||
Collect your Unifi Controller Client data and send it to an InfluxDB instance. Grafana dashboard included.
|
Collect your Unifi Controller Data and send it to an InfluxDB instance.
|
||||||
|
Grafana dashboards included.
|
||||||

|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
[See the Wiki!](https://github.com/davidnewhall/unifi-poller/wiki/Installation)
|
[See the Wiki!](https://github.com/davidnewhall/unifi-poller/wiki/Installation)
|
||||||
|
|
||||||
|
# Backstory
|
||||||
|
|
||||||
|
Okay, so here's the deal. I found a simple piece of code on github that
|
||||||
|
sorta did what I needed; we all know that story. I wanted more data, so
|
||||||
|
I added more data collection. I believe I've completely rewritten every
|
||||||
|
piece of original code, except the copyright/license file and that's fine
|
||||||
|
by me. I probably wouldn't have made it this far if
|
||||||
|
[Garrett](https://github.com/dewski/unifi) hadn't written the original
|
||||||
|
code I started with. Many props my man.
|
||||||
|
|
||||||
|
The original code pulled only the client data. This app now pulls data
|
||||||
|
for clients, access points, security gateways and switches. I currently
|
||||||
|
own two UAP-AC-PROs, one USG-3 and one US-24-250W. If your devices differ
|
||||||
|
this app may miss some data. I'm willing to help and make it better.
|
||||||
|
Open an [Issue](https://github.com/davidnewhall/unifi-poller/issues) and
|
||||||
|
we'll figure out how to get things working for you.
|
||||||
|
|
||||||
|
# What's this data good for?
|
||||||
|
|
||||||
|
I've been trying to get my UAP data into Grafana. Sure, google search that.
|
||||||
|
You'll find [this](https://community.ubnt.com/t5/UniFi-Wireless/Grafana-dashboard-for-UniFi-APs-now-available/td-p/1833532).
|
||||||
|
And that's all you'll find. What if you don't want to deal with SNMP?
|
||||||
|
Well, here you go. I've replicated 90% of what you see on those SNMP-powered
|
||||||
|
dashboards with this Go app running on the same mac as my Unifi controller.
|
||||||
|
All without enabling SNMP nor trying to understand those OIDs. Mad props
|
||||||
|
to [waterside](https://community.ubnt.com/t5/user/viewprofilepage/user-id/303058)
|
||||||
|
for making this dashboard; it gave me a fantastic start to making my own.
|
||||||
|
|
||||||
|
# What now...
|
||||||
|
|
||||||
|
- I probably suck at InfluxDB.
|
||||||
|
|
||||||
|
I don't know what should be a tag and what should be a field. I think
|
||||||
|
I did my best, but there's certainly room for improvements in both
|
||||||
|
the data input and the Grafana graphs (output).
|
||||||
|
|
||||||
|
|
||||||
|
- The USW and USG code needs love.
|
||||||
|
|
||||||
|
Up to this point, my focus has been on UAP. I have only included dashboards
|
||||||
|
that focus on UAP. I am still working on the other two, but it may be a while
|
||||||
|
before I get around to publishing them. Help is appreciated.
|
||||||
|
|
||||||
|
|
||||||
|
- Are there other devices that need to be included?
|
||||||
|
|
||||||
|
I have: switch, router, access point. Three total, and the type structs are
|
||||||
|
likely missing data for variants of these devices. e.g. Some UAPs have more
|
||||||
|
radios, I probably didn't properly account for that. Some gateways have more
|
||||||
|
ports, some switches have 10Gb, etc. These are things I do not have data on
|
||||||
|
to write code for. If you have these devices, and want them graphed, open an
|
||||||
|
Issue and lets discuss.
|
||||||
|
|
||||||
|
|
||||||
|
- Better Installation instructions.
|
||||||
|
|
||||||
|
If you're a nerd you can probably figure it out. I'd still like some pretty
|
||||||
|
pictures and maybe even a Twitch VOD.
|
||||||
|
|
||||||
|
|
||||||
|
- Sanity Checking
|
||||||
|
|
||||||
|
Did I actually graph the right data in the right way? Some validation would
|
||||||
|
be nice.
|
||||||
|
|
||||||
|
|
||||||
|
- Radios, Frequencies, Interfaces, vAPs
|
||||||
|
|
||||||
|
My access points only seem to have two radios, one interface and vAP per radio.
|
||||||
|
I'm not sure if the graphs, as-is, provide enough insight into APs with other
|
||||||
|
configurations. Help me figure that out?
|
||||||
|
|
||||||
|
# What's it look like?
|
||||||
|
|
||||||
|
Here's a picture of the Client dashboard.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Here's a picture of the UAP dashboard. This only shows one device, but you can
|
||||||
|
select multiple to put specific stats side-by-side.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
# Woah, there's a library in here.
|
||||||
|
|
||||||
|
Sure is. If you want to write your own code around unifi data, you can import
|
||||||
|
the `unidev` library and easily poll your controller. If you have interest in
|
||||||
|
how to use the library, or need more features, open an Issue. I'm not committed
|
||||||
|
to documenting it unless it seems useful.
|
||||||
|
|
||||||
|
|
||||||
## Copyright & License
|
## Copyright & License
|
||||||
Copyright © 2016 Garrett Bjerkhoel. See [MIT-LICENSE](MIT-LICENSE) for details.
|
- Copyright © 2016 Garrett Bjerkhoel.
|
||||||
|
- Copyright © 2018 David Newhall II.
|
||||||
|
- See [MIT-LICENSE](MIT-LICENSE) for license information.
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import "time"
|
||||||
|
|
||||||
// Version will be injected at build time.
|
// Version will be injected at build time.
|
||||||
var (
|
var (
|
||||||
Version = "v0.1"
|
Version = "v0.2"
|
||||||
Debug = false
|
Debug = false
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -83,14 +83,14 @@ func GetConfig(configFile string) (Config, error) {
|
||||||
func (c *Config) PollUnifiController(infdb influx.Client, unifi *unidev.AuthedReq) {
|
func (c *Config) PollUnifiController(infdb influx.Client, unifi *unidev.AuthedReq) {
|
||||||
ticker := time.NewTicker(c.Interval.value)
|
ticker := time.NewTicker(c.Interval.value)
|
||||||
for range ticker.C {
|
for range ticker.C {
|
||||||
clients, err := unifi.GetUnifiClients()
|
clients, err := unifi.GetUnifiClientAssets()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("unifi.GetUnifiClients():", err)
|
log.Println("unifi.GetUnifiClientsAssets():", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
devices, err := unifi.GetUnifiDevices()
|
devices, err := unifi.GetUnifiDeviceAssets()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("unifi.GetUnifiDevices():", err)
|
log.Println("unifi.GetUnifiDeviceAssets():", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
bp, err := influx.NewBatchPoints(influx.BatchPointsConfig{
|
bp, err := influx.NewBatchPoints(influx.BatchPointsConfig{
|
||||||
|
|
@ -102,10 +102,10 @@ func (c *Config) PollUnifiController(infdb influx.Client, unifi *unidev.AuthedRe
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, asset := range append(clients, devices...) {
|
for _, asset := range append(clients, devices...) {
|
||||||
if pt, errr := asset.Point(); errr != nil {
|
if pt, errr := asset.Points(); errr != nil {
|
||||||
log.Println("asset.Point():", errr)
|
log.Println("asset.Points():", errr)
|
||||||
} else {
|
} else {
|
||||||
bp.AddPoint(pt)
|
bp.AddPoints(pt)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
# Grafana Dashboards
|
||||||
|
|
||||||
|
Import these into Grafana to quickly visualize data from your devices.
|
||||||
|
|
||||||
|
They may/do use a few plugins: Clock, Discrete, Singlestat, Table
|
||||||
File diff suppressed because it is too large
Load Diff
|
Before Width: | Height: | Size: 246 KiB After Width: | Height: | Size: 246 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 141 KiB |
|
|
@ -7,8 +7,9 @@ import (
|
||||||
influx "github.com/influxdata/influxdb/client/v2"
|
influx "github.com/influxdata/influxdb/client/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Point generates a client's datapoint for InfluxDB.
|
// Points generates a client's datapoints for InfluxDB.
|
||||||
func (u UCL) Point() (*influx.Point, error) {
|
func (u UCL) Points() ([]*influx.Point, error) {
|
||||||
|
var points []*influx.Point
|
||||||
if u.Name == "" && u.Hostname != "" {
|
if u.Name == "" && u.Hostname != "" {
|
||||||
u.Name = u.Hostname
|
u.Name = u.Hostname
|
||||||
} else if u.Hostname == "" && u.Name != "" {
|
} else if u.Hostname == "" && u.Name != "" {
|
||||||
|
|
@ -95,6 +96,9 @@ func (u UCL) Point() (*influx.Point, error) {
|
||||||
"wired-tx_bytes-r": u.WiredTxBytesR,
|
"wired-tx_bytes-r": u.WiredTxBytesR,
|
||||||
"wired-tx_packets": u.WiredTxPackets,
|
"wired-tx_packets": u.WiredTxPackets,
|
||||||
}
|
}
|
||||||
|
pt, err := influx.NewPoint("clients", tags, fields, time.Now())
|
||||||
return influx.NewPoint("clients", tags, fields, time.Now())
|
if err == nil {
|
||||||
|
points = append(points, pt)
|
||||||
|
}
|
||||||
|
return points, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,12 @@ import (
|
||||||
influx "github.com/influxdata/influxdb/client/v2"
|
influx "github.com/influxdata/influxdb/client/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Point generates a device's datapoint for InfluxDB.
|
// Points generates a device's datapoints for InfluxDB.
|
||||||
func (u UAP) Point() (*influx.Point, error) {
|
func (u UAP) Points() ([]*influx.Point, error) {
|
||||||
|
/* I generally suck at InfluxDB, so if I got the tags/fields wrong,
|
||||||
|
please send me a PR or open an Issue to address my faults. Thanks!
|
||||||
|
*/
|
||||||
|
var points []*influx.Point
|
||||||
tags := map[string]string{
|
tags := map[string]string{
|
||||||
"id": u.ID,
|
"id": u.ID,
|
||||||
"mac": u.Mac,
|
"mac": u.Mac,
|
||||||
|
|
@ -35,7 +39,7 @@ func (u UAP) Point() (*influx.Point, error) {
|
||||||
"has_speaker": strconv.FormatBool(u.HasSpeaker),
|
"has_speaker": strconv.FormatBool(u.HasSpeaker),
|
||||||
"inform_ip": u.InformIP,
|
"inform_ip": u.InformIP,
|
||||||
"isolated": strconv.FormatBool(u.Isolated),
|
"isolated": strconv.FormatBool(u.Isolated),
|
||||||
"last_seen": strconv.Itoa(u.LastSeen),
|
"last_seen": strconv.FormatFloat(u.LastSeen, 'f', 6, 64),
|
||||||
"last_uplink_mac": u.LastUplink.UplinkMac,
|
"last_uplink_mac": u.LastUplink.UplinkMac,
|
||||||
"last_uplink_remote_port": strconv.Itoa(u.LastUplink.UplinkRemotePort),
|
"last_uplink_remote_port": strconv.Itoa(u.LastUplink.UplinkRemotePort),
|
||||||
"known_cfgversion": u.KnownCfgversion,
|
"known_cfgversion": u.KnownCfgversion,
|
||||||
|
|
@ -74,6 +78,7 @@ func (u UAP) Point() (*influx.Point, error) {
|
||||||
"loadavg_15": u.SysStats.Loadavg15,
|
"loadavg_15": u.SysStats.Loadavg15,
|
||||||
"mem_buffer": u.SysStats.MemBuffer,
|
"mem_buffer": u.SysStats.MemBuffer,
|
||||||
"mem_total": u.SysStats.MemTotal,
|
"mem_total": u.SysStats.MemTotal,
|
||||||
|
"mem_used": u.SysStats.MemUsed,
|
||||||
"cpu": u.SystemStats.CPU,
|
"cpu": u.SystemStats.CPU,
|
||||||
"mem": u.SystemStats.Mem,
|
"mem": u.SystemStats.Mem,
|
||||||
"system_uptime": u.SystemStats.Uptime,
|
"system_uptime": u.SystemStats.Uptime,
|
||||||
|
|
@ -165,5 +170,98 @@ func (u UAP) Point() (*influx.Point, error) {
|
||||||
"stat_wifi1-tx_packets": u.Stat.Wifi1TxPackets,
|
"stat_wifi1-tx_packets": u.Stat.Wifi1TxPackets,
|
||||||
"stat_wifi1-tx_retries": u.Stat.Wifi1TxRetries,
|
"stat_wifi1-tx_retries": u.Stat.Wifi1TxRetries,
|
||||||
}
|
}
|
||||||
return influx.NewPoint("uap", tags, fields, time.Now())
|
pt, err := influx.NewPoint("uap", tags, fields, time.Now())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
points = append(points, pt)
|
||||||
|
for _, p := range u.RadioTable {
|
||||||
|
tags := map[string]string{
|
||||||
|
"device_name": u.Name,
|
||||||
|
"device_id": u.ID,
|
||||||
|
"device_mac": u.Mac,
|
||||||
|
"name": p.Name,
|
||||||
|
"wlangroup_id": p.WlangroupID,
|
||||||
|
"channel": p.Channel, // not the channel #
|
||||||
|
"radio": p.Radio,
|
||||||
|
}
|
||||||
|
fields := map[string]interface{}{
|
||||||
|
"builtin_ant_gain": p.BuiltinAntGain,
|
||||||
|
"current_antenna_gain": p.CurrentAntennaGain,
|
||||||
|
"has_dfs": p.HasDfs,
|
||||||
|
"has_fccdfs": p.HasFccdfs,
|
||||||
|
"ht": p.Ht,
|
||||||
|
"is_11ac": p.Is11Ac,
|
||||||
|
"max_txpower": p.MaxTxpower,
|
||||||
|
"min_rssi_enabled": p.MinRssiEnabled,
|
||||||
|
"min_txpower": p.MinTxpower,
|
||||||
|
"nss": p.Nss,
|
||||||
|
"radio_caps": p.RadioCaps,
|
||||||
|
"tx_power": p.TxPower,
|
||||||
|
"tx_power_mode": p.TxPowerMode,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range u.RadioTableStats {
|
||||||
|
// This may be a tad slower but it allows putting
|
||||||
|
// all the radio stats into one table.
|
||||||
|
if p.Name == s.Name {
|
||||||
|
fields["ast_be_xmit"] = s.AstBeXmit
|
||||||
|
fields["ast_cst"] = s.AstCst
|
||||||
|
fields["channel"] = s.Channel
|
||||||
|
fields["ast_txto"] = s.AstTxto
|
||||||
|
fields["cu_self_rx"] = s.CuSelfRx
|
||||||
|
fields["cu_self_tx"] = s.CuSelfTx
|
||||||
|
fields["cu_total"] = s.CuTotal
|
||||||
|
fields["extchannel"] = s.Extchannel
|
||||||
|
fields["gain"] = s.Gain
|
||||||
|
fields["guest-num_sta"] = s.GuestNumSta
|
||||||
|
fields["num_sta"] = s.NumSta
|
||||||
|
fields["radio"] = s.Radio
|
||||||
|
fields["state"] = s.State
|
||||||
|
fields["radio_tx_packets"] = s.TxPackets
|
||||||
|
fields["radio_tx_power"] = s.TxPower
|
||||||
|
fields["radio_tx_retries"] = s.TxRetries
|
||||||
|
fields["user-num_sta"] = s.UserNumSta
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, s := range u.VapTable {
|
||||||
|
if p.Name == s.RadioName {
|
||||||
|
tags["ap_mac"] = s.ApMac
|
||||||
|
tags["bssid"] = s.Bssid
|
||||||
|
fields["ccq"] = s.Ccq
|
||||||
|
fields["essid"] = s.Essid
|
||||||
|
fields["extchannel"] = s.Extchannel
|
||||||
|
tags["vap_id"] = s.ID
|
||||||
|
fields["is_guest"] = s.IsGuest
|
||||||
|
fields["is_wep"] = s.IsWep
|
||||||
|
fields["mac_filter_rejections"] = s.MacFilterRejections
|
||||||
|
fields["map_id"] = s.MapID
|
||||||
|
tags["vap_name"] = s.Name
|
||||||
|
fields["rx_bytes"] = s.RxBytes
|
||||||
|
fields["rx_crypts"] = s.RxCrypts
|
||||||
|
fields["rx_dropped"] = s.RxDropped
|
||||||
|
fields["rx_errors"] = s.RxErrors
|
||||||
|
fields["rx_frags"] = s.RxFrags
|
||||||
|
fields["rx_nwids"] = s.RxNwids
|
||||||
|
fields["rx_packets"] = s.RxPackets
|
||||||
|
fields["tx_bytes"] = s.TxBytes
|
||||||
|
fields["tx_dropped"] = s.TxDropped
|
||||||
|
fields["tx_errors"] = s.TxErrors
|
||||||
|
fields["tx_latency_avg"] = s.TxLatencyAvg
|
||||||
|
fields["tx_latency_max"] = s.TxLatencyMax
|
||||||
|
fields["tx_latency_min"] = s.TxLatencyMin
|
||||||
|
fields["tx_packets"] = s.TxPackets
|
||||||
|
fields["tx_power"] = s.TxPower
|
||||||
|
fields["tx_retries"] = s.TxRetries
|
||||||
|
fields["usage"] = s.Usage
|
||||||
|
tags["wlanconf_id"] = s.WlanconfID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pt, err := influx.NewPoint("uap_radios", tags, fields, time.Now())
|
||||||
|
if err != nil {
|
||||||
|
return points, err
|
||||||
|
}
|
||||||
|
points = append(points, pt)
|
||||||
|
}
|
||||||
|
return points, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,12 @@
|
||||||
package unidev
|
package unidev
|
||||||
|
|
||||||
// UAP is a Unifi Access Point
|
// UAP is a Unifi Access Point.
|
||||||
type UAP struct {
|
type UAP struct {
|
||||||
|
/* This was auto generated and then slowly edited by hand
|
||||||
|
to get all the data types right and graphable.
|
||||||
|
No ones feelings will be hurt if you want to break this
|
||||||
|
up into multiple structs, and/or make it better in general.
|
||||||
|
*/
|
||||||
ID string `json:"_id"`
|
ID string `json:"_id"`
|
||||||
UUptime float64 `json:"_uptime"`
|
UUptime float64 `json:"_uptime"`
|
||||||
AdoptIP string `json:"adopt_ip,omitempty"`
|
AdoptIP string `json:"adopt_ip,omitempty"`
|
||||||
|
|
@ -36,17 +41,17 @@ type UAP struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
NumPort float64 `json:"num_port"`
|
NumPort float64 `json:"num_port"`
|
||||||
} `json:"ethernet_table"`
|
} `json:"ethernet_table"`
|
||||||
FwCaps int `json:"fw_caps"`
|
FwCaps int `json:"fw_caps"`
|
||||||
GuestNumSta int `json:"guest-num_sta"`
|
GuestNumSta int `json:"guest-num_sta"`
|
||||||
GuestToken string `json:"guest_token"`
|
GuestToken string `json:"guest_token"`
|
||||||
HasEth1 bool `json:"has_eth1"`
|
HasEth1 bool `json:"has_eth1"`
|
||||||
HasSpeaker bool `json:"has_speaker"`
|
HasSpeaker bool `json:"has_speaker"`
|
||||||
InformIP string `json:"inform_ip"`
|
InformIP string `json:"inform_ip"`
|
||||||
InformURL string `json:"inform_url"`
|
InformURL string `json:"inform_url"`
|
||||||
IP string `json:"ip"`
|
IP string `json:"ip"`
|
||||||
Isolated bool `json:"isolated"`
|
Isolated bool `json:"isolated"`
|
||||||
KnownCfgversion string `json:"known_cfgversion"`
|
KnownCfgversion string `json:"known_cfgversion"`
|
||||||
LastSeen int `json:"last_seen"`
|
LastSeen float64 `json:"last_seen"`
|
||||||
LastUplink struct {
|
LastUplink struct {
|
||||||
UplinkMac string `json:"uplink_mac"`
|
UplinkMac string `json:"uplink_mac"`
|
||||||
UplinkRemotePort int `json:"uplink_remote_port"`
|
UplinkRemotePort int `json:"uplink_remote_port"`
|
||||||
|
|
@ -132,28 +137,28 @@ type UAP struct {
|
||||||
Is11Ac bool `json:"is_11ac,omitempty"`
|
Is11Ac bool `json:"is_11ac,omitempty"`
|
||||||
} `json:"radio_table"`
|
} `json:"radio_table"`
|
||||||
RadioTableStats []struct {
|
RadioTableStats []struct {
|
||||||
AstBeXmit interface{} `json:"ast_be_xmit"`
|
AstBeXmit float64 `json:"ast_be_xmit"`
|
||||||
AstCst interface{} `json:"ast_cst"`
|
AstCst float64 `json:"ast_cst"`
|
||||||
AstTxto interface{} `json:"ast_txto"`
|
AstTxto float64 `json:"ast_txto"`
|
||||||
Channel float64 `json:"channel"`
|
Channel float64 `json:"channel"`
|
||||||
CuSelfRx float64 `json:"cu_self_rx"`
|
CuSelfRx float64 `json:"cu_self_rx"`
|
||||||
CuSelfTx float64 `json:"cu_self_tx"`
|
CuSelfTx float64 `json:"cu_self_tx"`
|
||||||
CuTotal float64 `json:"cu_total"`
|
CuTotal float64 `json:"cu_total"`
|
||||||
Extchannel float64 `json:"extchannel"`
|
Extchannel float64 `json:"extchannel"`
|
||||||
Gain float64 `json:"gain"`
|
Gain float64 `json:"gain"`
|
||||||
GuestNumSta float64 `json:"guest-num_sta"`
|
GuestNumSta float64 `json:"guest-num_sta"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
NumSta float64 `json:"num_sta"`
|
NumSta float64 `json:"num_sta"`
|
||||||
Radio string `json:"radio"`
|
Radio string `json:"radio"`
|
||||||
State string `json:"state"`
|
State string `json:"state"`
|
||||||
TxPackets float64 `json:"tx_packets"`
|
TxPackets float64 `json:"tx_packets"`
|
||||||
TxPower float64 `json:"tx_power"`
|
TxPower float64 `json:"tx_power"`
|
||||||
TxRetries float64 `json:"tx_retries"`
|
TxRetries float64 `json:"tx_retries"`
|
||||||
UserNumSta float64 `json:"user-num_sta"`
|
UserNumSta float64 `json:"user-num_sta"`
|
||||||
} `json:"radio_table_stats"`
|
} `json:"radio_table_stats"`
|
||||||
Rollupgrade bool `json:"rollupgrade"`
|
Rollupgrade bool `json:"rollupgrade"`
|
||||||
RxBytes int `json:"rx_bytes"`
|
RxBytes float64 `json:"rx_bytes"`
|
||||||
RxBytesD int `json:"rx_bytes-d"`
|
RxBytesD float64 `json:"rx_bytes-d"`
|
||||||
ScanRadioTable []interface{} `json:"scan_radio_table"`
|
ScanRadioTable []interface{} `json:"scan_radio_table"`
|
||||||
Scanning bool `json:"scanning"`
|
Scanning bool `json:"scanning"`
|
||||||
Serial string `json:"serial"`
|
Serial string `json:"serial"`
|
||||||
|
|
@ -257,20 +262,20 @@ type UAP struct {
|
||||||
} `json:"stat"`
|
} `json:"stat"`
|
||||||
State int `json:"state"`
|
State int `json:"state"`
|
||||||
SysStats struct {
|
SysStats struct {
|
||||||
Loadavg1 string `json:"loadavg_1"`
|
Loadavg1 float64 `json:"loadavg_1,string"`
|
||||||
Loadavg15 string `json:"loadavg_15"`
|
Loadavg15 float64 `json:"loadavg_15,string"`
|
||||||
Loadavg5 string `json:"loadavg_5"`
|
Loadavg5 float64 `json:"loadavg_5,string"`
|
||||||
MemBuffer int `json:"mem_buffer"`
|
MemBuffer float64 `json:"mem_buffer"`
|
||||||
MemTotal int `json:"mem_total"`
|
MemTotal float64 `json:"mem_total"`
|
||||||
MemUsed int `json:"mem_used"`
|
MemUsed float64 `json:"mem_used"`
|
||||||
} `json:"sys_stats"`
|
} `json:"sys_stats"`
|
||||||
SystemStats struct {
|
SystemStats struct {
|
||||||
CPU string `json:"cpu"`
|
CPU float64 `json:"cpu,string"`
|
||||||
Mem string `json:"mem"`
|
Mem float64 `json:"mem,string"`
|
||||||
Uptime string `json:"uptime"`
|
Uptime float64 `json:"uptime,string"`
|
||||||
} `json:"system-stats"`
|
} `json:"system-stats"`
|
||||||
TxBytes float64 `json:"tx_bytes"`
|
TxBytes float64 `json:"tx_bytes"`
|
||||||
TxBytesD int `json:"tx_bytes-d"`
|
TxBytesD float64 `json:"tx_bytes-d"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Upgradable bool `json:"upgradable"`
|
Upgradable bool `json:"upgradable"`
|
||||||
Uplink struct {
|
Uplink struct {
|
||||||
|
|
@ -284,63 +289,63 @@ type UAP struct {
|
||||||
Netmask string `json:"netmask"`
|
Netmask string `json:"netmask"`
|
||||||
NumPort int `json:"num_port"`
|
NumPort int `json:"num_port"`
|
||||||
RxBytes float64 `json:"rx_bytes"`
|
RxBytes float64 `json:"rx_bytes"`
|
||||||
RxBytesR int `json:"rx_bytes-r"`
|
RxBytesR float64 `json:"rx_bytes-r"`
|
||||||
RxDropped int `json:"rx_dropped"`
|
RxDropped float64 `json:"rx_dropped"`
|
||||||
RxErrors int `json:"rx_errors"`
|
RxErrors float64 `json:"rx_errors"`
|
||||||
RxMulticast int `json:"rx_multicast"`
|
RxMulticast float64 `json:"rx_multicast"`
|
||||||
RxPackets int `json:"rx_packets"`
|
RxPackets float64 `json:"rx_packets"`
|
||||||
Speed int `json:"speed"`
|
Speed float64 `json:"speed"`
|
||||||
TxBytes float64 `json:"tx_bytes"`
|
TxBytes float64 `json:"tx_bytes"`
|
||||||
TxBytesR int `json:"tx_bytes-r"`
|
TxBytesR float64 `json:"tx_bytes-r"`
|
||||||
TxDropped int `json:"tx_dropped"`
|
TxDropped float64 `json:"tx_dropped"`
|
||||||
TxErrors int `json:"tx_errors"`
|
TxErrors float64 `json:"tx_errors"`
|
||||||
TxPackets int `json:"tx_packets"`
|
TxPackets float64 `json:"tx_packets"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Up bool `json:"up"`
|
Up bool `json:"up"`
|
||||||
UplinkMac string `json:"uplink_mac"`
|
UplinkMac string `json:"uplink_mac"`
|
||||||
UplinkRemotePort int `json:"uplink_remote_port"`
|
UplinkRemotePort int `json:"uplink_remote_port"`
|
||||||
} `json:"uplink"`
|
} `json:"uplink"`
|
||||||
UplinkTable []interface{} `json:"uplink_table"`
|
UplinkTable []interface{} `json:"uplink_table"`
|
||||||
Uptime int `json:"uptime"`
|
Uptime float64 `json:"uptime"`
|
||||||
UserNumSta int `json:"user-num_sta"`
|
UserNumSta int `json:"user-num_sta"`
|
||||||
VapTable []struct {
|
VapTable []struct {
|
||||||
ApMac string `json:"ap_mac"`
|
ApMac string `json:"ap_mac"`
|
||||||
Bssid string `json:"bssid"`
|
Bssid string `json:"bssid"`
|
||||||
Ccq int `json:"ccq"`
|
Ccq int `json:"ccq"`
|
||||||
Channel int `json:"channel"`
|
Channel int `json:"channel"`
|
||||||
Essid string `json:"essid"`
|
Essid string `json:"essid"`
|
||||||
Extchannel int `json:"extchannel"`
|
Extchannel int `json:"extchannel"`
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
IsGuest bool `json:"is_guest"`
|
IsGuest bool `json:"is_guest"`
|
||||||
IsWep bool `json:"is_wep"`
|
IsWep bool `json:"is_wep"`
|
||||||
MacFilterRejections int `json:"mac_filter_rejections"`
|
MacFilterRejections int `json:"mac_filter_rejections"`
|
||||||
MapID interface{} `json:"map_id"`
|
MapID string `json:"map_id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
NumSta int `json:"num_sta"`
|
NumSta int `json:"num_sta"`
|
||||||
Radio string `json:"radio"`
|
Radio string `json:"radio"`
|
||||||
RadioName string `json:"radio_name"`
|
RadioName string `json:"radio_name"`
|
||||||
RxBytes int `json:"rx_bytes"`
|
RxBytes float64 `json:"rx_bytes"`
|
||||||
RxCrypts int `json:"rx_crypts"`
|
RxCrypts float64 `json:"rx_crypts"`
|
||||||
RxDropped int `json:"rx_dropped"`
|
RxDropped float64 `json:"rx_dropped"`
|
||||||
RxErrors int `json:"rx_errors"`
|
RxErrors float64 `json:"rx_errors"`
|
||||||
RxFrags int `json:"rx_frags"`
|
RxFrags float64 `json:"rx_frags"`
|
||||||
RxNwids int `json:"rx_nwids"`
|
RxNwids float64 `json:"rx_nwids"`
|
||||||
RxPackets int `json:"rx_packets"`
|
RxPackets float64 `json:"rx_packets"`
|
||||||
SiteID string `json:"site_id"`
|
SiteID string `json:"site_id"`
|
||||||
State string `json:"state"`
|
State string `json:"state"`
|
||||||
T string `json:"t"`
|
T string `json:"t"`
|
||||||
TxBytes int `json:"tx_bytes"`
|
TxBytes float64 `json:"tx_bytes"`
|
||||||
TxDropped int `json:"tx_dropped"`
|
TxDropped float64 `json:"tx_dropped"`
|
||||||
TxErrors int `json:"tx_errors"`
|
TxErrors float64 `json:"tx_errors"`
|
||||||
TxLatencyAvg float64 `json:"tx_latency_avg"`
|
TxLatencyAvg float64 `json:"tx_latency_avg"`
|
||||||
TxLatencyMax float64 `json:"tx_latency_max"`
|
TxLatencyMax float64 `json:"tx_latency_max"`
|
||||||
TxLatencyMin float64 `json:"tx_latency_min"`
|
TxLatencyMin float64 `json:"tx_latency_min"`
|
||||||
TxPackets int `json:"tx_packets"`
|
TxPackets float64 `json:"tx_packets"`
|
||||||
TxPower int `json:"tx_power"`
|
TxPower int `json:"tx_power"`
|
||||||
TxRetries int `json:"tx_retries"`
|
TxRetries int `json:"tx_retries"`
|
||||||
Up bool `json:"up"`
|
Up bool `json:"up"`
|
||||||
Usage string `json:"usage"`
|
Usage string `json:"usage"`
|
||||||
WlanconfID string `json:"wlanconf_id"`
|
WlanconfID string `json:"wlanconf_id"`
|
||||||
} `json:"vap_table"`
|
} `json:"vap_table"`
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
VersionIncompatible bool `json:"version_incompatible"`
|
VersionIncompatible bool `json:"version_incompatible"`
|
||||||
|
|
|
||||||
|
|
@ -13,9 +13,12 @@ import (
|
||||||
const LoginPath = "/api/login"
|
const LoginPath = "/api/login"
|
||||||
|
|
||||||
// Asset provides a common interface to retreive metrics from a device or client.
|
// Asset provides a common interface to retreive metrics from a device or client.
|
||||||
|
// It currently only supports InfluxDB, but could be amended to support other
|
||||||
|
// libraries that have a similar interface.
|
||||||
|
// This app only uses the .AddPoint/s() methods with the Asset type.
|
||||||
type Asset interface {
|
type Asset interface {
|
||||||
// Point() means this is useful to influxdb..
|
// Point() means this is useful to influxdb..
|
||||||
Point() (*influx.Point, error)
|
Points() ([]*influx.Point, error)
|
||||||
// Add more methods to achieve more usefulness from this library.
|
// Add more methods to achieve more usefulness from this library.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetUnifiClients returns a response full of clients' data from the Unifi Controller.
|
// GetUnifiClients returns a response full of clients' data from the Unifi Controller.
|
||||||
func (c *AuthedReq) GetUnifiClients() ([]Asset, error) {
|
func (c *AuthedReq) GetUnifiClients() ([]UCL, error) {
|
||||||
var response struct {
|
var response struct {
|
||||||
Clients []UCL `json:"data"`
|
Clients []UCL `json:"data"`
|
||||||
Meta struct {
|
Meta struct {
|
||||||
|
|
@ -34,68 +34,103 @@ func (c *AuthedReq) GetUnifiClients() ([]Asset, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if body, err := ioutil.ReadAll(resp.Body); err != nil {
|
} else if body, err := ioutil.ReadAll(resp.Body); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if err = json.Unmarshal(body, response); err != nil {
|
} else if err = json.Unmarshal(body, &response); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if err = resp.Body.Close(); err != nil {
|
} else if err = resp.Body.Close(); err != nil {
|
||||||
log.Println("resp.Body.Close():", err) // Not fatal? Just log it.
|
log.Println("resp.Body.Close():", err) // Not fatal? Just log it.
|
||||||
}
|
}
|
||||||
clients := []Asset{}
|
return response.Clients, nil
|
||||||
for _, r := range response.Clients {
|
}
|
||||||
clients = append(clients, r)
|
|
||||||
|
// GetUnifiClientAssets provides an interface to return common asset types.
|
||||||
|
func (c *AuthedReq) GetUnifiClientAssets() ([]Asset, error) {
|
||||||
|
clients, err := c.GetUnifiClients()
|
||||||
|
assets := []Asset{}
|
||||||
|
if err == nil {
|
||||||
|
for _, r := range clients {
|
||||||
|
assets = append(assets, r)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return clients, nil
|
return assets, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUnifiDevices returns a response full of devices' data from the Unifi Controller.
|
// GetUnifiDevices returns a response full of devices' data from the Unifi Controller.
|
||||||
func (c *AuthedReq) GetUnifiDevices() ([]Asset, error) {
|
func (c *AuthedReq) GetUnifiDevices() ([]USG, []USW, []UAP, error) {
|
||||||
var parsed struct {
|
var parsed struct {
|
||||||
Data []json.RawMessage `json:"data"`
|
Data []json.RawMessage `json:"data"`
|
||||||
Meta struct {
|
Meta struct {
|
||||||
Rc string `json:"rc"`
|
Rc string `json:"rc"`
|
||||||
} `json:"meta"`
|
} `json:"meta"`
|
||||||
}
|
}
|
||||||
assets := []Asset{}
|
|
||||||
if req, err := c.UniReq(DevicePath, ""); err != nil {
|
if req, err := c.UniReq(DevicePath, ""); err != nil {
|
||||||
return nil, err
|
return nil, nil, nil, err
|
||||||
} else if resp, err := c.Do(req); err != nil {
|
} else if resp, err := c.Do(req); err != nil {
|
||||||
return nil, err
|
return nil, nil, nil, err
|
||||||
} else if body, err := ioutil.ReadAll(resp.Body); err != nil {
|
} else if body, err := ioutil.ReadAll(resp.Body); err != nil {
|
||||||
return nil, err
|
return nil, nil, nil, err
|
||||||
} else if err = json.Unmarshal(body, &parsed); err != nil {
|
} else if err = json.Unmarshal(body, &parsed); err != nil {
|
||||||
return nil, err
|
return nil, nil, nil, err
|
||||||
} else if err = resp.Body.Close(); err != nil {
|
} else if err = resp.Body.Close(); err != nil {
|
||||||
log.Println("resp.Body.Close():", err) // Not fatal? Just log it.
|
log.Println("resp.Body.Close():", err) // Not fatal? Just log it.
|
||||||
}
|
}
|
||||||
|
var usgs []USG
|
||||||
|
var usws []USW
|
||||||
|
var uaps []UAP
|
||||||
for _, r := range parsed.Data {
|
for _, r := range parsed.Data {
|
||||||
|
var usg USG
|
||||||
|
var usw USW
|
||||||
|
var uap UAP
|
||||||
// Unamrshal into a map and check "type"
|
// Unamrshal into a map and check "type"
|
||||||
var obj map[string]interface{}
|
var obj map[string]interface{}
|
||||||
if err := json.Unmarshal(r, &obj); err != nil {
|
if err := json.Unmarshal(r, &obj); err != nil {
|
||||||
return nil, err
|
return nil, nil, nil, err
|
||||||
}
|
}
|
||||||
assetType := ""
|
assetType := "- missing -"
|
||||||
if t, ok := obj["type"].(string); ok {
|
if t, ok := obj["type"].(string); ok {
|
||||||
assetType = t
|
assetType = t
|
||||||
}
|
}
|
||||||
|
if Debug {
|
||||||
|
log.Println("Unmarshalling Device Type:", assetType)
|
||||||
|
}
|
||||||
// Unmarshal again into the correct type..
|
// Unmarshal again into the correct type..
|
||||||
var asset Asset
|
|
||||||
switch assetType {
|
switch assetType {
|
||||||
case "uap":
|
case "uap":
|
||||||
asset = &UAP{}
|
if err := json.Unmarshal(r, &uap); err != nil {
|
||||||
case "ugw":
|
return nil, nil, nil, err
|
||||||
asset = &USG{}
|
}
|
||||||
|
uaps = append(uaps, uap)
|
||||||
|
case "ugw", "usg": // in case they ever fix the name in the api.
|
||||||
|
if err := json.Unmarshal(r, &usg); err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
usgs = append(usgs, usg)
|
||||||
case "usw":
|
case "usw":
|
||||||
asset = &USW{}
|
if err := json.Unmarshal(r, &usw); err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
usws = append(usws, usw)
|
||||||
default:
|
default:
|
||||||
log.Println("unknown asset type -", assetType, "- skipping")
|
log.Println("unknown asset type -", assetType, "- skipping")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if Debug {
|
|
||||||
log.Println("Unmarshalling", assetType)
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(r, asset); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
assets = append(assets, asset)
|
|
||||||
}
|
}
|
||||||
return assets, nil
|
return usgs, usws, uaps, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUnifiDeviceAssets provides an interface to return common asset types.
|
||||||
|
func (c *AuthedReq) GetUnifiDeviceAssets() ([]Asset, error) {
|
||||||
|
usgs, usws, uaps, err := c.GetUnifiDevices()
|
||||||
|
assets := []Asset{}
|
||||||
|
if err == nil {
|
||||||
|
for _, r := range usgs {
|
||||||
|
assets = append(assets, r)
|
||||||
|
}
|
||||||
|
for _, r := range usws {
|
||||||
|
assets = append(assets, r)
|
||||||
|
}
|
||||||
|
for _, r := range uaps {
|
||||||
|
assets = append(assets, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return assets, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,9 @@ import (
|
||||||
influx "github.com/influxdata/influxdb/client/v2"
|
influx "github.com/influxdata/influxdb/client/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Point generates a device's datapoint for InfluxDB.
|
// Points generates a device's datapoints for InfluxDB.
|
||||||
func (u USG) Point() (*influx.Point, error) {
|
func (u USG) Points() ([]*influx.Point, error) {
|
||||||
|
var points []*influx.Point
|
||||||
tags := map[string]string{
|
tags := map[string]string{
|
||||||
"id": u.ID,
|
"id": u.ID,
|
||||||
"mac": u.Mac,
|
"mac": u.Mac,
|
||||||
|
|
@ -117,5 +118,9 @@ func (u USG) Point() (*influx.Point, error) {
|
||||||
"wan-tx_bytes": u.Stat.WanTxBytes,
|
"wan-tx_bytes": u.Stat.WanTxBytes,
|
||||||
"wan-tx_packets": u.Stat.WanTxPackets,
|
"wan-tx_packets": u.Stat.WanTxPackets,
|
||||||
}
|
}
|
||||||
return influx.NewPoint("usg", tags, fields, time.Now())
|
pt, err := influx.NewPoint("usg", tags, fields, time.Now())
|
||||||
|
if err == nil {
|
||||||
|
points = append(points, pt)
|
||||||
|
}
|
||||||
|
return points, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,9 @@ import (
|
||||||
influx "github.com/influxdata/influxdb/client/v2"
|
influx "github.com/influxdata/influxdb/client/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Point generates a device's datapoint for InfluxDB.
|
// Points generates a device's datapoints for InfluxDB.
|
||||||
func (u USW) Point() (*influx.Point, error) {
|
func (u USW) Points() ([]*influx.Point, error) {
|
||||||
|
var points []*influx.Point
|
||||||
tags := map[string]string{
|
tags := map[string]string{
|
||||||
"id": u.ID,
|
"id": u.ID,
|
||||||
"mac": u.Mac,
|
"mac": u.Mac,
|
||||||
|
|
@ -109,5 +110,9 @@ func (u USW) Point() (*influx.Point, error) {
|
||||||
"stat_tx_retries": u.Stat.TxRetries,
|
"stat_tx_retries": u.Stat.TxRetries,
|
||||||
// Add the port stats too.
|
// Add the port stats too.
|
||||||
}
|
}
|
||||||
return influx.NewPoint("usw", tags, fields, time.Now())
|
pt, err := influx.NewPoint("usw", tags, fields, time.Now())
|
||||||
|
if err == nil {
|
||||||
|
points = append(points, pt)
|
||||||
|
}
|
||||||
|
return points, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue