initial commit

This commit is contained in:
Tordarus 2025-02-01 17:38:29 +01:00
commit 751ddc2436
14 changed files with 726 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
*_test.go

33
ap_info.go Normal file
View File

@ -0,0 +1,33 @@
package omadaapi
import (
"fmt"
"git.tordarus.net/tordarus/ezhttp"
"git.tordarus.net/tordarus/omada-api/model"
)
func (api *Api) GetApInfo(siteID model.SiteID, macAddress string) (*model.ApInfo, error) {
req := ezhttp.Request(
ezhttp.Template(api.tmpl),
ezhttp.Method("GET"),
ezhttp.AppendPath(
fmt.Sprintf("/openapi/v1/%s/sites/%s/aps/%s",
api.config.OmadaID,
siteID,
macAddress)),
)
resp, err := ezhttp.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
response, err := ezhttp.ParseJsonResponse[model.Response[model.ApInfo]](resp.Body)
if err != nil {
return nil, err
}
return &response.Result, nil
}

151
api.go Normal file
View File

@ -0,0 +1,151 @@
package omadaapi
import (
"fmt"
"net/http"
"git.tordarus.net/tordarus/ezhttp"
"git.tordarus.net/tordarus/omada-api/model"
)
type Api struct {
tmpl *http.Request
config ApiConfig
accessToken string
refreshToken string
}
type ApiConfig struct {
BasePath string
OmadaID string
ClientID string
ClientSecret string
Username string
Password string
}
func NewApi(config ApiConfig) (*Api, error) {
tmpl := ezhttp.Request(ezhttp.URL(config.BasePath))
api := &Api{
tmpl: tmpl,
config: config,
}
loginResponse, err := api.Login()
if err != nil {
return nil, fmt.Errorf("login request failed: %w", err)
}
if loginResponse.ErrorCode != 0 {
return nil, fmt.Errorf("login request failed: %s", loginResponse.Message)
}
authCodeResponse, err := api.AuthCode(loginResponse.Result.CsrfToken, loginResponse.Result.SessionID)
if err != nil {
return nil, fmt.Errorf("auth code request failed: %w", err)
}
if authCodeResponse.ErrorCode != 0 {
return nil, fmt.Errorf("auth code request failed: %s", authCodeResponse.Message)
}
authTokenResponse, err := api.AuthToken(*authCodeResponse.Result)
if err != nil {
return nil, fmt.Errorf("auth token request failed: %w", err)
}
if authTokenResponse.ErrorCode != 0 {
return nil, fmt.Errorf("auth token request failed: %s", authTokenResponse.Message)
}
api.accessToken = authTokenResponse.Result.AccessToken
api.refreshToken = authTokenResponse.Result.RefreshToken
api.tmpl = ezhttp.Request(
ezhttp.Template(api.tmpl),
ezhttp.Headers("Authorization", "AccessToken="+api.accessToken),
)
return api, nil
}
func (api *Api) Login() (*model.LoginResponse, error) {
reqBody := model.LoginRequest{
Username: api.config.Username,
Password: api.config.Password,
}
req := ezhttp.Request(
ezhttp.Template(api.tmpl),
ezhttp.Method("POST"),
ezhttp.AppendPath("/openapi/authorize/login"),
ezhttp.Query("client_id", api.config.ClientID, "omadac_id", api.config.OmadaID),
ezhttp.Body(ezhttp.JSON(reqBody)),
)
resp, err := ezhttp.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
response, err := ezhttp.ParseJsonResponse[model.LoginResponse](resp.Body)
if err != nil {
return nil, err
}
return response, nil
}
func (api *Api) AuthCode(csrfToken, sessionID string) (*model.AuthCodeResponse, error) {
req := ezhttp.Request(
ezhttp.Template(api.tmpl),
ezhttp.Method("POST"),
ezhttp.AppendPath("/openapi/authorize/code"),
ezhttp.Query("client_id", api.config.ClientID, "omadac_id", api.config.OmadaID, "response_type", "code"),
ezhttp.Headers("Csrf-Token", csrfToken, "Cookie", fmt.Sprintf("TPOMADA_SESSIONID=%s", sessionID)),
)
resp, err := ezhttp.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
response, err := ezhttp.ParseJsonResponse[model.AuthCodeResponse](resp.Body)
if err != nil {
return nil, err
}
return response, nil
}
func (api *Api) AuthToken(authCode string) (*model.AuthTokenResponse, error) {
req := ezhttp.Request(
ezhttp.Template(api.tmpl),
ezhttp.Method("POST"),
ezhttp.AppendPath("/openapi/authorize/token"),
ezhttp.Query(
"code", authCode,
"grant_type", "authorization_code",
"client_id", api.config.ClientID,
"client_secret", api.config.ClientSecret,
),
)
resp, err := ezhttp.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
response, err := ezhttp.ParseJsonResponse[model.AuthTokenResponse](resp.Body)
if err != nil {
return nil, err
}
return response, nil
}

