From bac31b3d0583ac8a45a615ccedcef9dbd8c11508 Mon Sep 17 00:00:00 2001 From: Giovanni Harting <539@idlegandalf.com> Date: Fri, 18 Nov 2022 23:32:47 +0100 Subject: [PATCH] inital working commit --- .gitignore | 136 +++++++++++++ config.sample.yaml | 5 + go.mod | 16 ++ go.sum | 51 +++++ main.go | 478 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 686 insertions(+) create mode 100644 .gitignore create mode 100644 config.sample.yaml create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..75db782 --- /dev/null +++ b/.gitignore @@ -0,0 +1,136 @@ +# Created by https://www.toptal.com/developers/gitignore/api/goland+all,linux,windows +# Edit at https://www.toptal.com/developers/gitignore?templates=goland+all,linux,windows + +### GoLand+all ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### GoLand+all Patch ### +# Ignore everything but code style settings and run configurations +# that are supposed to be shared within teams. + +.idea/* + +!.idea/codeStyles +!.idea/runConfigurations + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# End of https://www.toptal.com/developers/gitignore/api/goland+all,linux,windows + +config.yaml + diff --git a/config.sample.yaml b/config.sample.yaml new file mode 100644 index 0000000..beb28e1 --- /dev/null +++ b/config.sample.yaml @@ -0,0 +1,5 @@ +logging: + level: DEBUG + +station_ip: "ip here" +password: "password here" \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..fda46a0 --- /dev/null +++ b/go.mod @@ -0,0 +1,16 @@ +module VodaDocsis + +go 1.19 + +require ( + github.com/influxdata/line-protocol/v2 v2.2.1 + github.com/sirupsen/logrus v1.9.0 + github.com/wercker/journalhook v0.0.0-20180428041537-5d0a5ae867b3 + golang.org/x/crypto v0.2.0 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf // indirect + golang.org/x/sys v0.2.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..caeddd5 --- /dev/null +++ b/go.sum @@ -0,0 +1,51 @@ +github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU= +github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/frankban/quicktest v1.11.0/go.mod h1:K+q6oSqb0W0Ininfk863uOk1lMy69l/P6txr3mVT54s= +github.com/frankban/quicktest v1.11.2/go.mod h1:K+q6oSqb0W0Ininfk863uOk1lMy69l/P6txr3mVT54s= +github.com/frankban/quicktest v1.13.0 h1:yNZif1OkDfNoDfb9zZa9aXIpejNR4F23Wely0c+Qdqk= +github.com/frankban/quicktest v1.13.0/go.mod h1:qLE0fzW0VuyUAJgPU19zByoIr0HtCHN/r/VLSOOIySU= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/influxdata/line-protocol-corpus v0.0.0-20210519164801-ca6fa5da0184/go.mod h1:03nmhxzZ7Xk2pdG+lmMd7mHDfeVOYFyhOgwO61qWU98= +github.com/influxdata/line-protocol-corpus v0.0.0-20210922080147-aa28ccfb8937 h1:MHJNQ+p99hFATQm6ORoLmpUCF7ovjwEFshs/NHzAbig= +github.com/influxdata/line-protocol-corpus v0.0.0-20210922080147-aa28ccfb8937/go.mod h1:BKR9c0uHSmRgM/se9JhFHtTT7JTO67X23MtKMHtZcpo= +github.com/influxdata/line-protocol/v2 v2.0.0-20210312151457-c52fdecb625a/go.mod h1:6+9Xt5Sq1rWx+glMgxhcg2c0DUaehK+5TDcPZ76GypY= +github.com/influxdata/line-protocol/v2 v2.1.0/go.mod h1:QKw43hdUBg3GTk2iC3iyCxksNj7PX9aUSeYOYE/ceHY= +github.com/influxdata/line-protocol/v2 v2.2.1 h1:EAPkqJ9Km4uAxtMRgUubJyqAr6zgWM0dznKMLRauQRE= +github.com/influxdata/line-protocol/v2 v2.2.1/go.mod h1:DmB3Cnh+3oxmG6LOBIxce4oaL4CPj3OmMPgvauXh+tM= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/wercker/journalhook v0.0.0-20180428041537-5d0a5ae867b3 h1:shC1HB1UogxN5Ech3Yqaaxj1X/P656PPCB4RbojIJqc= +github.com/wercker/journalhook v0.0.0-20180428041537-5d0a5ae867b3/go.mod h1:XCsSkdKK4gwBMNrOCZWww0pX6AOt+2gYc5Z6jBRrNVg= +golang.org/x/crypto v0.2.0 h1:BRXPfhNivWL5Yq0BGQ39a2sW6t44aODpfxkWjYdzewE= +golang.org/x/crypto v0.2.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..a0edc3e --- /dev/null +++ b/main.go @@ -0,0 +1,478 @@ +package main + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "flag" + "fmt" + "github.com/influxdata/line-protocol/v2/lineprotocol" + log "github.com/sirupsen/logrus" + "github.com/wercker/journalhook" + "golang.org/x/crypto/pbkdf2" + "gopkg.in/yaml.v3" + "io" + "net/http" + "net/http/cookiejar" + "net/url" + "os" + "os/signal" + "path" + "strconv" + "strings" + "syscall" + "time" +) + +type Conf struct { + Logging struct { + Level string + } + StationIP string `yaml:"station_ip"` + Password string `yaml:"password"` +} + +var ( + configFlag = flag.String("config", "config.yaml", "path of config file to use") + journalLogFlag = flag.Bool("journal", false, "log to systemd journal instead of stdout/stderr") + conf = Conf{} + httpClient *http.Client +) + +const ( + ITERATIONS = 1000 + KEYSIZEBITS = 128 + USERNAME = "admin" +) + +type ChannelType string + +//goland:noinspection ALL +const ( + OFDM ChannelType = "OFDM" + SCQAM ChannelType = "SC-QAM" + OFDMA ChannelType = "OFDMA" +) + +func (c ChannelType) String() string { + return string(c) +} + +type ChannelDirection string + +const ( + UP ChannelDirection = "UP" + DOWN ChannelDirection = "DOWN" +) + +func (c ChannelDirection) String() string { + return strings.ToLower(string(c)) +} + +type BaseResponse struct { + Error string `json:"error"` + Message string `json:"message"` + Data json.RawMessage `json:"data"` +} + +type LoginResponse struct { + Intf string `json:"intf"` + User string `json:"user"` + UID string `json:"uid"` + Dpd string `json:"Dpd"` + RemoteAddr string `json:"remoteAddr"` + UserAgent string `json:"userAgent"` + HTTPReferer string `json:"httpReferer"` +} + +type DOCSISResponse struct { + OfdmDownstream []*DownstreamOFDMChannelInfo `json:"ofdm_downstream"` + Downstream []*DownstreamChannelInfo `json:"downstream"` + OfdmaUpstream []*UpstreamOFDMChannelInfo `json:"ofdma_upstream"` + Upstream []*UpstreamChannelInfo `json:"upstream"` +} + +type DownstreamOFDMChannelInfo struct { + ID string `json:"__id"` + ChannelIDOfdm string `json:"channelid_ofdm"` + StartFrequency string `json:"start_frequency"` + EndFrequency string `json:"end_frequency"` + CentralFrequencyOfdm string `json:"CentralFrequency_ofdm"` + Bandwidth string `json:"bandwidth"` + PowerOfdm string `json:"power_ofdm"` + SNROfdm string `json:"SNR_ofdm"` + FFTOfdm string `json:"FFT_ofdm"` + LockedOfdm string `json:"locked_ofdm"` + ChannelType string `json:"ChannelType"` +} + +type UpstreamOFDMChannelInfo struct { + ID string `json:"__id"` + ChannelIDUp string `json:"channelidup"` + StartFrequency string `json:"start_frequency"` + EndFrequency string `json:"end_frequency"` + Power string `json:"power"` + CentralFrequency string `json:"CentralFrequency"` + Bandwidth string `json:"bandwidth"` + FFT string `json:"FFT"` + ChannelType string `json:"ChannelType"` + RangingStatus string `json:"RangingStatus"` +} + +type DownstreamChannelInfo struct { + ID string `json:"__id"` + ChannelID string `json:"channelid"` + CentralFrequency string `json:"CentralFrequency"` + Power string `json:"power"` + SNR string `json:"SNR"` + FFT string `json:"FFT"` + Locked string `json:"locked"` + ChannelType string `json:"ChannelType"` +} + +type UpstreamChannelInfo struct { + ID string `json:"__id"` + ChannelIDUp string `json:"channelidup"` + CentralFrequency string `json:"CentralFrequency"` + Power string `json:"power"` + ChannelType string `json:"ChannelType"` + FFT string `json:"FFT"` + RangingStatus string `json:"RangingStatus"` +} + +type SaltResponse struct { + Error string `json:"error"` + Salt string `json:"salt"` + SaltWebUI string `json:"saltwebui"` +} + +type DOCSISChannelInfo struct { + Type ChannelType + Power float64 + RangingStatus *string + SNR *float64 + Direction ChannelDirection + Frequency uint64 + ID string +} + +func main() { + killSignals := make(chan os.Signal, 1) + signal.Notify(killSignals, syscall.SIGINT, syscall.SIGTERM) + reloadSignals := make(chan os.Signal, 1) + signal.Notify(reloadSignals, syscall.SIGUSR1) + flag.Parse() + confStr, err := os.ReadFile(*configFlag) + if err != nil { + log.Fatalf("Unable to open config: %v", err) + } + err = yaml.Unmarshal(confStr, &conf) + if err != nil { + log.Fatalf("Unable to parse config: %v", err) + } + lvl, err := log.ParseLevel(conf.Logging.Level) + if err != nil { + log.Fatalf("Failure setting logging level: %v", err) + } + log.SetLevel(lvl) + if *journalLogFlag { + journalhook.Enable() + } + + cj, err := cookiejar.New(nil) + if err != nil { + log.Fatalf("error creating jar") + } + + httpClient = &http.Client{ + Timeout: time.Second * 30, + Jar: cj, + } + + if err := login(); err != nil { + log.Fatalf("error logging in: %v", err) + } + + docsisStart := time.Now() + var dData *DOCSISResponse + if dData, err = DOCSISStatus(); err != nil { + log.Errorf("error getting docsis status: %v", err) + err = logout() + if err != nil { + log.Fatalf("error logging out: %v", err) + } + return + } + docsisTime := time.Now() + + var enc lineprotocol.Encoder + enc.SetPrecision(lineprotocol.Microsecond) + + enc.StartLine("docsis_diagnostic") + enc.AddField("response_time_ms", lineprotocol.MustNewValue(time.Since(docsisStart).Milliseconds())) + enc.EndLine(docsisTime) + + for _, channel := range transformDOCSIS(dData) { + enc.StartLine("docsis") + enc.AddTag("channel_id", channel.ID) + enc.AddTag("direction", channel.Direction.String()) + enc.AddTag("type", channel.Type.String()) + enc.AddField("frequency", lineprotocol.MustNewValue(channel.Frequency)) + enc.AddField("power", lineprotocol.MustNewValue(channel.Power)) + + if channel.Direction == UP { + enc.AddField("ranging_status", lineprotocol.MustNewValue(*channel.RangingStatus)) + } else { + enc.AddField("snr", lineprotocol.MustNewValue(*channel.SNR)) + } + + enc.EndLine(docsisTime) + } + + if err = enc.Err(); err != nil { + log.Fatalf("influx line protocol encoding error: %v", err) + } + fmt.Printf("%s", enc.Bytes()) + + if err = logout(); err != nil { + log.Fatalf("error logging out: %v", err) + } +} + +func login() error { + postData := url.Values{} + postData.Add("username", USERNAME) + postData.Add("password", "seeksalthash") + postData.Add("logout", "true") + + req, err := http.NewRequest(http.MethodPost, "http://"+path.Join(conf.StationIP, "/api/v1/session/login"), strings.NewReader(postData.Encode())) + if err != nil { + return err + } + + req.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8") + req.Header.Set("Referer", "http://"+conf.StationIP+"/") + req.Header.Set("X-Requested-With", "XMLHttpRequest") + + sResp, err := httpClient.Do(req) + if err != nil { + return err + } + + if sResp.StatusCode != 200 { + return fmt.Errorf("login failed") + } + + rData, err := io.ReadAll(sResp.Body) + if err != nil { + return err + } + + err = sResp.Body.Close() + if err != nil { + return err + } + + saltResp := new(SaltResponse) + if err = json.Unmarshal(rData, saltResp); err != nil { + return err + } + + if saltResp.Error != "ok" { + return fmt.Errorf("login failed: %+v", saltResp.Error) + } + + finalKey := Key(Key(conf.Password, saltResp.Salt), saltResp.SaltWebUI) + + postData = url.Values{} + postData.Add("username", USERNAME) + postData.Add("password", finalKey) + + nResp, err := APIRequest(http.MethodPost, "/api/v1/session/login", postData) + if err != nil { + return err + } + + loginData := new(LoginResponse) + if err = json.Unmarshal(nResp.Data, loginData); err != nil { + return err + } + + _, err = APIRequest(http.MethodGet, "/api/v1/session/menu", nil) + if err != nil { + return err + } + + return nil +} + +func DOCSISStatus() (*DOCSISResponse, error) { + resp, err := APIRequest(http.MethodGet, "/api/v1/sta_docsis_status", nil) + if err != nil { + return nil, err + } + + docsisData := new(DOCSISResponse) + if err = json.Unmarshal(resp.Data, docsisData); err != nil { + return nil, err + } + + log.Debugf("docsis response: %+v", docsisData) + + return docsisData, nil +} + +func logout() error { + _, err := APIRequest(http.MethodPost, "/api/v1/session/logout", nil) + if err != nil { + return err + } + + return nil +} + +func Key(pw string, salt string) string { + return hex.EncodeToString(pbkdf2.Key([]byte(pw), []byte(salt), ITERATIONS, KEYSIZEBITS/8, sha256.New)) +} + +func APIRequest(method string, endpoint string, postData url.Values) (*BaseResponse, error) { + log.Debugf("[API] %s: %s", method, endpoint) + + var bodyReader io.Reader + if postData != nil { + bodyReader = strings.NewReader(postData.Encode()) + } + + req, err := http.NewRequest(method, "http://"+path.Join(conf.StationIP, endpoint), bodyReader) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8") + req.Header.Set("Referer", "http://"+conf.StationIP+"/") + req.Header.Set("X-Requested-With", "XMLHttpRequest") + + resp, err := httpClient.Do(req) + if err != nil { + return nil, err + } + + rData, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + err = resp.Body.Close() + if err != nil { + return nil, err + } + + if resp.StatusCode != 200 { + return nil, fmt.Errorf("[API] calling %s failed with %d: %s", endpoint, resp.StatusCode, string(rData)) + } + + baseResp := new(BaseResponse) + if err = json.Unmarshal(rData, baseResp); err != nil { + return nil, err + } + + if baseResp.Error != "ok" { + var data any + _ = json.Unmarshal(baseResp.Data, &data) + return nil, fmt.Errorf("%s failed: %s (data: %+v)", endpoint, baseResp.Message, data) + } + + return baseResp, nil +} + +func transformDOCSIS(rawDOCSIS *DOCSISResponse) (nChannels []*DOCSISChannelInfo) { + for _, channel := range rawDOCSIS.OfdmDownstream { + nChannels = append(nChannels, &DOCSISChannelInfo{ + Type: ChannelType(channel.ChannelType), + Power: powerStr2Power(channel.PowerOfdm), + SNR: snrStr2SNR(channel.SNROfdm), + Direction: DOWN, + Frequency: freqStr2Hz(channel.CentralFrequencyOfdm), + ID: channel.ChannelIDOfdm, + }) + } + + for _, channel := range rawDOCSIS.Downstream { + nChannels = append(nChannels, &DOCSISChannelInfo{ + Type: ChannelType(channel.ChannelType), + Power: powerStr2Power(channel.Power), + SNR: snrStr2SNR(channel.SNR), + Direction: DOWN, + Frequency: freqStr2Hz(channel.CentralFrequency), + ID: channel.ChannelID, + }) + } + + for _, channel := range rawDOCSIS.OfdmaUpstream { + nChannels = append(nChannels, &DOCSISChannelInfo{ + Type: ChannelType(channel.ChannelType), + Power: powerStr2Power(channel.Power), + RangingStatus: &channel.RangingStatus, + Direction: UP, + Frequency: freqStr2Hz(channel.CentralFrequency), + ID: channel.ChannelIDUp, + }) + } + + for _, channel := range rawDOCSIS.Upstream { + nChannels = append(nChannels, &DOCSISChannelInfo{ + Type: ChannelType(channel.ChannelType), + Power: powerStr2Power(channel.Power), + RangingStatus: &channel.RangingStatus, + Direction: UP, + Frequency: freqStr2Hz(channel.CentralFrequency), + ID: channel.ChannelIDUp, + }) + } + + return +} + +func snrStr2SNR(snrStr string) *float64 { + rSNR, unit := strUnit2Elements(snrStr) + + if strings.ToLower(unit) != "db" { + panic(fmt.Sprintf("error parsing power unit %s", snrStr)) + } + + return &rSNR +} + +func powerStr2Power(powerStr string) float64 { + rPower, unit := strUnit2Elements(powerStr) + + if strings.ToLower(unit) != "dbmv" { + panic(fmt.Sprintf("error parsing power unit %s", powerStr)) + } + + return rPower +} + +func freqStr2Hz(freqStr string) uint64 { + rFreq, unit := strUnit2Elements(freqStr) + + if strings.ToLower(unit) != "mhz" { + panic(fmt.Sprintf("error parsing frequency unit %s", freqStr)) + } + + return uint64(rFreq * 1000000) +} + +func strUnit2Elements(rawStr string) (float64, string) { + splitStr := strings.Split(rawStr, " ") + + if len(splitStr) < 2 { + panic(fmt.Sprintf("error parsing floatUnit %s", rawStr)) + } + + rFloat, err := strconv.ParseFloat(splitStr[0], 64) + if err != nil { + panic(fmt.Sprintf("error parsing floatUnit %s", splitStr[0])) + } + + return rFloat, splitStr[1] +}