commit 7241c4c499d65211b599b75c9b718dd7dba33e69 Author: Tordarus Date: Sat Feb 1 19:38:24 2025 +0100 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3245c1e --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.env +omada-bandwidth diff --git a/client_traffic.go b/client_traffic.go new file mode 100644 index 0000000..6870651 --- /dev/null +++ b/client_traffic.go @@ -0,0 +1,39 @@ +package main + +import ( + "time" + + omadamodel "git.tordarus.net/tordarus/omada-api/model" +) + +var TrafficByClient = map[ClientUniqueID]TrafficRate{} + +func CalculateClientTraffic(site *omadamodel.Site, client *omadamodel.Client, duration time.Duration) TrafficRate { + uniqueID := ClientUniqueID{ + SiteID: site.ID, + ClientMac: client.MacAddress, + } + + trafficTotal := TrafficRate{ + Received: uint64(client.TrafficDownBytes), + Transferred: uint64(client.TrafficUpBytes), + } + + defer func() { + TrafficByClient[uniqueID] = trafficTotal + }() + + lastTotal, ok := TrafficByClient[uniqueID] + + if !ok { + return TrafficRate{} + } + + trafficInDur := trafficTotal.Sub(lastTotal) + seconds := duration.Seconds() + + return TrafficRate{ + Received: uint64(float64(trafficInDur.Received) / seconds), + Transferred: uint64(float64(trafficInDur.Transferred) / seconds), + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..60e2df9 --- /dev/null +++ b/go.mod @@ -0,0 +1,15 @@ +module git.tordarus.net/tordarus/omada-bandwidth + +go 1.23.5 + +require ( + git.tordarus.net/tordarus/channel v0.1.18 + git.tordarus.net/tordarus/omada-api v0.0.1 + git.tordarus.net/tordarus/slices v0.0.12 +) + +require ( + git.milar.in/milarin/gmath v0.0.6 // indirect + git.tordarus.net/tordarus/envvars v0.0.0-20250114175450-d73e12b838a5 // indirect + git.tordarus.net/tordarus/ezhttp v0.0.3 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..5347a55 --- /dev/null +++ b/go.sum @@ -0,0 +1,12 @@ +git.milar.in/milarin/gmath v0.0.6 h1:NNtlh4PLsQT/hqq3F87Xm3u0PLapgBU/L2mN9mb8SPs= +git.milar.in/milarin/gmath v0.0.6/go.mod h1:HDLftG5RLpiNGKiIWh+O2G1PYkNzyLDADO8Cd/1abiE= +git.tordarus.net/tordarus/channel v0.1.18 h1:/9BDbkyXbVpFB+dQbToniX6g/ApBnzjslYt4NiycMQo= +git.tordarus.net/tordarus/channel v0.1.18/go.mod h1:8/dWFTdGO7g4AeSZ7cF6GerkGbe9c4dBVMVDBxOd9m4= +git.tordarus.net/tordarus/envvars v0.0.0-20250114175450-d73e12b838a5 h1:rKNDX/YGunqg8TEU6q1rgS2BcDKVmUW2cg61JOE/wws= +git.tordarus.net/tordarus/envvars v0.0.0-20250114175450-d73e12b838a5/go.mod h1:/qVGwrEmqtIrZyuuoIQl4vquSkPWUNJmlGNedDrdYfg= +git.tordarus.net/tordarus/ezhttp v0.0.3 h1:K6IlLmqkAFUF68HJsOTKcP3ejco7qfm+MuEagohoouo= +git.tordarus.net/tordarus/ezhttp v0.0.3/go.mod h1:Zq9o0Hibny61GqSCwJHa0PfGjVoUFv/zt2PjiQHXvmY= +git.tordarus.net/tordarus/omada-api v0.0.1 h1:w4WozETL00JygidOXGkGs9UxUQhUNlNnlfqWiYYriHo= +git.tordarus.net/tordarus/omada-api v0.0.1/go.mod h1:Ufp8hdXMyrXK7JFHq4WL1WSIlr9L6rhOBfQnqWNpXyM= +git.tordarus.net/tordarus/slices v0.0.12 h1:/0GDo1oGTIZB4+Nr/0dxUf7Z3mE8H8Ew+orFedGbxb8= +git.tordarus.net/tordarus/slices v0.0.12/go.mod h1:bu97dfPWRJ8iqwijWcpzRnFkry4sfY6mc+/bxLai4fU= diff --git a/main.go b/main.go new file mode 100644 index 0000000..8559ea3 --- /dev/null +++ b/main.go @@ -0,0 +1,87 @@ +package main + +import ( + "fmt" + "time" + + "git.milar.in/milarin/gmath" + "git.tordarus.net/tordarus/channel" + "git.tordarus.net/tordarus/envvars" + omadaapi "git.tordarus.net/tordarus/omada-api" + omadamodel "git.tordarus.net/tordarus/omada-api/model" + "git.tordarus.net/tordarus/slices" +) + +var ( // flags + FlagSiteNames = envvars.StringSlice("SITES", ",", []string{}) + FlagOmadaURL = envvars.String("OMADA_URL", "http://localhost:8088") + FlagOmadaID = envvars.String("OMADA_ID", "") + FlagOmadaClientID = envvars.String("OMADA_CLIENT_ID", "") + FlagOmadaClientSecret = envvars.String("OMADA_CLIENT_SECRET", "") + FlagOmadaUsername = envvars.String("OMADA_USERNAME", "") + FlagOmadaPassword = envvars.String("OMADA_PASSWORD", "") + FlagRefreshInterval = envvars.Duration("REFRESH_INTERVAL", 30*time.Second) +) + +var MinRefreshInterval = 30 * time.Second + +func main() { + FlagRefreshInterval = gmath.Max(FlagRefreshInterval, MinRefreshInterval) + + api, err := omadaapi.NewApi(omadaapi.ApiConfig{ + BasePath: FlagOmadaURL, + OmadaID: FlagOmadaID, + ClientID: FlagOmadaClientID, + ClientSecret: FlagOmadaClientSecret, + Username: FlagOmadaUsername, + Password: FlagOmadaPassword, + }) + + if err != nil { + panic(err) + } + + sites := slices.Filter(channel.ToSlice(api.GetSites()), FilterSitesByName(FlagSiteNames...)) + + ticker := time.NewTicker(FlagRefreshInterval) + + CalculateSiteTraffic(api, sites, 0) + + lastTick := time.Now() + for now := range ticker.C { + trafficStats := CalculateSiteTraffic(api, sites, now.Sub(lastTick)) + fmt.Println(trafficStats) + lastTick = now + } +} + +func CalculateSiteTraffic(api *omadaapi.Api, sites []*omadamodel.Site, duration time.Duration) *TrafficStats { + siteTraffics := map[string]*SiteTraffic{} + + for _, site := range sites { + trafficByClient := map[string]TrafficRate{} + + clients := channel.ToSlice(api.GetClients(site.ID)) + for _, client := range clients { + traffic := CalculateClientTraffic(site, client, duration) + trafficByClient[client.Name] = traffic + } + + siteTraffics[site.Name] = NewSiteTraffic(trafficByClient) + } + + return NewTrafficStats(siteTraffics) +} + +func FilterSitesByName(allowedSiteNames ...string) func(site *omadamodel.Site) bool { + if len(allowedSiteNames) == 0 { + return func(site *omadamodel.Site) bool { return true } + } + + siteNames := slices.ToStructMap(allowedSiteNames) + + return func(site *omadamodel.Site) bool { + _, ok := siteNames[site.Name] + return ok + } +} diff --git a/types.go b/types.go new file mode 100644 index 0000000..08a0c2a --- /dev/null +++ b/types.go @@ -0,0 +1,92 @@ +package main + +import ( + "encoding/json" + "fmt" + + "git.milar.in/milarin/gmath" + omadamodel "git.tordarus.net/tordarus/omada-api/model" +) + +type ClientUniqueID struct { + SiteID omadamodel.SiteID + ClientMac string +} + +type TrafficRate struct { + Received uint64 `json:"received"` // bytes per second + Transferred uint64 `json:"transferred"` // bytes per second +} + +func (tr TrafficRate) Add(o TrafficRate) TrafficRate { + return TrafficRate{ + Received: tr.Received + o.Received, + Transferred: tr.Transferred + o.Transferred, + } +} + +func (tr TrafficRate) Sub(o TrafficRate) TrafficRate { + return TrafficRate{ + Received: tr.Received - o.Received, + Transferred: tr.Transferred - o.Transferred, + } +} + +type SiteTraffic struct { + Total TrafficRate `json:"total"` + Clients map[string]TrafficRate `json:"clients"` +} + +func NewSiteTraffic(clients map[string]TrafficRate) *SiteTraffic { + total := TrafficRate{} + for _, rate := range clients { + total = total.Add(rate) + } + + return &SiteTraffic{ + Total: total, + Clients: clients, + } +} + +type TrafficStats struct { + Total TrafficRate `json:"total"` + Sites map[string]*SiteTraffic `json:"sites"` +} + +func NewTrafficStats(sites map[string]*SiteTraffic) *TrafficStats { + total := TrafficRate{} + for _, siteTraffic := range sites { + total = total.Add(siteTraffic.Total) + } + + return &TrafficStats{ + Total: total, + Sites: sites, + } +} + +func (tr TrafficRate) String() string { + return fmt.Sprintf("Rx: %s | Tx: %s", FormatBytes(tr.Received), FormatBytes(tr.Transferred)) +} + +func (stats TrafficStats) String() string { + data, _ := json.MarshalIndent(stats, "", "\t") + return string(data) +} + +func FormatBytes[T gmath.Integer](bytes T) string { + value := float64(bytes) + + if value >= 1000000000000 { + return fmt.Sprintf("%.02fT", value/1000000000000) + } else if value >= 1000000000 { + return fmt.Sprintf("%.02fG", value/1000000000) + } else if value >= 1000000 { + return fmt.Sprintf("%.02fM", value/1000000) + } else if value >= 1000 { + return fmt.Sprintf("%.02fK", value/1000) + } else { + return fmt.Sprintf("%.02fB", value) + } +}