From 8769ae489de5d6c87fd46abd042c4cb9dc9afeae Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Fri, 19 Jun 2026 14:31:09 +0200 Subject: [PATCH] Config UI: separate consumers from additional meters (#30243) --- AGENTS.md | 4 +- assets/js/components/Config/MeterCard.vue | 15 ++--- assets/js/components/Config/MeterModal.vue | 29 ++++----- .../js/components/Energyflow/Energyflow.vue | 9 +-- assets/js/components/History/GroupChart.vue | 20 +++--- assets/js/components/History/groups.ts | 11 +++- assets/js/components/Site/Site.vue | 1 + assets/js/types/evcc.ts | 3 +- assets/js/views/Config.vue | 48 +++++++++++--- assets/js/views/History.vue | 62 +++++++++--------- cmd/dump.go | 6 ++ cmd/migrate.go | 1 + cmd/refs.go | 3 +- core/keys/site.go | 12 ++-- core/metrics/collector.go | 3 +- core/metrics/db_history.go | 2 +- core/site.go | 64 ++++++++++++++----- core/site/api.go | 2 + core/site_api.go | 16 +++++ i18n/de.json | 20 ++++-- i18n/en.json | 18 ++++-- server/http_config_site_handler.go | 48 ++++++++------ tests/config-aux.spec.ts | 15 ++--- tests/config-consumer.spec.ts | 61 ++++++++++++++++++ tests/config-ext-meter.spec.ts | 35 ++-------- tests/device-colors.spec.ts | 37 +++++++---- tests/device-colors.sql | 28 +++++--- tests/energy-history.spec.ts | 34 ++++++++-- tests/energy-history.sql | 39 +++++++---- tests/energyflow-consumers.evcc.yaml | 32 ++++++++++ tests/energyflow-consumers.spec.ts | 27 ++++++++ 31 files changed, 494 insertions(+), 211 deletions(-) create mode 100644 tests/config-consumer.spec.ts create mode 100644 tests/energyflow-consumers.evcc.yaml create mode 100644 tests/energyflow-consumers.spec.ts diff --git a/AGENTS.md b/AGENTS.md index baae681a5..49b9e3a92 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -257,7 +257,9 @@ Deep documentation on specific subsystems is available in `docs/agents/`. Load w - Use `expectModalVisible()` and `expectModalHidden()` helpers - Test configuration persistence across application restarts - Standard structure: import `{ start, stop, baseUrl }` from `./evcc`, use `test.afterEach(stop)` -- Never use fixed timeouts, use existance of elements or wait for network idle +- Never use fixed timeouts. Wait on element state (visibility, count, value) instead. +- Never use `page.waitForLoadState("networkidle")`. SPAs keep emitting requests (websockets, polling), so it either races or hangs. Wait for the specific element / value you need instead. +- Keep test names and describe titles short and concrete. They should complement each other, not repeat. Prefer `describe("aux meter") test("create")` over `describe("aux meter") test("create aux meter and verify it appears")`. Drop scenario filler like "and lands in section", "appears correctly", "ensure". ## Device Integration & Configuration diff --git a/assets/js/components/Config/MeterCard.vue b/assets/js/components/Config/MeterCard.vue index fb5a23c04..508c53c13 100644 --- a/assets/js/components/Config/MeterCard.vue +++ b/assets/js/components/Config/MeterCard.vue @@ -39,7 +39,8 @@ export default { meterType: { type: String, required: true, - validator: (value) => ["grid", "pv", "battery", "aux", "ext"].includes(value), + validator: (value) => + ["grid", "pv", "battery", "aux", "ext", "consumer"].includes(value), }, hasError: { type: Boolean, @@ -77,11 +78,12 @@ export default { battery: this.$t("config.devices.batteryStorage"), aux: this.$t("config.devices.auxMeter"), ext: this.$t("config.devices.extMeter"), + consumer: this.$t("config.devices.consumer"), }; return titleMap[this.meterType]; }, isVehicleIcon() { - return this.meterType === "aux" || this.meterType === "ext"; + return ["aux", "ext", "consumer"].includes(this.meterType); }, iconComponent() { const iconMap = { @@ -92,13 +94,8 @@ export default { return iconMap[this.meterType]; }, iconName() { - if (this.meterType === "aux") { - return this.meter.deviceIcon || "smartconsumer"; - } - if (this.meterType === "ext") { - return this.meter.deviceIcon || "generic"; - } - return null; + const defaults = { aux: "smartconsumer", consumer: "generic", ext: "meter" }; + return this.meter.deviceIcon || defaults[this.meterType] || null; }, }, }; diff --git a/assets/js/components/Config/MeterModal.vue b/assets/js/components/Config/MeterModal.vue index fd5d6bb43..d42746572 100644 --- a/assets/js/components/Config/MeterModal.vue +++ b/assets/js/components/Config/MeterModal.vue @@ -121,7 +121,8 @@ const CUSTOM_FIELDS = ["usage", "modbus"]; const defaultIcons: Record = { aux: "smartconsumer", - ext: "generic", + consumer: "generic", + ext: "meter", }; export default defineComponent({ @@ -153,8 +154,8 @@ export default defineComponent({ selectedType(): MeterType | undefined { return getModal("meter")?.type as MeterType | undefined; }, - typeChoices(): MeterType[] { - return (getModal("meter")?.choices as MeterType[]) || []; + typeChoices(): string[] { + return (getModal("meter")?.choices as string[]) || []; }, modalTitle(): string { if (this.isNew) { @@ -169,23 +170,24 @@ export default defineComponent({ templateUsage(): MeterTemplateUsage | undefined { if (!this.selectedType) return undefined; - // For ext meters, the user selects the template usage explicitly - // For other meter types, the meter type IS the template usage + // ext is the only type where usage is user-selected, not the type itself if (this.selectedType === "ext") { return this.extMeterUsage; } - // For non-ext meters, selectedType directly maps to template usage - // (grid->grid, pv->pv, battery->battery, charge->charge, aux->aux) + // consumers are always charge meters + if (this.selectedType === "consumer") { + return "charge"; + } return this.selectedType; }, hasDeviceTitle(): boolean { - return ["pv", "battery", "aux", "ext"].includes(this.selectedType || ""); + return ["pv", "battery", "aux", "ext", "consumer"].includes(this.selectedType || ""); }, hasDeviceIcon(): boolean { - return ["aux", "ext"].includes(this.selectedType || ""); + return ["aux", "ext", "consumer"].includes(this.selectedType || ""); }, hasDescription(): boolean { - return ["ext", "aux"].includes(this.selectedType || ""); + return ["ext", "aux", "consumer"].includes(this.selectedType || ""); }, isNew(): boolean { return this.id === undefined; @@ -204,8 +206,8 @@ export default defineComponent({ this.extMeterUsage = values.usage; } }, - selectType(type: MeterType) { - replaceModal("meter", { id: this.id, type }); + selectType(type: string) { + replaceModal("meter", { id: this.id, type: type as MeterType }); }, provideTemplateOptions(products: Product[]): TemplateGroup[] { return [ @@ -241,9 +243,6 @@ export default defineComponent({ }, transformApiData(data: ApiData, values: DeviceValues): ApiData { if (values.type === ConfigType.Template) { - // Set the template usage (what the template should do) - // For ext meters: user-selected usage (grid, pv, battery, charge, aux) - // For other meters: selectedType itself is the usage data.usage = this.templateUsage; } return data; diff --git a/assets/js/components/Energyflow/Energyflow.vue b/assets/js/components/Energyflow/Energyflow.vue index 5cf51857b..3700c2bcd 100644 --- a/assets/js/components/Energyflow/Energyflow.vue +++ b/assets/js/components/Energyflow/Energyflow.vue @@ -197,9 +197,9 @@ @details-clicked="toggleCo2" @toggle="toggleConsumers" > -