evcc-io/server/http_config_loadpoint_handler.go

403 lines
10 KiB
Go

package server
import (
"errors"
"io"
"net/http"
"strconv"
"dario.cat/mergo"
"github.com/evcc-io/evcc/core"
"github.com/evcc-io/evcc/core/loadpoint"
coresettings "github.com/evcc-io/evcc/core/settings"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/config"
"github.com/evcc-io/evcc/util/templates"
"github.com/go-viper/mapstructure/v2"
"github.com/gorilla/mux"
)
func getLoadpointStaticConfig(lp loadpoint.API) loadpoint.StaticConfig {
return loadpoint.StaticConfig{
Charger: lp.GetChargerRef(),
Meter: lp.GetMeterRef(),
Circuit: lp.GetCircuitRef(),
Vehicle: lp.GetDefaultVehicleRef(),
}
}
func getLoadpointDynamicConfig(lp loadpoint.API) loadpoint.DynamicConfig {
planTime, planEnergy := lp.GetPlanEnergy()
return loadpoint.DynamicConfig{
Title: lp.GetTitle(),
DefaultMode: string(lp.GetDefaultMode()),
Priority: lp.GetPriority(),
PhasesConfigured: lp.GetPhasesConfigured(),
MinCurrent: lp.GetMinCurrent(),
MaxCurrent: lp.GetMaxCurrent(),
SmartCostLimit: lp.GetSmartCostLimit(),
SmartFeedInPriorityLimit: lp.GetSmartFeedInPriorityLimit(),
Thresholds: lp.GetThresholds(),
Soc: lp.GetSocConfig(),
UI: lp.GetUI(),
PlanEnergy: planEnergy,
PlanTime: planTime,
PlanStrategy: lp.GetPlanStrategy(),
BatteryBoostLimit: lp.GetBatteryBoostLimit(),
LimitEnergy: lp.GetLimitEnergy(),
LimitSoc: lp.GetLimitSoc(),
}
}
type loadpointFullConfig struct {
ID int `json:"id,omitempty"` // db row id
Name string `json:"name"` // either slice index (yaml) or db:<row id>
Disable bool `json:"disable,omitempty"`
// static config
loadpoint.StaticConfig
loadpoint.DynamicConfig
}
func loadpointSplitConfig(r io.Reader) (loadpoint.DynamicConfig, map[string]any, map[string]any, *bool, error) {
var payload map[string]any
if err := jsonDecoder(r).Decode(&payload); err != nil {
return loadpoint.DynamicConfig{}, nil, nil, nil, err
}
// nil if not part of the payload- keeps the persisted flag
var disable *bool
if v, ok := payload["disable"].(bool); ok {
disable = &v
}
delete(payload, "disable")
delete(payload, "id")
delete(payload, "name")
dynamic, static, err := loadpoint.SplitConfig(payload)
return dynamic, static, payload, disable, err
}
// setDeviceDisable persists the disable flag on a loadpoint's subdevice
func setDeviceDisable[T any](name string, h config.Handler[T], disable bool) error {
if name == "" {
return nil
}
dev, err := configurableDevice(name, h)
if err != nil {
// static device- cannot persist
return nil
}
props := dev.Properties()
if props.Disable == disable {
return nil
}
props.Disable = disable
return dev.Update(dev.Config().Other, dev.Instance(), config.WithProperties(props))
}
// loadpointConfig returns a single loadpoint's configuration
func loadpointConfig(dev config.Device[loadpoint.API]) (loadpointFullConfig, error) {
var (
id int
disable bool
)
if configurable, ok := dev.(config.ConfigurableDevice[loadpoint.API]); ok {
id = configurable.ID()
disable = configurable.Properties().Disable
}
lp := dev.Instance()
// disabled loadpoint has no live instance; decode static config from the database instead
if lp == nil {
dynamic, staticMap, err := loadpoint.SplitConfig(dev.Config().Other)
if err != nil {
return loadpointFullConfig{}, err
}
// non-strict: runtime settings (mode, ...) are persisted alongside the config
var static loadpoint.StaticConfig
if err := mapstructure.Decode(staticMap, &static); err != nil {
return loadpointFullConfig{}, err
}
return loadpointFullConfig{
ID: id,
Name: dev.Config().Name,
Disable: disable,
StaticConfig: static,
DynamicConfig: dynamic,
}, nil
}
res := loadpointFullConfig{
ID: id,
Name: dev.Config().Name,
Disable: disable,
StaticConfig: getLoadpointStaticConfig(lp),
DynamicConfig: getLoadpointDynamicConfig(lp),
}
return res, nil
}
// loadpointsConfigHandler returns a device configurations by class
func loadpointsConfigHandler() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
devices := config.Loadpoints().Devices()
res := make([]loadpointFullConfig, 0, len(devices))
for _, dev := range devices {
c, err := loadpointConfig(dev)
if err != nil {
jsonError(w, http.StatusBadRequest, err)
return
}
res = append(res, c)
}
jsonWrite(w, res)
}
}
// loadpointConfigHandler returns a device configurations by class
func loadpointConfigHandler() http.HandlerFunc {
h := config.Loadpoints()
return func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id, err := strconv.Atoi(vars["id"])
if err != nil {
jsonError(w, http.StatusBadRequest, err)
return
}
dev, err := h.ByName(config.NameForID(id))
if err != nil {
jsonError(w, http.StatusBadRequest, err)
return
}
res, err := loadpointConfig(dev)
if err != nil {
jsonError(w, http.StatusBadRequest, err)
return
}
jsonWrite(w, res)
}
}
// newLoadpointHandler creates a new loadpoint
func newLoadpointHandler() http.HandlerFunc {
h := config.Loadpoints()
// TODO revert charger, meter etc
return func(w http.ResponseWriter, r *http.Request) {
dynamic, static, _, disable, err := loadpointSplitConfig(r.Body)
if err != nil {
jsonError(w, http.StatusBadRequest, err)
return
}
id := len(h.Devices())
name := "lp-" + strconv.Itoa(id+1)
log := util.NewLoggerWithLoadpoint(name, id+1)
conf, err := config.AddConfig(templates.Loadpoint, static, config.WithProperties(config.Properties{Disable: disable != nil && *disable}))
if err != nil {
jsonError(w, http.StatusBadRequest, err)
return
}
settings := coresettings.NewConfigSettingsAdapter(log, &conf)
instance, err := core.NewLoadpointFromConfig(log, settings, nil, static)
if err != nil {
conf.Delete()
jsonError(w, http.StatusBadRequest, err)
return
}
dev := config.NewConfigurableDevice[loadpoint.API](&conf, instance)
if err := dynamic.Apply(instance); err != nil {
conf.Delete()
jsonError(w, http.StatusBadRequest, err)
return
}
if err := h.Add(dev); err != nil {
conf.Delete()
jsonError(w, http.StatusBadRequest, err)
return
}
setConfigDirty()
jsonWrite(w, struct {
ID int `json:"id"`
}{ID: conf.ID})
}
}
// updateLoadpointHandler returns a device configurations by class
func updateLoadpointHandler() http.HandlerFunc {
h := config.Loadpoints()
return func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id, err := strconv.Atoi(vars["id"])
if err != nil {
jsonError(w, http.StatusBadRequest, err)
return
}
dev, err := h.ByName(config.NameForID(id))
if err != nil {
jsonError(w, http.StatusBadRequest, err)
return
}
configurable, ok := dev.(config.ConfigurableDevice[loadpoint.API])
if !ok {
jsonError(w, http.StatusBadRequest, errors.New("not configurable"))
return
}
dynamic, static, payload, disable, err := loadpointSplitConfig(r.Body)
if err != nil {
jsonError(w, http.StatusBadRequest, err)
return
}
props := configurable.Properties()
if disable != nil {
props.Disable = *disable
}
instance := dev.Instance()
// merge static config to maintain the dynamic part; without live instance
// merge the full payload since dynamic setters cannot persist it
src := static
if instance == nil {
src = payload
}
other := configurable.Config().Other
if err := mergo.Merge(&other, src, mergo.WithOverride); err != nil {
jsonError(w, http.StatusBadRequest, err)
return
}
if err := configurable.Update(other, instance, config.WithProperties(props)); err != nil {
jsonError(w, http.StatusBadRequest, err)
return
}
// propagate disable to the loadpoint's charger and meter
chargerRef, _ := other["charger"].(string)
if err := setDeviceDisable(chargerRef, config.Chargers(), props.Disable); err != nil {
jsonError(w, http.StatusBadRequest, err)
return
}
meterRef, _ := other["meter"].(string)
if err := setDeviceDisable(meterRef, config.Meters(), props.Disable); err != nil {
jsonError(w, http.StatusBadRequest, err)
return
}
// dynamic; instance is nil for a disabled loadpoint, takes effect on next restart
if instance != nil {
if err := dynamic.Apply(instance); err != nil {
jsonError(w, http.StatusBadRequest, err)
return
}
}
setConfigDirty()
w.WriteHeader(http.StatusOK)
}
}
// deleteLoadpointHandler deletes a loadpoint
func deleteLoadpointHandler() http.HandlerFunc {
h := config.Loadpoints()
return func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id, err := strconv.Atoi(vars["id"])
if err != nil {
jsonError(w, http.StatusBadRequest, err)
return
}
// cleanup references
lp, err := configurableDevice(config.NameForID(id), h)
if err != nil {
jsonError(w, http.StatusBadRequest, err)
return
}
instance := lp.Instance()
// disabled loadpoint has no live instance; delete it without ref cleanup
if instance == nil {
if err := deleteDevice(id, h); err != nil {
jsonError(w, http.StatusBadRequest, err)
return
}
jsonWrite(w, struct {
ID int `json:"id"`
}{ID: id})
return
}
if dev, err := configurableDevice(instance.GetChargerRef(), config.Chargers()); err == nil {
if err := deleteDevice(dev.ID(), config.Chargers()); err != nil {
jsonError(w, http.StatusBadRequest, err)
return
}
setConfigDirty()
}
if dev, err := configurableDevice(instance.GetMeterRef(), config.Meters()); err == nil {
if err := deleteDevice(dev.ID(), config.Meters()); err != nil {
jsonError(w, http.StatusBadRequest, err)
return
}
setConfigDirty()
}
setConfigDirty()
if err := deleteDevice(id, h); err != nil {
jsonError(w, http.StatusBadRequest, err)
return
}
res := struct {
ID int `json:"id"`
}{
ID: id,
}
jsonWrite(w, res)
}
}