59
client.go Normal file
View File

@ -0,0 +1,59 @@
package omadaapi
import (
"fmt"
"strconv"
"git.tordarus.net/tordarus/ezhttp"
"git.tordarus.net/tordarus/omada-api/model"
)
func (api *Api) GetClients(siteID model.SiteID) <-chan *model.Client {
out := make(chan *model.Client, 1000)
go func() {
defer close(out)
for page := 1; ; page++ {
resp, err := api.getClients(page, siteID)
if err != nil {
return
}
for _, v := range resp.Result.Data {
out <- &v
}
if resp.Result.CurrentPage*resp.Result.CurrentSize >= resp.Result.TotalRows {
break
}
}
}()
return out
}
func (api *Api) getClients(page int, siteID model.SiteID) (*model.PagedResponse[model.Client], error) {
req := ezhttp.Request(
ezhttp.Template(api.tmpl),
ezhttp.Method("GET"),
ezhttp.AppendPath(fmt.Sprintf("/openapi/v1/%s/sites/%s/clients", api.config.OmadaID, siteID)),
ezhttp.Query(
"page", strconv.Itoa(page),
"pageSize", "100",
),
)
resp, err := ezhttp.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
response, err := ezhttp.ParseJsonResponse[model.PagedResponse[model.Client]](resp.Body)
if err != nil {
return nil, err
}
return response, nil
}

59
device.go Normal file
View File

@ -0,0 +1,59 @@
package omadaapi
import (
"fmt"
"strconv"
"git.tordarus.net/tordarus/ezhttp"
"git.tordarus.net/tordarus/omada-api/model"
)
func (api *Api) GetDevices(siteID model.SiteID) <-chan *model.Device {
out := make(chan *model.Device, 1000)
go func() {
defer close(out)
for page := 1; ; page++ {
resp, err := api.getDevices(page, siteID)
if err != nil {
return
}
for _, v := range resp.Result.Data {
out <- &v
}
if resp.Result.CurrentPage*resp.Result.CurrentSize >= resp.Result.TotalRows {
break
}
}
}()
return out
}
func (api *Api) getDevices(page int, siteID model.SiteID) (*model.PagedResponse[model.Device], error) {
req := ezhttp.Request(
ezhttp.Template(api.tmpl),
ezhttp.Method("GET"),
ezhttp.AppendPath(fmt.Sprintf("/openapi/v1/%s/sites/%s/devices", api.config.OmadaID, siteID)),
ezhttp.Query(
"page", strconv.Itoa(page),
"pageSize", "100",
),
)
resp, err := ezhttp.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
response, err := ezhttp.ParseJsonResponse[model.PagedResponse[model.Device]](resp.Body)
if err != nil {
return nil, err
}
return response, nil
}

5
go.mod Normal file
View File

@ -0,0 +1,5 @@
module git.tordarus.net/tordarus/omada-api
go 1.23.0
require git.tordarus.net/tordarus/ezhttp v0.0.3

2
go.sum Normal file
View File

@ -0,0 +1,2 @@
git.tordarus.net/tordarus/ezhttp v0.0.3 h1:K6IlLmqkAFUF68HJsOTKcP3ejco7qfm+MuEagohoouo=
git.tordarus.net/tordarus/ezhttp v0.0.3/go.mod h1:Zq9o0Hibny61GqSCwJHa0PfGjVoUFv/zt2PjiQHXvmY=

40
model/ap_info.go Normal file
View File

