evcc-io/tariff/embed.go
2026-06-04 12:21:11 +02:00

121 lines
2.7 KiB
Go

package tariff
import (
"errors"
"fmt"
"slices"
"time"
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/plugin/golang/stdlib"
"github.com/evcc-io/evcc/tariff/fixed"
"github.com/traefik/yaegi/interp"
)
type embed struct {
Features_ []api.Feature `mapstructure:"features"`
Charges float64 `mapstructure:"charges"`
ChargesZones_ []chargesZoneConfig `mapstructure:"chargesZones"`
Tax float64 `mapstructure:"tax"`
Formula string `mapstructure:"formula"`
chargesZones fixed.Zones
calc func(price, charges float64, ts time.Time) (float64, error)
}
type chargesZoneConfig struct {
Charges float64
Days, Hours, Months string
}
func (t *embed) init() (err error) {
defer func() {
if r := recover(); r != nil && err == nil {
err = fmt.Errorf("panic: %v", r)
}
}()
specs := make([]fixed.ZoneSpec, len(t.ChargesZones_))
for i, c := range t.ChargesZones_ {
specs[i] = fixed.ZoneSpec{Price: c.Charges, Days: c.Days, Hours: c.Hours, Months: c.Months}
}
zones, err := fixed.ParseZones(specs)
if err != nil {
return err
}
t.chargesZones = zones
if t.Formula == "" {
return nil
}
t.calc = func(price, charges float64, ts time.Time) (float64, error) {
vm := interp.New(interp.Options{})
if err := vm.Use(stdlib.Symbols); err != nil {
return 0, err
}
vm.ImportUsed()
if _, err := vm.Eval(fmt.Sprintf(`
var (
price float64 = %f
charges float64 = %f
tax float64 = %f
ts = time.Unix(%d, 0).Local()
)`, price, charges, t.Tax, ts.Unix())); err != nil {
return 0, err
}
res, err := vm.Eval(t.Formula)
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, t.Charges, time.Now())
return err
}
// effectiveCharges resolves the charge for ts in local time; later zones win.
func (t *embed) effectiveCharges(ts time.Time) float64 {
if len(t.chargesZones) == 0 {
return t.Charges
}
ts = ts.Local()
day := fixed.Day(int(ts.Weekday()))
month := fixed.Month(ts.Month() - 1)
hm := fixed.HourMin{Hour: ts.Hour(), Min: ts.Minute()}
zones := t.chargesZones.ForDayAndMonth(day, month)
for _, z := range slices.Backward(zones) {
if z.Hours.Contains(hm) {
return z.Price
}
}
return t.Charges
}
func (t *embed) totalPrice(price float64, ts time.Time) float64 {
charges := t.effectiveCharges(ts)
if t.calc != nil {
res, _ := t.calc(price, charges, ts)
return res
}
return (price + charges) * (1 + t.Tax)
}
var _ api.FeatureDescriber = (*embed)(nil)
func (t *embed) Features() []api.Feature {
return t.Features_
}