Skip to content

Commit

Permalink
Add Zendure (#17149)
Browse files Browse the repository at this point in the history
  • Loading branch information
andig authored Nov 9, 2024
1 parent 355c0e3 commit 189a3cf
Show file tree
Hide file tree
Showing 5 changed files with 270 additions and 0 deletions.
71 changes: 71 additions & 0 deletions meter/zendure.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package meter

import (
"fmt"
"time"

"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/meter/zendure"
"github.com/evcc-io/evcc/util"
)

func init() {
registry.Add("zendure", NewZendureFromConfig)
}

type Zendure struct {
usage string
conn *zendure.Connection
}

// NewZendureFromConfig creates a Zendure meter from generic config
func NewZendureFromConfig(other map[string]interface{}) (api.Meter, error) {
cc := struct {
Usage, Account, Serial string
Timeout time.Duration
}{
Timeout: 30 * time.Second,
}

if err := util.DecodeOther(other, &cc); err != nil {
return nil, err
}

conn, err := zendure.NewConnection(cc.Account, cc.Serial, cc.Timeout)
if err != nil {
return nil, err
}

c := &Zendure{
usage: cc.Usage,
conn: conn,
}

return c, err
}

// CurrentPower implements the api.Meter interface
func (c *Zendure) CurrentPower() (float64, error) {
res, err := c.conn.Data()
if err != nil {
return 0, err
}

switch c.usage {
case "pv":
return float64(res.SolarInputPower), nil
case "battery":
return float64(res.GridInputPower) - float64(res.OutputHomePower), nil
default:
return 0, fmt.Errorf("invalid usage: %s", c.usage)
}
}

// Soc implements the api.Battery interface
func (c *Zendure) Soc() (float64, error) {
res, err := c.conn.Data()
if err != nil {
return 0, err
}
return float64(res.ElectricLevel), nil
}
86 changes: 86 additions & 0 deletions meter/zendure/connection.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package zendure

import (
"encoding/json"
"net"
"strconv"
"sync"
"time"

"dario.cat/mergo"
"github.com/evcc-io/evcc/provider/mqtt"
"github.com/evcc-io/evcc/util"
)

var (
mu sync.Mutex
connections = make(map[string]*Connection)
)

type Connection struct {
log *util.Logger
data *util.Monitor[Data]
}

func NewConnection(account, serial string, timeout time.Duration) (*Connection, error) {
mu.Lock()
defer mu.Unlock()

key := account + serial
if conn, ok := connections[key]; ok {
return conn, nil
}

res, err := MqttCredentials(account, serial)
if err != nil {
return nil, err
}

log := util.NewLogger("zendure")
client, err := mqtt.NewClient(
log,
net.JoinHostPort(res.Data.MqttUrl, strconv.Itoa(res.Data.Port)), res.Data.AppKey, res.Data.Secret,
"", 0, false, "", "", "",
)
if err != nil {
return nil, err
}

conn := &Connection{
log: log,
data: util.NewMonitor[Data](timeout),
}

topic := res.Data.AppKey + "/#"
if err := client.Listen(topic, conn.handler); err != nil {
return nil, err
}

connections[key] = conn

return conn, nil
}

func (c *Connection) handler(data string) {
var res Payload
if err := json.Unmarshal([]byte(data), &res); err != nil {
c.log.ERROR.Println(err)
return
}

if res.Data == nil {
return
}

c.data.SetFunc(func(v Data) Data {
if err := mergo.Merge(&v, res.Data); err != nil {
c.log.ERROR.Println(err)
}

return v
})
}

func (c *Connection) Data() (Data, error) {
return c.data.Get()
}
31 changes: 31 additions & 0 deletions meter/zendure/credentials.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package zendure

import (
"errors"
"net/http"

"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/request"
)

const CredentialsUri = "https://app.zendure.tech/eu/developer/api/apply"

func MqttCredentials(account, serial string) (CredentialsResponse, error) {
client := request.NewHelper(util.NewLogger("zendure"))

data := CredentialsRequest{
SnNumber: serial,
Account: account,
}

req, _ := request.New(http.MethodPost, CredentialsUri, request.MarshalJSON(data), request.JSONEncoding)

var res CredentialsResponse
err := client.DoJSON(req, &res)

if err == nil && !res.Success {
err = errors.New(res.Msg)
}

return res, err
}
60 changes: 60 additions & 0 deletions meter/zendure/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package zendure

type CredentialsRequest struct {
SnNumber string `json:"snNumber"`
Account string `json:"account"`
}

type CredentialsResponse struct {
Success bool `json:"success"`
Data struct {
AppKey string `json:"appKey"`
Secret string `json:"secret"`
MqttUrl string `json:"mqttUrl"`
Port int `json:"port"`
}
Msg string `json:"msg"`
}

type Payload struct {
*Command
*Data
}

type Command struct {
CommandTopic string `json:"command_topic"`
DeviceClass string `json:"device_class"`
Name string `json:"name"`
PayloadOff bool `json:"payload_off"`
PayloadOn bool `json:"payload_on"`
StateOff bool `json:"state_off"`
StateOn bool `json:"state_on"`
StateTopic string `json:"state_topic"`
UniqueId string `json:"unique_id"`
UnitOfMeasurement string `json:"unit_of_measurement"`
ValueTemplate string `json:"value_template"`
}

type Data struct {
AcMode int `json:"acMode"` // 1,
BuzzerSwitch bool `json:"buzzerSwitch"` // false,
ElectricLevel int `json:"electricLevel"` // 7,
GridInputPower int `json:"gridInputPower"` // 99,
HeatState int `json:"heatState"` // 0,
HubState int `json:"hubState"` // 0,
HyperTmp int `json:"hyperTmp"` // 2981,
InputLimit int `json:"inputLimit"` // 100,
InverseMaxPower int `json:"inverseMaxPower"` // 1200,
MasterSwitch bool `json:"masterSwitch"` // true,
OutputLimit int `json:"outputLimit"` // 0,
OutputPackPower int `json:"outputPackPower"` // 70,
OutputHomePower int `json:"outputHomePower"` // 70,
PackNum int `json:"packNum"` // 1,
PackState int `json:"packState"` // 0,
RemainInputTime int `json:"remainInputTime"` // 59940,
RemainOutTime int `json:"remainOutTime"` // 59940,
Sn string `json:"sn"` // "EE1LH",
SocSet int `json:"socSet"` // 1000,
SolarInputPower int `json:"solarInputPower"` // 0,
WifiState bool `json:"wifiState"` // true
}
22 changes: 22 additions & 0 deletions templates/definition/meter/zendure.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
template: zendure
products:
- brand: Zendure
description:
generic: Hyper V
requirements:
evcc: ["skiptest"]
params:
- name: usage
choice: ["pv", "battery"]
- name: account
- name: serial
- name: capacity
default: 2
advanced: true
- name: timeout
render: |
type: zendure
usage: {{ .usage }}
account: {{ .account }}
serial: {{ .serial }}
timeout: {{ .timeout }}

0 comments on commit 189a3cf

Please sign in to comment.