diff --git a/go.mod b/go.mod index b119d2f..3fb28db 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index c32683a..71bfb80 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/growatt/client.go b/internal/growatt/client.go index 3ca4b89..9711d7e 100644 --- a/internal/growatt/client.go +++ b/internal/growatt/client.go @@ -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 } @@ -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 } @@ -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 } @@ -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 @@ -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 @@ -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 @@ -96,7 +135,7 @@ 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 @@ -104,3 +143,33 @@ func (h *Client) GetBatteryData(serialNumber string) (*BatteryInfo, error) { 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 +} diff --git a/internal/growatt/client_http.go b/internal/growatt/client_http.go index b1adaff..cea5eb5 100644 --- a/internal/growatt/client_http.go +++ b/internal/growatt/client_http.go @@ -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 diff --git a/internal/growatt/login_helper.go b/internal/growatt/login_helper.go new file mode 100644 index 0000000..bef3e89 --- /dev/null +++ b/internal/growatt/login_helper.go @@ -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() +} diff --git a/internal/growatt/models.go b/internal/growatt/models.go index adc5de3..d9f5c1f 100644 --- a/internal/growatt/models.go +++ b/internal/growatt/models.go @@ -1,5 +1,11 @@ package growatt +type TokenResponse struct { + Code int `json:"code"` + Data string `json:"data"` + Token string `json:"token"` +} + type LoginResult struct { Back struct { Msg string `json:"msg"` @@ -10,13 +16,151 @@ type LoginResult struct { } `json:"back"` } -type PlantList struct { - Back struct { - Data []struct { - PlantID string `json:"plantId"` - } `json:"data"` - Success bool `json:"success"` - } `json:"back"` +type PlantListV2 struct { + PlantList []struct { + FormulaCo2 int `json:"formulaCo2"` + CompanyName string `json:"companyName"` + EtodayCo2Text string `json:"etodayCo2Text"` + UserBean interface{} `json:"userBean"` + FormulaSo2 int `json:"formulaSo2"` + GridPort string `json:"gridPort"` + Children []interface{} `json:"children"` + PlantFromBean interface{} `json:"plantFromBean"` + ID int `json:"id"` + EYearMoneyText string `json:"EYearMoneyText"` + TempType int `json:"tempType"` + EtotalCoalText string `json:"etotalCoalText"` + EtotalSo2Text string `json:"etotalSo2Text"` + PlantLng string `json:"plant_lng"` + LocationImgName string `json:"locationImgName"` + DeviceCount int `json:"deviceCount"` + MapCountryID int `json:"map_countryId"` + MapLat string `json:"mapLat"` + PrMonth string `json:"prMonth"` + EtotalMoney int `json:"etotalMoney"` + PlantType int `json:"plantType"` + WindAngle int `json:"windAngle"` + FormulaMoney int `json:"formulaMoney"` + MapCity string `json:"mapCity"` + NominalPower int `json:"nominalPower"` + LogoImgName string `json:"logoImgName"` + LatitudeText string `json:"latitudeText"` + UserAccount string `json:"userAccount"` + StorageTodayToUser int `json:"storage_TodayToUser"` + EventMessBeanList []interface{} `json:"eventMessBeanList"` + MapCityID int `json:"map_cityId"` + CreateDateTextA string `json:"createDateTextA"` + Status int `json:"status"` + FormulaMoneyUnitID string `json:"formulaMoneyUnitId"` + EnergyMonth int `json:"energyMonth"` + City string `json:"city"` + PrToday string `json:"prToday"` + EtodayCoalText string `json:"etodayCoalText"` + CurrentPac int `json:"currentPac"` + ParentID string `json:"parentID"` + PlantAddress string `json:"plantAddress"` + EnvTemp int `json:"envTemp"` + FormulaCoal int `json:"formulaCoal"` + TreeID string `json:"treeID"` + HasStorage int `json:"hasStorage"` + StorageTotalToUser int `json:"storage_TotalToUser"` + FixedPowerPrice int `json:"fixedPowerPrice"` + EtodaySo2Text string `json:"etodaySo2Text"` + PanelTemp int `json:"panelTemp"` + CreateDate struct { + Date int `json:"date"` + Hours int `json:"hours"` + Seconds int `json:"seconds"` + Month int `json:"month"` + TimezoneOffset int `json:"timezoneOffset"` + Year int `json:"year"` + Minutes int `json:"minutes"` + Time int64 `json:"time"` + Day int `json:"day"` + } `json:"createDate"` + MapProvinceID int `json:"map_provinceId"` + PairViewUserAccount string `json:"pairViewUserAccount"` + EmonthSo2Text string `json:"emonthSo2Text"` + PeakPeriodPrice int `json:"peakPeriodPrice"` + HasDeviceOnLine int `json:"hasDeviceOnLine"` + StorageBattoryPercentage int `json:"storage_BattoryPercentage"` + EtodayMoney int `json:"etodayMoney"` + FormulaTree int `json:"formulaTree"` + PlantNmi string `json:"plantNmi"` + ProtocolID string `json:"protocolId"` + GridServerURL string `json:"gridServerUrl"` + MoneyUnitText string `json:"moneyUnitText"` + LongitudeD string `json:"longitude_d"` + Country string `json:"country"` + LongitudeF string `json:"longitude_f"` + EtodayMoneyText string `json:"etodayMoneyText"` + LongitudeM string `json:"longitude_m"` + PhoneNum string `json:"phoneNum"` + StorageTodayToGrid int `json:"storage_TodayToGrid"` + DesignCompany string `json:"designCompany"` + InstallMapName string `json:"installMapName"` + CurrentPacStr string `json:"currentPacStr"` + EtotalMoneyText string `json:"etotalMoneyText"` + WindSpeed int `json:"windSpeed"` + ValleyPeriodPrice int `json:"valleyPeriodPrice"` + LatitudeF string `json:"latitude_f"` + MapLng string `json:"mapLng"` + LatitudeD string `json:"latitude_d"` + Level int `json:"level"` + LatitudeM string `json:"latitude_m"` + EnergyYear int `json:"energyYear"` + LongitudeText string `json:"longitudeText"` + FlatPeriodPrice int `json:"flatPeriodPrice"` + EmonthCoalText string `json:"emonthCoalText"` + ParamBean interface{} `json:"paramBean"` + EtotalCo2Text string `json:"etotalCo2Text"` + ImgPath string `json:"imgPath"` + IsShare bool `json:"isShare"` + PlantLat string `json:"plant_lat"` + EmonthCo2Text string `json:"emonthCo2Text"` + Timezone int `json:"timezone"` + GridCompany string `json:"gridCompany"` + StorageEChargeToday int `json:"storage_eChargeToday"` + Remark string `json:"remark"` + StorageTotalToGrid int `json:"storage_TotalToGrid"` + DefaultPlant bool `json:"defaultPlant"` + CreateDateText string `json:"createDateText"` + CurrentPacTxt string `json:"currentPacTxt"` + UnitMap interface{} `json:"unitMap"` + AlarmValue int `json:"alarmValue"` + TreeName string `json:"treeName"` + Alias string `json:"alias"` + Irradiance int `json:"irradiance"` + FormulaMoneyStr string `json:"formulaMoneyStr"` + OnLineEnvCount int `json:"onLineEnvCount"` + StorageEDisChargeToday int `json:"storage_eDisChargeToday"` + TimezoneText string `json:"timezoneText"` + DataLogList []interface{} `json:"dataLogList"` + MapAreaID int `json:"map_areaId"` + EtotalFormulaTreeText string `json:"etotalFormulaTreeText"` + PlantImgName string `json:"plantImgName"` + EToday float64 `json:"eToday"` + ETotal float64 `json:"eTotal"` + EmonthMoneyText string `json:"emonthMoneyText"` + NominalPowerStr string `json:"nominalPowerStr"` + PlantName string `json:"plantName"` + } `json:"PlantList"` + StatusMap struct { + Offline int `json:"offline"` + FaultNum int `json:"faultNum"` + OnlineNum int `json:"onlineNum"` + AllNum int `json:"allNum"` + } `json:"statusMap"` + UsereTotalMoney int `json:"usereTotalMoney"` + UsereTotal float64 `json:"usereTotal"` + PlantNum int `json:"plantNum"` + CurrentPageNum int `json:"currentPageNum"` + UsereTodayMoney int `json:"usereTodayMoney"` + UsercurrentPac int `json:"usercurrentPac"` + UsernominalPower int `json:"usernominalPower"` + TotalPageNum int `json:"totalPageNum"` + MoneyUnitText string `json:"moneyUnitText"` + UsereToday float64 `json:"usereToday"` } type ResponseContainerV2[T any] struct { diff --git a/internal/growatt/password.go b/internal/growatt/password.go deleted file mode 100644 index 31ee259..0000000 --- a/internal/growatt/password.go +++ /dev/null @@ -1,19 +0,0 @@ -package growatt - -import ( - "crypto/md5" - "encoding/hex" -) - -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 -} diff --git a/internal/polling/parameters.go b/internal/polling/parameters.go new file mode 100644 index 0000000..f8ff853 --- /dev/null +++ b/internal/polling/parameters.go @@ -0,0 +1,34 @@ +package polling + +import ( + "encoding/json" + mqtt "github.com/eclipse/paho.mqtt.golang" + "log/slog" + "noah-mqtt/pkg/models" +) + +func (s *Service) parametersSubscription(sn string) func(client mqtt.Client, message mqtt.Message) { + return func(client mqtt.Client, message mqtt.Message) { + var payload models.ParameterPayload + if err := json.Unmarshal(message.Payload(), &payload); err != nil { + slog.Error("unable to unmarshal parameter command payload", slog.String("error", err.Error())) + } + + if payload.OutputPower != nil { + if err := s.options.GrowattClient.SetDefaultPower(sn, *payload.OutputPower); err != nil { + slog.Error("unable to set default power", slog.String("error", err.Error()), slog.String("device", sn)) + } else { + slog.Info("set default power", slog.String("device", sn), slog.Int("power", int(*payload.OutputPower))) + } + } + + if payload.ChargingLimit != nil && payload.DischargeLimit != nil { + if err := s.options.GrowattClient.SetSocLimit(sn, *payload.ChargingLimit, *payload.DischargeLimit); err != nil { + slog.Error("unable to set charging/discharge limit", slog.String("error", err.Error())) + } else { + slog.Info("set charging/discharge limit", slog.String("device", sn), slog.Float64("chargingLimit", *payload.ChargingLimit), slog.Float64("dischargeLimit", *payload.DischargeLimit)) + } + } + + } +} diff --git a/internal/polling/payload.go b/internal/polling/payload.go index 7f449ec..ab71f22 100644 --- a/internal/polling/payload.go +++ b/internal/polling/payload.go @@ -16,6 +16,7 @@ func devicePayload(n *growatt.NoahStatus) models.DevicePayload { BatteryNum: int(parseFloat(n.Obj.BatteryNum)), GenerationTotalEnergy: parseFloat(n.Obj.EacTotal), GenerationTodayEnergy: parseFloat(n.Obj.EacToday), + WorkMode: workModeFromString(n.Obj.WorkMode), } } @@ -42,11 +43,14 @@ func batteryPayload(n *growatt.BatteryDetails) models.BatteryPayload { } } -func parameterPayload(n *growatt.NoahInfo, workMode string) models.ParameterPayload { +func parameterPayload(n *growatt.NoahInfo) models.ParameterPayload { + chargingLimit := parseFloat(n.Obj.Noah.ChargingSocHighLimit) + dischargeLimit := parseFloat(n.Obj.Noah.ChargingSocLowLimit) + outputPower := parseFloat(n.Obj.Noah.DefaultPower) + return models.ParameterPayload{ - ChargingLimit: parseFloat(n.Obj.Noah.ChargingSocHighLimit), - DischargeLimit: parseFloat(n.Obj.Noah.ChargingSocLowLimit), - OutputPower: parseFloat(n.Obj.Noah.DefaultPower), - WorkMode: workModeFromString(workMode), + ChargingLimit: &chargingLimit, + DischargeLimit: &dischargeLimit, + OutputPower: &outputPower, } } diff --git a/internal/polling/service.go b/internal/polling/service.go index c148c2d..90609e6 100644 --- a/internal/polling/service.go +++ b/internal/polling/service.go @@ -68,7 +68,16 @@ func (s *Service) enumerateDevices() { } } + for _, sn := range s.serialNumbers { + s.options.MqttClient.Unsubscribe(s.parameterCommandTopic(sn)) + } + s.serialNumbers = serialNumbers + + for _, sn := range s.serialNumbers { + s.options.MqttClient.Subscribe(s.parameterCommandTopic(sn), 0, s.parametersSubscription(sn)) + } + s.options.HaClient.SetDevices(devices) } @@ -84,6 +93,10 @@ func (s *Service) parameterStateTopic(serialNumber string) string { return fmt.Sprintf("%s/%s/parameters", s.options.TopicPrefix, serialNumber) } +func (s *Service) parameterCommandTopic(serialNumber string) string { + return fmt.Sprintf("%s/%s/parameters/set", s.options.TopicPrefix, serialNumber) +} + func (s *Service) fetchNoahSerialNumbers() []string { slog.Info("fetching plant list") list, err := s.options.GrowattClient.GetPlantList() @@ -94,14 +107,14 @@ func (s *Service) fetchNoahSerialNumbers() []string { var serialNumbers []string - for _, plant := range list.Back.Data { - slog.Info("fetch plant details", slog.String("plantId", plant.PlantID)) - if info, err := s.options.GrowattClient.GetNoahPlantInfo(plant.PlantID); err != nil { - slog.Error("could not get plant info", slog.String("plantId", plant.PlantID), slog.String("error", err.Error())) + for _, plant := range list.PlantList { + slog.Info("fetch plant details", slog.Int("plantId", plant.ID)) + if info, err := s.options.GrowattClient.GetNoahPlantInfo(fmt.Sprintf("%d", plant.ID)); err != nil { + slog.Error("could not get plant info", slog.Int("plantId", plant.ID), slog.String("error", err.Error())) } else { if len(info.Obj.DeviceSn) > 0 { serialNumbers = append(serialNumbers, info.Obj.DeviceSn) - slog.Info("found device sn", slog.String("deviceSn", info.Obj.DeviceSn), slog.String("plantId", plant.PlantID), slog.String("topic", s.deviceStateTopic(info.Obj.DeviceSn))) + slog.Info("found device sn", slog.String("deviceSn", info.Obj.DeviceSn), slog.Int("plantId", plant.ID), slog.String("topic", s.deviceStateTopic(info.Obj.DeviceSn))) } } } @@ -119,12 +132,9 @@ func (s *Service) poll() { slog.Info("start polling growatt", slog.Int("interval", int(s.options.PollingInterval/time.Second))) for { for _, serialNumber := range s.serialNumbers { - var workMode string - if data, err := s.options.GrowattClient.GetNoahStatus(serialNumber); err != nil { slog.Error("could not get device data", slog.String("error", err.Error()), slog.String("device", serialNumber)) } else { - workMode = data.Obj.WorkMode if b, err := json.Marshal(devicePayload(data)); err != nil { slog.Error("could not marshal device data", slog.String("error", err.Error()), slog.String("device", serialNumber)) } else { @@ -149,7 +159,7 @@ func (s *Service) poll() { if data, err := s.options.GrowattClient.GetNoahInfo(serialNumber); err != nil { slog.Error("could not get parameter data", slog.String("error", err.Error()), slog.String("device", serialNumber)) } else { - if b, err := json.Marshal(parameterPayload(data, workMode)); err != nil { + if b, err := json.Marshal(parameterPayload(data)); err != nil { slog.Error("could not marshal parameter data", slog.String("error", err.Error()), slog.String("device", serialNumber)) } else { s.options.MqttClient.Publish(s.parameterStateTopic(serialNumber), 0, false, string(b)) diff --git a/pkg/models/payload.go b/pkg/models/payload.go index ab0c145..ada82da 100644 --- a/pkg/models/payload.go +++ b/pkg/models/payload.go @@ -8,14 +8,15 @@ const ( ) type DevicePayload struct { - OutputPower float64 `json:"output_w"` - SolarPower float64 `json:"solar_w"` - Soc float64 `json:"soc"` - ChargePower float64 `json:"charge_w"` - DischargePower float64 `json:"discharge_w"` - BatteryNum int `json:"battery_num"` - GenerationTotalEnergy float64 `json:"generation_total_kwh"` - GenerationTodayEnergy float64 `json:"generation_today_kwh"` + OutputPower float64 `json:"output_w"` + SolarPower float64 `json:"solar_w"` + Soc float64 `json:"soc"` + ChargePower float64 `json:"charge_w"` + DischargePower float64 `json:"discharge_w"` + BatteryNum int `json:"battery_num"` + GenerationTotalEnergy float64 `json:"generation_total_kwh"` + GenerationTodayEnergy float64 `json:"generation_today_kwh"` + WorkMode WorkMode `json:"work_mode,omitempty,omitempty"` } type BatteryPayload struct { @@ -25,8 +26,7 @@ type BatteryPayload struct { } type ParameterPayload struct { - ChargingLimit float64 `json:"charging_limit"` - DischargeLimit float64 `json:"discharge_limit"` - OutputPower float64 `json:"output_power_w"` - WorkMode WorkMode `json:"work_mode,omitempty"` + ChargingLimit *float64 `json:"charging_limit,omitempty"` + DischargeLimit *float64 `json:"discharge_limit,omitempty"` + OutputPower *float64 `json:"output_power_w,omitempty"` }