From bb8bf49f8d5dc7f22c4c43a7d9305637b6e6e3c0 Mon Sep 17 00:00:00 2001 From: andig Date: Wed, 6 Nov 2024 15:17:46 +0100 Subject: [PATCH] Tariffs: add formulas (#17002) --- evcc.dist.yaml | 7 +++ tariff/awattar.go | 4 ++ tariff/edf-tempo.go | 4 ++ tariff/elering.go | 4 ++ tariff/embed.go | 65 +++++++++++++++++++++++-- tariff/energinet.go | 4 ++ tariff/entsoe.go | 4 ++ tariff/pun.go | 4 ++ tariff/smartenergy.go | 4 ++ tariff/tariff.go | 4 ++ tariff/tibber.go | 4 ++ util/templates/defaults.yaml | 6 +++ util/templates/includes/tariff-base.tpl | 3 ++ 13 files changed, 114 insertions(+), 3 deletions(-) diff --git a/evcc.dist.yaml b/evcc.dist.yaml index 9f9088d2c5..08bb751296 100644 --- a/evcc.dist.yaml +++ b/evcc.dist.yaml @@ -197,6 +197,13 @@ tariffs: # uri: https://example.org/price.json # jq: .price.current + # type: template + # template: energy-charts-api # epex spot market prices + # bzn: DE-LU + # charges: 0.15 + # tax: 0.1 + # formula: math.Min((price + charges) * (1 + tax), 0.5) + feedin: # rate for feeding excess (pv) energy to the grid type: fixed diff --git a/tariff/awattar.go b/tariff/awattar.go index bf9ce03aae..1c4096862f 100644 --- a/tariff/awattar.go +++ b/tariff/awattar.go @@ -39,6 +39,10 @@ func NewAwattarFromConfig(other map[string]interface{}) (api.Tariff, error) { return nil, err } + if err := cc.init(); err != nil { + return nil, err + } + t := &Awattar{ embed: &cc.embed, log: util.NewLogger("awattar"), diff --git a/tariff/edf-tempo.go b/tariff/edf-tempo.go index 6365f45a08..2adf03c2ec 100644 --- a/tariff/edf-tempo.go +++ b/tariff/edf-tempo.go @@ -53,6 +53,10 @@ func NewEdfTempoFromConfig(other map[string]interface{}) (api.Tariff, error) { return nil, errors.New("missing credentials") } + if err := cc.init(); err != nil { + return nil, err + } + basic := transport.BasicAuthHeader(cc.ClientID, cc.ClientSecret) log := util.NewLogger("edf-tempo").Redact(basic) diff --git a/tariff/elering.go b/tariff/elering.go index e49e154073..057e09a42c 100644 --- a/tariff/elering.go +++ b/tariff/elering.go @@ -43,6 +43,10 @@ func NewEleringFromConfig(other map[string]interface{}) (api.Tariff, error) { return nil, errors.New("missing region") } + if err := cc.init(); err != nil { + return nil, err + } + t := &Elering{ embed: &cc.embed, log: util.NewLogger("elering"), diff --git a/tariff/embed.go b/tariff/embed.go index 50dad07440..9da6d63c8a 100644 --- a/tariff/embed.go +++ b/tariff/embed.go @@ -1,12 +1,71 @@ package tariff +import ( + "errors" + "fmt" + + "github.com/traefik/yaegi/interp" + "github.com/traefik/yaegi/stdlib" +) + type embed struct { - Margin float64 `mapstructure:"margin"` Charges float64 `mapstructure:"charges"` Tax float64 `mapstructure:"tax"` - Uplifts float64 `mapstructure:"uplifts"` + Formula string `mapstructure:"formula"` + + calc func(float64) (float64, error) +} + +func (t *embed) init() error { + if t.Formula == "" { + return nil + } + + vm := interp.New(interp.Options{}) + if err := vm.Use(stdlib.Symbols); err != nil { + return err + } + + if _, err := vm.Eval(`import "math"`); err != nil { + return err + } + + if _, err := vm.Eval("var price, charges, tax float64"); err != nil { + return err + } + + prg, err := vm.Compile(t.Formula) + if err != nil { + return err + } + + t.calc = func(price float64) (float64, error) { + if _, err := vm.Eval(fmt.Sprintf("price = %f", price)); err != nil { + return 0, err + } + + res, err := vm.Execute(prg) + if err != nil { + return 0, err + } + + if !res.CanFloat() { + return 0, errors.New("formula did not return a float value") + } + + return res.Float(), nil + } + + // test the formula + _, err = t.calc(0) + + return err } func (t *embed) totalPrice(price float64) float64 { - return (price*(1+t.Margin)+t.Charges)*(1+t.Tax) + t.Uplifts + if t.calc != nil { + res, _ := t.calc(price) + return res + } + return (price + t.Charges) * (1 + t.Tax) } diff --git a/tariff/energinet.go b/tariff/energinet.go index a5170f1da6..7dffb04290 100644 --- a/tariff/energinet.go +++ b/tariff/energinet.go @@ -42,6 +42,10 @@ func NewEnerginetFromConfig(other map[string]interface{}) (api.Tariff, error) { return nil, errors.New("missing region") } + if err := cc.init(); err != nil { + return nil, err + } + t := &Energinet{ embed: &cc.embed, log: util.NewLogger("energinet"), diff --git a/tariff/entsoe.go b/tariff/entsoe.go index 714117a3b0..894fb44e0d 100644 --- a/tariff/entsoe.go +++ b/tariff/entsoe.go @@ -51,6 +51,10 @@ func NewEntsoeFromConfig(other map[string]interface{}) (api.Tariff, error) { return nil, errors.New("missing domain") } + if err := cc.init(); err != nil { + return nil, err + } + domain, err := entsoe.Area(entsoe.BZN, strings.ToUpper(cc.Domain)) if err != nil { return nil, err diff --git a/tariff/pun.go b/tariff/pun.go index 1225928261..29703bcf68 100644 --- a/tariff/pun.go +++ b/tariff/pun.go @@ -55,6 +55,10 @@ func NewPunFromConfig(other map[string]interface{}) (api.Tariff, error) { return nil, err } + if err := cc.init(); err != nil { + return nil, err + } + t := &Pun{ log: util.NewLogger("pun"), embed: &cc, diff --git a/tariff/smartenergy.go b/tariff/smartenergy.go index 17b0116f7b..50ec47c2a3 100644 --- a/tariff/smartenergy.go +++ b/tariff/smartenergy.go @@ -33,6 +33,10 @@ func NewSmartEnergyFromConfig(other map[string]interface{}) (api.Tariff, error) return nil, err } + if err := cc.init(); err != nil { + return nil, err + } + t := &SmartEnergy{ embed: &cc.embed, log: util.NewLogger("smartenergy"), diff --git a/tariff/tariff.go b/tariff/tariff.go index 4aa16952ad..e9b6f7dfe5 100644 --- a/tariff/tariff.go +++ b/tariff/tariff.go @@ -46,6 +46,10 @@ func NewConfigurableFromConfig(ctx context.Context, other map[string]interface{} return nil, fmt.Errorf("must have either price or forecast") } + if err := cc.init(); err != nil { + return nil, err + } + var ( err error priceG func() (float64, error) diff --git a/tariff/tibber.go b/tariff/tibber.go index e339cad471..48c8271fa9 100644 --- a/tariff/tibber.go +++ b/tariff/tibber.go @@ -44,6 +44,10 @@ func NewTibberFromConfig(other map[string]interface{}) (api.Tariff, error) { return nil, errors.New("missing token") } + if err := cc.init(); err != nil { + return nil, err + } + log := util.NewLogger("tibber").Redact(cc.Token, cc.HomeID) t := &Tibber{ diff --git a/util/templates/defaults.yaml b/util/templates/defaults.yaml index 3fbe6676a0..881bf0d14b 100644 --- a/util/templates/defaults.yaml +++ b/util/templates/defaults.yaml @@ -333,6 +333,12 @@ presets: help: de: Zusätzlicher prozentualer Aufschlag (z.B. 0.2 für 20%) en: Additional percentage charge (e.g. 0.2 for 20%) + - name: formula + type: string + help: + de: Individuelle Formel zur Berechnung des Preises + en: Individual formula for calculating the price + example: "math.Max((price + charges) * (1 + tax), 0.0)" eebus: params: - name: ski diff --git a/util/templates/includes/tariff-base.tpl b/util/templates/includes/tariff-base.tpl index d530859a80..8f9e3e9647 100644 --- a/util/templates/includes/tariff-base.tpl +++ b/util/templates/includes/tariff-base.tpl @@ -5,4 +5,7 @@ charges: {{ .charges }} {{- if .tax }} tax: {{ .tax }} {{- end }} +{{- if .formula }} +formula: {{ .formula }} +{{- end }} {{- end }}