diff --git a/cmd/setup.go b/cmd/setup.go index 2cb82f8cc2..1b35ef7027 100644 --- a/cmd/setup.go +++ b/cmd/setup.go @@ -257,7 +257,7 @@ func configureMeters(static []config.Named, names ...string) error { return &DeviceError{cc.Name, fmt.Errorf("cannot create meter '%s': %w", cc.Name, err)} } - if err := config.Meters().Add(config.NewConfigurableDevice(conf, instance)); err != nil { + if err := config.Meters().Add(config.NewConfigurableDevice(&conf, instance)); err != nil { return &DeviceError{cc.Name, err} } @@ -319,7 +319,7 @@ func configureChargers(static []config.Named, names ...string) error { return fmt.Errorf("cannot create charger '%s': %w", cc.Name, err) } - if err := config.Chargers().Add(config.NewConfigurableDevice(conf, instance)); err != nil { + if err := config.Chargers().Add(config.NewConfigurableDevice(&conf, instance)); err != nil { return &DeviceError{cc.Name, err} } @@ -410,7 +410,7 @@ func configureVehicles(static []config.Named, names ...string) error { mu.Lock() defer mu.Unlock() - devs2 = append(devs2, config.NewConfigurableDevice(conf, instance)) + devs2 = append(devs2, config.NewConfigurableDevice(&conf, instance)) return nil }) @@ -979,22 +979,22 @@ func configureLoadpoints(conf globalconfig.All) error { id := len(config.Loadpoints().Devices()) name := "lp-" + strconv.Itoa(id+1) - log := util.NewLoggerWithLoadpoint(name, id+1) - dev := config.BlankConfigurableDevice[loadpoint.API]() - settings := coresettings.NewDeviceSettingsAdapter(dev) + settings := coresettings.NewConfigSettingsAdapter(log, &conf) - // TODO: proper handling of id/name - delete(cc.Other, "id") - delete(cc.Other, "name") + dynamic, static, err := loadpoint.SplitConfig(cc.Other) - instance, err := core.NewLoadpointFromConfig(log, settings, cc.Other) + instance, err := core.NewLoadpointFromConfig(log, settings, static) if err != nil { return fmt.Errorf("failed configuring loadpoint: %w", err) } - dev.Update(cc.Other, instance) + if err := dynamic.Apply(instance); err != nil { + return err + } + + dev := config.NewConfigurableDevice[loadpoint.API](&conf, instance) if err := config.Loadpoints().Add(dev); err != nil { return err } diff --git a/core/loadpoint/config.go b/core/loadpoint/config.go new file mode 100644 index 0000000000..8dd4107c64 --- /dev/null +++ b/core/loadpoint/config.go @@ -0,0 +1,75 @@ +package loadpoint + +import ( + "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/util" +) + +type StaticConfig struct { + // static config + Charger string `json:"charger,omitempty"` + Meter string `json:"meter,omitempty"` + Circuit string `json:"circuit,omitempty"` + Vehicle string `json:"vehicle,omitempty"` +} + +type DynamicConfig struct { + // dynamic config + Title string `json:"title"` + Mode string `json:"mode"` + Priority int `json:"priority"` + Phases int `json:"phases"` + MinCurrent float64 `json:"minCurrent"` + MaxCurrent float64 `json:"maxCurrent"` + SmartCostLimit *float64 `json:"smartCostLimit"` + + Thresholds ThresholdsConfig `json:"thresholds"` + Soc SocConfig `json:"soc"` +} + +func SplitConfig(payload map[string]any) (DynamicConfig, map[string]any, error) { + // split static and dynamic config via mapstructure + var cc struct { + DynamicConfig `mapstructure:",squash"` + Other map[string]any `mapstructure:",remain"` + } + + if err := util.DecodeOther(payload, &cc); err != nil { + return DynamicConfig{}, nil, err + } + + // TODO: proper handling of id/name + delete(cc.Other, "id") + delete(cc.Other, "name") + + return cc.DynamicConfig, cc.Other, nil +} + +func (payload DynamicConfig) Apply(lp API) error { + lp.SetTitle(payload.Title) + lp.SetPriority(payload.Priority) + lp.SetSmartCostLimit(payload.SmartCostLimit) + lp.SetThresholds(payload.Thresholds) + + // TODO mode warning + lp.SetSocConfig(payload.Soc) + + mode, err := api.ChargeModeString(payload.Mode) + if err == nil { + lp.SetMode(mode) + } + + if err == nil { + err = lp.SetPhases(payload.Phases) + } + + if err == nil { + err = lp.SetMinCurrent(payload.MinCurrent) + } + + if err == nil { + err = lp.SetMaxCurrent(payload.MaxCurrent) + } + + return err +} diff --git a/core/settings/config.go b/core/settings/config.go new file mode 100644 index 0000000000..1a71bdd888 --- /dev/null +++ b/core/settings/config.go @@ -0,0 +1,119 @@ +package settings + +import ( + "encoding/json" + "time" + + "github.com/evcc-io/evcc/util" + "github.com/evcc-io/evcc/util/config" + "github.com/spf13/cast" +) + +var _ Settings = (*ConfigSettings)(nil) + +type ConfigSettings struct { + log *util.Logger + conf *config.Config +} + +func NewConfigSettingsAdapter(log *util.Logger, conf *config.Config) *ConfigSettings { + return &ConfigSettings{log, conf} +} + +func (s *ConfigSettings) get(key string) any { + return s.conf.Named().Other[key] +} + +func (s *ConfigSettings) set(key string, val any) { + data := s.conf.Named().Other + data[key] = val + if err := s.conf.Update(data); err != nil { + s.log.ERROR.Println(err) + } +} + +func (s *ConfigSettings) SetString(key string, val string) { + if s == nil { + return + } + s.set(key, val) +} + +func (s *ConfigSettings) SetInt(key string, val int64) { + if s == nil { + return + } + s.set(key, val) +} + +func (s *ConfigSettings) SetFloat(key string, val float64) { + if s == nil { + return + } + s.set(key, val) +} + +func (s *ConfigSettings) SetTime(key string, val time.Time) { + if s == nil { + return + } + s.set(key, val) +} + +func (s *ConfigSettings) SetBool(key string, val bool) { + if s == nil { + return + } + s.set(key, val) +} + +func (s *ConfigSettings) SetJson(key string, val any) error { + if s == nil { + return nil + } + s.set(key, val) + return nil +} + +func (s *ConfigSettings) String(key string) (string, error) { + if s == nil { + return "", nil + } + return cast.ToStringE(s.get(key)) +} + +func (s *ConfigSettings) Int(key string) (int64, error) { + if s == nil { + return 0, nil + } + return cast.ToInt64E(s.get(key)) +} + +func (s *ConfigSettings) Float(key string) (float64, error) { + if s == nil { + return 0, nil + } + return cast.ToFloat64E(s.get(key)) +} + +func (s *ConfigSettings) Time(key string) (time.Time, error) { + if s == nil { + return time.Time{}, nil + } + return cast.ToTimeE(s.get(key)) +} + +func (s *ConfigSettings) Bool(key string) (bool, error) { + if s == nil { + return false, nil + } + return cast.ToBoolE(s.get(key)) +} + +func (s *ConfigSettings) Json(key string, res any) error { + str, err := s.String(key) + if str == "" || err != nil { + return err + } + return json.Unmarshal([]byte(str), &res) +} diff --git a/core/settings/device.go b/core/settings/device.go deleted file mode 100644 index c6d6c23a73..0000000000 --- a/core/settings/device.go +++ /dev/null @@ -1,121 +0,0 @@ -package settings - -import ( - "encoding/json" - "time" - - "github.com/evcc-io/evcc/api" - "github.com/evcc-io/evcc/util/config" - "github.com/spf13/cast" -) - -var _ Settings = (*DeviceSettings[api.Vehicle])(nil) - -type DeviceSettings[T any] struct { - dev config.ConfigurableDevice[T] -} - -func NewDeviceSettingsAdapter[T any](dev config.ConfigurableDevice[T]) *DeviceSettings[T] { - return &DeviceSettings[T]{dev} -} - -func (s *DeviceSettings[T]) Update(dev config.ConfigurableDevice[T]) { - s.dev = dev -} - -func (s *DeviceSettings[T]) get(key string) any { - conf := s.dev.Config().Other - return conf[key] -} - -func (s *DeviceSettings[T]) set(key string, val any) { - conf := s.dev.Config().Other - conf[key] = val - s.dev.Update(conf, s.dev.Instance()) -} - -func (s *DeviceSettings[T]) SetString(key string, val string) { - if s == nil { - return - } - s.set(key, val) -} - -func (s *DeviceSettings[T]) SetInt(key string, val int64) { - if s == nil { - return - } - s.set(key, val) -} - -func (s *DeviceSettings[T]) SetFloat(key string, val float64) { - if s == nil { - return - } - s.set(key, val) -} - -func (s *DeviceSettings[T]) SetTime(key string, val time.Time) { - if s == nil { - return - } - s.set(key, val) -} - -func (s *DeviceSettings[T]) SetBool(key string, val bool) { - if s == nil { - return - } - s.set(key, val) -} - -func (s *DeviceSettings[T]) SetJson(key string, val any) error { - if s == nil { - return nil - } - s.set(key, val) - return nil -} - -func (s *DeviceSettings[T]) String(key string) (string, error) { - if s == nil { - return "", nil - } - return cast.ToStringE(s.get(key)) -} - -func (s *DeviceSettings[T]) Int(key string) (int64, error) { - if s == nil { - return 0, nil - } - return cast.ToInt64E(s.get(key)) -} - -func (s *DeviceSettings[T]) Float(key string) (float64, error) { - if s == nil { - return 0, nil - } - return cast.ToFloat64E(s.get(key)) -} - -func (s *DeviceSettings[T]) Time(key string) (time.Time, error) { - if s == nil { - return time.Time{}, nil - } - return cast.ToTimeE(s.get(key)) -} - -func (s *DeviceSettings[T]) Bool(key string) (bool, error) { - if s == nil { - return false, nil - } - return cast.ToBoolE(s.get(key)) -} - -func (s *DeviceSettings[T]) Json(key string, res any) error { - str, err := s.String(key) - if str == "" || err != nil { - return err - } - return json.Unmarshal([]byte(str), &res) -} diff --git a/server/http_config_device_handler.go b/server/http_config_device_handler.go index c1a3e1a3df..308496d8be 100644 --- a/server/http_config_device_handler.go +++ b/server/http_config_device_handler.go @@ -203,7 +203,7 @@ func newDevice[T any](class templates.Class, req map[string]any, newFromConf fun return nil, err } - return &conf, h.Add(config.NewConfigurableDevice[T](conf, instance)) + return &conf, h.Add(config.NewConfigurableDevice[T](&conf, instance)) } // newDeviceHandler creates a new device by class diff --git a/server/http_config_loadpoint_handler.go b/server/http_config_loadpoint_handler.go index 3d090514d4..27b01aee8b 100644 --- a/server/http_config_loadpoint_handler.go +++ b/server/http_config_loadpoint_handler.go @@ -6,7 +6,6 @@ import ( "net/http" "strconv" - "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/core" "github.com/evcc-io/evcc/core/loadpoint" coresettings "github.com/evcc-io/evcc/core/settings" @@ -17,16 +16,8 @@ import ( "github.com/samber/lo" ) -type loadpointStaticConfig struct { - // static config - Charger string `json:"charger,omitempty"` - Meter string `json:"meter,omitempty"` - Circuit string `json:"circuit,omitempty"` - Vehicle string `json:"vehicle,omitempty"` -} - -func getLoadpointStaticConfig(lp loadpoint.API) loadpointStaticConfig { - return loadpointStaticConfig{ +func getLoadpointStaticConfig(lp loadpoint.API) loadpoint.StaticConfig { + return loadpoint.StaticConfig{ Charger: lp.GetChargerName(), Meter: lp.GetMeterName(), Circuit: lp.GetCircuitName(), @@ -34,22 +25,8 @@ func getLoadpointStaticConfig(lp loadpoint.API) loadpointStaticConfig { } } -type loadpointDynamicConfig struct { - // dynamic config - Title string `json:"title"` - Mode string `json:"mode"` - Priority int `json:"priority"` - Phases int `json:"phases"` - MinCurrent float64 `json:"minCurrent"` - MaxCurrent float64 `json:"maxCurrent"` - SmartCostLimit *float64 `json:"smartCostLimit"` - - Thresholds loadpoint.ThresholdsConfig `json:"thresholds"` - Soc loadpoint.SocConfig `json:"soc"` -} - -func getLoadpointDynamicConfig(lp loadpoint.API) loadpointDynamicConfig { - return loadpointDynamicConfig{ +func getLoadpointDynamicConfig(lp loadpoint.API) loadpoint.DynamicConfig { + return loadpoint.DynamicConfig{ Title: lp.GetTitle(), Mode: string(lp.GetMode()), Priority: lp.GetPriority(), @@ -62,62 +39,23 @@ func getLoadpointDynamicConfig(lp loadpoint.API) loadpointDynamicConfig { } } -func loadpointUpdateDynamicConfig(payload loadpointDynamicConfig, lp loadpoint.API) error { - lp.SetTitle(payload.Title) - lp.SetPriority(payload.Priority) - lp.SetSmartCostLimit(payload.SmartCostLimit) - lp.SetThresholds(payload.Thresholds) - - // TODO mode warning - lp.SetSocConfig(payload.Soc) - - mode, err := api.ChargeModeString(payload.Mode) - if err == nil { - lp.SetMode(mode) - } - - if err == nil { - err = lp.SetPhases(payload.Phases) - } - - if err == nil { - err = lp.SetMinCurrent(payload.MinCurrent) - } - - if err == nil { - err = lp.SetMaxCurrent(payload.MaxCurrent) - } - - return err -} - type loadpointFullConfig struct { ID int `json:"id,omitempty"` // db row id Name string `json:"name"` // either slice index (yaml) or db: // static config - loadpointStaticConfig - loadpointDynamicConfig + loadpoint.StaticConfig + loadpoint.DynamicConfig } -func loadpointSplitConfig(r io.Reader) (loadpointDynamicConfig, map[string]any, error) { +func loadpointSplitConfig(r io.Reader) (loadpoint.DynamicConfig, map[string]any, error) { var payload map[string]any if err := jsonDecoder(r).Decode(&payload); err != nil { - return loadpointDynamicConfig{}, nil, err - } - - // split static and dynamic config via mapstructure - var cc struct { - loadpointDynamicConfig `mapstructure:",squash"` - Other map[string]any `mapstructure:",remain"` - } - - if err := util.DecodeOther(payload, &cc); err != nil { - return loadpointDynamicConfig{}, nil, err + return loadpoint.DynamicConfig{}, nil, err } - return cc.loadpointDynamicConfig, cc.Other, nil + return loadpoint.SplitConfig(payload) } // loadpointConfig returns a single loadpoint's configuration @@ -133,8 +71,8 @@ func loadpointConfig(dev config.Device[loadpoint.API]) loadpointFullConfig { ID: id, Name: dev.Config().Name, - loadpointStaticConfig: getLoadpointStaticConfig(lp), - loadpointDynamicConfig: getLoadpointDynamicConfig(lp), + StaticConfig: getLoadpointStaticConfig(lp), + DynamicConfig: getLoadpointDynamicConfig(lp), } return res @@ -180,6 +118,8 @@ func loadpointConfigHandler() http.HandlerFunc { func newLoadpointHandler() http.HandlerFunc { h := config.Loadpoints() + // TODO revert charger, meter etc + return func(w http.ResponseWriter, r *http.Request) { dynamic, static, err := loadpointSplitConfig(r.Body) if err != nil { @@ -189,30 +129,31 @@ func newLoadpointHandler() http.HandlerFunc { id := len(h.Devices()) name := "lp-" + strconv.Itoa(id+1) - log := util.NewLoggerWithLoadpoint(name, id+1) - dev := config.BlankConfigurableDevice[loadpoint.API]() - settings := coresettings.NewDeviceSettingsAdapter(dev) - - instance, err := core.NewLoadpointFromConfig(log, settings, static) + conf, err := config.AddConfig(templates.Loadpoint, "", static) if err != nil { jsonError(w, http.StatusBadRequest, err) return } - dev.Update(static, instance) - if err := loadpointUpdateDynamicConfig(dynamic, instance); err != nil { + settings := coresettings.NewConfigSettingsAdapter(log, &conf) + instance, err := core.NewLoadpointFromConfig(log, settings, static) + if err != nil { + conf.Delete() jsonError(w, http.StatusBadRequest, err) + return } - conf, err := config.AddConfig(templates.Loadpoint, "", static) - if err != nil { + 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 } @@ -265,7 +206,7 @@ func updateLoadpointHandler() http.HandlerFunc { } // dynamic - if err := loadpointUpdateDynamicConfig(dynamic, instance); err != nil { + if err := dynamic.Apply(instance); err != nil { jsonError(w, http.StatusBadRequest, err) return } diff --git a/util/config/device.go b/util/config/device.go index 7b5d3696b1..b499e4baa3 100644 --- a/util/config/device.go +++ b/util/config/device.go @@ -7,31 +7,24 @@ type Device[T any] interface { type ConfigurableDevice[T any] interface { Device[T] ID() int + // Assign(T) + // Update1(map[string]any) error Update(map[string]any, T) error Delete() error } type configurableDevice[T any] struct { - config Config + config *Config instance T } -func NewConfigurableDevice[T any](config Config, instance T) ConfigurableDevice[T] { +func NewConfigurableDevice[T any](config *Config, instance T) ConfigurableDevice[T] { return &configurableDevice[T]{ config: config, instance: instance, } } -func BlankConfigurableDevice[T any]() ConfigurableDevice[T] { - // NOTE: creating loadpoint will read from settings, hence config.Value must be valid json - return &configurableDevice[T]{ - config: Config{ - Value: "{}", - }, - } -} - func (d *configurableDevice[T]) Config() Named { return d.config.Named() } @@ -44,6 +37,14 @@ func (d *configurableDevice[T]) ID() int { return d.config.ID } +// func (d *configurableDevice[T]) Assign(instance T) { +// d.instance = instance +// } + +// func (d *configurableDevice[T]) Update(config map[string]any) error { +// return d.config.Update(config) +// } + func (d *configurableDevice[T]) Update(config map[string]any, instance T) error { if err := d.config.Update(config); err != nil { return err