diff --git a/assets/js/components/Config/ChargerModal.vue b/assets/js/components/Config/ChargerModal.vue index d9b685574..d93b0c2fc 100644 --- a/assets/js/components/Config/ChargerModal.vue +++ b/assets/js/components/Config/ChargerModal.vue @@ -10,9 +10,10 @@
+

Loading ...

@@ -250,6 +258,9 @@ export default { description() { return this.template?.Requirements?.Description; }, + productName() { + return this.values.deviceProduct || this.templateName; + }, sponsorTokenRequired() { const list = this.template?.Requirements?.EVCC || []; return list.includes("sponsorship") && !this.isSponsor; @@ -298,6 +309,10 @@ export default { try { const charger = (await api.get(`config/devices/charger/${this.id}`)).data.result; this.values = charger.config; + // convert structure to flat list + // TODO: adjust GET response to match POST/PUT formats + this.values.type = charger.type; + this.values.deviceProduct = charger.deviceProduct; this.applyDefaultsFromTemplate(); this.templateName = this.values.template; } catch (e) { @@ -317,6 +332,7 @@ export default { }, async loadTemplate() { this.template = null; + if (!this.templateName) return; this.loadingTemplate = true; try { const opts = { @@ -342,6 +358,13 @@ export default { }); }, async create() { + // persist selected template product + if (this.template) { + const select = this.$refs.templateSelect; + const name = select.options[select.selectedIndex].text; + this.values.deviceProduct = name; + } + if (this.testUnknown) { const success = await this.test(this.testCharger); if (!success) return; diff --git a/assets/js/components/Config/LoadpointModal.vue b/assets/js/components/Config/LoadpointModal.vue index 2637cfb51..049c391a3 100644 --- a/assets/js/components/Config/LoadpointModal.vue +++ b/assets/js/components/Config/LoadpointModal.vue @@ -632,7 +632,7 @@ export default { }, chargerTitle() { if (!this.charger) return ""; - const title = this.charger.config?.template || "unknown"; + const title = this.charger.deviceProduct || this.charger.config?.template || "unknown"; return `${title} [${this.values.charger}]`; }, chargerStatus() { @@ -651,7 +651,7 @@ export default { const name = this.values.meter; if (!name) return ""; const meter = this.meters.find((m) => m.name === name); - const title = meter?.config?.template || "unknown"; + const title = meter?.deviceProduct || meter?.config?.template || "unknown"; return `${title} [${name}]`; }, isDeletable() { diff --git a/assets/js/components/Config/MeterModal.vue b/assets/js/components/Config/MeterModal.vue index d0e43db98..86b0ff734 100644 --- a/assets/js/components/Config/MeterModal.vue +++ b/assets/js/components/Config/MeterModal.vue @@ -39,6 +39,7 @@ - @@ -47,6 +48,13 @@ +

Loading ...

@@ -273,11 +281,11 @@ export default { isModalVisible: false, templates: [], products: [], - saving: false, templateName: null, template: null, - values: { ...initialValues }, + saving: false, loadingTemplate: false, + values: { ...initialValues }, }; }, computed: { @@ -286,6 +294,7 @@ export default { online: this.products.filter((p) => !p.group && p.template !== "offline"), generic: this.products.filter((p) => p.group === "generic"), scooter: this.products.filter((p) => p.group === "scooter"), + offline: this.products.find((p) => p.template === "offline") || {}, }; }, templateParams() { @@ -316,6 +325,9 @@ export default { description() { return this.template?.Requirements?.Description; }, + productName() { + return this.values.deviceProduct || this.templateName; + }, apiData() { const data = { template: this.templateName, @@ -372,6 +384,10 @@ export default { try { const vehicle = (await api.get(`config/devices/vehicle/${this.id}`)).data.result; this.values = vehicle.config; + // convert structure to flat list + // TODO: adjust GET response to match POST/PUT formats + this.values.type = vehicle.type; + this.values.deviceProduct = vehicle.deviceProduct; this.applyDefaultsFromTemplate(); this.templateName = this.values.template; } catch (e) { @@ -379,6 +395,9 @@ export default { } }, async loadProducts() { + if (!this.isModalVisible) { + return; + } try { const opts = { params: { lang: this.$i18n?.locale } }; this.products = (await api.get("config/products/vehicle", opts)).data.result; @@ -388,6 +407,7 @@ export default { }, async loadTemplate() { this.template = null; + if (!this.templateName) return; this.loadingTemplate = true; try { const opts = { @@ -396,7 +416,8 @@ export default { name: this.templateName, }, }; - this.template = (await api.get("config/templates/vehicle", opts)).data.result; + const result = await api.get("config/templates/vehicle", opts); + this.template = result.data.result; this.applyDefaultsFromTemplate(); } catch (e) { console.error(e); @@ -412,10 +433,17 @@ export default { }); }, async create() { + // persist selected template product + if (this.template) { + const select = this.$refs.templateSelect; + const name = select.options[select.selectedIndex].text; + this.values.deviceProduct = name; + } + if (this.testUnknown) { const success = await this.test(this.testVehicle); if (!success) return; - await sleep(250); + await sleep(100); } this.saving = true; try { @@ -423,8 +451,7 @@ export default { this.$emit("vehicle-changed"); this.closed(); } catch (e) { - console.error(e); - alert("create failed"); + this.handleCreateError(e); } this.saving = false; }, @@ -450,8 +477,7 @@ export default { this.$emit("vehicle-changed"); this.closed(); } catch (e) { - console.error(e); - alert("update failed"); + this.handleUpdateError(e); } this.saving = false; }, @@ -461,8 +487,7 @@ export default { this.$emit("vehicle-changed"); this.closed(); } catch (e) { - console.error(e); - alert("delete failed"); + this.handleRemoveError(e); } }, open() { diff --git a/i18n/de.toml b/i18n/de.toml index c68771cdd..1f18c255d 100644 --- a/i18n/de.toml +++ b/i18n/de.toml @@ -357,7 +357,6 @@ validate = "prüfen" cancel = "Abbrechen" delete = "Fahrzeug löschen" generic = "Weitere Integrationen" -offline = "Generisches Fahrzeug" online = "Fahrzeuge mit Schnittstelle" save = "Speichern" scooter = "Elektroroller" diff --git a/i18n/en.toml b/i18n/en.toml index eae80cab8..2a726150a 100644 --- a/i18n/en.toml +++ b/i18n/en.toml @@ -356,7 +356,6 @@ validate = "validate" cancel = "Cancel" delete = "Delete" generic = "Other integrations" -offline = "Generic vehicle" online = "Vehicles with online API" save = "Save" scooter = "Scooter" diff --git a/templates/definition/vehicle/offline.yaml b/templates/definition/vehicle/offline.yaml index e0d238e6d..8c5c3b513 100644 --- a/templates/definition/vehicle/offline.yaml +++ b/templates/definition/vehicle/offline.yaml @@ -1,7 +1,8 @@ template: offline products: - description: - generic: Offline + en: Generic vehicle + de: Generisches Fahrzeug group: generic params: - preset: vehicle-common diff --git a/tests/config-loadpoint.spec.js b/tests/config-loadpoint.spec.js index a3424cb1c..ace65cf57 100644 --- a/tests/config-loadpoint.spec.js +++ b/tests/config-loadpoint.spec.js @@ -104,6 +104,16 @@ test.describe("loadpoint", async () => { await page.getByTestId("loadpoint").getByRole("button", { name: "edit" }).click(); await expect(lpModal).toBeVisible(); await lpModal.getByTestId("chargerPower-22kw").click(); + + // update charger mode + await expect(lpModal.getByRole("textbox", { name: "Charger" })).toHaveValue( + "Demo charger [db:1]" + ); + await lpModal.getByRole("textbox", { name: "Charger" }).click(); + await chargerModal.getByLabel("Charge status").selectOption("A"); + await chargerModal.getByRole("button", { name: "Save" }).click(); + await expect(chargerModal).not.toBeVisible(); + await lpModal.getByRole("button", { name: "Save" }).click(); await expect(lpModal).not.toBeVisible(); @@ -111,6 +121,8 @@ test.describe("loadpoint", async () => { await restart(CONFIG_EMPTY); await page.reload(); await expect(page.getByTestId("loadpoint")).toHaveCount(1); + await expect(page.getByTestId("loadpoint")).toContainText("not connected"); + await page.getByTestId("loadpoint").getByRole("button", { name: "edit" }).click(); await expect(lpModal).toBeVisible(); await expect(lpModal.getByTestId("chargerPower-22kw")).toHaveClass(/active/);