Skip to content

Commit 2a0429a

Browse files
authored
Config UI: fix preserving modbus fields (#25029)
1 parent 6591514 commit 2a0429a

File tree

5 files changed

+119
-19
lines changed

5 files changed

+119
-19
lines changed

server/http_config_helper.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,12 +110,21 @@ func templateForConfig(class templates.Class, conf map[string]any) (templates.Te
110110
func filterValidTemplateParams(tmpl *templates.Template, conf map[string]any) map[string]any {
111111
res := make(map[string]any)
112112

113+
// check if template has modbus capability
114+
hasModbus := len(tmpl.ModbusChoices()) > 0
115+
113116
for k, v := range conf {
114117
if k == "template" {
115118
res[k] = v
116119
continue
117120
}
118121

122+
// preserve modbus fields if template supports modbus
123+
if hasModbus && slices.Contains(templates.ModbusParams, k) {
124+
res[k] = v
125+
continue
126+
}
127+
119128
if i, _ := tmpl.ParamByName(k); i >= 0 {
120129
res[k] = v
121130
}

tests/config-modbus-fields.spec.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { test, expect, type Page, type Locator } from "@playwright/test";
2+
import { start, stop, baseUrl } from "./evcc";
3+
import { expectModalVisible, enableExperimental } from "./utils";
4+
5+
const CONFIG_MODBUS_FIELDS = "config-modbus-fields.sql";
6+
7+
test.use({ baseURL: baseUrl() });
8+
9+
test.beforeAll(async () => {
10+
await start(undefined, CONFIG_MODBUS_FIELDS);
11+
});
12+
13+
test.afterAll(async () => {
14+
await stop();
15+
});
16+
17+
async function openMeterModal(page: Page, title: string): Promise<Locator> {
18+
await page.goto("/#/config");
19+
await enableExperimental(page, true);
20+
await page
21+
.getByTestId("pv")
22+
.filter({ hasText: title })
23+
.getByRole("button", { name: "edit" })
24+
.click();
25+
const modal = page.getByTestId("meter-modal");
26+
await expectModalVisible(modal);
27+
return modal;
28+
}
29+
30+
test.describe("modbus fields", async () => {
31+
test("tcpip", async ({ page }) => {
32+
const modal = await openMeterModal(page, "TCP Test");
33+
await expect(page.getByLabel("Network")).toBeChecked();
34+
await expect(page.getByLabel("TCP")).toBeChecked();
35+
await expect(modal.getByLabel("IP address or hostname")).toHaveValue("192.168.1.10");
36+
await expect(modal.getByLabel("Port", { exact: true })).toHaveValue("5020");
37+
await expect(modal.getByLabel("Modbus ID")).toHaveValue("10");
38+
});
39+
40+
test("rs485tcpip", async ({ page }) => {
41+
const modal = await openMeterModal(page, "RTU/IP Test");
42+
await expect(page.getByLabel("Network")).toBeChecked();
43+
await expect(page.getByLabel("RTU")).toBeChecked();
44+
await expect(modal.getByLabel("IP address or hostname")).toHaveValue("192.168.1.20");
45+
await expect(modal.getByLabel("Port", { exact: true })).toHaveValue("8899");
46+
await expect(modal.getByLabel("Modbus ID")).toHaveValue("20");
47+
});
48+
49+
test("rs485serial", async ({ page }) => {
50+
const modal = await openMeterModal(page, "Serial Test");
51+
await expect(page.getByLabel("Serial / USB")).toBeChecked();
52+
await expect(modal.getByLabel("Device name")).toHaveValue("/dev/ttyUSB5");
53+
await expect(modal.getByLabel("Baud rate")).toHaveValue("19200");
54+
await expect(modal.getByLabel("ComSet")).toHaveValue("8E1");
55+
await expect(modal.getByLabel("Modbus ID")).toHaveValue("30");
56+
});
57+
});

tests/config-modbus-fields.sql

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
BEGIN;
2+
3+
CREATE TABLE `configs` (
4+
`id` integer PRIMARY KEY AUTOINCREMENT
5+
, `class` integer
6+
, `type` text
7+
, `title` text
8+
, `icon` text
9+
, `product` text
10+
, `value` text
11+
);
12+
CREATE TABLE `settings` (
13+
`key` text
14+
, `value` text
15+
, PRIMARY KEY(`key`)
16+
);
17+
18+
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"}');
19+
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"}');
20+
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"}');
21+
22+
INSERT INTO settings("key", value) VALUES('pvMeters', 'db:1,db:2,db:3');
23+
24+
COMMIT;

util/templates/template_modbus.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,19 +74,19 @@ func (t *Template) ModbusValues(renderMode int, values map[string]any) {
7474
var defaultValue string
7575

7676
switch p.Name {
77-
case ModbusParamNameId:
77+
case ModbusParamId:
7878
if modbusParam.ID != 0 {
7979
defaultValue = strconv.Itoa(modbusParam.ID)
8080
}
81-
case ModbusParamNamePort:
81+
case ModbusParamPort:
8282
if modbusParam.Port != 0 {
8383
defaultValue = strconv.Itoa(modbusParam.Port)
8484
}
85-
case ModbusParamNameBaudrate:
85+
case ModbusParamBaudrate:
8686
if modbusParam.Baudrate != 0 {
8787
defaultValue = strconv.Itoa(modbusParam.Baudrate)
8888
}
89-
case ModbusParamNameComset:
89+
case ModbusParamComset:
9090
if modbusParam.Comset != "" {
9191
defaultValue = modbusParam.Comset
9292
}

util/templates/types.go

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,14 @@ const (
2525
ModbusKeyTCPIP = "tcpip"
2626
ModbusKeyUDP = "udp"
2727

28-
ModbusParamNameId = "id"
29-
ModbusParamNameDevice = "device"
30-
ModbusParamNameBaudrate = "baudrate"
31-
ModbusParamNameComset = "comset"
32-
ModbusParamNameURI = "uri"
33-
ModbusParamNameHost = "host"
34-
ModbusParamNamePort = "port"
35-
ModbusParamNameRTU = "rtu"
28+
ModbusParamId = "id"
29+
ModbusParamDevice = "device"
30+
ModbusParamBaudrate = "baudrate"
31+
ModbusParamComset = "comset"
32+
ModbusParamURI = "uri"
33+
ModbusParamHost = "host"
34+
ModbusParamPort = "port"
35+
ModbusParamRTU = "rtu"
3636
)
3737

3838
const (
@@ -41,7 +41,19 @@ const (
4141
RenderModeInstance
4242
)
4343

44-
var ValidModbusChoices = []string{ModbusChoiceRS485, ModbusChoiceTCPIP, ModbusChoiceUDP}
44+
var (
45+
ValidModbusChoices = []string{ModbusChoiceRS485, ModbusChoiceTCPIP, ModbusChoiceUDP}
46+
47+
// ModbusParams contains all field names used by modbus templates
48+
ModbusParams = []string{
49+
ModbusParamId, ModbusParamDevice, ModbusParamBaudrate, ModbusParamComset,
50+
ModbusParamURI, ModbusParamHost, ModbusParamPort, ModbusParamRTU,
51+
}
52+
53+
ModbusConnectionTypes = []string{
54+
ModbusKeyTCPIP, ModbusKeyUDP, ModbusKeyRS485Serial, ModbusKeyRS485TCPIP,
55+
}
56+
)
4557

4658
const (
4759
CapabilityISO151182 = "iso151182" // ISO 15118-2 support
@@ -63,12 +75,10 @@ const (
6375

6476
var ValidRequirements = []string{RequirementEEBUS, RequirementMQTT, RequirementSponsorship, RequirementSkipTest}
6577

66-
var predefinedTemplateProperties = []string{
67-
"type", "template", "name",
68-
ModbusParamNameId, ModbusParamNameDevice, ModbusParamNameBaudrate, ModbusParamNameComset,
69-
ModbusParamNameURI, ModbusParamNameHost, ModbusParamNamePort, ModbusParamNameRTU,
70-
ModbusKeyTCPIP, ModbusKeyUDP, ModbusKeyRS485Serial, ModbusKeyRS485TCPIP,
71-
}
78+
var predefinedTemplateProperties = append(
79+
[]string{"type", "template", "name"},
80+
append(ModbusParams, ModbusConnectionTypes...)...,
81+
)
7282

7383
// TextLanguage contains language-specific texts
7484
type TextLanguage struct {

0 commit comments

Comments
 (0)