202 lines
4.5 KiB
Go
202 lines
4.5 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"slices"
|
|
"sync"
|
|
|
|
"github.com/evcc-io/evcc/api"
|
|
"github.com/evcc-io/evcc/util/config"
|
|
"github.com/evcc-io/evcc/util/templates"
|
|
)
|
|
|
|
const (
|
|
// typeTemplate is the updatable configuration type
|
|
typeTemplate = "template"
|
|
|
|
// masked indicates a masked config parameter value
|
|
masked = "***"
|
|
)
|
|
|
|
type newFromConfFunc[T any] func(context.Context, string, map[string]any) (T, error)
|
|
|
|
var (
|
|
dirty bool
|
|
mu sync.Mutex
|
|
)
|
|
|
|
// ConfigDirty returns the dirty flag
|
|
func ConfigDirty() bool {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
|
|
return dirty
|
|
}
|
|
|
|
// setConfigDirty sets the dirty flag indicating that a restart is required
|
|
func setConfigDirty() {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
|
|
dirty = true
|
|
}
|
|
|
|
func templateForConfig(class templates.Class, conf map[string]any) (templates.Template, error) {
|
|
typ, ok := conf[typeTemplate].(string)
|
|
if !ok {
|
|
return templates.Template{}, errors.New("config template not found")
|
|
}
|
|
|
|
return templates.ByName(class, typ)
|
|
}
|
|
|
|
func sanitizeMasked(class templates.Class, conf map[string]any) (map[string]any, error) {
|
|
tmpl, err := templateForConfig(class, conf)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
res := make(map[string]any, len(conf))
|
|
|
|
for k, v := range conf {
|
|
if i, p := tmpl.ParamByName(k); i >= 0 && p.IsMasked() {
|
|
v = masked
|
|
}
|
|
|
|
res[k] = v
|
|
}
|
|
|
|
return res, nil
|
|
}
|
|
|
|
func mergeMasked(class templates.Class, conf, old map[string]any) (map[string]any, error) {
|
|
tmpl, err := templateForConfig(class, conf)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
res := make(map[string]any, len(conf))
|
|
|
|
for k, v := range conf {
|
|
if i, p := tmpl.ParamByName(k); i >= 0 && p.IsMasked() && v == masked {
|
|
v = old[k]
|
|
}
|
|
|
|
res[k] = v
|
|
}
|
|
|
|
return res, nil
|
|
}
|
|
|
|
func deviceInstanceFromMergedConfig[T any](id int, class templates.Class, conf map[string]any, newFromConf newFromConfFunc[T], h config.Handler[T]) (config.Device[T], T, map[string]any, error) {
|
|
var zero T
|
|
|
|
dev, err := h.ByName(config.NameForID(id))
|
|
if err != nil {
|
|
return nil, zero, nil, err
|
|
}
|
|
|
|
merged, err := mergeMasked(class, conf, dev.Config().Other)
|
|
if err != nil {
|
|
return nil, zero, nil, err
|
|
}
|
|
|
|
instance, err := newFromConf(context.TODO(), typeTemplate, merged)
|
|
|
|
return dev, instance, merged, err
|
|
}
|
|
|
|
type testResult = struct {
|
|
Value any `json:"value"`
|
|
Error string `json:"error"`
|
|
}
|
|
|
|
// testInstance tests the given instance similar to dump
|
|
// TODO refactor together with dump
|
|
func testInstance(instance any) map[string]testResult {
|
|
res := make(map[string]testResult)
|
|
|
|
makeResult := func(val any, err error) testResult {
|
|
res := testResult{Value: val}
|
|
if err != nil {
|
|
res.Error = err.Error()
|
|
}
|
|
return res
|
|
}
|
|
|
|
if dev, ok := instance.(api.Meter); ok {
|
|
val, err := dev.CurrentPower()
|
|
res["power"] = makeResult(val, err)
|
|
}
|
|
|
|
if dev, ok := instance.(api.MeterEnergy); ok {
|
|
val, err := dev.TotalEnergy()
|
|
res["energy"] = makeResult(val, err)
|
|
}
|
|
|
|
if dev, ok := instance.(api.Battery); ok {
|
|
val, err := dev.Soc()
|
|
key := "soc"
|
|
if fd, ok := instance.(api.FeatureDescriber); ok && slices.Contains(fd.Features(), api.Heating) {
|
|
key = "temp"
|
|
}
|
|
res[key] = makeResult(val, err)
|
|
}
|
|
|
|
if _, ok := instance.(api.BatteryController); ok {
|
|
res["controllable"] = makeResult(true, nil)
|
|
}
|
|
|
|
if dev, ok := instance.(api.VehicleOdometer); ok {
|
|
val, err := dev.Odometer()
|
|
res["odometer"] = makeResult(val, err)
|
|
}
|
|
|
|
if dev, ok := instance.(api.BatteryCapacity); ok {
|
|
val := dev.Capacity()
|
|
res["capacity"] = makeResult(val, nil)
|
|
}
|
|
|
|
if dev, ok := instance.(api.PhaseCurrents); ok {
|
|
i1, i2, i3, err := dev.Currents()
|
|
res["phaseCurrents"] = makeResult([]float64{i1, i2, i3}, err)
|
|
}
|
|
|
|
if dev, ok := instance.(api.PhaseVoltages); ok {
|
|
u1, u2, u3, err := dev.Voltages()
|
|
res["phaseVoltages"] = makeResult([]float64{u1, u2, u3}, err)
|
|
}
|
|
|
|
if dev, ok := instance.(api.PhasePowers); ok {
|
|
p1, p2, p3, err := dev.Powers()
|
|
res["phasePowers"] = makeResult([]float64{p1, p2, p3}, err)
|
|
}
|
|
|
|
if dev, ok := instance.(api.ChargeState); ok {
|
|
val, err := dev.Status()
|
|
res["chargeStatus"] = makeResult(val, err)
|
|
}
|
|
|
|
if dev, ok := instance.(api.Charger); ok {
|
|
val, err := dev.Enabled()
|
|
res["enabled"] = makeResult(val, err)
|
|
}
|
|
|
|
if dev, ok := instance.(api.ChargeRater); ok {
|
|
val, err := dev.ChargedEnergy()
|
|
res["chargedEnergy"] = makeResult(val, err)
|
|
}
|
|
|
|
if dev, ok := instance.(api.VehicleRange); ok {
|
|
val, err := dev.Range()
|
|
res["range"] = makeResult(val, err)
|
|
}
|
|
|
|
if dev, ok := instance.(api.SocLimiter); ok {
|
|
val, err := dev.GetLimitSoc()
|
|
res["socLimit"] = makeResult(val, err)
|
|
}
|
|
|
|
return res
|
|
}
|