From 30ab9a45cc07fed87139726446e4c1dc21c8ad2a Mon Sep 17 00:00:00 2001 From: andig Date: Mon, 24 Feb 2025 22:57:26 +0100 Subject: [PATCH] Heating: add generic heatpump charger (#19008) --- charger/charger.go | 15 +- charger/heatpump.go | 159 +++++++++++++ charger/heatpump_decorators.go | 221 ++++++++++++++++++ charger/measurement/energy.go | 31 +++ charger/measurement/heating.go | 31 +++ charger/sgready.go | 98 +++----- charger/vaillant.go | 2 +- meter/mbmd.go | 3 +- meter/measurement/energy.go | 31 +++ meter/{helper.go => measurement/phases.go} | 32 +-- meter/meter.go | 12 +- meter/openwb.go | 3 +- templates/definition/charger/idm.yaml | 53 +++++ .../definition/charger/lambda-zewotherm.yaml | 36 +++ 14 files changed, 620 insertions(+), 107 deletions(-) create mode 100644 charger/heatpump.go create mode 100644 charger/heatpump_decorators.go create mode 100644 charger/measurement/energy.go create mode 100644 charger/measurement/heating.go create mode 100644 meter/measurement/energy.go rename meter/{helper.go => measurement/phases.go} (55%) create mode 100644 templates/definition/charger/idm.yaml create mode 100644 templates/definition/charger/lambda-zewotherm.yaml diff --git a/charger/charger.go b/charger/charger.go index 08e94d4d9..cf3601633 100644 --- a/charger/charger.go +++ b/charger/charger.go @@ -6,7 +6,8 @@ import ( "fmt" "github.com/evcc-io/evcc/api" - "github.com/evcc-io/evcc/meter" + "github.com/evcc-io/evcc/charger/measurement" + meter "github.com/evcc-io/evcc/meter/measurement" "github.com/evcc-io/evcc/plugin" "github.com/evcc-io/evcc/util" ) @@ -36,12 +37,8 @@ func NewConfigurableFromConfig(ctx context.Context, other map[string]interface{} Wakeup *plugin.Config Soc *plugin.Config Tos bool - - // optional measurements - Power *plugin.Config - Energy *plugin.Config - - Currents, Voltages []plugin.Config + measurement.Energy `mapstructure:",squash"` // optional + meter.Phases `mapstructure:",squash"` // optional } if err := util.DecodeOther(other, &cc); err != nil { @@ -123,12 +120,12 @@ func NewConfigurableFromConfig(ctx context.Context, other map[string]interface{} } // decorate measurements - powerG, energyG, err := meter.BuildMeasurements(ctx, cc.Power, cc.Energy) + powerG, energyG, err := cc.Energy.Configure(ctx) if err != nil { return nil, err } - currentsG, voltagesG, _, err := meter.BuildPhaseMeasurements(ctx, cc.Currents, cc.Voltages, nil) + currentsG, voltagesG, _, err := cc.Phases.Configure(ctx) if err != nil { return nil, err } diff --git a/charger/heatpump.go b/charger/heatpump.go new file mode 100644 index 000000000..e2b24ae38 --- /dev/null +++ b/charger/heatpump.go @@ -0,0 +1,159 @@ +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" + + "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/charger/measurement" + "github.com/evcc-io/evcc/plugin" + "github.com/evcc-io/evcc/util" +) + +// Heatpump charger implementation +type Heatpump struct { + *embed + phases int + power int64 + maxPowerG func() (int64, error) + maxPowerS func(int64) error +} + +func init() { + registry.AddCtx("heatpump", NewHeatpumpFromConfig) +} + +//go:generate go tool decorate -f decorateHeatpump -b *Heatpump -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)" + +// NewHeatpumpFromConfig creates heatpump configurable charger from generic config +func NewHeatpumpFromConfig(ctx context.Context, other map[string]interface{}) (api.Charger, error) { + cc := struct { + embed `mapstructure:",squash"` + SetMaxPower plugin.Config + GetMaxPower *plugin.Config // optional + measurement.Temperature `mapstructure:",squash"` + measurement.Energy `mapstructure:",squash"` + 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 + } + + maxPowerG, err := cc.GetMaxPower.IntGetter(ctx) + if err != nil { + return nil, err + } + + maxPowerS, err := cc.SetMaxPower.IntSetter(ctx, "maxpower") + if err != nil { + return nil, err + } + + // if !sponsor.IsAuthorized() { + // return nil, api.ErrSponsorRequired + // } + + res, err := NewHeatpump(ctx, &cc.embed, maxPowerS, maxPowerG, cc.Phases) + if err != nil { + return nil, err + } + + powerG, energyG, err := cc.Energy.Configure(ctx) + if err != nil { + return nil, err + } + + tempG, limitTempG, err := cc.Temperature.Configure(ctx) + if err != nil { + return nil, err + } + + return decorateHeatpump(res, powerG, energyG, tempG, limitTempG), nil +} + +// NewHeatpump creates heatpump charger +func NewHeatpump(ctx context.Context, embed *embed, maxPowerS func(int64) error, maxPowerG func() (int64, error), phases int) (*Heatpump, error) { + res := &Heatpump{ + embed: embed, + maxPowerG: maxPowerG, + maxPowerS: maxPowerS, + phases: phases, + } + + return res, nil +} + +func (wb *Heatpump) getMaxPower() (int64, error) { + if wb.maxPowerG == nil { + return wb.power, nil + } + return wb.maxPowerG() +} + +func (wb *Heatpump) setMaxPower(power int64) error { + err := wb.maxPowerS(power) + if err == nil { + wb.power = power + } + + return err +} + +// Status implements the api.Charger interface +func (wb *Heatpump) Status() (api.ChargeStatus, error) { + power, err := wb.getMaxPower() + if err != nil { + return api.StatusNone, err + } + + status := map[bool]api.ChargeStatus{false: api.StatusB, true: api.StatusC} + return status[power > 0], nil +} + +// Enabled implements the api.Charger interface +func (wb *Heatpump) Enabled() (bool, error) { + power, err := wb.getMaxPower() + return power > 0, err +} + +// Enable implements the api.Charger interface +func (wb *Heatpump) Enable(enable bool) error { + var power int64 + if enable { + power = wb.power + } + return wb.setMaxPower(power) +} + +// MaxCurrent implements the api.Charger interface +func (wb *Heatpump) MaxCurrent(current int64) error { + return wb.MaxCurrentEx(float64(current)) +} + +// MaxCurrent implements the api.Charger interface +func (wb *Heatpump) MaxCurrentEx(current float64) error { + return wb.setMaxPower(int64(230 * current * float64(wb.phases))) +} diff --git a/charger/heatpump_decorators.go b/charger/heatpump_decorators.go new file mode 100644 index 000000000..9a44bb2a6 --- /dev/null +++ b/charger/heatpump_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 decorateHeatpump(base *Heatpump, 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 { + *Heatpump + api.Meter + }{ + Heatpump: base, + Meter: &decorateHeatpumpMeterImpl{ + meter: meter, + }, + } + + case battery == nil && meter != nil && meterEnergy != nil && socLimiter == nil: + return &struct { + *Heatpump + api.Meter + api.MeterEnergy + }{ + Heatpump: base, + Meter: &decorateHeatpumpMeterImpl{ + meter: meter, + }, + MeterEnergy: &decorateHeatpumpMeterEnergyImpl{ + meterEnergy: meterEnergy, + }, + } + + case battery != nil && meter == nil && socLimiter == nil: + return &struct { + *Heatpump + api.Battery + }{ + Heatpump: base, + Battery: &decorateHeatpumpBatteryImpl{ + battery: battery, + }, + } + + case battery != nil && meter != nil && meterEnergy == nil && socLimiter == nil: + return &struct { + *Heatpump + api.Battery + api.Meter + }{ + Heatpump: base, + Battery: &decorateHeatpumpBatteryImpl{ + battery: battery, + }, + Meter: &decorateHeatpumpMeterImpl{ + meter: meter, + }, + } + + case battery != nil && meter != nil && meterEnergy != nil && socLimiter == nil: + return &struct { + *Heatpump + api.Battery + api.Meter + api.MeterEnergy + }{ + Heatpump: base, + Battery: &decorateHeatpumpBatteryImpl{ + battery: battery, + }, + Meter: &decorateHeatpumpMeterImpl{ + meter: meter, + }, + MeterEnergy: &decorateHeatpumpMeterEnergyImpl{ + meterEnergy: meterEnergy, + }, + } + + case battery == nil && meter == nil && socLimiter != nil: + return &struct { + *Heatpump + api.SocLimiter + }{ + Heatpump: base, + SocLimiter: &decorateHeatpumpSocLimiterImpl{ + socLimiter: socLimiter, + }, + } + + case battery == nil && meter != nil && meterEnergy == nil && socLimiter != nil: + return &struct { + *Heatpump + api.Meter + api.SocLimiter + }{ + Heatpump: base, + Meter: &decorateHeatpumpMeterImpl{ + meter: meter, + }, + SocLimiter: &decorateHeatpumpSocLimiterImpl{ + socLimiter: socLimiter, + }, + } + + case battery == nil && meter != nil && meterEnergy != nil && socLimiter != nil: + return &struct { + *Heatpump + api.Meter + api.MeterEnergy + api.SocLimiter + }{ + Heatpump: base, + Meter: &decorateHeatpumpMeterImpl{ + meter: meter, + }, + MeterEnergy: &decorateHeatpumpMeterEnergyImpl{ + meterEnergy: meterEnergy, + }, + SocLimiter: &decorateHeatpumpSocLimiterImpl{ + socLimiter: socLimiter, + }, + } + + case battery != nil && meter == nil && socLimiter != nil: + return &struct { + *Heatpump + api.Battery + api.SocLimiter + }{ + Heatpump: base, + Battery: &decorateHeatpumpBatteryImpl{ + battery: battery, + }, + SocLimiter: &decorateHeatpumpSocLimiterImpl{ + socLimiter: socLimiter, + }, + } + + case battery != nil && meter != nil && meterEnergy == nil && socLimiter != nil: + return &struct { + *Heatpump + api.Battery + api.Meter + api.SocLimiter + }{ + Heatpump: base, + Battery: &decorateHeatpumpBatteryImpl{ + battery: battery, + }, + Meter: &decorateHeatpumpMeterImpl{ + meter: meter, + }, + SocLimiter: &decorateHeatpumpSocLimiterImpl{ + socLimiter: socLimiter, + }, + } + + case battery != nil && meter != nil && meterEnergy != nil && socLimiter != nil: + return &struct { + *Heatpump + api.Battery + api.Meter + api.MeterEnergy + api.SocLimiter + }{ + Heatpump: base, + Battery: &decorateHeatpumpBatteryImpl{ + battery: battery, + }, + Meter: &decorateHeatpumpMeterImpl{ + meter: meter, + }, + MeterEnergy: &decorateHeatpumpMeterEnergyImpl{ + meterEnergy: meterEnergy, + }, + SocLimiter: &decorateHeatpumpSocLimiterImpl{ + socLimiter: socLimiter, + }, + } + } + + return nil +} + +type decorateHeatpumpBatteryImpl struct { + battery func() (float64, error) +} + +func (impl *decorateHeatpumpBatteryImpl) Soc() (float64, error) { + return impl.battery() +} + +type decorateHeatpumpMeterImpl struct { + meter func() (float64, error) +} + +func (impl *decorateHeatpumpMeterImpl) CurrentPower() (float64, error) { + return impl.meter() +} + +type decorateHeatpumpMeterEnergyImpl struct { + meterEnergy func() (float64, error) +} + +func (impl *decorateHeatpumpMeterEnergyImpl) TotalEnergy() (float64, error) { + return impl.meterEnergy() +} + +type decorateHeatpumpSocLimiterImpl struct { + socLimiter func() (int64, error) +} + +func (impl *decorateHeatpumpSocLimiterImpl) GetLimitSoc() (int64, error) { + return impl.socLimiter() +} diff --git a/charger/measurement/energy.go b/charger/measurement/energy.go new file mode 100644 index 000000000..ed173643c --- /dev/null +++ b/charger/measurement/energy.go @@ -0,0 +1,31 @@ +package measurement + +import ( + "context" + "fmt" + + "github.com/evcc-io/evcc/plugin" +) + +type Energy struct { + Power *plugin.Config // optional + Energy *plugin.Config // optional +} + +func (cc *Energy) Configure(ctx context.Context) ( + func() (float64, error), + func() (float64, error), + error, +) { + powerG, err := cc.Power.FloatGetter(ctx) + if err != nil { + return nil, nil, fmt.Errorf("power: %w", err) + } + + energyG, err := cc.Energy.FloatGetter(ctx) + if err != nil { + return nil, nil, fmt.Errorf("energy: %w", err) + } + + return powerG, energyG, nil +} diff --git a/charger/measurement/heating.go b/charger/measurement/heating.go new file mode 100644 index 000000000..3e865fa90 --- /dev/null +++ b/charger/measurement/heating.go @@ -0,0 +1,31 @@ +package measurement + +import ( + "context" + "fmt" + + "github.com/evcc-io/evcc/plugin" +) + +type Temperature struct { + Temp *plugin.Config // optional + LimitTemp *plugin.Config // optional +} + +func (cc *Temperature) Configure(ctx context.Context) ( + func() (float64, error), + func() (int64, error), + error, +) { + tempG, err := cc.Temp.FloatGetter(ctx) + if err != nil { + return nil, nil, fmt.Errorf("temp: %w", err) + } + + limitTempG, err := cc.LimitTemp.IntGetter(ctx) + if err != nil { + return nil, nil, fmt.Errorf("limit temp: %w", err) + } + + return tempG, limitTempG, nil +} diff --git a/charger/sgready.go b/charger/sgready.go index 323b50667..653dc9c19 100644 --- a/charger/sgready.go +++ b/charger/sgready.go @@ -20,9 +20,9 @@ package charger import ( "context" "errors" - "fmt" "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/charger/measurement" "github.com/evcc-io/evcc/plugin" "github.com/evcc-io/evcc/util" ) @@ -30,11 +30,9 @@ import ( // SgReady charger implementation type SgReady struct { *embed - _mode int64 - phases int - get func() (int64, error) - set func(int64) error - maxPower func(int64) error + mode int64 + modeS func(int64) error + modeG func() (int64, error) } func init() { @@ -53,15 +51,12 @@ const ( // 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 plugin.Config - GetMode *plugin.Config // optional - MaxPower *plugin.Config // optional - Power *plugin.Config // optional - Energy *plugin.Config // optional - Temp *plugin.Config // optional - LimitTemp *plugin.Config // optional - Phases int + embed `mapstructure:",squash"` + SetMode plugin.Config + GetMode *plugin.Config // optional + measurement.Temperature `mapstructure:",squash"` + measurement.Energy `mapstructure:",squash"` + Phases int }{ embed: embed{ Icon_: "heatpump", @@ -74,76 +69,56 @@ func NewSgReadyFromConfig(ctx context.Context, other map[string]interface{}) (ap return nil, err } - set, err := cc.SetMode.IntSetter(ctx, "mode") + modeS, err := cc.SetMode.IntSetter(ctx, "mode") if err != nil { return nil, err } - get, err := cc.GetMode.IntGetter(ctx) + modeG, err := cc.GetMode.IntGetter(ctx) if err != nil { return nil, err } - maxPower, err := cc.MaxPower.IntSetter(ctx, "maxpower") + res, err := NewSgReady(ctx, &cc.embed, modeS, modeG) if err != nil { return nil, err } - res, err := NewSgReady(ctx, &cc.embed, set, get, maxPower, cc.Phases) + powerG, energyG, err := cc.Energy.Configure(ctx) if err != nil { return nil, err } - // decorate power - powerG, err := cc.Power.FloatGetter(ctx) + tempG, limitTempG, err := cc.Temperature.Configure(ctx) if err != nil { - return nil, fmt.Errorf("power: %w", err) - } - - // decorate energy - energyG, err := cc.Energy.FloatGetter(ctx) - if err != nil { - return nil, fmt.Errorf("energy: %w", err) - } - - // decorate temp - tempG, err := cc.Temp.FloatGetter(ctx) - if err != nil { - return nil, fmt.Errorf("temp: %w", err) - } - - limitTempG, err := cc.LimitTemp.IntGetter(ctx) - if err != nil { - return nil, fmt.Errorf("limit temp: %w", err) + return nil, 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) { +func NewSgReady(ctx context.Context, embed *embed, modeS func(int64) error, modeG func() (int64, error)) (*SgReady, error) { res := &SgReady{ - embed: embed, - _mode: Normal, - set: set, - get: get, - maxPower: maxPower, - phases: phases, + embed: embed, + mode: Normal, + modeS: modeS, + modeG: modeG, } return res, nil } -func (wb *SgReady) mode() (int64, error) { - if wb.get == nil { - return wb._mode, nil +func (wb *SgReady) getMode() (int64, error) { + if wb.modeG == nil { + return wb.mode, nil } - return wb.get() + return wb.modeG() } // Status implements the api.Charger interface func (wb *SgReady) Status() (api.ChargeStatus, error) { - mode, err := wb.mode() + mode, err := wb.getMode() if err != nil { return api.StatusNone, err } @@ -152,36 +127,27 @@ func (wb *SgReady) Status() (api.ChargeStatus, error) { return api.StatusNone, errors.New("stop mode") } - status := map[int64]api.ChargeStatus{Boost: api.StatusC, Normal: api.StatusB}[mode] - return status, nil + status := map[int64]api.ChargeStatus{Boost: api.StatusC, Normal: api.StatusB} + return status[mode], nil } // Enabled implements the api.Charger interface func (wb *SgReady) Enabled() (bool, error) { - mode, err := wb.mode() + mode, err := wb.getMode() 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) + err := wb.modeS(mode) if err == nil { - wb._mode = mode + 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))) + return nil } diff --git a/charger/vaillant.go b/charger/vaillant.go index 69789673c..d12c58e88 100644 --- a/charger/vaillant.go +++ b/charger/vaillant.go @@ -112,7 +112,7 @@ func NewVaillantFromConfig(ctx context.Context, other map[string]interface{}) (a } } - sgr, err := NewSgReady(ctx, &cc.embed, set, nil, nil, cc.Phases) + sgr, err := NewSgReady(ctx, &cc.embed, set, nil) if err != nil { return nil, err } diff --git a/meter/mbmd.go b/meter/mbmd.go index a5505076d..059a7bb90 100644 --- a/meter/mbmd.go +++ b/meter/mbmd.go @@ -7,6 +7,7 @@ import ( "time" "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/meter/measurement" "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/modbus" "github.com/volkszaehler/mbmd/meters" @@ -155,7 +156,7 @@ func (m *ModbusMbmd) buildPhaseProviders(readings []string) (func() (float64, fl } } - return collectPhaseProviders(phases), nil + return measurement.CombinePhases(phases), nil } // floatGetter executes configured modbus read operation and implements func() (float64, error) diff --git a/meter/measurement/energy.go b/meter/measurement/energy.go new file mode 100644 index 000000000..658a91460 --- /dev/null +++ b/meter/measurement/energy.go @@ -0,0 +1,31 @@ +package measurement + +import ( + "context" + "fmt" + + "github.com/evcc-io/evcc/plugin" +) + +type Energy struct { + Power plugin.Config + Energy *plugin.Config // optional +} + +func (cc *Energy) Configure(ctx context.Context) ( + func() (float64, error), + func() (float64, error), + error, +) { + powerG, err := cc.Power.FloatGetter(ctx) + if err != nil { + return nil, nil, fmt.Errorf("power: %w", err) + } + + energyG, err := cc.Energy.FloatGetter(ctx) + if err != nil { + return nil, nil, fmt.Errorf("energy: %w", err) + } + + return powerG, energyG, nil +} diff --git a/meter/helper.go b/meter/measurement/phases.go similarity index 55% rename from meter/helper.go rename to meter/measurement/phases.go index bcfa96859..b364e7417 100644 --- a/meter/helper.go +++ b/meter/measurement/phases.go @@ -1,4 +1,4 @@ -package meter +package measurement import ( "context" @@ -8,39 +8,27 @@ import ( "github.com/evcc-io/evcc/plugin" ) -// BuildMeasurements returns typical meter measurement getters from config -func BuildMeasurements(ctx context.Context, power, energy *plugin.Config) (func() (float64, error), func() (float64, error), error) { - powerG, err := power.FloatGetter(ctx) - if err != nil { - return nil, nil, fmt.Errorf("power: %w", err) - } - - energyG, err := energy.FloatGetter(ctx) - if err != nil { - return nil, nil, fmt.Errorf("energy: %w", err) - } - - return powerG, energyG, nil +type Phases struct { + Currents, Voltages, Powers []plugin.Config // optional } -// BuildPhaseMeasurements returns typical meter measurement getters from config -func BuildPhaseMeasurements(ctx context.Context, currents, voltages, powers []plugin.Config) ( +func (cc *Phases) Configure(ctx context.Context) ( func() (float64, float64, float64, error), func() (float64, float64, float64, error), func() (float64, float64, float64, error), error, ) { - currentsG, err := buildPhaseProviders(ctx, currents) + currentsG, err := buildPhaseProviders(ctx, cc.Currents) if err != nil { return nil, nil, nil, fmt.Errorf("currents: %w", err) } - voltagesG, err := buildPhaseProviders(ctx, voltages) + voltagesG, err := buildPhaseProviders(ctx, cc.Voltages) if err != nil { return nil, nil, nil, fmt.Errorf("voltages: %w", err) } - powersG, err := buildPhaseProviders(ctx, powers) + powersG, err := buildPhaseProviders(ctx, cc.Powers) if err != nil { return nil, nil, nil, fmt.Errorf("powers: %w", err) } @@ -68,11 +56,11 @@ func buildPhaseProviders(ctx context.Context, providers []plugin.Config) (func() phases[idx] = c } - return collectPhaseProviders(phases), nil + return CombinePhases(phases), nil } -// collectPhaseProviders combines phase getters into combined api function -func collectPhaseProviders(g [3]func() (float64, error)) func() (float64, float64, float64, error) { +// CombinePhases combines phase getters into combined api function +func CombinePhases(g [3]func() (float64, error)) func() (float64, float64, float64, error) { return func() (float64, float64, float64, error) { var res [3]float64 for idx, currentG := range g { diff --git a/meter/meter.go b/meter/meter.go index 7f7bfa534..66850f164 100644 --- a/meter/meter.go +++ b/meter/meter.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/meter/measurement" "github.com/evcc-io/evcc/plugin" "github.com/evcc-io/evcc/util" ) @@ -18,11 +19,8 @@ func init() { // NewConfigurableFromConfig creates api.Meter from config func NewConfigurableFromConfig(ctx context.Context, other map[string]interface{}) (api.Meter, error) { cc := struct { - Power plugin.Config - Energy *plugin.Config // optional - Currents []plugin.Config // optional - Voltages []plugin.Config // optional - Powers []plugin.Config // optional + measurement.Energy `mapstructure:",squash"` // energy optional + measurement.Phases `mapstructure:",squash"` // optional // battery capacity `mapstructure:",squash"` @@ -42,12 +40,12 @@ func NewConfigurableFromConfig(ctx context.Context, other map[string]interface{} return nil, err } - powerG, energyG, err := BuildMeasurements(ctx, &cc.Power, cc.Energy) + powerG, energyG, err := cc.Energy.Configure(ctx) if err != nil { return nil, err } - currentsG, voltagesG, powersG, err := BuildPhaseMeasurements(ctx, cc.Currents, cc.Voltages, cc.Powers) + currentsG, voltagesG, powersG, err := cc.Phases.Configure(ctx) if err != nil { return nil, err } diff --git a/meter/openwb.go b/meter/openwb.go index c5f731c5b..6a8cda7cb 100644 --- a/meter/openwb.go +++ b/meter/openwb.go @@ -8,6 +8,7 @@ import ( "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/charger/openwb" + "github.com/evcc-io/evcc/meter/measurement" "github.com/evcc-io/evcc/plugin" "github.com/evcc-io/evcc/plugin/mqtt" "github.com/evcc-io/evcc/util" @@ -73,7 +74,7 @@ func NewOpenWBFromConfig(other map[string]interface{}) (api.Meter, error) { curr[i] = current } - currents = collectPhaseProviders(curr) + currents = measurement.CombinePhases(curr) case "pv": // first pv diff --git a/templates/definition/charger/idm.yaml b/templates/definition/charger/idm.yaml new file mode 100644 index 000000000..a0b9dcc49 --- /dev/null +++ b/templates/definition/charger/idm.yaml @@ -0,0 +1,53 @@ +template: idm +products: + - brand: IDM +group: heating +requirements: + # evcc: ["sponsorship"] +params: + - name: modbus + choice: ["tcpip"] + - name: tempsource + type: choice + choice: ["", "warmwater", "buffer"] + description: + de: "Temperaturquelle" + en: "Temperature source" + - name: phases + type: int + advanced: true +render: | + type: heatpump + phases: {{ .phases }} + setmaxpower: + source: modbus + {{- include "modbus" . | indent 2 }} + register: + address: 74 # PV Überschussleistung + type: holding + decode: float32s + scale: 0.001 + power: + source: modbus + {{- include "modbus" . | indent 2 }} + register: + address: 4122 # aktuelle Aufnahmeleistung der WP + type: holding + decode: float32s + scale: 1000 + energy: + source: modbus + {{- include "modbus" . | indent 2 }} + register: + address: 4128 + type: holding + decode: float32s + {{- if .tempsource }} + temp: + source: modbus + {{- include "modbus" . | indent 2 }} + register: + address: {{ if eq .tempsource "warmwater" -}} 1014 {{ else }} 1008 {{- end }} # 1014 Trinkwasser, 1008 Wärmespeicher + type: holding + decode: float32s + {{- end }} diff --git a/templates/definition/charger/lambda-zewotherm.yaml b/templates/definition/charger/lambda-zewotherm.yaml new file mode 100644 index 000000000..58c72448a --- /dev/null +++ b/templates/definition/charger/lambda-zewotherm.yaml @@ -0,0 +1,36 @@ +template: lambda-zewotherm +products: + - brand: Lambda + description: + de: EU-L Serie + en: EU-L Series + - brand: Zewotherm + description: + de: EU-L Serie + en: EU-L Series +group: heating +requirements: + # evcc: ["sponsorship"] +params: + - name: modbus + choice: ["tcpip"] + - name: phases + type: int + advanced: true +render: | + type: heatpump + phases: {{ .phases }} + setmaxpower: + source: modbus + {{- include "modbus" . | indent 2 }} + register: + address: 103 # PV Überschussleistung + type: holding + decode: int16 + power: + source: modbus + {{- include "modbus" . | indent 2 }} + register: + address: 103 # aktuelle Aufnahmeleistung der WP + type: holding + decode: int16