From a3427962898fb231c4996c9f07ce55dd5bb56a5f Mon Sep 17 00:00:00 2001 From: andig Date: Fri, 1 Nov 2024 17:15:58 +0100 Subject: [PATCH 01/11] Add Zendure --- meter/zendure/types.go | 17 +++++++++++++++++ meter/zendure/zendure.go | 26 ++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 meter/zendure/types.go create mode 100644 meter/zendure/zendure.go diff --git a/meter/zendure/types.go b/meter/zendure/types.go new file mode 100644 index 0000000000..41d38f3ad0 --- /dev/null +++ b/meter/zendure/types.go @@ -0,0 +1,17 @@ +package zendure + +type CredentialsRequest struct { + SnNumber string `json:"snNumber"` + Account string `json:"account"` +} + +type CredentialsResponse struct { + Success string `json:"success"` // true, + Data struct { + AppKey string `json:"appKey"` // "zendure", + Secret string `json:"secret"` // "zendureSecret", + MqttUrl string `json:"mqttUrl"` // "mqtt.zen-iot.com", + Port int `json:"port"` // 1883 + } + Msg string `json:"msg"` // "Successful operation" +} diff --git a/meter/zendure/zendure.go b/meter/zendure/zendure.go new file mode 100644 index 0000000000..e3443edc98 --- /dev/null +++ b/meter/zendure/zendure.go @@ -0,0 +1,26 @@ +package zendure + +import ( + "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(serial, account 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) + + return res, err +} From 5237fe9b7631030ceb0edf2c4233ad1beef9eb54 Mon Sep 17 00:00:00 2001 From: andig Date: Fri, 1 Nov 2024 17:34:46 +0100 Subject: [PATCH 02/11] wip --- templates/definition/meter/zendure.yaml | 34 +++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 templates/definition/meter/zendure.yaml diff --git a/templates/definition/meter/zendure.yaml b/templates/definition/meter/zendure.yaml new file mode 100644 index 0000000000..e0564108c9 --- /dev/null +++ b/templates/definition/meter/zendure.yaml @@ -0,0 +1,34 @@ +template: zendure +products: + - brand: Zendure + description: + generic: Hyper V +requirements: + # description: + # de: | + # Zur Erfassung der PV-Produktion wird ein extern angebundenener S0-Erzeugungszähler benötigt. + # en: | + # An externally connected S0 generation meter is required to record the solar production. +params: + - name: usage + choice: ["battery"] + - name: host + - name: topic + - name: capacity + default: 2 + advanced: true +render: | + type: custom + {{- if eq .usage "battery" }} + power: + source: mqtt + topic: {{ .topic }}/state + jq: .outputHomePower // if .gridInputPower > 0 then -.gridInputPower else 0 end + # timeout: 5m + soc: + source: mqtt + topic: {{ .topic }}/state + jq: .electricLevel // 0 + # timeout: 30m + capacity: {{ .capacity }} # kWh + {{- end}} From 628fbbf0d8c2c9d8fc687758f27ba352c6cd0949 Mon Sep 17 00:00:00 2001 From: andig Date: Fri, 1 Nov 2024 17:57:11 +0100 Subject: [PATCH 03/11] wip --- provider/collect.go | 148 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 provider/collect.go diff --git a/provider/collect.go b/provider/collect.go new file mode 100644 index 0000000000..0f17af1d2d --- /dev/null +++ b/provider/collect.go @@ -0,0 +1,148 @@ +package provider + +import ( + "context" + "encoding/json" + "fmt" + "math" + "strconv" + "time" + + "dario.cat/mergo" + "github.com/evcc-io/evcc/util" + "github.com/evcc-io/evcc/util/request" + "github.com/spf13/cast" +) + +// Collect collects and combines JSON maps +type Collect struct { + log *util.Logger + get func() (string, error) + data map[string]any + cache time.Duration + timeout time.Duration + updated time.Time + scale float64 +} + +func init() { + registry.AddCtx("collect", NewCollectProviderFromConfig) +} + +// NewCollectProviderFromConfig creates a collect provider. +func NewCollectProviderFromConfig(ctx context.Context, other map[string]interface{}) (Provider, error) { + cc := struct { + Get Config + Scale float64 + Timeout time.Duration + Cache time.Duration + }{ + Timeout: request.Timeout, + Scale: 1, + } + + if err := util.DecodeOther(other, &cc); err != nil { + return nil, err + } + + g, err := NewStringGetterFromConfig(ctx, cc.Get) + if err != nil { + return nil, fmt.Errorf("get: %w", err) + } + + p, err := NewCollectProvider(g, cc.Timeout, cc.Scale, cc.Cache) + + return p, err +} + +// NewCollectProvider creates a collect provider. +// Collect execution is aborted after given timeout. +func NewCollectProvider(get func() (string, error), timeout time.Duration, scale float64, cache time.Duration) (*Collect, error) { + s := &Collect{ + log: util.NewLogger("collect"), + get: get, + data: make(map[string]any), + cache: cache, + timeout: timeout, + scale: scale, + } + + return s, nil +} + +func (p *Collect) update() error { + v, err := p.get() + if err != nil { + return err + } + + var new map[string]any + if err := json.Unmarshal([]byte(v), &new); err != nil { + return err + } + + return mergo.Merge(&p.data, v, mergo.WithOverride) +} + +var _ StringProvider = (*Collect)(nil) + +// StringGetter returns string from exec result. Only STDOUT is considered. +func (p *Collect) StringGetter() (func() (string, error), error) { + return func() (string, error) { + if err := p.update(); err != nil { + return "", err + } + + b, err := json.Marshal(p.data) + return string(b), err + }, nil +} + +var _ FloatProvider = (*Collect)(nil) + +// FloatGetter parses float from exec result +func (p *Collect) FloatGetter() (func() (float64, error), error) { + g, err := p.StringGetter() + + return func() (float64, error) { + s, err := g() + if err != nil { + return 0, err + } + + f, err := strconv.ParseFloat(s, 64) + if err == nil { + f *= p.scale + } + + return f, err + }, err +} + +var _ IntProvider = (*Collect)(nil) + +// IntGetter parses int64 from exec result +func (p *Collect) IntGetter() (func() (int64, error), error) { + g, err := p.FloatGetter() + + return func() (int64, error) { + f, err := g() + return int64(math.Round(f)), err + }, err +} + +var _ BoolProvider = (*Collect)(nil) + +// BoolGetter parses bool from exec result. "on", "true" and 1 are considered truish. +func (p *Collect) BoolGetter() (func() (bool, error), error) { + g, err := p.StringGetter() + + return func() (bool, error) { + s, err := g() + if err != nil { + return false, err + } + + return cast.ToBoolE(s) + }, err +} From 997413b9956b591aba5259b622b5d215b0d739c7 Mon Sep 17 00:00:00 2001 From: andig Date: Sun, 3 Nov 2024 12:05:58 +0100 Subject: [PATCH 04/11] wip --- provider/collect.go | 62 +++------------------------------------------ 1 file changed, 4 insertions(+), 58 deletions(-) diff --git a/provider/collect.go b/provider/collect.go index 0f17af1d2d..aa96b5dc7b 100644 --- a/provider/collect.go +++ b/provider/collect.go @@ -4,25 +4,22 @@ import ( "context" "encoding/json" "fmt" - "math" - "strconv" "time" "dario.cat/mergo" "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/request" - "github.com/spf13/cast" ) // Collect collects and combines JSON maps type Collect struct { + *getter log *util.Logger get func() (string, error) data map[string]any cache time.Duration timeout time.Duration updated time.Time - scale float64 } func init() { @@ -33,12 +30,10 @@ func init() { func NewCollectProviderFromConfig(ctx context.Context, other map[string]interface{}) (Provider, error) { cc := struct { Get Config - Scale float64 Timeout time.Duration Cache time.Duration }{ Timeout: request.Timeout, - Scale: 1, } if err := util.DecodeOther(other, &cc); err != nil { @@ -50,21 +45,21 @@ func NewCollectProviderFromConfig(ctx context.Context, other map[string]interfac return nil, fmt.Errorf("get: %w", err) } - p, err := NewCollectProvider(g, cc.Timeout, cc.Scale, cc.Cache) + p, err := NewCollectProvider(g, cc.Timeout, cc.Cache) + p.getter = defaultGetters(p, 1) return p, err } // NewCollectProvider creates a collect provider. // Collect execution is aborted after given timeout. -func NewCollectProvider(get func() (string, error), timeout time.Duration, scale float64, cache time.Duration) (*Collect, error) { +func NewCollectProvider(get func() (string, error), timeout time.Duration, cache time.Duration) (*Collect, error) { s := &Collect{ log: util.NewLogger("collect"), get: get, data: make(map[string]any), cache: cache, timeout: timeout, - scale: scale, } return s, nil @@ -97,52 +92,3 @@ func (p *Collect) StringGetter() (func() (string, error), error) { return string(b), err }, nil } - -var _ FloatProvider = (*Collect)(nil) - -// FloatGetter parses float from exec result -func (p *Collect) FloatGetter() (func() (float64, error), error) { - g, err := p.StringGetter() - - return func() (float64, error) { - s, err := g() - if err != nil { - return 0, err - } - - f, err := strconv.ParseFloat(s, 64) - if err == nil { - f *= p.scale - } - - return f, err - }, err -} - -var _ IntProvider = (*Collect)(nil) - -// IntGetter parses int64 from exec result -func (p *Collect) IntGetter() (func() (int64, error), error) { - g, err := p.FloatGetter() - - return func() (int64, error) { - f, err := g() - return int64(math.Round(f)), err - }, err -} - -var _ BoolProvider = (*Collect)(nil) - -// BoolGetter parses bool from exec result. "on", "true" and 1 are considered truish. -func (p *Collect) BoolGetter() (func() (bool, error), error) { - g, err := p.StringGetter() - - return func() (bool, error) { - s, err := g() - if err != nil { - return false, err - } - - return cast.ToBoolE(s) - }, err -} From 316d04987086519f40c4f30326db486652a58649 Mon Sep 17 00:00:00 2001 From: andig Date: Sun, 3 Nov 2024 15:19:33 +0100 Subject: [PATCH 05/11] wip --- meter/zendure/types.go | 12 ++++++------ meter/zendure/zendure.go | 7 ++++++- zendure.go | 38 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 7 deletions(-) create mode 100644 zendure.go diff --git a/meter/zendure/types.go b/meter/zendure/types.go index 41d38f3ad0..a2afe6d3d1 100644 --- a/meter/zendure/types.go +++ b/meter/zendure/types.go @@ -6,12 +6,12 @@ type CredentialsRequest struct { } type CredentialsResponse struct { - Success string `json:"success"` // true, + Success bool `json:"success"` Data struct { - AppKey string `json:"appKey"` // "zendure", - Secret string `json:"secret"` // "zendureSecret", - MqttUrl string `json:"mqttUrl"` // "mqtt.zen-iot.com", - Port int `json:"port"` // 1883 + AppKey string `json:"appKey"` + Secret string `json:"secret"` + MqttUrl string `json:"mqttUrl"` + Port int `json:"port"` } - Msg string `json:"msg"` // "Successful operation" + Msg string `json:"msg"` } diff --git a/meter/zendure/zendure.go b/meter/zendure/zendure.go index e3443edc98..a64bdccd5d 100644 --- a/meter/zendure/zendure.go +++ b/meter/zendure/zendure.go @@ -1,6 +1,7 @@ package zendure import ( + "errors" "net/http" "github.com/evcc-io/evcc/util" @@ -9,7 +10,7 @@ import ( const CredentialsUri = "https://app.zendure.tech/eu/developer/api/apply" -func MqttCredentials(serial, account string) (CredentialsResponse, error) { +func MqttCredentials(account, serial string) (CredentialsResponse, error) { client := request.NewHelper(util.NewLogger("zendure")) data := CredentialsRequest{ @@ -22,5 +23,9 @@ func MqttCredentials(serial, account string) (CredentialsResponse, error) { var res CredentialsResponse err := client.DoJSON(req, &res) + if err == nil && !res.Success { + err = errors.New(res.Msg) + } + return res, err } diff --git a/zendure.go b/zendure.go new file mode 100644 index 0000000000..22428dddcf --- /dev/null +++ b/zendure.go @@ -0,0 +1,38 @@ +package main + +import ( + "fmt" + "net" + "os" + "strconv" + "time" + + "github.com/evcc-io/evcc/meter/zendure" + "github.com/evcc-io/evcc/provider/mqtt" + "github.com/evcc-io/evcc/util" + _ "github.com/joho/godotenv/autoload" +) + +func main() { + util.LogLevel("trace", nil) + + res, err := zendure.MqttCredentials(os.Getenv("ZENDURE_ACCOUNT"), os.Getenv("ZENDURE_SERIAL")) + if err != nil { + panic(err) + } + + fmt.Println(res) + + client, err := mqtt.NewClient(util.NewLogger("mqtt"), net.JoinHostPort(res.Data.MqttUrl, strconv.Itoa(res.Data.Port)), res.Data.AppKey, res.Data.Secret, "", 0, false, "", "", "") + if err != nil { + panic(err) + } + + if err := client.Listen("#", func(data string) { + fmt.Println(data) + }); err != nil { + panic(err) + } + + time.Sleep(time.Hour) +} From 2041adef047e6a6a285cefe4cd53650279ef6953 Mon Sep 17 00:00:00 2001 From: andig Date: Sun, 3 Nov 2024 18:36:30 +0100 Subject: [PATCH 06/11] wip --- meter/zendure/types.go | 16 +++++ zendure.go | 132 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 145 insertions(+), 3 deletions(-) diff --git a/meter/zendure/types.go b/meter/zendure/types.go index a2afe6d3d1..6a35648d84 100644 --- a/meter/zendure/types.go +++ b/meter/zendure/types.go @@ -15,3 +15,19 @@ type CredentialsResponse struct { } Msg string `json:"msg"` } + +type Command struct { + CommandTopic string `json:"command_topic"` + DeviceClass string `json:"device_class"` + ElectricLevel int `json:"electricLevel"` + Name string `json:"name"` + PayloadOff bool `json:"payload_off"` + PayloadOn bool `json:"payload_on"` + Sn string `json:"sn"` + 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"` +} diff --git a/zendure.go b/zendure.go index 22428dddcf..571b04544d 100644 --- a/zendure.go +++ b/zendure.go @@ -1,18 +1,99 @@ package main import ( + "encoding/json" "fmt" "net" + "net/http" "os" "strconv" + "strings" "time" + "dario.cat/mergo" "github.com/evcc-io/evcc/meter/zendure" "github.com/evcc-io/evcc/provider/mqtt" "github.com/evcc-io/evcc/util" _ "github.com/joho/godotenv/autoload" ) +// Topics + +// acOutputPower +// acSwitch +// buzzerSwitch +// electricLevel +// gridInputPower +// outputHomePower +// outputLimit +// passMode +// remainOutTime +// socSet +// solarInputPower +// solarPower1 +// solarPower2 + +// outputPackPower +// packData +// packInputPower +// packNum +// packState + +func gridPower() (int64, error) { + var state struct { + Result struct { + GridPower float64 `json:"gridPower"` + } + } + + resp, err := http.Get("http://nas.fritz.box:7070/api/state") + if err == nil { + err = json.NewDecoder(resp.Body).Decode(&state) + } + + return int64(state.Result.GridPower), err +} + +func evcc(client *mqtt.Client, commands map[string]zendure.Command) { + const ( + Margin = 100 + MaxCharge = -1000 + MaxDischarge = 500 + ) + + var grid int64 + + for range time.Tick(10 * time.Second) { + res, err := gridPower() + if err != nil { + fmt.Println(err) + continue + } + + new := grid + res + + if cmd, ok := commands["outputHomePower"]; ok && new >= 0 { + if new = max(0, min(new-Margin, MaxDischarge)); new != grid { + fmt.Println("vvv") + fmt.Println("set outputHomePower:", new) + + client.Publish(cmd.CommandTopic, false, strconv.FormatInt(new, 10)) + + grid = new + } + } else if cmd, ok := commands["gridInputPower"]; ok && new < 0 { + if new = min(0, max(new+Margin, MaxCharge)); new != grid { + fmt.Println("^^^") + fmt.Println("set gridInputPower:", new) + + client.Publish(cmd.CommandTopic, false, strconv.FormatInt(-new, 10)) + + grid = new + } + } + } +} + func main() { util.LogLevel("trace", nil) @@ -23,13 +104,58 @@ func main() { fmt.Println(res) - client, err := mqtt.NewClient(util.NewLogger("mqtt"), net.JoinHostPort(res.Data.MqttUrl, strconv.Itoa(res.Data.Port)), res.Data.AppKey, res.Data.Secret, "", 0, false, "", "", "") + client, err := mqtt.NewClient( + util.NewLogger("mqtt"), + net.JoinHostPort(res.Data.MqttUrl, strconv.Itoa(res.Data.Port)), res.Data.AppKey, res.Data.Secret, + "", 0, false, "", "", "", + ) if err != nil { panic(err) } - if err := client.Listen("#", func(data string) { - fmt.Println(data) + state := make(map[string]any) + commands := make(map[string]zendure.Command) + + go evcc(client, commands) + + topic := res.Data.AppKey + "/#" + + if err := client.Listen(topic, func(data string) { + fmt.Println(topic, ":", data) + + var cmd zendure.Command + err := json.Unmarshal([]byte(data), &cmd) + if err != nil { + panic(err) + } + + if full, ok := strings.CutSuffix(cmd.CommandTopic, "/set"); ok { + segs := strings.Split(full, "/") + key := segs[len(segs)-1] + + commands[key] = cmd + + if res, err := json.MarshalIndent(commands, "", " "); err == nil { + fmt.Println("===") + fmt.Println(string(res)) + } + + return + } + + var new map[string]any + if err := json.Unmarshal([]byte(data), &new); err != nil { + panic(err) + } + + if err := mergo.Merge(&state, new, mergo.WithOverride); err != nil { + panic(err) + } + + if res, err := json.MarshalIndent(state, "", " "); err == nil { + fmt.Println("---") + fmt.Println(string(res)) + } }); err != nil { panic(err) } From 8041803a33f13af70705bfcf05235ccf84848fe3 Mon Sep 17 00:00:00 2001 From: andig Date: Mon, 4 Nov 2024 09:19:54 +0100 Subject: [PATCH 07/11] wip --- zendure.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/zendure.go b/zendure.go index 571b04544d..bb9691e729 100644 --- a/zendure.go +++ b/zendure.go @@ -72,10 +72,10 @@ func evcc(client *mqtt.Client, commands map[string]zendure.Command) { new := grid + res - if cmd, ok := commands["outputHomePower"]; ok && new >= 0 { + if cmd, ok := commands["outputLimit"]; ok && new >= 0 { if new = max(0, min(new-Margin, MaxDischarge)); new != grid { fmt.Println("vvv") - fmt.Println("set outputHomePower:", new) + fmt.Println("set outputLimit:", new) client.Publish(cmd.CommandTopic, false, strconv.FormatInt(new, 10)) From cc731fae4a7e077c44a235c9bd760e5835169375 Mon Sep 17 00:00:00 2001 From: andig Date: Sat, 9 Nov 2024 12:17:14 +0100 Subject: [PATCH 08/11] wip --- meter/zendure.go | 72 ++++++++ meter/zendure/connection.go | 87 ++++++++++ meter/zendure/{zendure.go => credentials.go} | 0 meter/zendure/types.go | 31 +++- templates/definition/meter/zendure.yaml | 30 +--- zendure.go | 164 ------------------- 6 files changed, 196 insertions(+), 188 deletions(-) create mode 100644 meter/zendure.go create mode 100644 meter/zendure/connection.go rename meter/zendure/{zendure.go => credentials.go} (100%) delete mode 100644 zendure.go diff --git a/meter/zendure.go b/meter/zendure.go new file mode 100644 index 0000000000..259e7f2458 --- /dev/null +++ b/meter/zendure.go @@ -0,0 +1,72 @@ +package meter + +import ( + "fmt" + "time" + + "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/meter/zendure" + "github.com/evcc-io/evcc/util" + _ "github.com/joho/godotenv/autoload" +) + +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 +} diff --git a/meter/zendure/connection.go b/meter/zendure/connection.go new file mode 100644 index 0000000000..7603618e2d --- /dev/null +++ b/meter/zendure/connection.go @@ -0,0 +1,87 @@ +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 + usage string + 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() +} diff --git a/meter/zendure/zendure.go b/meter/zendure/credentials.go similarity index 100% rename from meter/zendure/zendure.go rename to meter/zendure/credentials.go diff --git a/meter/zendure/types.go b/meter/zendure/types.go index 6a35648d84..bdba8ae5a4 100644 --- a/meter/zendure/types.go +++ b/meter/zendure/types.go @@ -16,14 +16,17 @@ type CredentialsResponse struct { Msg string `json:"msg"` } +type Payload struct { + *Command + *Data +} + type Command struct { CommandTopic string `json:"command_topic"` DeviceClass string `json:"device_class"` - ElectricLevel int `json:"electricLevel"` Name string `json:"name"` PayloadOff bool `json:"payload_off"` PayloadOn bool `json:"payload_on"` - Sn string `json:"sn"` StateOff bool `json:"state_off"` StateOn bool `json:"state_on"` StateTopic string `json:"state_topic"` @@ -31,3 +34,27 @@ type Command struct { 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 +} diff --git a/templates/definition/meter/zendure.yaml b/templates/definition/meter/zendure.yaml index e0564108c9..206a30af51 100644 --- a/templates/definition/meter/zendure.yaml +++ b/templates/definition/meter/zendure.yaml @@ -3,32 +3,18 @@ products: - brand: Zendure description: generic: Hyper V -requirements: - # description: - # de: | - # Zur Erfassung der PV-Produktion wird ein extern angebundenener S0-Erzeugungszähler benötigt. - # en: | - # An externally connected S0 generation meter is required to record the solar production. params: - name: usage - choice: ["battery"] - - name: host - - name: topic + choice: ["pv", "battery"] + - name: account + - name: serial - name: capacity default: 2 advanced: true + - name: timeout render: | type: custom - {{- if eq .usage "battery" }} - power: - source: mqtt - topic: {{ .topic }}/state - jq: .outputHomePower // if .gridInputPower > 0 then -.gridInputPower else 0 end - # timeout: 5m - soc: - source: mqtt - topic: {{ .topic }}/state - jq: .electricLevel // 0 - # timeout: 30m - capacity: {{ .capacity }} # kWh - {{- end}} + usage: {{ .usage }} + account: {{ .account }} + serial: {{ .serial }} + timeout: {{ .timeout }} diff --git a/zendure.go b/zendure.go deleted file mode 100644 index bb9691e729..0000000000 --- a/zendure.go +++ /dev/null @@ -1,164 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - "net" - "net/http" - "os" - "strconv" - "strings" - "time" - - "dario.cat/mergo" - "github.com/evcc-io/evcc/meter/zendure" - "github.com/evcc-io/evcc/provider/mqtt" - "github.com/evcc-io/evcc/util" - _ "github.com/joho/godotenv/autoload" -) - -// Topics - -// acOutputPower -// acSwitch -// buzzerSwitch -// electricLevel -// gridInputPower -// outputHomePower -// outputLimit -// passMode -// remainOutTime -// socSet -// solarInputPower -// solarPower1 -// solarPower2 - -// outputPackPower -// packData -// packInputPower -// packNum -// packState - -func gridPower() (int64, error) { - var state struct { - Result struct { - GridPower float64 `json:"gridPower"` - } - } - - resp, err := http.Get("http://nas.fritz.box:7070/api/state") - if err == nil { - err = json.NewDecoder(resp.Body).Decode(&state) - } - - return int64(state.Result.GridPower), err -} - -func evcc(client *mqtt.Client, commands map[string]zendure.Command) { - const ( - Margin = 100 - MaxCharge = -1000 - MaxDischarge = 500 - ) - - var grid int64 - - for range time.Tick(10 * time.Second) { - res, err := gridPower() - if err != nil { - fmt.Println(err) - continue - } - - new := grid + res - - if cmd, ok := commands["outputLimit"]; ok && new >= 0 { - if new = max(0, min(new-Margin, MaxDischarge)); new != grid { - fmt.Println("vvv") - fmt.Println("set outputLimit:", new) - - client.Publish(cmd.CommandTopic, false, strconv.FormatInt(new, 10)) - - grid = new - } - } else if cmd, ok := commands["gridInputPower"]; ok && new < 0 { - if new = min(0, max(new+Margin, MaxCharge)); new != grid { - fmt.Println("^^^") - fmt.Println("set gridInputPower:", new) - - client.Publish(cmd.CommandTopic, false, strconv.FormatInt(-new, 10)) - - grid = new - } - } - } -} - -func main() { - util.LogLevel("trace", nil) - - res, err := zendure.MqttCredentials(os.Getenv("ZENDURE_ACCOUNT"), os.Getenv("ZENDURE_SERIAL")) - if err != nil { - panic(err) - } - - fmt.Println(res) - - client, err := mqtt.NewClient( - util.NewLogger("mqtt"), - net.JoinHostPort(res.Data.MqttUrl, strconv.Itoa(res.Data.Port)), res.Data.AppKey, res.Data.Secret, - "", 0, false, "", "", "", - ) - if err != nil { - panic(err) - } - - state := make(map[string]any) - commands := make(map[string]zendure.Command) - - go evcc(client, commands) - - topic := res.Data.AppKey + "/#" - - if err := client.Listen(topic, func(data string) { - fmt.Println(topic, ":", data) - - var cmd zendure.Command - err := json.Unmarshal([]byte(data), &cmd) - if err != nil { - panic(err) - } - - if full, ok := strings.CutSuffix(cmd.CommandTopic, "/set"); ok { - segs := strings.Split(full, "/") - key := segs[len(segs)-1] - - commands[key] = cmd - - if res, err := json.MarshalIndent(commands, "", " "); err == nil { - fmt.Println("===") - fmt.Println(string(res)) - } - - return - } - - var new map[string]any - if err := json.Unmarshal([]byte(data), &new); err != nil { - panic(err) - } - - if err := mergo.Merge(&state, new, mergo.WithOverride); err != nil { - panic(err) - } - - if res, err := json.MarshalIndent(state, "", " "); err == nil { - fmt.Println("---") - fmt.Println(string(res)) - } - }); err != nil { - panic(err) - } - - time.Sleep(time.Hour) -} From 16d7cc26e05edd95e60b5d4ad650c3710493f8c3 Mon Sep 17 00:00:00 2001 From: andig Date: Sat, 9 Nov 2024 12:19:42 +0100 Subject: [PATCH 09/11] wip --- meter/zendure.go | 1 - 1 file changed, 1 deletion(-) diff --git a/meter/zendure.go b/meter/zendure.go index 259e7f2458..e3fb541b25 100644 --- a/meter/zendure.go +++ b/meter/zendure.go @@ -7,7 +7,6 @@ import ( "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/meter/zendure" "github.com/evcc-io/evcc/util" - _ "github.com/joho/godotenv/autoload" ) func init() { From add994a28bd3ec5fee91fff1239bfd572188e433 Mon Sep 17 00:00:00 2001 From: andig Date: Sat, 9 Nov 2024 12:21:52 +0100 Subject: [PATCH 10/11] wip --- templates/definition/meter/zendure.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/definition/meter/zendure.yaml b/templates/definition/meter/zendure.yaml index 206a30af51..9ea1e781dc 100644 --- a/templates/definition/meter/zendure.yaml +++ b/templates/definition/meter/zendure.yaml @@ -13,7 +13,7 @@ params: advanced: true - name: timeout render: | - type: custom + type: zendure usage: {{ .usage }} account: {{ .account }} serial: {{ .serial }} From 832317397dc7bcdbcc241b837e7c74846d90dc36 Mon Sep 17 00:00:00 2001 From: andig Date: Sat, 9 Nov 2024 12:28:41 +0100 Subject: [PATCH 11/11] wip --- meter/zendure/connection.go | 5 +- provider/collect.go | 94 ------------------------- templates/definition/meter/zendure.yaml | 2 + 3 files changed, 4 insertions(+), 97 deletions(-) delete mode 100644 provider/collect.go diff --git a/meter/zendure/connection.go b/meter/zendure/connection.go index 7603618e2d..758d4ad83a 100644 --- a/meter/zendure/connection.go +++ b/meter/zendure/connection.go @@ -18,9 +18,8 @@ var ( ) type Connection struct { - log *util.Logger - usage string - data *util.Monitor[Data] + log *util.Logger + data *util.Monitor[Data] } func NewConnection(account, serial string, timeout time.Duration) (*Connection, error) { diff --git a/provider/collect.go b/provider/collect.go deleted file mode 100644 index aa96b5dc7b..0000000000 --- a/provider/collect.go +++ /dev/null @@ -1,94 +0,0 @@ -package provider - -import ( - "context" - "encoding/json" - "fmt" - "time" - - "dario.cat/mergo" - "github.com/evcc-io/evcc/util" - "github.com/evcc-io/evcc/util/request" -) - -// Collect collects and combines JSON maps -type Collect struct { - *getter - log *util.Logger - get func() (string, error) - data map[string]any - cache time.Duration - timeout time.Duration - updated time.Time -} - -func init() { - registry.AddCtx("collect", NewCollectProviderFromConfig) -} - -// NewCollectProviderFromConfig creates a collect provider. -func NewCollectProviderFromConfig(ctx context.Context, other map[string]interface{}) (Provider, error) { - cc := struct { - Get Config - Timeout time.Duration - Cache time.Duration - }{ - Timeout: request.Timeout, - } - - if err := util.DecodeOther(other, &cc); err != nil { - return nil, err - } - - g, err := NewStringGetterFromConfig(ctx, cc.Get) - if err != nil { - return nil, fmt.Errorf("get: %w", err) - } - - p, err := NewCollectProvider(g, cc.Timeout, cc.Cache) - p.getter = defaultGetters(p, 1) - - return p, err -} - -// NewCollectProvider creates a collect provider. -// Collect execution is aborted after given timeout. -func NewCollectProvider(get func() (string, error), timeout time.Duration, cache time.Duration) (*Collect, error) { - s := &Collect{ - log: util.NewLogger("collect"), - get: get, - data: make(map[string]any), - cache: cache, - timeout: timeout, - } - - return s, nil -} - -func (p *Collect) update() error { - v, err := p.get() - if err != nil { - return err - } - - var new map[string]any - if err := json.Unmarshal([]byte(v), &new); err != nil { - return err - } - - return mergo.Merge(&p.data, v, mergo.WithOverride) -} - -var _ StringProvider = (*Collect)(nil) - -// StringGetter returns string from exec result. Only STDOUT is considered. -func (p *Collect) StringGetter() (func() (string, error), error) { - return func() (string, error) { - if err := p.update(); err != nil { - return "", err - } - - b, err := json.Marshal(p.data) - return string(b), err - }, nil -} diff --git a/templates/definition/meter/zendure.yaml b/templates/definition/meter/zendure.yaml index 9ea1e781dc..f57b901722 100644 --- a/templates/definition/meter/zendure.yaml +++ b/templates/definition/meter/zendure.yaml @@ -3,6 +3,8 @@ products: - brand: Zendure description: generic: Hyper V +requirements: + evcc: ["skiptest"] params: - name: usage choice: ["pv", "battery"]