Config UI: handle breaking changes in templates (#24618)
This commit is contained in:
parent
454a1923ce
commit
ab37e8d967
5 changed files with 134 additions and 4 deletions
|
|
@ -101,7 +101,6 @@ func deviceConfigMap[T any](class templates.Class, dev config.Device[T]) (map[st
|
|||
}
|
||||
|
||||
if conf.Type == typeTemplate {
|
||||
// template device, mask config
|
||||
params, err := sanitizeMasked(class, conf.Other)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
|||
|
|
@ -26,7 +26,9 @@ const (
|
|||
masked = "***" // masked indicates a masked config parameter value
|
||||
)
|
||||
|
||||
var customTypes = []string{"custom", "template", "heatpump", "switchsocket", "sgready", "sgready-boost"}
|
||||
var (
|
||||
customTypes = []string{"custom", "template", "heatpump", "switchsocket", "sgready", "sgready-boost"}
|
||||
)
|
||||
|
||||
type configReq struct {
|
||||
config.Properties `json:",inline" mapstructure:",squash"`
|
||||
|
|
@ -105,6 +107,23 @@ func templateForConfig(class templates.Class, conf map[string]any) (templates.Te
|
|||
return templates.ByName(class, typ)
|
||||
}
|
||||
|
||||
func filterValidTemplateParams(tmpl *templates.Template, conf map[string]any) map[string]any {
|
||||
res := make(map[string]any)
|
||||
|
||||
for k, v := range conf {
|
||||
if k == "template" {
|
||||
res[k] = v
|
||||
continue
|
||||
}
|
||||
|
||||
if i, _ := tmpl.ParamByName(k); i >= 0 {
|
||||
res[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func sanitizeMasked(class templates.Class, conf map[string]any) (map[string]any, error) {
|
||||
tmpl, err := templateForConfig(class, conf)
|
||||
if err != nil {
|
||||
|
|
@ -121,7 +140,7 @@ func sanitizeMasked(class templates.Class, conf map[string]any) (map[string]any,
|
|||
res[k] = v
|
||||
}
|
||||
|
||||
return res, nil
|
||||
return filterValidTemplateParams(&tmpl, res), nil
|
||||
}
|
||||
|
||||
func mergeMasked(class templates.Class, conf, old map[string]any) (map[string]any, error) {
|
||||
|
|
@ -140,7 +159,7 @@ func mergeMasked(class templates.Class, conf, old map[string]any) (map[string]an
|
|||
res[k] = v
|
||||
}
|
||||
|
||||
return res, nil
|
||||
return filterValidTemplateParams(&tmpl, res), nil
|
||||
}
|
||||
|
||||
func startDeviceTimeout() (context.Context, context.CancelFunc, chan struct{}) {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"github.com/evcc-io/evcc/api/globalconfig"
|
||||
"github.com/evcc-io/evcc/plugin/mqtt"
|
||||
"github.com/evcc-io/evcc/util/config"
|
||||
"github.com/evcc-io/evcc/util/templates"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
|
@ -134,3 +135,47 @@ func TestSquashedMergeMaskedAny(t *testing.T) {
|
|||
assert.Equal(t, "new", new.User)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeMaskedFiltersBehavior(t *testing.T) {
|
||||
conf := map[string]any{
|
||||
"template": "demo-meter",
|
||||
"power": 200.0,
|
||||
}
|
||||
|
||||
old := map[string]any{
|
||||
"template": "demo-meter",
|
||||
"power": 100.0,
|
||||
"outdatedField": "old-value",
|
||||
}
|
||||
|
||||
result, err := mergeMasked(templates.Meter, conf, old)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 200.0, result["power"])
|
||||
assert.Equal(t, "demo-meter", result["template"])
|
||||
assert.NotContains(t, result, "outdatedField")
|
||||
}
|
||||
|
||||
func TestFilterValidTemplateParams(t *testing.T) {
|
||||
conf := map[string]any{
|
||||
"template": "generic",
|
||||
"usage": "grid",
|
||||
"capacity": 50.0,
|
||||
"power": 100.0,
|
||||
"outdatedField": "should-be-removed",
|
||||
}
|
||||
|
||||
result := filterValidTemplateParams(&templates.Template{
|
||||
TemplateDefinition: templates.TemplateDefinition{
|
||||
Params: []templates.Param{
|
||||
{Name: "usage"},
|
||||
{Name: "power"},
|
||||
{Name: "capacity"},
|
||||
},
|
||||
},
|
||||
}, conf)
|
||||
|
||||
assert.Equal(t, "generic", result["template"], "template")
|
||||
assert.Equal(t, "grid", result["usage"], "usage")
|
||||
assert.NotContains(t, result, "outdatedField")
|
||||
}
|
||||
|
|
|
|||
46
tests/config-invalid-template.spec.ts
Normal file
46
tests/config-invalid-template.spec.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import { test, expect } from "@playwright/test";
|
||||
import { start, stop, restart, baseUrl } from "./evcc";
|
||||
import { expectModalVisible, expectModalHidden, enableExperimental } from "./utils";
|
||||
|
||||
const CONFIG_INVALID_TEMPLATE = "config-invalid-template.sql";
|
||||
|
||||
test.use({ baseURL: baseUrl() });
|
||||
|
||||
test.afterEach(async () => {
|
||||
await stop();
|
||||
});
|
||||
|
||||
test.describe("invalid template migration", async () => {
|
||||
test("fix broken grid meter with outdated field", async ({ page }) => {
|
||||
await start(undefined, CONFIG_INVALID_TEMPLATE);
|
||||
|
||||
await page.goto("/#/config");
|
||||
await enableExperimental(page, false);
|
||||
|
||||
// startup error
|
||||
await expect(page.getByTestId("fatal-error")).toBeVisible();
|
||||
await expect(page.getByTestId("fatal-error")).toContainText("invalid key: power_old");
|
||||
await expect(page.getByTestId("grid")).toBeVisible();
|
||||
|
||||
// edit and save broken meter
|
||||
await page.getByTestId("grid").getByRole("button", { name: "edit" }).click();
|
||||
const meterModal = page.getByTestId("meter-modal");
|
||||
await expectModalVisible(meterModal);
|
||||
await expect(meterModal.getByLabel("Manufacturer")).toHaveValue("Demo meter");
|
||||
await meterModal.getByLabel("Power").fill("222");
|
||||
await meterModal.getByRole("button", { name: "Validate & save" }).click();
|
||||
await expectModalHidden(meterModal);
|
||||
await expect(page.getByTestId("grid")).toBeVisible();
|
||||
|
||||
// verify restart
|
||||
await restart();
|
||||
await page.reload();
|
||||
await expect(page.getByTestId("fatal-error")).not.toBeVisible();
|
||||
await expect(page.getByTestId("grid")).toBeVisible();
|
||||
await page.getByTestId("grid").getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(meterModal);
|
||||
await expect(meterModal.getByLabel("Power")).toHaveValue("222");
|
||||
await meterModal.getByRole("button", { name: "Close" }).click();
|
||||
await expectModalHidden(meterModal);
|
||||
});
|
||||
});
|
||||
21
tests/config-invalid-template.sql
Normal file
21
tests/config-invalid-template.sql
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
BEGIN;
|
||||
|
||||
CREATE TABLE `configs` (
|
||||
`id` integer PRIMARY KEY AUTOINCREMENT
|
||||
, `class` integer
|
||||
, `type` text
|
||||
, `title` text
|
||||
, `icon` text
|
||||
, `product` text
|
||||
, `value` text
|
||||
);
|
||||
CREATE TABLE `settings` (
|
||||
`key` text
|
||||
, `value` text
|
||||
, PRIMARY KEY(`key`)
|
||||
);
|
||||
|
||||
INSERT INTO configs(id, class, type, title, icon, product, value) VALUES(1, 2, 'template', '', '', 'Demo meter', '{"maxacpower":"0","power_old":222,"template":"demo-meter","usage":"grid"}');
|
||||
INSERT INTO settings("key", value) VALUES('gridMeter', 'db:1');
|
||||
|
||||
COMMIT;
|
||||
Loading…
Add table
Add a link
Reference in a new issue