tariff/embed.go
Rama master · réplica de lectura del repositorio original.
package tariff
import (
"errors"
"fmt"
"go/ast"
"go/parser"
"go/token"
"reflect"
"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
}
vm := interp.New(interp.Options{})
if err := vm.Use(stdlib.Symbols); err != nil {
return err
}
vm.ImportUsed()
body, err := formulaBody(t.Formula)
if err != nil {
return err
}
// Compile the formula into a callable function, avoiding any per-call parsing
src := fmt.Sprintf("var calc = func(price, charges, tax float64, ts time.Time) float64 {\n%s\n}", body)
if _, err := vm.Eval(src); err != nil {
return err
}
calcFn := vm.Globals()["calc"]
t.calc = func(price, charges float64, ts time.Time) (float64, error) {
var (
res []reflect.Value
err error
)
func() {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic: %v", r)
}
}()
res = calcFn.Call([]reflect.Value{
reflect.ValueOf(price),
reflect.ValueOf(charges),
reflect.ValueOf(t.Tax),
reflect.ValueOf(time.Unix(ts.Unix(), 0).Local()),
})
}()
if err != nil {
return 0, err
}
return res[0].Float(), nil
}
// test the formula
_, err = t.calc(0, t.Charges, time.Now())
return err
}
// formulaBody turns the formula's trailing expression into the return statement,
// allowing multi-statement formulas like `x := 1; if ... { x = 2 }; price + x`
func formulaBody(formula string) (string, error) {
const prefix = "package p; func _() {\n"
f, err := parser.ParseFile(token.NewFileSet(), "", prefix+formula+"\n}", 0)
if err != nil {
return "", err
}
stmts := f.Decls[0].(*ast.FuncDecl).Body.List
if len(stmts) == 0 {
return "", errors.New("empty formula")
}
last, ok := stmts[len(stmts)-1].(*ast.ExprStmt)
if !ok {
return "", errors.New("formula must end with an expression")
}
// file base is 1, offsets are relative to the formula
start, end := int(last.Pos())-1-len(prefix), int(last.End())-1-len(prefix)
return formula[:start] + "return float64(" + formula[start:end] + ")" + formula[end:], nil
}
// 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_
}