Config UI: fix editing custom device with type override (#29379)
This commit is contained in:
parent
23c46dd311
commit
e711ccaa86
6 changed files with 61 additions and 40 deletions
|
|
@ -11,8 +11,6 @@ import (
|
||||||
"github.com/evcc-io/evcc/server/db/settings"
|
"github.com/evcc-io/evcc/server/db/settings"
|
||||||
"github.com/evcc-io/evcc/util"
|
"github.com/evcc-io/evcc/util"
|
||||||
"github.com/evcc-io/evcc/util/config"
|
"github.com/evcc-io/evcc/util/config"
|
||||||
"github.com/spf13/cast"
|
|
||||||
"go.yaml.in/yaml/v4"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// parseLogLevels parses --log area:level[,...] switch into levels per log area
|
// parseLogLevels parses --log area:level[,...] switch into levels per log area
|
||||||
|
|
@ -97,29 +95,6 @@ func wrapFatalError(err error) error {
|
||||||
return &FatalError{err}
|
return &FatalError{err}
|
||||||
}
|
}
|
||||||
|
|
||||||
// customDevice promotes yaml type to top level type
|
|
||||||
func customDevice(typ string, other map[string]any) (string, map[string]any, error) {
|
|
||||||
// no embedded yaml
|
|
||||||
customYaml, ok := other["yaml"].(string)
|
|
||||||
if !ok {
|
|
||||||
return typ, other, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var res map[string]any
|
|
||||||
if err := yaml.Unmarshal([]byte(customYaml), &res); err != nil {
|
|
||||||
return typ, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// type override
|
|
||||||
if typ := cast.ToString(res["type"]); typ != "" {
|
|
||||||
delete(res, "type")
|
|
||||||
return typ, res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// no override
|
|
||||||
return typ, res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func deviceHeader[T any](dev config.Device[T]) string {
|
func deviceHeader[T any](dev config.Device[T]) string {
|
||||||
name := dev.Config().Name
|
name := dev.Config().Name
|
||||||
|
|
||||||
|
|
|
||||||
10
cmd/setup.go
10
cmd/setup.go
|
|
@ -222,7 +222,7 @@ NEXT:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
typ, other, err := customDevice(cc.Type, cc.Other)
|
typ, other, err := config.CustomDevice(cc.Type, cc.Other)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot decode custom circuit '%s': %w", cc.Name, err)
|
return fmt.Errorf("cannot decode custom circuit '%s': %w", cc.Name, err)
|
||||||
}
|
}
|
||||||
|
|
@ -327,7 +327,7 @@ func configurableInstance[T any](typ string, conf *config.Config, newFromConf ne
|
||||||
cc := conf.Named()
|
cc := conf.Named()
|
||||||
ctx, cancel := context.WithCancel(util.WithLogger(context.TODO(), loggerForConfig(conf))) //nolint:govet
|
ctx, cancel := context.WithCancel(util.WithLogger(context.TODO(), loggerForConfig(conf))) //nolint:govet
|
||||||
|
|
||||||
typ, other, err := customDevice(cc.Type, cc.Other)
|
typ, other, err := config.CustomDevice(cc.Type, cc.Other)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = &DeviceError{cc.Name, fmt.Errorf("cannot decode custom %s '%s': %w", typ, cc.Name, err)}
|
err = &DeviceError{cc.Name, fmt.Errorf("cannot decode custom %s '%s': %w", typ, cc.Name, err)}
|
||||||
}
|
}
|
||||||
|
|
@ -443,7 +443,7 @@ func configureChargers(static []config.Named, names ...string) error {
|
||||||
func vehicleInstance(cc config.Named) (api.Vehicle, error) {
|
func vehicleInstance(cc config.Named) (api.Vehicle, error) {
|
||||||
ctx := util.WithLogger(context.TODO(), util.NewLogger(cc.Name))
|
ctx := util.WithLogger(context.TODO(), util.NewLogger(cc.Name))
|
||||||
|
|
||||||
typ, other, err := customDevice(cc.Type, cc.Other)
|
typ, other, err := config.CustomDevice(cc.Type, cc.Other)
|
||||||
|
|
||||||
var instance api.Vehicle
|
var instance api.Vehicle
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|
@ -791,7 +791,7 @@ func configureHEMS(conf *globalconfig.Hems, site *core.Site) (hemsapi.API, error
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
typ, other, err := customDevice(conf.Type, conf.Other)
|
typ, other, err := config.CustomDevice(conf.Type, conf.Other)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("cannot decode custom hems '%s': %w", conf.Type, err)
|
return nil, fmt.Errorf("cannot decode custom hems '%s': %w", conf.Type, err)
|
||||||
}
|
}
|
||||||
|
|
@ -968,7 +968,7 @@ func configureMessengers(confMessaging *globalconfig.Messaging, confEvents *glob
|
||||||
func tariffInstance(name string, conf config.Typed) (api.Tariff, error) {
|
func tariffInstance(name string, conf config.Typed) (api.Tariff, error) {
|
||||||
ctx := util.WithLogger(context.TODO(), util.NewLogger(name))
|
ctx := util.WithLogger(context.TODO(), util.NewLogger(name))
|
||||||
|
|
||||||
typ, other, err := customDevice(conf.Type, conf.Other)
|
typ, other, err := config.CustomDevice(conf.Type, conf.Other)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("cannot decode custom tariff '%s': %w", name, err)
|
return nil, fmt.Errorf("cannot decode custom tariff '%s': %w", name, err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -280,7 +280,12 @@ func deviceStatusHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func newDevice[T any](ctx context.Context, class templates.Class, req configReq, newFromConf newFromConfFunc[T], h config.Handler[T], force bool) (*config.Config, error) {
|
func newDevice[T any](ctx context.Context, class templates.Class, req configReq, newFromConf newFromConfFunc[T], h config.Handler[T], force bool) (*config.Config, error) {
|
||||||
instance, err := newFromConf(ctx, req.Type, req.Other)
|
typ, other, err := config.CustomDevice(req.Type, req.Other)
|
||||||
|
if err != nil && !force {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
instance, err := newFromConf(ctx, typ, other)
|
||||||
if err != nil && !force {
|
if err != nil && !force {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -616,7 +621,12 @@ func deleteDeviceHandler(site site.API) func(w http.ResponseWriter, r *http.Requ
|
||||||
|
|
||||||
func testConfig[T any](ctx context.Context, id int, class templates.Class, req configReq, newFromConf newFromConfFunc[T], h config.Handler[T]) (T, error) {
|
func testConfig[T any](ctx context.Context, id int, class templates.Class, req configReq, newFromConf newFromConfFunc[T], h config.Handler[T]) (T, error) {
|
||||||
if id == 0 {
|
if id == 0 {
|
||||||
return newFromConf(ctx, req.Type, req.Other)
|
typ, other, err := config.CustomDevice(req.Type, req.Other)
|
||||||
|
if err != nil {
|
||||||
|
var zero T
|
||||||
|
return zero, err
|
||||||
|
}
|
||||||
|
return newFromConf(ctx, typ, other)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, instance, _, err := deviceInstanceFromMergedConfig(ctx, id, class, req, newFromConf, h)
|
_, instance, _, err := deviceInstanceFromMergedConfig(ctx, id, class, req, newFromConf, h)
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ import (
|
||||||
"github.com/evcc-io/evcc/util/templates"
|
"github.com/evcc-io/evcc/util/templates"
|
||||||
"github.com/go-viper/mapstructure/v2"
|
"github.com/go-viper/mapstructure/v2"
|
||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
"github.com/spf13/cast"
|
|
||||||
"go.yaml.in/yaml/v4"
|
"go.yaml.in/yaml/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -226,7 +225,11 @@ func deviceInstanceFromMergedConfig[T any](ctx context.Context, id int, class te
|
||||||
|
|
||||||
// TODO merge custom config
|
// TODO merge custom config
|
||||||
if req.Yaml != "" {
|
if req.Yaml != "" {
|
||||||
instance, err := newFromConf(ctx, conf.Type, req.Other)
|
typ, other, err := config.CustomDevice(conf.Type, req.Other)
|
||||||
|
if err != nil {
|
||||||
|
return nil, zero, nil, err
|
||||||
|
}
|
||||||
|
instance, err := newFromConf(ctx, typ, other)
|
||||||
return dev, instance, req.Serialise(), err
|
return dev, instance, req.Serialise(), err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -441,7 +444,7 @@ func (maskedTransformer) Transformer(typ reflect.Type) func(dst, src reflect.Val
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// decodeDeviceConfig extracts device configuration and yaml details plus type override
|
// decodeDeviceConfig extracts device configuration and yaml details
|
||||||
func decodeDeviceConfig(r io.Reader) (configReq, error) {
|
func decodeDeviceConfig(r io.Reader) (configReq, error) {
|
||||||
var res configReq
|
var res configReq
|
||||||
|
|
||||||
|
|
@ -463,14 +466,13 @@ func decodeDeviceConfig(r io.Reader) (configReq, error) {
|
||||||
return configReq{}, errors.New("invalid config: cannot mix yaml and other")
|
return configReq{}, errors.New("invalid config: cannot mix yaml and other")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := yaml.Unmarshal([]byte(res.Yaml), &res.Other); err != nil && err != io.EOF {
|
// validate yaml syntax
|
||||||
|
var tmp map[string]any
|
||||||
|
if err := yaml.Unmarshal([]byte(res.Yaml), &tmp); err != nil && err != io.EOF {
|
||||||
return configReq{}, err
|
return configReq{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if typ := cast.ToString(res.Other["type"]); typ != "" {
|
res.Other = map[string]any{"yaml": res.Yaml}
|
||||||
res.Type = typ
|
|
||||||
delete(res.Other, "type")
|
|
||||||
}
|
|
||||||
|
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,5 +51,12 @@ uri: http://${simulatorHost()}`
|
||||||
await expect(page.getByTestId("fatal-error")).not.toBeVisible();
|
await expect(page.getByTestId("fatal-error")).not.toBeVisible();
|
||||||
await expect(page.getByTestId("grid")).toBeVisible();
|
await expect(page.getByTestId("grid")).toBeVisible();
|
||||||
await expect(page.getByTestId("grid")).toContainText(["Energy", "0.0 kWh"].join(""));
|
await expect(page.getByTestId("grid")).toContainText(["Energy", "0.0 kWh"].join(""));
|
||||||
|
|
||||||
|
// edit
|
||||||
|
await page.getByTestId("grid").getByRole("button", { name: "edit" }).click();
|
||||||
|
await expectModalVisible(meterModal);
|
||||||
|
const editAgain = meterModal.getByTestId("yaml-editor");
|
||||||
|
await expect(editAgain).toBeVisible();
|
||||||
|
await expect(editAgain).toContainText("type: shelly");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
27
util/config/custom.go
Normal file
27
util/config/custom.go
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/spf13/cast"
|
||||||
|
"go.yaml.in/yaml/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CustomDevice promotes an embedded yaml type to the top-level type
|
||||||
|
func CustomDevice(typ string, other map[string]any) (string, map[string]any, error) {
|
||||||
|
customYaml, ok := other["yaml"].(string)
|
||||||
|
if !ok {
|
||||||
|
return typ, other, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var res map[string]any
|
||||||
|
if err := yaml.Unmarshal([]byte(customYaml), &res); err != nil {
|
||||||
|
return typ, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// type override
|
||||||
|
if override := cast.ToString(res["type"]); override != "" {
|
||||||
|
delete(res, "type")
|
||||||
|
return override, res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return typ, res, nil
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue