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" > -