Skip to content

Commit

Permalink
- switch to newer growatt api
Browse files Browse the repository at this point in the history
- add write support for charging-limit, discharge-limit, output-power
  • Loading branch information
mtrossbach committed Aug 5, 2024
1 parent 2a8b11b commit e876b64
Show file tree
Hide file tree
Showing 11 changed files with 382 additions and 119 deletions.
5 changes: 4 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ module noah-mqtt

go 1.22

require github.com/eclipse/paho.mqtt.golang v1.5.0
require (
github.com/eclipse/paho.mqtt.golang v1.5.0
github.com/google/uuid v1.6.0
)

require (
github.com/gorilla/websocket v1.5.3 // indirect
Expand Down
10 changes: 2 additions & 8 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,16 +1,10 @@
github.com/eclipse/paho.mqtt.golang v1.4.3 h1:2kwcUGn8seMUfWndX0hGbvH8r7crgcJguQNCyp70xik=
github.com/eclipse/paho.mqtt.golang v1.4.3/go.mod h1:CSYvoAlsMkhYOXh/oKyxa8EcBci6dVkLCbo5tTC1RIE=
github.com/eclipse/paho.mqtt.golang v1.5.0 h1:EH+bUVJNgttidWFkLLVKaQPGmkTUfQQqjOsyvMGvD6o=
github.com/eclipse/paho.mqtt.golang v1.5.0/go.mod h1:du/2qNQVqJf/Sqs4MEL77kR8QTqANF7XU7Fk0aOTAgk=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
91 changes: 80 additions & 11 deletions internal/growatt/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,23 @@ package growatt

import (
"fmt"
"github.com/google/uuid"
"math"
"net/http"
"net/http/cookiejar"
"net/url"
"time"
)

const growattUrl = "https://server-api.growatt.com"

type Client struct {
client *http.Client
username string
password string
userAgent string
userId string
token string
jar *cookiejar.Jar
}

Expand All @@ -36,11 +41,41 @@ func NewClient(username string, password string) *Client {
}
}

func (h *Client) loginGetToken() error {
var data TokenResponse
if _, err := h.postForm("https://evcharge.growatt.com/ocpp/user", url.Values{
"cmd": {"shineLogin"},
"userId": {fmt.Sprintf("SHINE%s", h.username)},
"password": {h.password},
"lan": {"1"},
}, &data); err != nil {
return err
}

h.token = data.Token
return nil
}

