Config UI: make circuit/vehicle refs changeable (#21539)

This commit is contained in:
Michael Geers 2025-05-31 10:18:31 +02:00 • committed by GitHub
parent 4d2ba9ba0b
commit e30102270c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 211 additions and 4 deletions

View file

@ -7,6 +7,7 @@
:defaultYaml="defaultYaml" :defaultYaml="defaultYaml"
removeKey="circuits" removeKey="circuits"
endpoint="/config/circuits" endpoint="/config/circuits"
data-testid="circuits-modal"
@changed="$emit('changed')" @changed="$emit('changed')"
/> />
</template> </template>

View file

@ -1007,7 +1007,7 @@ CONTINUE:
} }
if !isRoot && !instance.HasMeter() { if !isRoot && !instance.HasMeter() {
return fmt.Errorf("circuit %s has no meter and no loadpoint assigned", dev.Config().Name) log.INFO.Printf("circuit %s has no meter and no loadpoint assigned", dev.Config().Name)
} }
} }

View file

@ -102,8 +102,7 @@ loadpoints:
// circuit without device // circuit without device
err = validateCircuits(lps) err = validateCircuits(lps)
suite.Require().Error(err) suite.Require().NoError(err)
suite.Require().Equal("circuit slave has no meter and no loadpoint assigned", err.Error())
} }
func (suite *circuitsTestSuite) TestMissingRootCircuit() { func (suite *circuitsTestSuite) TestMissingRootCircuit() {

View file

@ -193,6 +193,7 @@
"chargerPowerCustomHelp": "Definiere einen eigenen Strombereich.", "chargerPowerCustomHelp": "Definiere einen eigenen Strombereich.",
"chargerTypeLabel": "Ladetyp", "chargerTypeLabel": "Ladetyp",
"chargingTitle": "Laden", "chargingTitle": "Laden",
"circuitHelp": "Lastmanagement-Zuordnung, um die Leistungs- und Stromgrenzen nicht zu überschreiten.",
"circuitLabel": "Stromkreis", "circuitLabel": "Stromkreis",
"circuitUnassigned": "nicht zugewiesen", "circuitUnassigned": "nicht zugewiesen",
"defaultModeHelp": "Lademodus beim Anschließen des Fahrzeugs.", "defaultModeHelp": "Lademodus beim Anschließen des Fahrzeugs.",

View file

@ -193,6 +193,7 @@
"chargerPowerCustomHelp": "Define a custom current range.", "chargerPowerCustomHelp": "Define a custom current range.",
"chargerTypeLabel": "Charger type", "chargerTypeLabel": "Charger type",
"chargingTitle": "Charging", "chargingTitle": "Charging",
"circuitHelp": "Load management assignment to ensure power and current limits are not exceeded.",
"circuitLabel": "Circuit", "circuitLabel": "Circuit",
"circuitUnassigned": "unassigned", "circuitUnassigned": "unassigned",
"defaultModeHelp": "Charging mode when connecting the vehicle.", "defaultModeHelp": "Charging mode when connecting the vehicle.",

View file

@ -206,7 +206,7 @@ func updateLoadpointHandler() http.HandlerFunc {
// merge here to maintain dynamic part of the config // merge here to maintain dynamic part of the config
other := configurable.Config().Other other := configurable.Config().Other
if err := mergo.Merge(&other, static); err != nil { if err := mergo.Merge(&other, static, mergo.WithOverride); err != nil {
jsonError(w, http.StatusBadRequest, err) jsonError(w, http.StatusBadRequest, err)
return return
} }

30
tests/config-circuit.evcc.yaml Executable file
View file

@ -0,0 +1,30 @@
site:
meters:
grid: grid
meters:
- name: grid
type: template
template: demo-meter
power: 2070
currentL1: 3
currentL2: 3
currentL3: 3
loadpoints:
- title: Carport
charger: charger
circuit: main
circuits:
- name: main
meter: grid
maxcurrent: 16
chargers:
- name: charger
type: template
template: demo-charger
status: C
enabled: true
power: 1000

View file

@ -0,0 +1,175 @@
import { test, expect } from "@playwright/test";
import { start, stop, restart, baseUrl } from "./evcc";
import {
enableExperimental,
expectModalVisible,
expectModalHidden,
editorClear,
editorType,
} from "./utils";
const CONFIG_YAML = "config-circuit.evcc.yaml";
const CONFIG_EMPTY = "config-empty.evcc.yaml";
test.use({ baseURL: baseUrl() });
test.afterEach(async () => {
await stop();
});
test.describe("circuit", async () => {
test("from yaml", async ({ page }) => {
await start(CONFIG_YAML);
await page.goto("/#/config");
await enableExperimental(page);
await expect(page.getByTestId("loadpoint")).toHaveCount(1);
await expect(page.getByTestId("loadpoint")).toContainText(["Power", "1.0 kW"].join(""));
await expect(page.getByTestId("grid")).toHaveCount(1);
await expect(page.getByTestId("grid")).toContainText(["Power", "2.1 kW"].join(""));
await expect(page.getByTestId("grid")).toContainText(
["Current L1, L2, L3", "3.0 · 3.0 · 3.0 A"].join("")
);
await expect(page.getByTestId("circuits")).toHaveCount(1);
await expect(page.getByTestId("circuits")).toContainText(["Power", "2.1 kW"].join(""));
await expect(page.getByTestId("circuits")).toContainText(
["Current", "3.0 A / 16.0 A"].join("")
);
});
test("via ui", async ({ page }) => {
await start(CONFIG_EMPTY);
await page.goto("/#/config");
await enableExperimental(page);
// add grid meter
await page.getByRole("button", { name: "Add grid meter" }).click();
const meterModal = page.getByTestId("meter-modal");
await expectModalVisible(meterModal);
await meterModal.getByLabel("Manufacturer").selectOption("Demo meter");
await meterModal.getByLabel("Power").fill("2070");
await page.getByRole("button", { name: "Show advanced settings" }).click();
await meterModal.getByLabel("L1 current").fill("3");
await meterModal.getByLabel("L2 current").fill("3");
await meterModal.getByLabel("L3 current").fill("3");
await meterModal.getByRole("button", { name: "Save" }).click();
await expectModalHidden(meterModal);
// add loadpoint and charger
const lpModal = page.getByTestId("loadpoint-modal");
await page.getByRole("button", { name: "Add charge point" }).click();
await expectModalVisible(lpModal);
await lpModal.getByLabel("Title").fill("Carport");
// add charger
await lpModal.getByRole("button", { name: "Add charger" }).click();
const chargerModal = page.getByTestId("charger-modal");
await expectModalVisible(chargerModal);
await chargerModal.getByLabel("Manufacturer").selectOption("Demo charger");
await chargerModal.getByLabel("Charge status").selectOption("C");
await chargerModal.getByLabel("Power").fill("1000");
await chargerModal.getByRole("button", { name: "Save" }).click();
await expectModalHidden(chargerModal);
await expectModalVisible(lpModal);
await lpModal.getByRole("button", { name: "Save" }).click();
await expectModalHidden(lpModal);
// add circuit via ui as yaml input
await page.getByTestId("circuits").getByRole("button", { name: "edit" }).click();
const circuitsModal = page.getByTestId("circuits-modal");
await expectModalVisible(circuitsModal);
const editor = circuitsModal.getByTestId("yaml-editor");
await editorClear(editor);
await editorType(editor, [
// prettier-ignore
"- name: main",
" meter: db:1",
"maxcurrent: 16",
"Shift+Tab",
"- name: house",
" title: House",
"maxcurrent: 10",
"parent: main",
"Shift+Tab",
"- name: garage",
" title: Garage",
"maxcurrent: 8",
"parent: main",
]);
await circuitsModal.getByRole("button", { name: "Save" }).click();
await expectModalHidden(circuitsModal);
// restart
const restartButton = await page
.getByTestId("bottom-banner")
.getByRole("button", { name: "Restart" });
await expect(restartButton).toBeVisible();
await restart(CONFIG_EMPTY);
await page.reload();
// assign loadpoint to circuit
await page.getByTestId("loadpoint").getByRole("button", { name: "edit" }).click();
await expectModalVisible(lpModal);
await lpModal.getByLabel("Circuit").selectOption("Garage [garage]");
await lpModal.getByLabel("Circuit").selectOption("House [house]");
await lpModal.getByRole("button", { name: "Save" }).click();
await expectModalHidden(lpModal);
// save, restart and check values
await expect(restartButton).toBeVisible();
await restart(CONFIG_EMPTY);
await page.reload();
// verify the configuration matches the yaml test
await expect(page.getByTestId("loadpoint")).toHaveCount(1);
await expect(page.getByTestId("loadpoint")).toContainText(["Power", "1.0 kW"].join(""));
await expect(page.getByTestId("grid")).toHaveCount(1);
await expect(page.getByTestId("grid")).toContainText(["Power", "2.1 kW"].join(""));
await expect(page.getByTestId("grid")).toContainText(
["Current L1, L2, L3", "3.0 · 3.0 · 3.0 A"].join("")
);
await expect(page.getByTestId("circuits")).toHaveCount(1);
await expect(page.getByTestId("circuits")).toContainText(
["(main)", "Power", "2.1 kW", "Current", "3.0 A / 16.0 A"].join("")
);
await expect(page.getByTestId("circuits")).toContainText(
["House (house)", "Power", "1.0 kW", "Current", "6.0 A / 10.0 A"].join("")
);
await expect(page.getByTestId("circuits")).toContainText(
["Garage (garage)", "Power", "0.0 kW", "Current", "0.0 A / 8.0 A"].join("")
);
// assign to garage
await page.getByTestId("loadpoint").getByRole("button", { name: "edit" }).click();
await expectModalVisible(lpModal);
await lpModal.getByLabel("Circuit").selectOption("Garage [garage]");
await lpModal.getByRole("button", { name: "Save" }).click();
await expectModalHidden(lpModal);
// save, restart and check values
await expect(restartButton).toBeVisible();
await restart(CONFIG_EMPTY);
await page.reload();
// verify circuits
await expect(page.getByTestId("circuits")).toHaveCount(1);
await expect(page.getByTestId("circuits")).toContainText(
["(main)", "Power", "2.1 kW", "Current", "3.0 A / 16.0 A"].join("")
);
await expect(page.getByTestId("circuits")).toContainText(
["House (house)", "Power", "0.0 kW", "Current", "0.0 A / 10.0 A"].join("")
);
await expect(page.getByTestId("circuits")).toContainText(
["Garage (garage)", "Power", "1.0 kW", "Current", "6.0 A / 8.0 A"].join("")
);
});
});