@ -0,0 +1,40 @@
package model
import "encoding/json"
type ApInfo struct {
Type string `json:"type"`
MacAddress string `json:"mac"`
Name string `json:"name"`
IP string `json:"ip"`
IPv6 []string `json:"ipv6List"`
WlanGroupID string `json:"wlanId"`
WirelessUplinkInfo *ApWirelessUplinkInfo `json:"wirelessUplink"`
Model string `json:"model"`
FirmwareVersion string `json:"firmwareVersion"`
CpuUtil int `json:"cpuUtil"`
MemUtil int `json:"memoryUtil"`
UptimeSeconds int `json:"uptimeLong"`
}
func (i ApInfo) String() string {
data, _ := json.MarshalIndent(i, "", "\t")
return string(data)
}
type ApWirelessUplinkInfo struct {
UplinkMacAddress string `json:"uplinkMac"`
Name string `json:"name"`
Channel int `json:"channel"`
SignalStrengh int `json:"rssi"`
SignalNoiseRatio int `json:"snr"`
TransferRate string `json:"txRate"`
TransferRateMbps int `json:"txRateInt"`
ReceiveRate string `json:"rxRate"`
ReceiveRateMbps int `json:"rxRateInt"`
UpBytes int `json:"upBytes"`
DownBytes int `json:"downBytes"`
UpPackets int `json:"upPackets"`
DownPackets int `json:"downPackets"`
Activity int `json:"activity"`
}

209
model/client.go Normal file
View File

