From 2a0429aa7611a7f104f9611f47067ede6af997d8 Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Wed, 5 Nov 2025 00:12:00 -0800 Subject: [PATCH] Config UI: fix preserving modbus fields (#25029) --- server/http_config_helper.go | 9 +++++ tests/config-modbus-fields.spec.ts | 57 ++++++++++++++++++++++++++++++ tests/config-modbus-fields.sql | 24 +++++++++++++ util/templates/template_modbus.go | 8 ++--- util/templates/types.go | 40 +++++++++++++-------- 5 files changed, 119 insertions(+), 19 deletions(-) create mode 100644 tests/config-modbus-fields.spec.ts create mode 100644 tests/config-modbus-fields.sql diff --git a/server/http_config_helper.go b/server/http_config_helper.go index 1963730f1..ce94c9800 100644 --- a/server/http_config_helper.go +++ b/server/http_config_helper.go @@ -110,12 +110,21 @@ func templateForConfig(class templates.Class, conf map[string]any) (templates.Te func filterValidTemplateParams(tmpl *templates.Template, conf map[string]any) map[string]any { res := make(map[string]any) + // check if template has modbus capability + hasModbus := len(tmpl.ModbusChoices()) > 0 + for k, v := range conf { if k == "template" { res[k] = v continue } + // preserve modbus fields if template supports modbus + if hasModbus && slices.Contains(templates.ModbusParams, k) { + res[k] = v + continue + } + if i, _ := tmpl.ParamByName(k); i >= 0 { res[k] = v } diff --git a/tests/config-modbus-fields.spec.ts b/tests/config-modbus-fields.spec.ts new file mode 100644 index 000000000..63b30cb2a --- /dev/null +++ b/tests/config-modbus-fields.spec.ts @@ -0,0 +1,57 @@ +import { test, expect, type Page, type Locator } from "@playwright/test"; +import { start, stop, baseUrl } from "./evcc"; +import { expectModalVisible, enableExperimental } from "./utils"; + +const CONFIG_MODBUS_FIELDS = "config-modbus-fields.sql"; + +test.use({ baseURL: baseUrl() }); + +test.beforeAll(async () => { + await start(undefined, CONFIG_MODBUS_FIELDS); +}); + +test.afterAll(async () => { + await stop(); +}); + +async function openMeterModal(page: Page, title: string): Promise { + await page.goto("/#/config"); + await enableExperimental(page, true); + await page + .getByTestId("pv") + .filter({ hasText: title }) + .getByRole("button", { name: "edit" }) + .click(); + const modal = page.getByTestId("meter-modal"); + await expectModalVisible(modal); + return modal; +} + +test.describe("modbus fields", async () => { + test("tcpip", async ({ page }) => { + const modal = await openMeterModal(page, "TCP Test"); + await expect(page.getByLabel("Network")).toBeChecked(); + await expect(page.getByLabel("TCP")).toBeChecked(); + await expect(modal.getByLabel("IP address or hostname")).toHaveValue("192.168.1.10"); + await expect(modal.getByLabel("Port", { exact: true })).toHaveValue("5020"); + await expect(modal.getByLabel("Modbus ID")).toHaveValue("10"); + }); + + test("rs485tcpip", async ({ page }) => { + const modal = await openMeterModal(page, "RTU/IP Test"); + await expect(page.getByLabel("Network")).toBeChecked(); + await expect(page.getByLabel("RTU")).toBeChecked(); + await expect(modal.getByLabel("IP address or hostname")).toHaveValue("192.168.1.20"); + await expect(modal.getByLabel("Port", { exact: true })).toHaveValue("8899"); + await expect(modal.getByLabel("Modbus ID")).toHaveValue("20"); + }); + + test("rs485serial", async ({ page }) => { + const modal = await openMeterModal(page, "Serial Test"); + await expect(page.getByLabel("Serial / USB")).toBeChecked(); + await expect(modal.getByLabel("Device name")).toHaveValue("/dev/ttyUSB5"); + await expect(modal.getByLabel("Baud rate")).toHaveValue("19200"); + await expect(modal.getByLabel("ComSet")).toHaveValue("8E1"); + await expect(modal.getByLabel("Modbus ID")).toHaveValue("30"); + }); +}); diff --git a/tests/config-modbus-fields.sql b/tests/config-modbus-fields.sql new file mode 100644 index 000000000..4f597bfeb --- /dev/null +++ b/tests/config-modbus-fields.sql @@ -0,0 +1,24 @@ +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', 'TCP Test', '', 'SunSpec Inverter', '{"host":"192.168.1.10","id":10,"modbus":"tcpip","port":5020,"template":"sunspec-inverter","usage":"pv"}'); +INSERT INTO configs(id, class, type, title, icon, product, value) VALUES(2, 2, 'template', 'RTU/IP Test', '', 'SunSpec Inverter', '{"host":"192.168.1.20","id":20,"modbus":"rs485tcpip","port":8899,"template":"sunspec-inverter","usage":"pv"}'); +INSERT INTO configs(id, class, type, title, icon, product, value) VALUES(3, 2, 'template', 'Serial Test', '', 'SunSpec Inverter', '{"baudrate":19200,"comset":"8E1","device":"/dev/ttyUSB5","id":30,"modbus":"rs485serial","template":"sunspec-inverter","usage":"pv"}'); + +INSERT INTO settings("key", value) VALUES('pvMeters', 'db:1,db:2,db:3'); + +COMMIT; diff --git a/util/templates/template_modbus.go b/util/templates/template_modbus.go index 36b766361..5d4c81aa8 100644 --- a/util/templates/template_modbus.go +++ b/util/templates/template_modbus.go @@ -74,19 +74,19 @@ func (t *Template) ModbusValues(renderMode int, values map[string]any) { var defaultValue string switch p.Name { - case ModbusParamNameId: + case ModbusParamId: if modbusParam.ID != 0 { defaultValue = strconv.Itoa(modbusParam.ID) } - case ModbusParamNamePort: + case ModbusParamPort: if modbusParam.Port != 0 { defaultValue = strconv.Itoa(modbusParam.Port) } - case ModbusParamNameBaudrate: + case ModbusParamBaudrate: if modbusParam.Baudrate != 0 { defaultValue = strconv.Itoa(modbusParam.Baudrate) } - case ModbusParamNameComset: + case ModbusParamComset: if modbusParam.Comset != "" { defaultValue = modbusParam.Comset } diff --git a/util/templates/types.go b/util/templates/types.go index ffa6c1bd0..c88ace85e 100644 --- a/util/templates/types.go +++ b/util/templates/types.go @@ -25,14 +25,14 @@ const ( ModbusKeyTCPIP = "tcpip" ModbusKeyUDP = "udp" - ModbusParamNameId = "id" - ModbusParamNameDevice = "device" - ModbusParamNameBaudrate = "baudrate" - ModbusParamNameComset = "comset" - ModbusParamNameURI = "uri" - ModbusParamNameHost = "host" - ModbusParamNamePort = "port" - ModbusParamNameRTU = "rtu" + ModbusParamId = "id" + ModbusParamDevice = "device" + ModbusParamBaudrate = "baudrate" + ModbusParamComset = "comset" + ModbusParamURI = "uri" + ModbusParamHost = "host" + ModbusParamPort = "port" + ModbusParamRTU = "rtu" ) const ( @@ -41,7 +41,19 @@ const ( RenderModeInstance ) -var ValidModbusChoices = []string{ModbusChoiceRS485, ModbusChoiceTCPIP, ModbusChoiceUDP} +var ( + ValidModbusChoices = []string{ModbusChoiceRS485, ModbusChoiceTCPIP, ModbusChoiceUDP} + + // ModbusParams contains all field names used by modbus templates + ModbusParams = []string{ + ModbusParamId, ModbusParamDevice, ModbusParamBaudrate, ModbusParamComset, + ModbusParamURI, ModbusParamHost, ModbusParamPort, ModbusParamRTU, + } + + ModbusConnectionTypes = []string{ + ModbusKeyTCPIP, ModbusKeyUDP, ModbusKeyRS485Serial, ModbusKeyRS485TCPIP, + } +) const ( CapabilityISO151182 = "iso151182" // ISO 15118-2 support @@ -63,12 +75,10 @@ const ( var ValidRequirements = []string{RequirementEEBUS, RequirementMQTT, RequirementSponsorship, RequirementSkipTest} -var predefinedTemplateProperties = []string{ - "type", "template", "name", - ModbusParamNameId, ModbusParamNameDevice, ModbusParamNameBaudrate, ModbusParamNameComset, - ModbusParamNameURI, ModbusParamNameHost, ModbusParamNamePort, ModbusParamNameRTU, - ModbusKeyTCPIP, ModbusKeyUDP, ModbusKeyRS485Serial, ModbusKeyRS485TCPIP, -} +var predefinedTemplateProperties = append( + []string{"type", "template", "name"}, + append(ModbusParams, ModbusConnectionTypes...)..., +) // TextLanguage contains language-specific texts type TextLanguage struct {