diff --git a/meter/ecoflow-powerocean.go b/meter/ecoflow-powerocean.go deleted file mode 100644 index fc6a173d0..000000000 --- a/meter/ecoflow-powerocean.go +++ /dev/null @@ -1,162 +0,0 @@ -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/ecoflow.go b/meter/ecoflow.go new file mode 100644 index 000000000..af468a265 --- /dev/null +++ b/meter/ecoflow.go @@ -0,0 +1,173 @@ +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" +) + +// EcoFlow represents the EcoFlow meter +type EcoFlow struct { + ctx context.Context + usage string + serial string + cache time.Duration + client *ecoflow.Client + dataG func() (*ecoflow.GetCmdResponse, error) + + gridPower, pvPower, batteryPower, batterySoc string +} + +func init() { + registry.AddCtx("ecoflow", NewEcoFlowFromConfig) +} + +// NewEcoFlowFromConfig creates an EcoFlow meter from generic config +func NewEcoFlowFromConfig(ctx context.Context, other map[string]any) (api.Meter, error) { + cc := struct { + Usage string + AccessKey, SecretKey, Serial, Region string + GridPower, PvPower, BatteryPower, BatterySoc 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.Serial == "" { + return nil, errors.New("missing serial") + } + if cc.Usage == "" { + return nil, errors.New("missing usage") + } + + var uri string + switch cc.Region { + case "auto": + uri = "https://api.ecoflow.com" + case "europe": + uri = "https://api-e.ecoflow.com" + case "america": + uri = "https://api-a.ecoflow.com" + default: + return nil, fmt.Errorf("invalid region: %s", cc.Region) + } + + m, err := NewEcoFlow(ctx, cc.AccessKey, cc.SecretKey, cc.Serial, cc.Usage, uri, + cc.GridPower, cc.PvPower, cc.BatteryPower, cc.BatterySoc, cc.Cache) + if err != nil { + return nil, err + } + + if cc.Usage == "battery" { + return &EcoFlowBattery{m}, nil + } + + return m, nil +} + +// NewEcoFlow constructs the EcoFlow struct +func NewEcoFlow(ctx context.Context, accessKey, secretKey, serial, usage, uri string, + gridPower, pvPower, batteryPower, batterySoc string, cache time.Duration) (*EcoFlow, error) { + m := &EcoFlow{ + ctx: ctx, + serial: serial, + usage: usage, + cache: cache, + client: ecoflow.NewEcoflowClient(accessKey, secretKey, ecoflow.WithBaseUrl(uri)), + gridPower: gridPower, + pvPower: pvPower, + batteryPower: batteryPower, + batterySoc: batterySoc, + } + + m.dataG = util.Cached(m.getData, cache) + + return m, nil +} + +// getData retrieves device parameters from EcoFlow API +func (m *EcoFlow) getData() (*ecoflow.GetCmdResponse, error) { + ctx, cancel := context.WithTimeout(m.ctx, 30*time.Second) + defer cancel() + + var params []string + switch m.usage { + case "grid": + params = []string{m.gridPower} + case "pv": + params = []string{m.pvPower} + case "battery": + params = []string{m.batteryPower, m.batterySoc} + } + + return m.client.GetDeviceParameters(ctx, m.serial, params) +} + +var _ api.Meter = (*EcoFlow)(nil) + +// CurrentPower implements the api.Meter interface +func (m *EcoFlow) 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 ecoflowValue(response.Data, m.gridPower) + case "pv": + return ecoflowValue(response.Data, m.pvPower) + case "battery": + pwr, err := ecoflowValue(response.Data, m.batteryPower) + 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 ecoflowValue(data map[string]any, key string) (float64, error) { + if data != nil { + if v, ok := data[key]; ok { + return cast.ToFloat64E(v) + } + } + return 0, api.ErrNotAvailable +} + +// EcoFlowBattery represents the EcoFlow battery decorator +type EcoFlowBattery struct { + *EcoFlow +} + +// Soc implements the api.Battery interface for battery usage +func (m *EcoFlowBattery) Soc() (float64, error) { + response, err := m.dataG() + if err != nil { + return 0, err + } + + return ecoflowValue(response.Data, m.batterySoc) +} diff --git a/templates/definition/meter/ecoflow-powerocean.yaml b/templates/definition/meter/ecoflow-powerocean.yaml index f697be437..bd4361e95 100644 --- a/templates/definition/meter/ecoflow-powerocean.yaml +++ b/templates/definition/meter/ecoflow-powerocean.yaml @@ -9,14 +9,14 @@ requirements: 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 + - Die Seriennummer 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 + - The serial number of your PowerOcean system These credentials can be obtained through the EcoFlow Developer Console after registering your application. params: @@ -37,19 +37,29 @@ params: en: Secret Key from EcoFlow Developer Console de: Secret Key aus der EcoFlow Developer Console - name: deviceid + deprecated: true + - name: serial required: true + - name: region + choice: ["auto", "europe", "america"] + default: auto description: - generic: Device ID + generic: "API Region: auto (default), europe, or america." help: - en: Device ID of your EcoFlow PowerOcean system (e.g., HJ31ZD1AZH5G0342) - de: Geräte-ID Ihres EcoFlow PowerOcean Systems (z.B. HJ31ZD1AZH5G0342) + en: 'If you sometimes see an error "8513: accessKey is invalid", which for example Starlink users do, you can set API region explicitly. Passible values: auto (default), europe, or america.' + de: 'Manche Nutzer, z.B. mit Starlink-Internet, sehen in den Logs den "Fehler 8513: accessKey is invalid". Um das zu beheben, kann man die API Region manuell setzen. Mögliche Werte: auto (Standard), europe oder america.' - name: cache default: 30s advanced: true render: | - type: ecoflow-powerocean + type: ecoflow accesskey: {{ .accesskey }} secretkey: {{ .secretkey }} - deviceid: {{ .deviceid }} + serial: {{ if .serial }}{{ .serial }}{{ else }}{{ .deviceid }}{{ end }} usage: {{ .usage }} + region: {{ .region }} cache: {{ .cache }} + gridpower: sysGridPwr + pvpower: mpptPwr + batterypower: bpPwr + batterysoc: bpSoc diff --git a/templates/definition/meter/ecoflow-stream.yaml b/templates/definition/meter/ecoflow-stream.yaml new file mode 100644 index 000000000..56b1b32b9 --- /dev/null +++ b/templates/definition/meter/ecoflow-stream.yaml @@ -0,0 +1,62 @@ +template: ecoflow-stream +products: + - brand: EcoFlow + description: + generic: Stream +requirements: + description: + de: | + Für die Nutzung des EcoFlow Stream Meters benötigen Sie: + - Einen gültigen Access Key von der EcoFlow Developer Console + - Den entsprechenden Secret Key + - Die Seriennummer des Hauptgerätes Ihres Stream Systems + + Diese Credentials erhalten Sie über die EcoFlow Developer Console nach der Registrierung Ihrer Anwendung. + en: | + To use the EcoFlow Stream meter you need: + - A valid Access Key from the EcoFlow Developer Console + - The corresponding Secret Key + - The main device serial number of your Stream system + + These credentials can be obtained through the EcoFlow Developer Console after registering your application. +params: + - name: usage + choice: ["grid", "pv", "battery"] + - 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: serial + - name: region + choice: ["auto", "europe", "america"] + default: auto + description: + generic: "API Region: auto (default), europe, or america." + help: + en: 'If you sometimes see an error "8513: accessKey is invalid", which for example Starlink users do, you can set API region explicitly. Passible values: auto (default), europe, or america.' + de: 'Manche Nutzer, z.B. mit Starlink-Internet, sehen in den Logs den "Fehler 8513: accessKey is invalid". Um das zu beheben, kann man die API Region manuell setzen. Mögliche Werte: auto (Standard), europe oder america.' + - name: cache + default: 30s + advanced: true +render: | + type: ecoflow + accesskey: {{ .accesskey }} + secretkey: {{ .secretkey }} + serial: {{ if .serial }}{{ .serial }}{{ else }}{{ .deviceid }}{{ end }} + usage: {{ .usage }} + cache: {{ .cache }} + region: {{ .region }} + gridpower: powGetSysGrid + pvpower: powGetPvSum + batterypower: powGetBpCms + batterysoc: cmsBattSoc