@ -0,0 +1,209 @@
package model
import "encoding/json"
type Client struct {
ID string `json:"id"`
MacAddress string `json:"mac"`
Name string `json:"name"`
Hostname string `json:"hostName"`
Vendor string `json:"vendor"`
DeviceType string `json:"deviceType"`
DeviceCategory string `json:"deviceCategory"`
OsName string `json:"osName"`
IP string `json:"ip"`
IPv6List []string `json:"ipv6List"`
ConnectionType ConnectionType `json:"connectType"`
ConnectionDevType ConnectionDeviceType `json:"connectDevType"`
ConnectedToWirelessRouter bool `json:"connectedToWirelessRouter"`
Wireless bool `json:"wireless"`
SSID string `json:"ssid"`
SignalLevel int `json:"signalLevel"`
HealthScore int `json:"healthScore"`
SignalRank int `json:"signalRank"`
WifiMode WifiMode `json:"wifiMode"`
ApName string `json:"apName"`
ApMac string `json:"apMac"`
RadioID RadioID `json:"radioId"`
Channel int `json:"channel"`
ReceiveRateKbits int `json:"rxRate"`
TransferRateKbits int `json:"txRate"`
PowerSave bool `json:"powerSave"`
SignalStrengh int `json:"rssi"`
SignalNoiseRatio int `json:"snr"`
SwitchMac string `json:"switchMac"`
SwitchName string `json:"switchName"`
GatewayMac string `json:"gatewayMac"`
GatewayName string `json:"gatewayName"`
VLanID int `json:"vid"`
NetworkName string `json:"networkName"`
Dot1xIdentity string `json:"dot1xIdentity"`
Dot1xVlan int `json:"dot1xVlan"`
Port int `json:"port"`
LagID int `json:"lagId"`
Activity int `json:"activity"`
TrafficDownBytes int `json:"trafficDown"`
TrafficUpBytes int `json:"trafficUp"`
UptimeSeconds int `json:"uptime"`
LastSeen int `json:"lastSeen"`
AuthStatus AuthStatus `json:"authStatus"`
Blocked bool `json:"blocked"`
Guest bool `json:"guest"`
Active bool `json:"active"`
Manager bool `json:"manager"`
IpSetting *ClientIpSetting `json:"ipSetting"`
DownPacket int `json:"downPacket"`
UpPacket int `json:"upPacket"`
RateLimit *ClientRateLimit `json:"rateLimit"`
ClientLockToApSetting *ClientLockToApSetting `json:"clientLockToApSetting"`
MultiLink []*ClientMultiFrequencyInfo `json:"multiLink"`
Unit int `json:"unit"`
StandardPort string `json:"standardPort"`
BlockDisable bool `json:"blockDisable"`
DhcpLeaseTimeSeconds int `json:"dhcpLeaseTime"`
SystemName string `json:"systemName"`
Description string `json:"description"`
Capabilities []string `json:"capabilities"`
ClientStat *ClientStat `json:"clientStat"`
}
func (c Client) String() string {
data, _ := json.MarshalIndent(c, "", "\t")
return string(data)
}
type ConnectionType int
const (
ConnectionTypeWirelessGuest ConnectionType = iota
ConnectionTypeWirelessUser
ConnectionTypeWiredUser
)
type ConnectionDeviceType string
const (
ConnectionDeviceTypeAP ConnectionDeviceType = "ap"
ConnectionDeviceTypeSwitch ConnectionDeviceType = "switch"
ConnectionDeviceTypeGateway ConnectionDeviceType = "gateway"
)
type WifiMode int
const (
WifiMode11a WifiMode = iota
WifiMode11b
WifiMode11g
WifiMode11na
WifiMode11ng
WifiMode11ac
WifiMode11axa
WifiMode11axg
WifiMode11beg
WifiMode11bea
)
type RadioID int
const (
RadioID_2_4G RadioID = iota
RadioID_5G RadioID = iota
RadioID_5G2 RadioID = iota
RadioID_6G RadioID = iota
)
type AuthStatus int
const (
AuthStatusConnected AuthStatus = iota
AuthStatusPending
AuthStatusAuthorized
AuthStatusAuthFree
)
type ClientIpSetting struct {
UseFixedAddr bool `json:"useFixedAddr"`
NetID string `json:"netId"`
IP string `json:"ip"`
}
type ClientRateLimit struct {
Mode RateLimitMode `json:"mode"`
RateLimitProfileID string `json:"rateLimitProfileId"`
CustomRateLimit *CustomRateLimit `json:"customRateLimit"`
}
type RateLimitMode int
const (
RateLimitModeCustom RateLimitMode = iota
RateLimitModeProfile
)
type CustomRateLimit struct {
UpEnable bool `json:"upEnable"`
UpUnit SpeedUnit `json:"upUnit"`
UpLimit int `json:"upLimit"`
DownEnable bool `json:"downEnable"`
DownUnit SpeedUnit `json:"downUnit"`
DownLimit int `json:"downLimit"`
}
type SpeedUnit int
const (
SpeedUnitKbps SpeedUnit = iota
SpeedUnitMbps
)
type ClientLockToApSetting struct {
Enable bool `json:"enable"`
APs []*ApBriefInfo `json:"aps"`
}
type ApBriefInfo struct {
Name string `json:"name"`
MacAddress string `json:"mac"`
}
type ClientMultiFrequencyInfo struct {
RadioID int `json:"radioId"`
WifiMode WifiMode `json:"wifiMode"`
Channel int `json:"channel"`
ReceiveRateKbits int `json:"rxRate"`
TransferRateKbits int `json:"txRate"`
PowerSave bool `json:"powerSave"`
SignalStrengh int `json:"rssi"`
SignalNoiseRatio int `json:"snr"`
SignalLevel int `json:"signalLevel"`
SignalRank int `json:"signalRank"`
UpPacket int `json:"upPacket"`
DownPacket int `json:"downPacket"`
TrafficDownBytes int `json:"trafficDown"`
TrafficUpBytes int `json:"trafficUp"`
Activity int `json:"activity"`
SignalLevelAndRank int `json:"signalLevelAndRank"`
}
type ClientStat struct {
Total int `json:"total"`
Wireless int `json:"wireless"`
Wired int `json:"wired"`
Num2G int `json:"num2g"`
Num5G int `json:"num5g"`
Num6G int `json:"num6g"`
NumUser int `json:"numUser"`
NumGuest int `json:"numGuest"`
NumWirelessUser int `json:"numWirelessUser"`
NumWirelessGuest int `json:"numWirelessGuest"`
Num2gUser int `json:"num2gUser"`
Num5gUser int `json:"num5gUser"`
Num6gUser int `json:"num6gUser"`
Num2gGuest int `json:"num2gGuest"`
Num5gGuest int `json:"num5gGuest"`
Num6gGuest int `json:"num6gGuest"`
Poor int `json:"poor"`
Fair int `json:"fair"`
NoData int `json:"noData"`
Good int `json:"good"`
}

45
model/device.go Normal file
View File