func (h *Client) Login() error {
if err := h.loginGetToken(); err != nil {
return err
}

var data LoginResult
if _, err := h.postForm("https://openapi.growatt.com/newTwoLoginAPI.do", url.Values{
"userName": {h.username},
"password": {h.password},
if _, err := h.postForm(growattUrl+"/newTwoLoginAPIV2.do", url.Values{
"userName": {h.username},
"password": {h.password},
"newLogin": {"1"},
"phoneType": {"android"},
"shinephoneVersion": {"8.1.8.1"},
"phoneSn": {uuid.New().String()},
"ipvcpc": {ipvcpc(h.username)},
"language": {"1"},
"systemVersion": {"9"},
"phoneModel": {"Mi A1"},
"loginTime": {time.Now().Format(time.DateTime)},
"appType": {"ShinePhone"},
"timestamp": {timestamp()},
}, &data); err != nil {
return err
}
Expand All @@ -53,10 +88,14 @@ func (h *Client) Login() error {
return nil
}

func (h *Client) GetPlantList() (*PlantList, error) {
var data PlantList
if _, err := h.get("https://openapi.growatt.com/PlantListAPI.do", url.Values{
"userId": {h.userId},
func (h *Client) GetPlantList() (*PlantListV2, error) {
var data PlantListV2
if _, err := h.postForm(growattUrl+"/newTwoPlantAPI.do?op=getAllPlantListTwo", url.Values{
"plantStatus": {""},
"pageSize": {"20"},
"language": {"1"},
"toPageNum": {"1"},
"order": {"1"},
}, &data); err != nil {
return nil, err
}
Expand All @@ -65,7 +104,7 @@ func (h *Client) GetPlantList() (*PlantList, error) {

func (h *Client) GetNoahPlantInfo(plantId string) (*NoahPlantInfo, error) {
var data NoahPlantInfo
if _, err := h.postForm("https://openapi.growatt.com/noahDeviceApi/noah/isPlantNoahSystem", url.Values{
if _, err := h.postForm(growattUrl+"/noahDeviceApi/noah/isPlantNoahSystem", url.Values{
"plantId": {plantId},
}, &data); err != nil {
return nil, err
Expand All @@ -75,7 +114,7 @@ func (h *Client) GetNoahPlantInfo(plantId string) (*NoahPlantInfo, error) {

func (h *Client) GetNoahStatus(serialNumber string) (*NoahStatus, error) {
var data NoahStatus
if _, err := h.postForm("https://openapi.growatt.com/noahDeviceApi/noah/getSystemStatus", url.Values{
if _, err := h.postForm(growattUrl+"/noahDeviceApi/noah/getSystemStatus", url.Values{
"deviceSn": {serialNumber},
}, &data); err != nil {
return nil, err
Expand All @@ -85,7 +124,7 @@ func (h *Client) GetNoahStatus(serialNumber string) (*NoahStatus, error) {

func (h *Client) GetNoahInfo(serialNumber string) (*NoahInfo, error) {
var data NoahInfo
if _, err := h.postForm("https://openapi.growatt.com/noahDeviceApi/noah/getNoahInfoBySn", url.Values{
if _, err := h.postForm(growattUrl+"/noahDeviceApi/noah/getNoahInfoBySn", url.Values{
"deviceSn": {serialNumber},
}, &data); err != nil {
return nil, err
Expand All @@ -96,11 +135,41 @@ func (h *Client) GetNoahInfo(serialNumber string) (*NoahInfo, error) {

func (h *Client) GetBatteryData(serialNumber string) (*BatteryInfo, error) {
var data BatteryInfo
if _, err := h.postForm("https://openapi.growatt.com/noahDeviceApi/noah/getBatteryData", url.Values{
if _, err := h.postForm(growattUrl+"/noahDeviceApi/noah/getBatteryData", url.Values{
"deviceSn": {serialNumber},
}, &data); err != nil {
return nil, err
}

return &data, nil
}

func (h *Client) SetDefaultPower(serialNumber string, power float64) error {
p := math.Max(10, math.Min(800, power))
var data map[string]any
if _, err := h.postForm(growattUrl+"/noahDeviceApi/noah/set", url.Values{
"serialNum": {serialNumber},
"type": {"default_power"},
"param1": {fmt.Sprintf("%.0f", p)},
}, &data); err != nil {
return err
}

return nil
}

func (h *Client) SetSocLimit(serialNumber string, chargingLimit float64, dischargeLimit float64) error {
c := math.Max(70, math.Min(100, chargingLimit))
d := math.Max(0, math.Min(30, dischargeLimit))
var data map[string]any
if _, err := h.postForm(growattUrl+"/noahDeviceApi/noah/set", url.Values{
"serialNum": {serialNumber},
"type": {"charging_soc"},
"param1": {fmt.Sprintf("%.0f", c)},
"param2": {fmt.Sprintf("%.0f", d)},
}, &data); err != nil {
return err
}

return nil
}
51 changes: 4 additions & 47 deletions internal/growatt/client_http.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,59 +10,16 @@ import (
"time"
)

func (h *Client) get(urlStr string, query url.Values, responseBody any) (*http.Response, error) {
u, err := url.Parse(urlStr)
if err != nil {
return nil, err
}
u.RawQuery = query.Encode()

req, err := http.NewRequest("GET", u.String(), nil)
if err != nil {
return nil, err
}

resp, err := h.client.Do(req)
if err != nil {
return nil, err
}

defer func(Body io.ReadCloser) {
_ = Body.Close()
}(resp.Body)

b, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}

if resp.StatusCode != 200 {
return nil, fmt.Errorf("request failed: (HTTP %s) %s", resp.Status, string(b))
}

if responseBody != nil {
if err := json.Unmarshal(b, &responseBody); err != nil {
if strings.Contains(err.Error(), "invalid character '<' looking for beginning of value") {
if err := h.Login(); err != nil {
<-time.After(60 * time.Second)
panic(err)
}
return h.get(urlStr, query, responseBody)
} else {
return nil, err
}
}
}

return resp, nil
}

func (h *Client) postForm(url string, data url.Values, responseBody any) (*http.Response, error) {
req, err := http.NewRequest("POST", url, strings.NewReader(data.Encode()))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
if len(h.token) > 0 {
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", h.token))
}
req.Header.Set("User-Agent", "Dalvik/2.1.0 (Linux; U; Android 9; Mi A1 MIUI/V10.0.24.0.PDHMIXM)")
resp, err := h.client.Do(req)
if err != nil {
return nil, err
Expand Down
67 changes: 67 additions & 0 deletions internal/growatt/login_helper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package growatt

import (
"crypto/md5"
"encoding/binary"
"encoding/hex"
"fmt"
"github.com/google/uuid"
"strconv"
"time"
)

func hashPassword(password string) string {
hash := md5.Sum([]byte(password))
digest := hex.EncodeToString(hash[:])

for i := 0; i < len(digest); i = i + 2 {
if digest[i] == '0' {
digest = digest[:i] + "c" + digest[i+1:]
}
}

return digest
}

func timestamp() string {
// Get the current time in milliseconds and convert it to a string
valueOf := strconv.FormatInt(time.Now().UnixMilli(), 10)

// Extract characters at positions 1, 3, 5, and 7
extracted := string(valueOf[1]) + string(valueOf[3]) + string(valueOf[5]) + string(valueOf[7])

// Parse the extracted string to an integer and take modulo 98
parsedInt, _ := strconv.Atoi(extracted)
parsedInt %= 98

// Start building the final string
result := valueOf[:11]

// Conditionally format the parsed integer
if parsedInt < 10 {
result += "0" + strconv.Itoa(parsedInt)
} else {
result += strconv.Itoa(parsedInt)
}

return result
}

func ipvcpc(username string) string {
hash := []byte(hashPassword(username + "★☆i₰₭" + fmt.Sprintf("%d", time.Now().UnixMilli())))
hashCode := int64(binary.LittleEndian.Uint64(hash[:8]))

// Create a UUID using the hash code as the most significant bits and a fixed number for the least significant bits.
u := uuid.New()
u[0] = byte(hashCode >> 56)
u[1] = byte(hashCode >> 48)
u[2] = byte(hashCode >> 40)
u[3] = byte(hashCode >> 32)
u[4] = byte(hashCode >> 24)
u[5] = byte(hashCode >> 16)
u[6] = byte(hashCode >> 8)
u[7] = byte(hashCode)

// Convert to string and return.
return u.String()
}
Loading

0 comments on commit e876b64

Please sign in to comment.