Add Zendure (#17149)
This commit is contained in:
parent
355c0e3ffe
commit
189a3cf57f
5 changed files with 270 additions and 0 deletions
71
meter/zendure.go
Normal file
71
meter/zendure.go
Normal file
|
|
@ -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
meter/zendure/connection.go
Normal file
86
meter/zendure/connection.go
Normal file
|
|
@ -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
meter/zendure/credentials.go
Normal file
31
meter/zendure/credentials.go
Normal file
|
|
@ -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
meter/zendure/types.go
Normal file
60
meter/zendure/types.go
Normal file
|
|
@ -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
templates/definition/meter/zendure.yaml
Normal file
22
templates/definition/meter/zendure.yaml
Normal file
|
|
@ -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 }}
|
||||
Loading…
Add table
Add a link
Reference in a new issue