commit
						7bd61d0be0
					
				|  | @ -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"` | ||||||
|  | @ -46,7 +51,7 @@ type UAP struct { | ||||||
| 	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,9 +137,9 @@ 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"` | ||||||
|  | @ -152,8 +157,8 @@ type UAP struct { | ||||||
| 		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,24 +289,24 @@ 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"` | ||||||
|  | @ -314,28 +319,28 @@ type UAP struct { | ||||||
| 		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"` | ||||||
|  |  | ||||||
|  | @ -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 usgs, usws, uaps, nil | ||||||
| 			return nil, err | } | ||||||
| 		} | 
 | ||||||
| 		assets = append(assets, asset) | // GetUnifiDeviceAssets provides an interface to return common asset types.
 | ||||||
| 	} | func (c *AuthedReq) GetUnifiDeviceAssets() ([]Asset, error) { | ||||||
| 	return assets, nil | 	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