From eda16fe102a125ec386971288b1f938989848df5 Mon Sep 17 00:00:00 2001 From: Jelle den Burger Date: Thu, 25 Dec 2025 11:54:29 +0100 Subject: [PATCH] Add Ecoflow PowerOcean (#24012) --- go.mod | 1 + go.sum | 2 + meter/ecoflow-powerocean.go | 162 ++++++++++++++++++ meter/template_test.go | 1 + .../definition/meter/ecoflow-powerocean.yaml | 56 ++++++ 5 files changed, 222 insertions(+) create mode 100644 meter/ecoflow-powerocean.go create mode 100644 templates/definition/meter/ecoflow-powerocean.yaml diff --git a/go.mod b/go.mod index 6ff14520e..951448c5d 100644 --- a/go.mod +++ b/go.mod @@ -98,6 +98,7 @@ require ( github.com/stianeikeland/go-rpio/v4 v4.6.0 github.com/stretchr/testify v1.11.1 github.com/teslamotors/vehicle-command v0.4.0 + github.com/tess1o/go-ecoflow v1.1.1-0.20251003083510-2ccc15a17e29 github.com/traefik/yaegi v0.16.1 github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c github.com/volkszaehler/mbmd v0.0.0-20250808161051-499ae856f44e diff --git a/go.sum b/go.sum index ace5f3ce2..128b1858c 100644 --- a/go.sum +++ b/go.sum @@ -751,6 +751,8 @@ github.com/teivah/onecontext v1.3.0 h1:tbikMhAlo6VhAuEGCvhc8HlTnpX4xTNPTOseWuhO1 github.com/teivah/onecontext v1.3.0/go.mod h1:hoW1nmdPVK/0jrvGtcx8sCKYs2PiS4z0zzfdeuEVyb0= github.com/teslamotors/vehicle-command v0.4.0 h1:39JJ7wGdS3dmW3tlsshg2C9/YWx+fxaUw6aRKfqWmpc= github.com/teslamotors/vehicle-command v0.4.0/go.mod h1:aL0IRpLu+l205NxejRkuxpqAovw69rDKriBKKyP8gLI= +github.com/tess1o/go-ecoflow v1.1.1-0.20251003083510-2ccc15a17e29 h1:whgDCnunp97KCC7TF94E3l1hGo080ICSRs6ei8jCnzs= +github.com/tess1o/go-ecoflow v1.1.1-0.20251003083510-2ccc15a17e29/go.mod h1:kqrfk4Y14pHCPKIwp6o7ZbgpohUzQHh7EMX+RUA4JCU= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/traefik/yaegi v0.16.1 h1:f1De3DVJqIDKmnasUF6MwmWv1dSEEat0wcpXhD2On3E= github.com/traefik/yaegi v0.16.1/go.mod h1:4eVhbPb3LnD2VigQjhYbEJ69vDRFdT2HQNrXx8eEwUY= diff --git a/meter/ecoflow-powerocean.go b/meter/ecoflow-powerocean.go new file mode 100644 index 000000000..fc6a173d0 --- /dev/null +++ b/meter/ecoflow-powerocean.go @@ -0,0 +1,162 @@ +package meter + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/util" + "github.com/spf13/cast" + "github.com/tess1o/go-ecoflow" +) + +// EcoFlowPowerOcean represents the EcoFlow PowerOcean meter +type EcoFlowPowerOcean struct { + usage string + accessKey string + secretKey string + deviceId string + cache time.Duration + client *ecoflow.Client + dataG func() (*ecoflow.GetCmdResponse, error) +} + +func init() { + registry.Add("ecoflow-powerocean", NewEcoFlowPowerOceanFromConfig) +} + +// NewEcoFlowPowerOceanFromConfig creates an EcoFlow PowerOcean meter from generic config +func NewEcoFlowPowerOceanFromConfig(other map[string]any) (api.Meter, error) { + cc := struct { + AccessKey string + SecretKey string + DeviceId string + Usage string + Cache time.Duration + }{ + Cache: 30 * time.Second, + } + + if err := util.DecodeOther(other, &cc); err != nil { + return nil, err + } + if cc.AccessKey == "" { + return nil, errors.New("missing access key") + } + if cc.SecretKey == "" { + return nil, errors.New("missing secret key") + } + if cc.DeviceId == "" { + return nil, errors.New("missing device ID") + } + if cc.Usage == "" { + return nil, errors.New("missing usage") + } + + m, err := NewEcoFlowPowerOcean(cc.AccessKey, cc.SecretKey, cc.DeviceId, cc.Usage, cc.Cache) + if err != nil { + return nil, err + } + + if cc.Usage == "battery" { + return &EcoFlowPowerOceanBattery{m}, nil + } + + return m, nil +} + +// NewEcoFlowPowerOcean constructs the EcoFlowPowerOcean struct +func NewEcoFlowPowerOcean(accessKey, secretKey, deviceId, usage string, cache time.Duration) (*EcoFlowPowerOcean, error) { + client := ecoflow.NewEcoflowClient(accessKey, secretKey) + m := &EcoFlowPowerOcean{ + accessKey: accessKey, + secretKey: secretKey, + deviceId: deviceId, + usage: usage, + cache: cache, + client: client, + } + m.dataG = util.Cached(m.getData, cache) + return m, nil +} + +// getData retrieves device parameters from EcoFlow API +func (m *EcoFlowPowerOcean) getData() (*ecoflow.GetCmdResponse, error) { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + var params []string + switch m.usage { + case "grid": + params = []string{"sysGridPwr"} + case "pv": + params = []string{"mpptPwr"} + case "battery": + params = []string{"bpPwr", "bpSoc"} + } + + return m.client.GetDeviceParameters(ctx, m.deviceId, params) +} + +var _ api.Meter = (*EcoFlowPowerOcean)(nil) + +// CurrentPower implements the api.Meter interface +func (m *EcoFlowPowerOcean) CurrentPower() (float64, error) { + response, err := m.dataG() + if err != nil { + return 0, err + } + + // sysGridPwr responds with int + // mpptPwr responds with an array of floats + // bpPwr responds with int + // bpSoc responds with int + switch m.usage { + case "grid": + return extractFloat(response.Data, "sysGridPwr") + case "pv": + return extractFloat(response.Data, "mpptPwr") + case "battery": + pwr, err := extractFloat(response.Data, "bpPwr") + if err != nil { + return 0, err + } + return -pwr, nil // invert battery power: ecoflow returns negative when discharging and positive when charging. + default: + return 0, fmt.Errorf("invalid usage: %s", m.usage) + } +} + +// extractFloat extracts a float64 or int value from a map by key. +func extractFloat(data map[string]any, key string) (float64, error) { + if data != nil { + if v, ok := data[key]; ok { + return cast.ToFloat64E(v) + } + } + return 0, fmt.Errorf("data not available for key: %s", key) +} + +// EcoFlowPowerOceanBattery represents the EcoFlow PowerOcean battery decorator +type EcoFlowPowerOceanBattery struct { + *EcoFlowPowerOcean +} + +// Soc implements the api.Battery interface for battery usage +func (m *EcoFlowPowerOceanBattery) Soc() (float64, error) { + response, err := m.dataG() + if err != nil { + return 0, err + } + + // Access the data from the GetCmdResponse + if response.Data != nil { + if soc, ok := response.Data["bpSoc"]; ok { + return cast.ToFloat64E(soc) + } + } + + return 0, api.ErrNotAvailable +} diff --git a/meter/template_test.go b/meter/template_test.go index 34b6db94b..227ca0f06 100644 --- a/meter/template_test.go +++ b/meter/template_test.go @@ -33,6 +33,7 @@ var acceptable = []string{ "no Speedwire ping response for 127.0.0.1", // SMA "no such network interface", // SMA "missing config values: username, password, key", // E3DC + "missing access key", // Ecoflow "eebus not configured", // EEBus "missing token", // HomeAssistant } diff --git a/templates/definition/meter/ecoflow-powerocean.yaml b/templates/definition/meter/ecoflow-powerocean.yaml new file mode 100644 index 000000000..b524cc24c --- /dev/null +++ b/templates/definition/meter/ecoflow-powerocean.yaml @@ -0,0 +1,56 @@ +template: ecoflow-powerocean +products: + - brand: EcoFlow + description: + generic: PowerOcean +requirements: + description: + de: | + Für die Nutzung des EcoFlow PowerOcean Meters benötigen Sie: + - Einen gültigen Access Key von der EcoFlow Developer Console + - Den entsprechenden Secret Key + - Die Device ID Ihres PowerOcean Systems + + Diese Credentials erhalten Sie über die EcoFlow Developer Console nach der Registrierung Ihrer Anwendung. + en: | + To use the EcoFlow PowerOcean meter you need: + - A valid Access Key from the EcoFlow Developer Console + - The corresponding Secret Key + - The Device ID of your PowerOcean system + + These credentials can be obtained through the EcoFlow Developer Console after registering your application. +params: + - name: usage + choice: ["grid", "pv", "battery"] + allinone: true + - name: accesskey + required: true + description: + generic: Access Key + help: + en: Access Key from EcoFlow Developer Console + de: Access Key aus der EcoFlow Developer Console + - name: secretkey + required: true + description: + generic: Secret Key + help: + en: Secret Key from EcoFlow Developer Console + de: Secret Key aus der EcoFlow Developer Console + - name: deviceid + required: true + description: + generic: Device ID + help: + en: Device ID of your EcoFlow PowerOcean system (e.g., HJ31ZD1AZH5G0342) + de: Geräte-ID Ihres EcoFlow PowerOcean Systems (z.B. HJ31ZD1AZH5G0342) + - name: cache + default: 30s + advanced: true +render: | + type: ecoflow-powerocean + accesskey: {{ .accesskey }} + secretkey: {{ .secretkey }} + deviceid: {{ .deviceid }} + usage: {{ .usage }} + cache: {{ .cache }}