From fc38d155c2e47a86faf5b37697e56cacd9332aa5 Mon Sep 17 00:00:00 2001 From: andig Date: Sun, 22 Dec 2024 12:47:19 +0100 Subject: [PATCH] Add SG Ready charger (#17735) --- charger/sgready.go | 205 ++++++++++++++++ charger/sgready_decorators.go | 221 ++++++++++++++++++ provider/map.go | 31 ++- templates/definition/charger/ac-elwa-2.yaml | 1 + templates/definition/charger/stiebel-lwa.yaml | 114 +++++++++ templates/definition/charger/stiebel-wpm.yaml | 111 +++++++++ util/templates/defaults.yaml | 4 + 7 files changed, 682 insertions(+), 5 deletions(-) create mode 100644 charger/sgready.go create mode 100644 charger/sgready_decorators.go create mode 100644 templates/definition/charger/stiebel-lwa.yaml create mode 100644 templates/definition/charger/stiebel-wpm.yaml diff --git a/charger/sgready.go b/charger/sgready.go new file mode 100644 index 000000000..fea351a01 --- /dev/null +++ b/charger/sgready.go @@ -0,0 +1,205 @@ +package charger + +// LICENSE + +// Copyright (c) 2024 andig + +// This module is NOT covered by the MIT license. All rights reserved. + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import ( + "context" + "errors" + "fmt" + + "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/provider" + "github.com/evcc-io/evcc/util" +) + +// SgReady charger implementation +type SgReady struct { + *embed + _mode int64 + phases int + get func() (int64, error) + set func(int64) error + maxPower func(int64) error +} + +func init() { + registry.AddCtx("sgready", NewSgReadyFromConfig) +} + +const ( + _ int64 = iota + Normal + Boost + Stop +) + +//go:generate decorate -f decorateSgReady -b *SgReady -r api.Charger -t "api.Meter,CurrentPower,func() (float64, error)" -t "api.MeterEnergy,TotalEnergy,func() (float64, error)" -t "api.Battery,Soc,func() (float64, error)" -t "api.SocLimiter,GetLimitSoc,func() (int64, error)" + +// NewSgReadyFromConfig creates an SG Ready configurable charger from generic config +func NewSgReadyFromConfig(ctx context.Context, other map[string]interface{}) (api.Charger, error) { + cc := struct { + embed `mapstructure:",squash"` + SetMode provider.Config + GetMode *provider.Config // optional + MaxPower *provider.Config // optional + Power *provider.Config // optional + Energy *provider.Config // optional + Temp *provider.Config // optional + LimitTemp *provider.Config // optional + Phases int + }{ + embed: embed{ + Icon_: "heatpump", + Features_: []api.Feature{api.Heating, api.IntegratedDevice}, + }, + Phases: 1, + } + + if err := util.DecodeOther(other, &cc); err != nil { + return nil, err + } + + set, err := provider.NewIntSetterFromConfig(ctx, "mode", cc.SetMode) + if err != nil { + return nil, err + } + + var get func() (int64, error) + if cc.GetMode != nil { + get, err = provider.NewIntGetterFromConfig(ctx, *cc.GetMode) + if err != nil { + return nil, err + } + } + + var maxPower func(int64) error + if cc.MaxPower != nil { + maxPower, err = provider.NewIntSetterFromConfig(ctx, "maxpower", *cc.MaxPower) + if err != nil { + return nil, err + } + } + + res, err := NewSgReady(ctx, &cc.embed, set, get, maxPower, cc.Phases) + if err != nil { + return nil, err + } + + // decorate power + var powerG func() (float64, error) + if cc.Power != nil { + powerG, err = provider.NewFloatGetterFromConfig(ctx, *cc.Power) + if err != nil { + return nil, fmt.Errorf("power: %w", err) + } + } + + // decorate energy + var energyG func() (float64, error) + if cc.Energy != nil { + energyG, err = provider.NewFloatGetterFromConfig(ctx, *cc.Energy) + if err != nil { + return nil, fmt.Errorf("energy: %w", err) + } + } + + // decorate temp + var tempG func() (float64, error) + if cc.Temp != nil { + tempG, err = provider.NewFloatGetterFromConfig(ctx, *cc.Temp) + if err != nil { + return nil, fmt.Errorf("temp: %w", err) + } + } + + var limitTempG func() (int64, error) + if cc.LimitTemp != nil { + limitTempG, err = provider.NewIntGetterFromConfig(ctx, *cc.LimitTemp) + if err != nil { + return nil, fmt.Errorf("limit temp: %w", err) + } + } + + return decorateSgReady(res, powerG, energyG, tempG, limitTempG), nil +} + +// NewSgReady creates SG Ready charger +func NewSgReady(ctx context.Context, embed *embed, set func(int64) error, get func() (int64, error), maxPower func(int64) error, phases int) (*SgReady, error) { + res := &SgReady{ + embed: embed, + _mode: Normal, + set: set, + get: get, + maxPower: maxPower, + phases: phases, + } + + return res, nil +} + +func (wb *SgReady) mode() (int64, error) { + if wb.get == nil { + return wb._mode, nil + } + return wb.get() +} + +// Status implements the api.Charger interface +func (wb *SgReady) Status() (api.ChargeStatus, error) { + mode, err := wb.mode() + if err != nil { + return api.StatusNone, err + } + + if mode == Stop { + return api.StatusE, errors.New("stop mode") + } + + status := map[int64]api.ChargeStatus{Boost: api.StatusC, Normal: api.StatusB}[mode] + return status, nil +} + +// Enabled implements the api.Charger interface +func (wb *SgReady) Enabled() (bool, error) { + mode, err := wb.mode() + return mode == Boost, err +} + +// Enable implements the api.Charger interface +func (wb *SgReady) Enable(enable bool) error { + mode := map[bool]int64{false: Normal, true: Boost}[enable] + err := wb.set(mode) + if err == nil { + wb._mode = mode + } + return err +} + +// MaxCurrent implements the api.Charger interface +func (wb *SgReady) MaxCurrent(current int64) error { + return wb.MaxCurrentEx(float64(current)) +} + +// MaxCurrent implements the api.Charger interface +func (wb *SgReady) MaxCurrentEx(current float64) error { + if wb.maxPower == nil { + return nil + } + + return wb.maxPower(int64(230 * current * float64(wb.phases))) +} diff --git a/charger/sgready_decorators.go b/charger/sgready_decorators.go new file mode 100644 index 000000000..c127e30b9 --- /dev/null +++ b/charger/sgready_decorators.go @@ -0,0 +1,221 @@ +package charger + +// Code generated by github.com/evcc-io/evcc/cmd/tools/decorate.go. DO NOT EDIT. + +import ( + "github.com/evcc-io/evcc/api" +) + +func decorateSgReady(base *SgReady, meter func() (float64, error), meterEnergy func() (float64, error), battery func() (float64, error), socLimiter func() (int64, error)) api.Charger { + switch { + case battery == nil && meter == nil && socLimiter == nil: + return base + + case battery == nil && meter != nil && meterEnergy == nil && socLimiter == nil: + return &struct { + *SgReady + api.Meter + }{ + SgReady: base, + Meter: &decorateSgReadyMeterImpl{ + meter: meter, + }, + } + + case battery == nil && meter != nil && meterEnergy != nil && socLimiter == nil: + return &struct { + *SgReady + api.Meter + api.MeterEnergy + }{ + SgReady: base, + Meter: &decorateSgReadyMeterImpl{ + meter: meter, + }, + MeterEnergy: &decorateSgReadyMeterEnergyImpl{ + meterEnergy: meterEnergy, + }, + } + + case battery != nil && meter == nil && socLimiter == nil: + return &struct { + *SgReady + api.Battery + }{ + SgReady: base, + Battery: &decorateSgReadyBatteryImpl{ + battery: battery, + }, + } + + case battery != nil && meter != nil && meterEnergy == nil && socLimiter == nil: + return &struct { + *SgReady + api.Battery + api.Meter + }{ + SgReady: base, + Battery: &decorateSgReadyBatteryImpl{ + battery: battery, + }, + Meter: &decorateSgReadyMeterImpl{ + meter: meter, + }, + } + + case battery != nil && meter != nil && meterEnergy != nil && socLimiter == nil: + return &struct { + *SgReady + api.Battery + api.Meter + api.MeterEnergy + }{ + SgReady: base, + Battery: &decorateSgReadyBatteryImpl{ + battery: battery, + }, + Meter: &decorateSgReadyMeterImpl{ + meter: meter, + }, + MeterEnergy: &decorateSgReadyMeterEnergyImpl{ + meterEnergy: meterEnergy, + }, + } + + case battery == nil && meter == nil && socLimiter != nil: + return &struct { + *SgReady + api.SocLimiter + }{ + SgReady: base, + SocLimiter: &decorateSgReadySocLimiterImpl{ + socLimiter: socLimiter, + }, + } + + case battery == nil && meter != nil && meterEnergy == nil && socLimiter != nil: + return &struct { + *SgReady + api.Meter + api.SocLimiter + }{ + SgReady: base, + Meter: &decorateSgReadyMeterImpl{ + meter: meter, + }, + SocLimiter: &decorateSgReadySocLimiterImpl{ + socLimiter: socLimiter, + }, + } + + case battery == nil && meter != nil && meterEnergy != nil && socLimiter != nil: + return &struct { + *SgReady + api.Meter + api.MeterEnergy + api.SocLimiter + }{ + SgReady: base, + Meter: &decorateSgReadyMeterImpl{ + meter: meter, + }, + MeterEnergy: &decorateSgReadyMeterEnergyImpl{ + meterEnergy: meterEnergy, + }, + SocLimiter: &decorateSgReadySocLimiterImpl{ + socLimiter: socLimiter, + }, + } + + case battery != nil && meter == nil && socLimiter != nil: + return &struct { + *SgReady + api.Battery + api.SocLimiter + }{ + SgReady: base, + Battery: &decorateSgReadyBatteryImpl{ + battery: battery, + }, + SocLimiter: &decorateSgReadySocLimiterImpl{ + socLimiter: socLimiter, + }, + } + + case battery != nil && meter != nil && meterEnergy == nil && socLimiter != nil: + return &struct { + *SgReady + api.Battery + api.Meter + api.SocLimiter + }{ + SgReady: base, + Battery: &decorateSgReadyBatteryImpl{ + battery: battery, + }, + Meter: &decorateSgReadyMeterImpl{ + meter: meter, + }, + SocLimiter: &decorateSgReadySocLimiterImpl{ + socLimiter: socLimiter, + }, + } + + case battery != nil && meter != nil && meterEnergy != nil && socLimiter != nil: + return &struct { + *SgReady + api.Battery + api.Meter + api.MeterEnergy + api.SocLimiter + }{ + SgReady: base, + Battery: &decorateSgReadyBatteryImpl{ + battery: battery, + }, + Meter: &decorateSgReadyMeterImpl{ + meter: meter, + }, + MeterEnergy: &decorateSgReadyMeterEnergyImpl{ + meterEnergy: meterEnergy, + }, + SocLimiter: &decorateSgReadySocLimiterImpl{ + socLimiter: socLimiter, + }, + } + } + + return nil +} + +type decorateSgReadyBatteryImpl struct { + battery func() (float64, error) +} + +func (impl *decorateSgReadyBatteryImpl) Soc() (float64, error) { + return impl.battery() +} + +type decorateSgReadyMeterImpl struct { + meter func() (float64, error) +} + +func (impl *decorateSgReadyMeterImpl) CurrentPower() (float64, error) { + return impl.meter() +} + +type decorateSgReadyMeterEnergyImpl struct { + meterEnergy func() (float64, error) +} + +func (impl *decorateSgReadyMeterEnergyImpl) TotalEnergy() (float64, error) { + return impl.meterEnergy() +} + +type decorateSgReadySocLimiterImpl struct { + socLimiter func() (int64, error) +} + +func (impl *decorateSgReadySocLimiterImpl) GetLimitSoc() (int64, error) { + return impl.socLimiter() +} diff --git a/provider/map.go b/provider/map.go index ddc156ca0..894bbcac3 100644 --- a/provider/map.go +++ b/provider/map.go @@ -8,9 +8,9 @@ import ( ) type mapProvider struct { - ctx context.Context - values map[int64]int64 - set Config + ctx context.Context + values map[int64]int64 + get, set Config } func init() { @@ -20,8 +20,8 @@ func init() { // NewMapFromConfig creates type conversion provider func NewMapFromConfig(ctx context.Context, other map[string]interface{}) (Provider, error) { var cc struct { - Values map[int64]int64 - Set Config + Values map[int64]int64 + Get, Set Config } if err := util.DecodeOther(other, &cc); err != nil { @@ -34,6 +34,7 @@ func NewMapFromConfig(ctx context.Context, other map[string]interface{}) (Provid o := &mapProvider{ ctx: ctx, + get: cc.Get, set: cc.Set, values: cc.Values, } @@ -41,6 +42,26 @@ func NewMapFromConfig(ctx context.Context, other map[string]interface{}) (Provid return o, nil } +var _ IntProvider = (*mapProvider)(nil) + +func (o *mapProvider) IntGetter() (func() (int64, error), error) { + get, err := NewIntGetterFromConfig(o.ctx, o.get) + + return func() (int64, error) { + val, err := get() + if err != nil { + return 0, err + } + + res, ok := o.values[val] + if !ok { + return 0, fmt.Errorf("map: value not found: %d", val) + } + + return res, nil + }, err +} + var _ SetIntProvider = (*mapProvider)(nil) func (o *mapProvider) IntSetter(param string) (func(int64) error, error) { diff --git a/templates/definition/charger/ac-elwa-2.yaml b/templates/definition/charger/ac-elwa-2.yaml index 470aec07e..c001ed511 100644 --- a/templates/definition/charger/ac-elwa-2.yaml +++ b/templates/definition/charger/ac-elwa-2.yaml @@ -3,6 +3,7 @@ products: - brand: my-PV description: generic: AC ELWA 2 +group: heating requirements: evcc: ["sponsorship"] params: diff --git a/templates/definition/charger/stiebel-lwa.yaml b/templates/definition/charger/stiebel-lwa.yaml new file mode 100644 index 000000000..b10ce469b --- /dev/null +++ b/templates/definition/charger/stiebel-lwa.yaml @@ -0,0 +1,114 @@ +template: stiebel-lwa +products: + - brand: Stiebel Eltron + description: + generic: LWA/LWZ (SG Ready) + - brand: Tecalor + description: + generic: THZ (SG Ready) +group: heating +# requirements: +# evcc: ["sponsorship"] +params: + - name: modbus + choice: ["tcpip"] + - name: tempsource + validvalues: ["", "warmwater"] + description: + de: "Temperaturquelle" + en: "Temperature source" +render: | + type: sgready + getmode: + source: map + values: + 1: 3 # Frostschutz + 2: 1 # Normal + 3: 2 # Forcierter Betrieb + 4: 2 # Sofortige Ansteuerung + get: + source: modbus + {{- include "modbus" . | indent 4 }} + register: + address: 5000 + type: input + encoding: uint16 + setmode: + source: switch + switch: + - case: 1 # normal + set: + source: sequence + set: + - source: const + value: 0 + set: + source: modbus + {{- include "modbus" . | indent 10 }} + register: + address: 4001 + type: writesingle + encoding: uint16 + - source: const + value: 0 + set: + source: modbus + {{- include "modbus" . | indent 10 }} + register: + address: 4002 + type: writesingle + encoding: uint16 + - case: 2 # boost + set: + source: sequence + set: + - source: const + value: 1 + set: + source: modbus + {{- include "modbus" . | indent 10 }} + register: + address: 4001 + type: writesingle + encoding: uint16 + - source: const + value: 0 + set: + source: modbus + {{- include "modbus" . | indent 10 }} + register: + address: 4002 + type: writesingle + encoding: uint16 + - case: 3 # dimm + set: + source: sequence + set: + - source: const + value: 0 + set: + source: modbus + {{- include "modbus" . | indent 10 }} + register: + address: 4001 + type: writesingle + encoding: uint16 + - source: const + value: 1 + set: + source: modbus + {{- include "modbus" . | indent 10 }} + register: + address: 4002 + type: writesingle + encoding: uint16 + {{- if .tempsource }} + temp: + source: modbus + {{- include "modbus" . | indent 2 }} + register: + address: 15 + type: input + encoding: int16 + scale: 0.1 + {{- end }} diff --git a/templates/definition/charger/stiebel-wpm.yaml b/templates/definition/charger/stiebel-wpm.yaml new file mode 100644 index 000000000..78c1ca354 --- /dev/null +++ b/templates/definition/charger/stiebel-wpm.yaml @@ -0,0 +1,111 @@ +template: stiebel-wpm +products: + - brand: Stiebel Eltron + description: + generic: WPM (SG Ready) +group: heating +# requirements: +# evcc: ["sponsorship"] +params: + - name: modbus + choice: ["tcpip"] + - name: tempsource + validvalues: ["", "warmwater", "buffer"] + description: + de: "Temperaturquelle" + en: "Temperature source" +render: | + type: sgready + getmode: + source: map + values: + 1: 3 # Frostschutz + 2: 1 # Normal + 3: 2 # Forcierter Betrieb + 4: 2 # Sofortige Ansteuerung + get: + source: modbus + {{- include "modbus" . | indent 4 }} + register: + address: 5000 + type: input + encoding: uint16 + setmode: + source: switch + switch: + - case: 1 # normal + set: + source: sequence + set: + - source: const + value: 0 + set: + source: modbus + {{- include "modbus" . | indent 10 }} + register: + address: 4001 + type: writesingle + encoding: uint16 + - source: const + value: 0 + set: + source: modbus + {{- include "modbus" . | indent 10 }} + register: + address: 4002 + type: writesingle + encoding: uint16 + - case: 2 # boost + set: + source: sequence + set: + - source: const + value: 1 + set: + source: modbus + {{- include "modbus" . | indent 10 }} + register: + address: 4001 + type: writesingle + encoding: uint16 + - source: const + value: 0 + set: + source: modbus + {{- include "modbus" . | indent 10 }} + register: + address: 4002 + type: writesingle + encoding: uint16 + - case: 3 # dimm + set: + source: sequence + set: + - source: const + value: 0 + set: + source: modbus + {{- include "modbus" . | indent 10 }} + register: + address: 4001 + type: writesingle + encoding: uint16 + - source: const + value: 1 + set: + source: modbus + {{- include "modbus" . | indent 10 }} + register: + address: 4002 + type: writesingle + encoding: uint16 + {{- if .tempsource }} + temp: + source: modbus + {{- include "modbus" . | indent 2 }} + register: + address: {{ if eq .tempsource "warmwater" -}} 521 {{ else }} 517 {{- end }} + type: input + encoding: int16 + scale: 0.1 + {{- end }} diff --git a/util/templates/defaults.yaml b/util/templates/defaults.yaml index d0a69f429..ad8b618a7 100644 --- a/util/templates/defaults.yaml +++ b/util/templates/defaults.yaml @@ -321,6 +321,7 @@ params: en: Power limit for grid charging. de: Leistungslimit für Netzladung. default: 100 + presets: vehicle-base: params: @@ -575,6 +576,9 @@ devicegroups: generic: de: Generische Unterstützung en: Generic support + heating: + de: Wärmeerzeuger + en: Heating devices switchsockets: de: Schaltbare Steckdosen en: Switchable sockets