@ -0,0 +1,45 @@
package model
import (
"encoding/json"
)
type Device struct {
MacAddress string `json:"mac"`
Name string `json:"name"`
Type DeviceType `json:"type"`
Subtype string `json:"subtype"`
DeviceSeriesType int `json:"deviceSeriesType"`
Model string `json:"model"`
IP string `json:"ip"`
IPv6 []string `json:"ipv6"`
Uptime string `json:"uptime"`
Status DeviceStatus `json:"status"`
LastSeen int `json:"lastSeen"`
CpuUtil int `json:"cpuUtil"`
MemUtil int `json:"memUtil"`
SerialNumber string `json:"sn"`
LicenseStatus int `json:"licenseStatus"`
TagName string `json:"tagName"`
}
func (d Device) String() string {
data, _ := json.MarshalIndent(d, "", "\t")
return string(data)
}
type DeviceStatus int
const (
DeviceStatusDisconnected DeviceStatus = iota
DeviceStatusConnected
DeviceStatusPending
DeviceStatusHeartbeatMissed
DeviceStatusIsolated
)
type DeviceType string
const (
DeviceTypeAP DeviceType = "ap"
)

26
model/login.go Normal file
View File

@ -0,0 +1,26 @@
package model
type LoginRequest struct {
Username string `json:"username"`
Password string `json:"password"`
}
type LoginResponse Response[LoginResponseResult]
type LoginResponseResult struct {
CsrfToken string `json:"csrfToken"`
SessionID string `json:"sessionId"`
}
type AuthCodeResponse struct {
ErrorCode int `json:"errorCode"`
Message string `json:"msg"`
Result *string `json:"result"`
}
type AuthTokenResponse Response[AuthTokenResponseResult]
type AuthTokenResponseResult struct {
AccessToken string `json:"accessToken"`
TokenType string `json:"tokenType"`
ExpiresIn int `json:"expiresIn"`
RefreshToken string `json:"refreshToken"`
}

15
model/response.go Normal file
View File

@ -0,0 +1,15 @@
package model
type Response[T any] struct {
ErrorCode int `json:"errorCode"`
Message string `json:"msg"`
Result T `json:"result"`
}
type PagedResponse[T any] Response[PagedResponseResult[T]]
type PagedResponseResult[T any] struct {
TotalRows int `json:"totalRows"`
CurrentPage int `json:"currentPage"`
CurrentSize int `json:"currentSize"`
Data []T `json:"data"`
}

22
model/site.go Normal file
View File

@ -0,0 +1,22 @@
package model
import "encoding/json"
type SiteID string
type Site struct {
ID SiteID `json:"siteId"`
Name string `json:"name"`
TagIDs []string `json:"tagIds"`
Region string `json:"region"`
TimeZone string `json:"timeZone"`
Scenario string `json:"scenario"`
Longitude float64 `json:"longitude"`
Latitude float64 `json:"latitude"`
Address string `json:"address"`
Type int `json:"type,omitempty"`
}
func (s Site) String() string {
data, _ := json.MarshalIndent(s, "", "\t")
return string(data)
}

59
site.go Normal file
View File

@ -0,0 +1,59 @@
package omadaapi
import (
"fmt"
"strconv"
"git.tordarus.net/tordarus/ezhttp"
"git.tordarus.net/tordarus/omada-api/model"
)
func (api *Api) GetSites() <-chan *model.Site {
out := make(chan *model.Site, 1000)
go func() {
defer close(out)
for page := 1; ; page++ {
resp, err := api.getSites(page)
if err != nil {
return
}
for _, v := range resp.Result.Data {
out <- &v
}
if resp.Result.CurrentPage*resp.Result.CurrentSize >= resp.Result.TotalRows {
break
}
}
}()
return out
}
func (api *Api) getSites(page int) (*model.PagedResponse[model.Site], error) {
req := ezhttp.Request(
ezhttp.Template(api.tmpl),
ezhttp.Method("GET"),
ezhttp.AppendPath(fmt.Sprintf("/openapi/v1/%s/sites", api.config.OmadaID)),
ezhttp.Query(
"page", strconv.Itoa(page),
"pageSize", "100",
),
)
resp, err := ezhttp.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
response, err := ezhttp.ParseJsonResponse[model.PagedResponse[model.Site]](resp.Body)
if err != nil {
return nil, err
}
return response, nil
}