diff --git a/.DS_Store b/.DS_Store index 6959c34..fc263ad 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/.gitignore b/.gitignore index 15f4493..0b59c55 100644 --- a/.gitignore +++ b/.gitignore @@ -28,5 +28,3 @@ bin/ output*.* out*.* out.txt - -.idea/ diff --git a/.goreleaser.yaml b/.goreleaser.yaml index ce9a1a1..a65edfe 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -12,8 +12,7 @@ builds: goos: - linux - windows - - darwin - main: ./cmd/gogoodwe + - darwin archives: - format: tar.gz diff --git a/CHANGELOG.md b/CHANGELOG.md index c2d9720..c3d8e47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,21 +1,4 @@ -# GoGoodwe - CHANGELOG - -## v2.0.0 (2024-01-22) - -- Major refactoring to cleanup project structure -- Simplified package structure - -## v1.4.0 (2023-08-30) - -- Major refactoring to move non-shared code to /internal folder -- Abstracted core away from main() - -## v1.1.0 (2023-08-16) - -- refactored code to make errors bubble back to main package and better error reporting/logging -- refactored main package to include run() method -- removed staticcheck as there is a bug with Go v1.21 +# CHANGELOG ## v1.0.0 (2023-08-10) - - initial version 1.0 release diff --git a/LICENSE b/LICENSE index 06c0e27..8480972 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024, Aaron Saikovski +Copyright (c) 2023, Aaron Saikovski Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Makefile b/Makefile index 6d7edde..d82dc9a 100644 --- a/Makefile +++ b/Makefile @@ -1,91 +1,68 @@ # Define Go command and flags GO = go GOFLAGS = -ldflags="-s -w" -TARGET = gogoodwe -MAINAPPPATH = ./main.go -default: help +#export PATH=$PATH:$HOME/go/bin; + +# Define the target executable +TARGET = gogoodwe -.PHONY: help ## help - Display help about make targets for this Makefile help: @cat Makefile | grep '^## ' --color=never | cut -c4- | sed -e "`printf 's/ - /\t- /;'`" | column -s "`printf '\t'`" -t - -.PHONY: release -## release - Builds the project in preparation for (local)release -release: vet lint seccheck - go build $(GOFLAGS) -o bin/${TARGET} ${MAINAPPPATH} +## localrelease - Builds the project in preparation for (local)release +localrelease: + go build $(GOFLAGS) -o bin/${TARGET} main.go file bin/${TARGET} - -.PHONY: goreleaser -## goreleaser - Builds the project in preparation for release -goreleaser: - goreleaser release --snapshot --clean - - -.PHONY: docs -## docs - updates the swagger docs -docs: - swag init - - -.PHONY: build -## build - Builds the project in preparation for debug +## release - Builds the project in preparation for release +release: + goreleaser release --snapshot --clean + +## debug - Builds the project in preparation for debug build: - go build -o bin/${TARGET} ${MAINAPPPATH} + go build -o bin/${TARGET} main.go file bin/${TARGET} +## buildandrun - builds and runs the program on the target platform +buildandrun: build + ./bin/${TARGET} -.PHONY: run -## run - builds and runs the program on the target platform -run: - go run ${MAINAPPPATH} +## run - runs main.go for testing +run: dep + go run main.go -.PHONY: clean ## clean - Remove the old builds and any debug information clean: go clean rm -rf dist rm bin/${TARGET} - -.PHONY: test -## test - executes unit tests +## test - executes unit test test: - go test -v ./test/... + go test ./... - -.PHONY: deps -## deps - fetches any external dependencies and updates -deps: +## dep - fetches any external dependencies +dep: go mod tidy go mod download - go get -u ./... - -.PHONY: vet ## vet - Vet examines Go source code and reports suspicious constructs vet: go vet ./... +## staticcheck - Runs static code analyzer staticcheck +staticcheck: + go run honnef.co/go/tools/cmd/staticcheck@latest -checks=all,-ST1000,-U1000 ./... -.PHONY: staticcheck -## staticcheck - Runs static code analyzer staticcheck - currently broken -staticcheck: - staticcheck ./... - - -.PHONY: seccheck ## seccheck - Code vulnerability check seccheck: + brew install govulncheck govulncheck ./... - -.PHONY: lint ## lint - format code and tidy modules lint: go fmt ./... - go mod tidy -v + go mod tidy -v \ No newline at end of file diff --git a/README.md b/README.md index f1fc363..49ece3e 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,11 @@
-# GoGoodwe +# GoGgoodwe -A command line tool and query the GOODWE SEMS Inverter APIs - written in 100% Go. +A command line tool and Go packages to query the GOODWE SEMS Portal APIs - written in 100% Go. [![Build Status](https://github.com/AaronSaikovski/gogoodwe/workflows/build/badge.svg)](https://github.com/AaronSaikovski/gogoodwe/actions) +[![Coverage Status](https://coveralls.io/repos/github/AaronSaikovski/gogoodwe/badge.svg?branch=main)](https://coveralls.io/github/AaronSaikovski/gogoodwe?branch=main) [![Licence](https://img.shields.io/github/license/AaronSaikovski/gogoodwe)](LICENSE)
@@ -14,19 +15,17 @@ A command line tool and query the GOODWE SEMS Inverter APIs - written in 100% Go The toolchain is mainly driven by the Makefile. ```bash -help - Display help about make targets for this Makefile -release - Builds the project in preparation for (local)release -goreleaser - Builds the project in preparation for release -docs - updates the swagger docs -build - Builds the project in preparation for debug -run - builds and runs the program on the target platform -clean - Remove the old builds and any debug information -test - executes unit tests -deps - fetches any external dependencies and updates -vet - Vet examines Go source code and reports suspicious constructs -staticcheck - Runs static code analyzer staticcheck - currently broken -seccheck - Code vulnerability check -lint - format code and tidy modules +* help - Display help about make targets for this Makefile +* release - Builds the project in preparation for release +* debug - Builds the project in preparation for debug +* buildandrun - builds and runs the program on the target platform +* run - runs main.go for testing +* clean - Remove the old builds and any debug information +* test - executes unit test +* dep - fetches any external dependencies +* vet - Vet examines Go source code and reports suspicious constructs +* staticcheck - Runs static code analyzer staticcheck +* lint - format code and tidy modules ``` To get started type, diff --git a/TODO.md b/TODO.md index ef01c7d..3143314 100644 --- a/TODO.md +++ b/TODO.md @@ -1,22 +1,13 @@ -# GoGoodwe V2 - TODO +# gogoodwe TODO -### ToDo +GoGoodwe backlog -- [ ] Add ability to output inverter data to a file. -- [ ] Format the inverter output to make it more human readable. -- [ ] Add the ability to query historical data for a single day. -- [ ] Have the ability to have a realtime logging to the screen or to a file in 5 minute intervals. -- [ ] Add the ability to produce a daily summary of key data (Generation today, Income today, total generation, total income). -- [ ] Add the ability to query the inverter status for Generation today and Status (check if operational). -- [ ] Add goroutines and wait groups for the API calls and maybe channels for success/failed API calls. -- [ ] Add Cobra for command flag parsing and processing. -- [ ] Investigate the ability to generate .CSV files as output. -- [ ] +### Todo -### In Progress +- [ ] Add ability to output to file with a flag +- [ ] Add ability to have a smaller output struct of just key reporting data +- [ ] Add Golang contexts for API calls -- [ ] add unit tests +### In Progress ### Done ✓ - -- [ ] diff --git a/app/app.go b/app/app.go deleted file mode 100644 index 2726e33..0000000 --- a/app/app.go +++ /dev/null @@ -1,29 +0,0 @@ -package app - -// Main package - This is the main program entry point -import ( - "github.com/AaronSaikovski/gogoodwe/inverter" - "github.com/AaronSaikovski/gogoodwe/utils" - "github.com/alexflint/go-arg" -) - -// Run - main program runner -func Run() error { - - //Get the args input data - var args utils.Args - p := arg.MustParse(&args) - - //check for valid email address input - if !utils.CheckValidEmail(args.Account) { - p.Fail("Invalid Email address format - should be: 'user@somedomain.com'.") - } - - //check for valid powerstation Id - if !utils.CheckValidPowerstationID(args.PowerStationID) { - p.Fail("Invalid Powerstation ID format: - should be: 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX'.") - } - - // Get the data from the API, return any errors. Pass in args as string - return inverter.FetchData(args.Account, args.Password, args.PowerStationID) -} diff --git a/constants/constants.go b/constants/constants.go index 70ba5dd..cb423b5 100644 --- a/constants/constants.go +++ b/constants/constants.go @@ -1,13 +1,8 @@ -/* -# Name: constants - shared constants -# Author: Aaron Saikovski - asaikovski@outlook.com -*/ - package constants const ( // Auth Login Url - AuthLoginUrl string = "https://www.semsportal.com/api/v2/Common/CrossLogin" + AuthLoginUrL string = "https://www.semsportal.com/api/v2/Common/CrossLogin" // Powerstation API Url PowerStationURL string = "v2/PowerStation/GetMonitorDetailByPowerstationId" @@ -16,7 +11,7 @@ const ( HTTPTimeout = 20 // Version string - VersionString string = "gogoodwe v2.0.0" + VersionString string = "gogoodwe v0.0.7" //API login success response message SemsLoginSuccessResponse string = "Successful" diff --git a/go.mod b/go.mod index fcddca2..c0b1f97 100644 --- a/go.mod +++ b/go.mod @@ -7,4 +7,4 @@ require ( github.com/logrusorgru/aurora v2.0.3+incompatible ) -require github.com/alexflint/go-scalar v1.2.0 // indirect +require github.com/alexflint/go-scalar v1.1.0 // indirect diff --git a/go.sum b/go.sum index 899e74f..d3619e9 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,7 @@ github.com/alexflint/go-arg v1.4.3 h1:9rwwEBpMXfKQKceuZfYcwuc/7YY7tWJbFsgG5cAU/uo= github.com/alexflint/go-arg v1.4.3/go.mod h1:3PZ/wp/8HuqRZMUUgu7I+e1qcpUbvmS258mRXkFH4IA= +github.com/alexflint/go-scalar v1.1.0 h1:aaAouLLzI9TChcPXotr6gUhq+Scr8rl0P9P4PnltbhM= github.com/alexflint/go-scalar v1.1.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o= -github.com/alexflint/go-scalar v1.2.0 h1:WR7JPKkeNpnYIOfHRa7ivM21aWAdHD0gEWHCx+WQBRw= -github.com/alexflint/go-scalar v1.2.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o= 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= diff --git a/inverter/data.go b/inverter/data.go deleted file mode 100644 index 92d961b..0000000 --- a/inverter/data.go +++ /dev/null @@ -1,72 +0,0 @@ -/* -# Name: data - fetches data from the goodwe API - and processes it to pass back to caller -# Author: Aaron Saikovski - asaikovski@outlook.com -*/ -package inverter - -import ( - "errors" - "fmt" - - "github.com/AaronSaikovski/gogoodwe/authentication" - "github.com/AaronSaikovski/gogoodwe/types" - "github.com/AaronSaikovski/gogoodwe/utils" - "github.com/logrusorgru/aurora" -) - -// apiLogin - Login to the API -func apiLogin(SemsUserLogin *types.LoginCredentials, SemsResponseData *types.LoginResponse) error { - - // Do the login - update the pointer to the struct SemsResponseData - autherr := authentication.DoLogin(SemsResponseData, SemsUserLogin) - if autherr != nil { - utils.HandleError(autherr) - return autherr - } else { - return nil - } -} - -// FetchData - Main API fetch function -func FetchData(Account string, Password string, PowerStationID string) error { - - // Data types - var SemsResponseData types.LoginResponse - var PowerstationData types.InverterData - - // Create a new SemsLoginCreds object via a struct literal - var SemsUserLogin = types.LoginCredentials{ - Account: Account, - Password: Password, - PowerStationID: PowerStationID, - } - - // Do the login..check for errors - err := apiLogin(&SemsUserLogin, &SemsResponseData) - if err == nil { - - // Fetch the data - dataerr := fetchInverterData(&SemsResponseData, &SemsUserLogin, &PowerstationData) - if dataerr != nil { - utils.HandleError(errors.New("error: fetching powerstation data, check powerstationid is correct")) - return dataerr - } else { - // Get output - dataOutput, jsonerr := GetDataJSON(&PowerstationData) - if jsonerr != nil { - utils.HandleError(errors.New("error: converting powerstation data")) - return jsonerr - - } else { - //Display output - fmt.Println(aurora.BrightYellow(string(dataOutput))) - } - } - - } else { - utils.HandleError(err) - return err - } - - return nil -} diff --git a/main.go b/main.go index a9fe9bb..62ad0ad 100644 --- a/main.go +++ b/main.go @@ -1,20 +1,59 @@ /* -# Name: GoGoodwe - Authenticates to and queries the SEMS Solar inverter API +# Name: main package - Authenticates to and queries the SEMS Solar inverter API # Author: Aaron Saikovski - asaikovski@outlook.com */ package main +// Main package - This is the main program entry point import ( - "github.com/AaronSaikovski/gogoodwe/app" + "github.com/AaronSaikovski/gogoodwe/constants" + "github.com/AaronSaikovski/gogoodwe/pkg/goodwe/fetchdata" + "github.com/AaronSaikovski/gogoodwe/types" + "github.com/AaronSaikovski/gogoodwe/utils" + "github.com/alexflint/go-arg" ) +// args - srtruct using go-arg- https://github.com/alexflint/go-arg +type args struct { + Account string `arg:"required,-a,--account" help:"SEMS Email Account."` + Pwd string `arg:"required,-p,--pwd" help:"SEMS Account password."` + PowerStationID string `arg:"required,-i,--powerstationid" help:"SEMS Powerstation ID."` +} + +// Description - App description +func (args) Description() string { + return "A command line tool and GoLang package to query the GOODWE SEMS Portal APIs and Solar SEMS API." +} + +// Version - Version info +func (args) Version() string { + return constants.VersionString +} + // main - program main func main() { - //setup and run app - err := app.Run() + //Get the args input data + var args args + p := arg.MustParse(&args) - if err != nil { - panic(err) + //check for valid email address input + if !utils.CheckValidEmail(args.Account) { + p.Fail("Invalid Email address format - should be: 'user@somedomain.com'.") } + + //check for valid powerstation Id + if !utils.CheckValidPowerstationID(args.PowerStationID) { + p.Fail("Invalid Powerstation ID format: - should be: 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX'.") + } + + // Create a new SemsLoginCreds object via a struct literal + SemsUserLogin := types.SemsLoginCreds{ + Account: args.Account, + Pwd: args.Pwd, + PowerStationID: args.PowerStationID, + } + + // Get the data from the API + fetchdata.GetData(&SemsUserLogin) } diff --git a/authentication/authentication.go b/pkg/goodwe/authentication/authentication.go similarity index 59% rename from authentication/authentication.go rename to pkg/goodwe/authentication/authentication.go index 85b4bd3..5b52eae 100644 --- a/authentication/authentication.go +++ b/pkg/goodwe/authentication/authentication.go @@ -15,34 +15,41 @@ import ( "github.com/AaronSaikovski/gogoodwe/utils" ) +// SetHeaders - Set the login headers for the SEMS API login +func SetHeaders(r *http.Request) { + r.Header.Add("Content-Type", "application/json") + r.Header.Add("Token", "{\"version\":\"v2.1.0\",\"client\":\"ios\",\"language\":\"en\"}") +} + // DoLogin - Main public login function // Logs into the SEMs API -func DoLogin(SemsResponseData *types.LoginResponse, UserLogin *types.LoginCredentials) error { +func DoLogin(SemsResponseData *types.SemsResponseData, UserLogin *types.SemsLoginCreds) error { //check if the UserLogin struct is empty - if usererr := checkUserLoginInfo(UserLogin); usererr != nil { + usererr := CheckUserLoginInfo(UserLogin) + if usererr != nil { + utils.HandleError(usererr) return usererr } // User login struct to be converted to JSON - jsonData, jsonErr := utils.MarshalStructToJSON(UserLogin) - if jsonErr != nil { - return jsonErr - } + jsonData, _ := utils.MarshalStructToJSON(UserLogin) // Create a new http request - req, err := http.NewRequest(http.MethodPost, constants.AuthLoginUrl, bytes.NewBuffer(jsonData)) + req, err := http.NewRequest(http.MethodPost, constants.AuthLoginUrL, bytes.NewBuffer(jsonData)) if err != nil { + utils.HandleError(err) return err } //Add headers pass in the pointer to set the headers on the request object - setHeaders(req) + SetHeaders(req) //make the API Call client := &http.Client{Timeout: constants.HTTPTimeout * time.Second} resp, err := client.Do(req) if err != nil { + utils.HandleError(err) return err } @@ -50,22 +57,13 @@ func DoLogin(SemsResponseData *types.LoginResponse, UserLogin *types.LoginCreden defer resp.Body.Close() // Get the response body - respBody, respErr := utils.FetchResponseBody(resp.Body) - if respErr != nil { - return respErr - } + respBody, _ := utils.FetchResponseBody(resp.Body) //marshall response to SemsRespInfo struct - dataErr := utils.UnmarshalDataToStruct(respBody, &SemsResponseData) - if dataErr != nil { - return dataErr - } + utils.UnmarshalDataToStruct(respBody, &SemsResponseData) // check for successful login return value..return a login error - loginErr := checkUserLoginResponse(SemsResponseData.Msg) - if loginErr != nil { - return loginErr - } + CheckUserLoginResponse(SemsResponseData.Msg) return nil diff --git a/authentication/authutils.go b/pkg/goodwe/authentication/authhelper.go similarity index 50% rename from authentication/authutils.go rename to pkg/goodwe/authentication/authhelper.go index 49f004a..b103423 100644 --- a/authentication/authutils.go +++ b/pkg/goodwe/authentication/authhelper.go @@ -1,5 +1,5 @@ /* -# Name: authentication - auth helper functions +# Name: authhelper - auth helper functions # Author: Aaron Saikovski - asaikovski@outlook.com */ @@ -7,22 +7,17 @@ package authentication import ( "errors" - "net/http" "strings" "github.com/AaronSaikovski/gogoodwe/constants" "github.com/AaronSaikovski/gogoodwe/types" + "github.com/AaronSaikovski/gogoodwe/utils" ) -// SetHeaders - Set the login headers for the SEMS API login -func setHeaders(r *http.Request) { - r.Header.Add("Content-Type", "application/json") - r.Header.Add("Token", "{\"version\":\"v2.1.0\",\"client\":\"ios\",\"language\":\"en\"}") -} - // CheckUserLoginInfo - Check user login struct is valid/not null -func checkUserLoginInfo(UserLogin *types.LoginCredentials) error { - if (*UserLogin == types.LoginCredentials{}) { +func CheckUserLoginInfo(UserLogin *types.SemsLoginCreds) error { + //check if the UserLogin struct is empty + if (*UserLogin == types.SemsLoginCreds{}) { return errors.New("**Error: User Login details are empty or invalid..**") } else { return nil @@ -30,10 +25,9 @@ func checkUserLoginInfo(UserLogin *types.LoginCredentials) error { } // CheckUserLoginResponse - check for successful login return value..return a login error -func checkUserLoginResponse(loginResponse string) error { +func CheckUserLoginResponse(loginResponse string) { if strings.Compare(loginResponse, constants.SemsLoginSuccessResponse) != 0 { - return errors.New("**API Login Error: " + loginResponse) - } else { - return nil + authErr := errors.New("API Login Error: " + loginResponse) + utils.HandleError(authErr) } } diff --git a/pkg/goodwe/fetchdata/fetchdata.go b/pkg/goodwe/fetchdata/fetchdata.go new file mode 100644 index 0000000..e12d91b --- /dev/null +++ b/pkg/goodwe/fetchdata/fetchdata.go @@ -0,0 +1,62 @@ +/* +# Name: fetchdata - fetches data from the goodwe API - and processes it to pass back to caller +# Author: Aaron Saikovski - asaikovski@outlook.com +*/ +package fetchdata + +import ( + "errors" + "fmt" + + "github.com/AaronSaikovski/gogoodwe/pkg/goodwe/authentication" + "github.com/AaronSaikovski/gogoodwe/pkg/goodwe/powerstationdata" + + "github.com/AaronSaikovski/gogoodwe/types" + "github.com/AaronSaikovski/gogoodwe/utils" + "github.com/logrusorgru/aurora" +) + +// doLogin - Login to the API +func doLogin(SemsUserLogin *types.SemsLoginCreds, SemsResponseData *types.SemsResponseData) error { + + // Do the login - update the pointer to the struct SemsResponseData + autherr := authentication.DoLogin(SemsResponseData, SemsUserLogin) + if autherr != nil { + utils.HandleError(autherr) + return autherr + } else { + return nil + } +} + +// GetData - Main process data function +func GetData(SemsUserLogin *types.SemsLoginCreds) { + + // Data types + var SemsResponseData types.SemsResponseData + var PowerstationData types.StationResponseData + + // Do the login..check for errors + err := doLogin(SemsUserLogin, &SemsResponseData) + if err == nil { + + // Fetch the data + dataerr := powerstationdata.FetchData(&SemsResponseData, SemsUserLogin, &PowerstationData) + if dataerr != nil { + utils.HandleError(errors.New("error: fetching powerstation data, check powerstationid is correct")) + } else { + // Get output + dataOutput, jsonerr := powerstationdata.GetDataJSON(&PowerstationData) + if jsonerr != nil { + utils.HandleError(errors.New("error: converting powerstation data")) + + } else { + //Display output + fmt.Println(aurora.BrightYellow(string(dataOutput))) + } + } + + } else { + utils.HandleError(err) + } +} diff --git a/inverter/inverter.go b/pkg/goodwe/powerstationdata/powerstationdata.go similarity index 51% rename from inverter/inverter.go rename to pkg/goodwe/powerstationdata/powerstationdata.go index ae47f7c..e4bc859 100644 --- a/inverter/inverter.go +++ b/pkg/goodwe/powerstationdata/powerstationdata.go @@ -1,8 +1,8 @@ /* -# Name: inverter - gets data from the goodwe API - "v2/PowerStation/GetMonitorDetailByPowerstationId" +# Name: powerstationdata - gets data from the goodwe API - "v2/PowerStation/GetMonitorDetailByPowerstationId" # Author: Aaron Saikovski - asaikovski@outlook.com */ -package inverter +package powerstationdata import ( "bytes" @@ -14,20 +14,22 @@ import ( "github.com/AaronSaikovski/gogoodwe/utils" ) -// fetchInverterData - Fetches Data from the Inverter via the specified PowerstationID using theSEMs API -func fetchInverterData(SemsResponseData *types.LoginResponse, UserLogin *types.LoginCredentials, PowerstationOutputData *types.InverterData) error { +// setHeaders - Set the headers for the SEMS Data API +func setHeaders(r *http.Request, tokenstring []byte) { + r.Header.Add("Content-Type", "application/json") + r.Header.Add("Token", string(tokenstring)) +} + +// FetchData - Fetches Data from the specified PowerstationID via tht SEMs API +func FetchData(SemsResponseData *types.SemsResponseData, + UserLogin *types.SemsLoginCreds, + PowerstationOutputData *types.StationResponseData) error { // get the Token header data - tokenMapJSONData, tokenMapJSONErr := dataTokenJSON(SemsResponseData) - if tokenMapJSONErr != nil { - return tokenMapJSONErr - } + tokenMapJSONData, _ := DataTokenJSON(SemsResponseData) // get the Powerstation ID header data - powerStationMapJSONData, powerStationMapJSONErr := powerStationIDJSON(UserLogin) - if powerStationMapJSONErr != nil { - return powerStationMapJSONErr - } + powerStationMapJSONData, _ := PowerStationIDJSON(UserLogin) //Get the url from the Auth API and append the data url part url := SemsResponseData.API + constants.PowerStationURL @@ -35,7 +37,7 @@ func fetchInverterData(SemsResponseData *types.LoginResponse, UserLogin *types.L // Create a new http request req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(powerStationMapJSONData)) if err != nil { - return err + utils.HandleError(err) } //Add headers pass in the pointer to set the headers on the request object @@ -45,6 +47,7 @@ func fetchInverterData(SemsResponseData *types.LoginResponse, UserLogin *types.L client := &http.Client{Timeout: constants.HTTPTimeout * time.Second} resp, err := client.Do(req) if err != nil { + utils.HandleError(err) return err } @@ -52,15 +55,12 @@ func fetchInverterData(SemsResponseData *types.LoginResponse, UserLogin *types.L defer resp.Body.Close() // Get the response body - respBody, respBodyErr := utils.FetchResponseBody(resp.Body) - if respBodyErr != nil { - return respBodyErr - } + respBody, _ := utils.FetchResponseBody(resp.Body) //marshall response to SemsRespInfo struct - dataStructErr := utils.UnmarshalDataToStruct(respBody, &PowerstationOutputData) - if dataStructErr != nil { - return dataStructErr + dataerr := utils.UnmarshalDataToStruct(respBody, &PowerstationOutputData) + if dataerr != nil { + return dataerr } return nil diff --git a/inverter/datautils.go b/pkg/goodwe/powerstationdata/powerstationdatahelper.go similarity index 64% rename from inverter/datautils.go rename to pkg/goodwe/powerstationdata/powerstationdatahelper.go index b7c7386..e6a809b 100644 --- a/inverter/datautils.go +++ b/pkg/goodwe/powerstationdata/powerstationdatahelper.go @@ -1,31 +1,25 @@ /* -# Name: powerstationhelper - helper functions to get the Powerstation Data from the API +# Name: powerstationdatahelper - helper functions to get the Powerstation Data from the API # Author: Aaron Saikovski - asaikovski@outlook.com */ -package inverter +package powerstationdata import ( "encoding/json" - "net/http" "strconv" "github.com/AaronSaikovski/gogoodwe/types" "github.com/AaronSaikovski/gogoodwe/utils" ) -// setHeaders - Set the headers for the SEMS Data API -func setHeaders(r *http.Request, tokenstring []byte) { - r.Header.Add("Content-Type", "application/json") - r.Header.Add("Token", string(tokenstring)) -} - // DataTokenJSON - Makes a map for the token to be passed to the Data API header and returns a JSON string -func dataTokenJSON(SemsResponseData *types.LoginResponse) ([]byte, error) { +func DataTokenJSON(SemsResponseData *types.SemsResponseData) ([]byte, error) { + tokenMap := make(map[string]string) tokenMap["version"] = "v2.1.0" tokenMap["client"] = "ios" tokenMap["language"] = "en" - tokenMap["timestamp"] = strconv.FormatInt(SemsResponseData.Data.Timestamp, 10) + tokenMap["timestamp"] = strconv.Itoa(SemsResponseData.Data.Timestamp) tokenMap["uid"] = SemsResponseData.Data.UID tokenMap["token"] = SemsResponseData.Data.Token @@ -35,7 +29,7 @@ func dataTokenJSON(SemsResponseData *types.LoginResponse) ([]byte, error) { } // PowerStationIDJSON - Makes a map for the powerStationId to be passed to the Data API header and returns a JSON string -func powerStationIDJSON(UserLogin *types.LoginCredentials) ([]byte, error) { +func PowerStationIDJSON(UserLogin *types.SemsLoginCreds) ([]byte, error) { powerStationMap := make(map[string]string) powerStationMap["powerStationId"] = UserLogin.PowerStationID @@ -45,7 +39,7 @@ func powerStationIDJSON(UserLogin *types.LoginCredentials) ([]byte, error) { } // GetDataJSON - Returns the PowerstationOutputData as JSON -func GetDataJSON(PowerstationOutputData *types.InverterData) ([]byte, error) { +func GetDataJSON(PowerstationOutputData *types.StationResponseData) ([]byte, error) { // Get the response and return any errors resp, err := utils.MarshalStructToJSON(&PowerstationOutputData) diff --git a/test/README.md b/test/README.md deleted file mode 100644 index cdcf65f..0000000 --- a/test/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# `/test` - -Additional external test apps and test data. Feel free to structure the `/test` directory anyway you want. For bigger projects it makes sense to have a data subdirectory. For example, you can have `/test/data` or `/test/testdata` if you need Go to ignore what's in that directory. Note that Go will also ignore directories or files that begin with "." or "_", so you have more flexibility in terms of how you name your test data directory. - -Examples: - -* https://github.com/openshift/origin/tree/master/test (test data is in the `/testdata` subdirectory) - - diff --git a/test/constants_test.go b/test/constants_test.go deleted file mode 100644 index 5b95341..0000000 --- a/test/constants_test.go +++ /dev/null @@ -1,51 +0,0 @@ -package testing - -import ( - "testing" - - "github.com/AaronSaikovski/gogoodwe/constants" -) - -// TestAuthLoginUrl Test Auth Login Url -func TestAuthLoginUrl(t *testing.T) { - - AuthLoginUrlExpected := "https://www.semsportal.com/api/v2/Common/CrossLogin" - if constants.AuthLoginUrl != AuthLoginUrlExpected { - t.Errorf("AuthLoginUrl Const expected '%s' but got '%s'", AuthLoginUrlExpected, constants.AuthLoginUrl) - } -} - -// TestPowerStationURL Test Powerstation API Url -func TestPowerStationURL(t *testing.T) { - - PowerStationURLExpected := "v2/PowerStation/GetMonitorDetailByPowerstationId" - if constants.PowerStationURL != PowerStationURLExpected { - t.Errorf("PowerStationURL const expected '%s' but got '%s'", PowerStationURLExpected, constants.PowerStationURL) - } -} - -// HTTPTimeout Test HTTPTimeout value -// func TestHTTPTimeout(t *testing.T) { - -// HTTPTimeoutPowerStationURLExpected = 20 - -// PowerStationURLExpected := "v2/PowerStation/GetMonitorDetailByPowerstationId" -// if constants.HTTPTimeout != PowerStationURLExpected { -// t.Errorf("PowerStationURL const expected '%d' but got '%d'", PowerStationURLExpected, constants.PowerStationURL) -// } -// } - -// Default timeout value - -//API login success response message -//SemsLoginSuccessResponse string = "Successful" - -// TestSemsLoginSuccessResponse Test SemsLoginSuccessResponse test -func TestSemsLoginSuccessResponse(t *testing.T) { - - SemsLoginSuccessResponseExpected := "Successful" - - if constants.SemsLoginSuccessResponse != SemsLoginSuccessResponseExpected { - t.Errorf("SemsLoginSuccessResponse const expected '%s' but got '%s'", SemsLoginSuccessResponseExpected, constants.SemsLoginSuccessResponse) - } -} diff --git a/test/samplemodule_test.go b/test/samplemodule_test.go deleted file mode 100644 index c7bdb64..0000000 --- a/test/samplemodule_test.go +++ /dev/null @@ -1,22 +0,0 @@ -package testing - -/* -A Sample test harness. -*/ - -// import ( -// "testing" - -// "github.com/AaronSaikovski/gostarter/internal/pkg/samplemodule" -// ) - -// // A testing function. -// func TestSampleFunction(t *testing.T) { - -// msg := samplemodule.SampleFunction() -// expected := "OK" - -// if msg != expected { -// t.Errorf("Module expected '%q' but got '%q'", expected, msg) -// } -// } diff --git a/test/samplestruct_test.go b/test/samplestruct_test.go deleted file mode 100644 index d34e611..0000000 --- a/test/samplestruct_test.go +++ /dev/null @@ -1,21 +0,0 @@ -/* -A Sample test harness. -*/ - -package testing - -// import ( -// "github.com/AaronSaikovski/gostarter/internal/app/types" -// "testing" -// ) - -// // A testing function. -// func TestSampleStructString(t *testing.T) { - -// expected := "test data" -// ateststruct := types.Sample{SampleString: "test data", SampleInt: 1} - -// if ateststruct.SampleString != expected { -// t.Errorf("struct expected '%s' but got '%s'", expected, ateststruct.SampleString) -// } -// } diff --git a/types/logincredentials.go b/types/logincredentials.go deleted file mode 100644 index 64a12c1..0000000 --- a/types/logincredentials.go +++ /dev/null @@ -1,12 +0,0 @@ -/* -# Name: LoginCredentials - Struct to hold User login data -# Author: Aaron Saikovski - asaikovski@outlook.com -*/ -package types - -// LoginCredentials - Struct to hold User login data -type LoginCredentials struct { - Account string `json:"account"` - Password string `json:"pwd"` - PowerStationID string `json:"powerstationid"` -} diff --git a/types/semslogincreds.go b/types/semslogincreds.go new file mode 100644 index 0000000..b08db27 --- /dev/null +++ b/types/semslogincreds.go @@ -0,0 +1,8 @@ +package types + +// SemsLoginCreds - Struct to hold User login data +type SemsLoginCreds struct { + Account string `json:"account"` + Pwd string `json:"pwd"` + PowerStationID string `json:"powerstationid"` +} diff --git a/types/loginresponse.go b/types/semsresponsedata.go similarity index 59% rename from types/loginresponse.go rename to types/semsresponsedata.go index 5d11885..5abcd61 100644 --- a/types/loginresponse.go +++ b/types/semsresponsedata.go @@ -1,19 +1,15 @@ -/* -# Name: LoginResponse - SEMS API Response Data struct -# Contains all the JSON Response data returned from the authentication API - "https://www.semsportal.com/api/v2/Common/CrossLogin" -# Will be unmarshalled to a struct via a pointer -# Author: Aaron Saikovski - asaikovski@outlook.com -*/ package types -// LoginResponse - SEMS API Response Data struct -type LoginResponse struct { +// SemsResponseData - SEMS API Response Data struct +// Contains all the JSON Response data returned from the authentication API - "https://www.semsportal.com/api/v2/Common/CrossLogin" +// Will be unmarshalled to a struct via a pointer +type SemsResponseData struct { HasError bool `json:"hasError"` Code int32 `json:"code"` Msg string `json:"msg"` Data struct { UID string `json:"uid"` - Timestamp int64 `json:"timestamp"` + Timestamp int `json:"timestamp"` Token string `json:"token"` Client string `json:"client"` Version string `json:"version"` diff --git a/types/inverterrdata.go b/types/stationresponsedata.go similarity index 72% rename from types/inverterrdata.go rename to types/stationresponsedata.go index 81cdbfb..8239905 100644 --- a/types/inverterrdata.go +++ b/types/stationresponsedata.go @@ -1,18 +1,43 @@ -/* -# Name: InverterData - Struct to hold data returned from the Powerstation API -# Minimised version - removed any sensitive data -# Author: Aaron Saikovski - asaikovski@outlook.com -*/ package types -// InverterData - Struct to hold data returned from the Inverter Powerstation API -type InverterData struct { +// StationResponseData - Struct to hold data returned from the Powerstation API +type StationResponseData struct { Language string `json:"language"` Function []string `json:"function"` HasError bool `json:"hasError"` Msg string `json:"msg"` Code string `json:"code"` Data struct { + Info struct { + PowerstationID string `json:"powerstation_id"` + Time string `json:"time"` + DateFormat string `json:"date_format"` + DateFormatYm string `json:"date_format_ym"` + Stationname string `json:"stationname"` + Address string `json:"address"` + OwnerName string `json:"owner_name"` + OwnerPhone string `json:"owner_phone"` + OwnerEmail string `json:"owner_email"` + BatteryCapacity float64 `json:"battery_capacity"` + TurnonTime string `json:"turnon_time"` + CreateTime string `json:"create_time"` + Capacity float64 `json:"capacity"` + Longitude float64 `json:"longitude"` + Latitude float64 `json:"latitude"` + PowerstationType string `json:"powerstation_type"` + Status int `json:"status"` + IsStored bool `json:"is_stored"` + IsPowerflow bool `json:"is_powerflow"` + ChartsType int `json:"charts_type"` + HasPv bool `json:"has_pv"` + HasStatisticsCharts bool `json:"has_statistics_charts"` + OnlyBps bool `json:"only_bps"` + OnlyBpu bool `json:"only_bpu"` + TimeSpan float64 `json:"time_span"` + PrValue string `json:"pr_value"` + OrgCode string `json:"org_code"` + OrgName string `json:"org_name"` + } `json:"info"` Kpi struct { MonthGeneration float64 `json:"month_generation"` Pac float64 `json:"pac"` @@ -25,18 +50,80 @@ type InverterData struct { } `json:"kpi"` PowercontrolStatus int `json:"powercontrol_status"` Images []any `json:"images"` - Inverter []struct { - IsStored bool `json:"is_stored"` - InPac float64 `json:"in_pac"` - OutPac float64 `json:"out_pac"` - Eday float64 `json:"eday"` - Emonth float64 `json:"emonth"` - Etotal float64 `json:"etotal"` - Status int `json:"status"` - TurnonTime string `json:"turnon_time"` - Type string `json:"type"` - Capacity float64 `json:"capacity"` - D struct { + Weather struct { + HeWeather6 []struct { + Basic struct { + Cid any `json:"cid"` + Location any `json:"location"` + ParentCity any `json:"parent_city"` + AdminArea any `json:"admin_area"` + Cnty any `json:"cnty"` + Lat any `json:"lat"` + Lon any `json:"lon"` + Tz any `json:"tz"` + } `json:"basic"` + Update struct { + Loc any `json:"loc"` + Utc any `json:"utc"` + } `json:"update"` + Status string `json:"status"` + DailyForecast []struct { + CondCodeD string `json:"cond_code_d"` + CondCodeN string `json:"cond_code_n"` + CondTxtD string `json:"cond_txt_d"` + CondTxtN string `json:"cond_txt_n"` + Date string `json:"date"` + Hum string `json:"hum"` + Pcpn string `json:"pcpn"` + Pop string `json:"pop"` + Pres string `json:"pres"` + TmpMax string `json:"tmp_max"` + TmpMin string `json:"tmp_min"` + UvIndex string `json:"uv_index"` + Vis string `json:"vis"` + WindDeg string `json:"wind_deg"` + WindDir string `json:"wind_dir"` + WindSc string `json:"wind_sc"` + WindSpd string `json:"wind_spd"` + } `json:"daily_forecast"` + } `json:"HeWeather6"` + } `json:"weather"` + Inverter []struct { + Sn string `json:"sn"` + Dict struct { + Left []struct { + IsHT bool `json:"isHT"` + IsStoreSkip bool `json:"isStoreSkip"` + Key string `json:"key"` + Value string `json:"value"` + Unit string `json:"unit"` + IsFaultMsg int `json:"isFaultMsg"` + FaultMsgCode int `json:"faultMsgCode"` + } `json:"left"` + Right []struct { + IsHT bool `json:"isHT"` + IsStoreSkip bool `json:"isStoreSkip"` + Key string `json:"key"` + Value string `json:"value"` + Unit string `json:"unit"` + IsFaultMsg int `json:"isFaultMsg"` + FaultMsgCode int `json:"faultMsgCode"` + } `json:"right"` + } `json:"dict"` + IsStored bool `json:"is_stored"` + Name string `json:"name"` + InPac float64 `json:"in_pac"` + OutPac float64 `json:"out_pac"` + Eday float64 `json:"eday"` + Emonth float64 `json:"emonth"` + Etotal float64 `json:"etotal"` + Status int `json:"status"` + TurnonTime string `json:"turnon_time"` + ReleationID string `json:"releation_id"` + Type string `json:"type"` + Capacity float64 `json:"capacity"` + D struct { + PwID string `json:"pw_id"` Capacity string `json:"capacity"` Model string `json:"model"` OutputPower string `json:"output_power"` @@ -110,7 +197,10 @@ type InverterData struct { InvertFull struct { CtSolutionType int `json:"ct_solution_type"` Cts any `json:"cts"` + Sn string `json:"sn"` CheckCode string `json:"check_code"` + PowerstationID string `json:"powerstation_id"` + Name string `json:"name"` ModelType string `json:"model_type"` ChangeType int `json:"change_type"` ChangeTime int `json:"change_time"` @@ -271,29 +361,43 @@ type InverterData struct { BatteryCharging string `json:"battery_charging"` LastRefreshTime string `json:"last_refresh_time"` BmsStatus string `json:"bms_status"` + PwID string `json:"pw_id"` FaultMessage string `json:"fault_message"` WarningCode any `json:"warning_code"` BatteryPower float64 `json:"battery_power"` - BackupPloadS float64 `json:"backup_pload_s"` - BackupVloadS float64 `json:"backup_vload_s"` - BackupIloadS float64 `json:"backup_iload_s"` - BackupPloadT float64 `json:"backup_pload_t"` - BackupVloadT float64 `json:"backup_vload_t"` - BackupIloadT float64 `json:"backup_iload_t"` - EtotalBuy any `json:"etotal_buy"` - EdayBuy float64 `json:"eday_buy"` - EbatteryCharge any `json:"ebattery_charge"` - EchargeDay float64 `json:"echarge_day"` - EbatteryDischarge any `json:"ebattery_discharge"` - EdischargeDay float64 `json:"edischarge_day"` - BattStrings any `json:"batt_strings"` - MeterConnectStatus any `json:"meter_connect_status"` - MtactivepowerR float64 `json:"mtactivepower_r"` - MtactivepowerS float64 `json:"mtactivepower_s"` - MtactivepowerT float64 `json:"mtactivepower_t"` - HasTigo bool `json:"has_tigo"` - CanStartIV bool `json:"canStartIV"` - BatteryCount any `json:"battery_count"` + PointIndex string `json:"point_index"` + Points []struct { + TargetIndex int `json:"target_index"` + TargetName string `json:"target_name"` + Display string `json:"display"` + Unit string `json:"unit"` + TargetKey string `json:"target_key"` + TextCn string `json:"text_cn"` + TargetSnSix any `json:"target_sn_six"` + TargetSnSeven any `json:"target_sn_seven"` + TargetType any `json:"target_type"` + StorageName any `json:"storage_name"` + } `json:"points"` + BackupPloadS float64 `json:"backup_pload_s"` + BackupVloadS float64 `json:"backup_vload_s"` + BackupIloadS float64 `json:"backup_iload_s"` + BackupPloadT float64 `json:"backup_pload_t"` + BackupVloadT float64 `json:"backup_vload_t"` + BackupIloadT float64 `json:"backup_iload_t"` + EtotalBuy any `json:"etotal_buy"` + EdayBuy float64 `json:"eday_buy"` + EbatteryCharge any `json:"ebattery_charge"` + EchargeDay float64 `json:"echarge_day"` + EbatteryDischarge any `json:"ebattery_discharge"` + EdischargeDay float64 `json:"edischarge_day"` + BattStrings any `json:"batt_strings"` + MeterConnectStatus any `json:"meter_connect_status"` + MtactivepowerR float64 `json:"mtactivepower_r"` + MtactivepowerS float64 `json:"mtactivepower_s"` + MtactivepowerT float64 `json:"mtactivepower_t"` + HasTigo bool `json:"has_tigo"` + CanStartIV bool `json:"canStartIV"` + BatteryCount any `json:"battery_count"` } `json:"inverter"` Hjgx struct { Co2 float64 `json:"co2"` @@ -362,6 +466,7 @@ type InverterData struct { Environmental []any `json:"environmental"` Equipment []struct { Type string `json:"type"` + Title string `json:"title"` Status int `json:"status"` Model any `json:"model"` StatusText any `json:"statusText"` @@ -374,6 +479,8 @@ type InverterData struct { IsStored bool `json:"isStored"` Soc string `json:"soc"` IsChange bool `json:"isChange"` + RelationID string `json:"relationId"` + Sn string `json:"sn"` HasTigo bool `json:"has_tigo"` IsSec bool `json:"is_sec"` IsSecs bool `json:"is_secs"` @@ -382,4 +489,11 @@ type InverterData struct { TitleSn any `json:"titleSn"` } `json:"equipment"` } `json:"data"` + Components struct { + Para string `json:"para"` + LangVer int `json:"langVer"` + TimeSpan int `json:"timeSpan"` + API string `json:"api"` + MsgSocketAdr any `json:"msgSocketAdr"` + } `json:"components"` } diff --git a/utils/args.go b/utils/args.go deleted file mode 100644 index 56786cd..0000000 --- a/utils/args.go +++ /dev/null @@ -1,20 +0,0 @@ -package utils - -import "github.com/AaronSaikovski/gogoodwe/constants" - -// Args - struct using go-arg- https://github.com/alexflint/go-arg -type Args struct { - Account string `arg:"required,-a,--account" help:"SEMS Email Account."` - Password string `arg:"required,-p,--password" help:"SEMS Account password."` - PowerStationID string `arg:"required,-i,--powerstationid" help:"SEMS Powerstation ID."` -} - -// Description - App description -func (Args) Description() string { - return "A command line tool to query the GOODWE SEMS Portal APIs and Solar SEMS API." -} - -// Version - Version info -func (Args) Version() string { - return constants.VersionString -} diff --git a/utils/errorhandler.go b/utils/errorhandler.go index 2beccae..a365476 100644 --- a/utils/errorhandler.go +++ b/utils/errorhandler.go @@ -2,7 +2,6 @@ package utils import ( "log" - "github.com/logrusorgru/aurora" )