fix/refactor: modbus form (#28226)

Co-authored-by: Michael Geers <michael@geers.tv>
This commit is contained in:
Maschga 2026-03-20 13:13:34 +01:00 • committed by GitHub
parent 62eb3be00a
commit 452a1301b1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 192 additions and 75 deletions

View file

@ -46,7 +46,7 @@
class="me-2"
required
:model-value="id || defaultId"
@input="$emit('update:id', $event.target.value)"
@update:model-value="(v) => $emit('update:id', v)"
/>
</FormRow>
<div v-if="connection === MODBUS_CONNECTION.TCPIP">
@ -62,7 +62,7 @@
class="me-2"
required
:model-value="host"
@input="$emit('update:host', $event.target.value)"
@update:model-value="(v) => $emit('update:host', v)"
/>
</FormRow>
<FormRow :id="formId('modbusPort')" :label="$t('config.modbus.port')">
@ -73,7 +73,7 @@
class="me-2 w-50"
required
:model-value="port || defaultPort"
@input="$emit('update:port', $event.target.value)"
@update:model-value="(v) => $emit('update:port', v)"
/>
</FormRow>
<FormRow
@ -142,7 +142,7 @@
:choice="baudrateOptions"
required
:model-value="baudrate || defaultBaudrate"
@input="$emit('update:baudrate', parseInt($event.target.value))"
@update:model-value="(v) => $emit('update:baudrate', parseInt(v))"
/>
</FormRow>
<FormRow :id="formId('modbusComset')" :label="$t('config.modbus.comset')">
@ -154,7 +154,7 @@
:choice="comsetOptions"
required
:model-value="comset || defaultComset"
@input="$emit('update:comset', $event.target.value)"
@update:model-value="(v) => $emit('update:comset', v)"
/>
</FormRow>
</div>
@ -263,9 +263,9 @@ export default defineComponent({
this.setProtocolByCapabilities(newValue);
this.$emit("update:modbus", this.selectedModbus);
},
modbus(newValue: MODBUS_TYPE) {
modbus(newValue: MODBUS_TYPE, oldValue: MODBUS_TYPE) {
if (newValue) {
this.setConnectionAndProtocolByModbus(newValue);
this.setConnectionAndProtocolByModbus(newValue, oldValue);
}
},
connection(newValue: MODBUS_CONNECTION, oldValue: MODBUS_CONNECTION) {
@ -298,8 +298,8 @@ export default defineComponent({
? MODBUS_PROTOCOL.TCP
: MODBUS_PROTOCOL.RTU;
},
setConnectionAndProtocolByModbus(modbus?: MODBUS_TYPE) {
switch (modbus) {
setConnectionAndProtocolByModbus(newModbus?: MODBUS_TYPE, oldModbus?: MODBUS_TYPE) {
switch (newModbus) {
case MODBUS_TYPE.RS485_SERIAL:
this.connection = MODBUS_CONNECTION.SERIAL;
this.protocol = MODBUS_PROTOCOL.RTU;
@ -313,6 +313,11 @@ export default defineComponent({
this.protocol = MODBUS_PROTOCOL.TCP;
break;
}
// when switching from serial to TCP/IP, default protocol to Modbus-TCP (rtu=false)
if (oldModbus === MODBUS_TYPE.RS485_SERIAL && newModbus === MODBUS_TYPE.RS485_TCPIP) {
this.protocol = MODBUS_PROTOCOL.TCP;
}
},
formId(name: string): string {
return `${name}-${this.componentId}`;

View file

@ -0,0 +1,126 @@
<template>
<Modbus
v-model:baudrate="localConnection.settings.baudrate"
v-model:comset="localConnection.settings.comset"
v-model:device="localConnection.settings.device"
:component-id="`proxy-${index}`"
:host="getHost(localConnection.settings.uri)"
:port="getPort(localConnection.settings.uri)"
:capabilities="['rs485', 'tcpip']"
hide-modbus-id
:default-baudrate="DEFAULT_BAUDRATE"
:default-comset="DEFAULT_COMSET"
:default-port="DEFAULT_PORT"
:modbus="initialModbusType"
@update:host="(host) => updateHost(host)"
@update:port="(port) => updatePort(port)"
@update:modbus="(modbus) => updateModbus(modbus)"
/>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import Modbus from "./DeviceModal/Modbus.vue";
import {
MODBUS_BAUDRATE,
MODBUS_COMSET,
MODBUS_TYPE,
type ModbusProxy,
type ModbusProxySettings,
} from "@/types/evcc";
import deepClone from "@/utils/deepClone";
export const DEFAULT_BAUDRATE = MODBUS_BAUDRATE._9600;
export const DEFAULT_COMSET = MODBUS_COMSET._8N1;
export const DEFAULT_PORT = 502;
function getModbusType(s: ModbusProxySettings) {
if (s.device) {
return MODBUS_TYPE.RS485_SERIAL;
}
return s.rtu ? MODBUS_TYPE.RS485_TCPIP : MODBUS_TYPE.TCPIP;
}
export default defineComponent({
name: "ModbusProxyConnection",
components: { Modbus },
props: {
connection: {
type: Object as () => ModbusProxy,
required: true,
},
index: {
type: Number,
required: true,
},
},
emits: ["update:connection"],
data() {
return {
DEFAULT_BAUDRATE,
DEFAULT_COMSET,
DEFAULT_PORT,
localConnection: deepClone(this.connection),
initialModbusType: getModbusType(this.connection.settings),
};
},
watch: {
localConnection: {
handler(newVal: ModbusProxy) {
if (newVal) {
this.$emit("update:connection", newVal);
}
},
deep: true,
},
},
methods: {
getHost(uri?: string) {
return uri?.split(":")[0] || "";
},
getPort(uri?: string) {
return uri?.split(":")[1] || "";
},
updateHost(newHost?: string) {
const port = this.getPort(this.localConnection.settings.uri);
if (port === "" && newHost === undefined) {
this.localConnection.settings.uri = undefined;
} else {
this.localConnection.settings.uri = `${newHost === undefined ? "" : newHost}:${port}`;
}
},
updatePort(newPort?: string) {
const host = this.getHost(this.localConnection.settings.uri);
if (host === "" && newPort === undefined) {
this.localConnection.settings.uri = undefined;
} else {
this.localConnection.settings.uri = `${host}:${newPort === undefined ? "" : newPort}`;
}
},
updateModbus(modbus: MODBUS_TYPE) {
this.initialModbusType = modbus;
switch (modbus) {
case MODBUS_TYPE.RS485_SERIAL:
this.localConnection.settings.uri = undefined;
this.localConnection.settings.rtu = undefined;
if (!this.localConnection.settings.baudrate) {
this.localConnection.settings.baudrate = DEFAULT_BAUDRATE;
}
if (!this.localConnection.settings.comset) {
this.localConnection.settings.comset = DEFAULT_COMSET;
}
break;
case MODBUS_TYPE.RS485_TCPIP:
case MODBUS_TYPE.TCPIP:
this.localConnection.settings.device = undefined;
this.localConnection.settings.baudrate = undefined;
this.localConnection.settings.comset = undefined;
this.localConnection.settings.rtu = modbus === MODBUS_TYPE.RS485_TCPIP;
break;
}
},
},
});
</script>

View file

@ -85,19 +85,12 @@
</div>
</h5>
</div>
<Modbus
v-model:baudrate="c.settings.baudrate"
v-model:comset="c.settings.comset"
v-model:device="c.settings.device"
:modbus="getModbus(c.settings)"
:component-id="`proxy-${index}`"
:host="getHost(c.settings.uri)"
:port="getPort(c.settings.uri)"
:capabilities="['rs485', 'tcpip']"
hide-modbus-id
@update:host="(host) => updateHost(host, c.settings)"
@update:port="(port) => updatePort(port, c.settings)"
@update:modbus="(modbus) => updateModbus(c.settings, modbus)"
<ModbusProxyConnection
:connection="c"
:index="index"
@update:connection="
(connection) => (values[index] = connection)
"
/>
</div>
</div>
@ -143,17 +136,13 @@ import "@h2d2/shopicons/es/regular/arrowdown";
import "@h2d2/shopicons/es/regular/plus";
import "@h2d2/shopicons/es/regular/trash";
import JsonModal from "./JsonModal.vue";
import {
MODBUS_COMSET,
MODBUS_CONNECTION,
MODBUS_PROTOCOL,
MODBUS_PROXY_READONLY,
MODBUS_TYPE,
type ModbusProxy,
type ModbusProxySettings,
} from "@/types/evcc";
import { MODBUS_PROXY_READONLY, type ModbusProxy } from "@/types/evcc";
import ASCII_DIAGRAM from "./modbus-diagram.txt?raw";
import Modbus from "./DeviceModal/Modbus.vue";
import ModbusProxyConnection, {
DEFAULT_BAUDRATE,
DEFAULT_COMSET,
DEFAULT_PORT,
} from "./ModbusProxyConnection.vue";
import PropertyField from "./PropertyField.vue";
import FormRow from "./FormRow.vue";
import SponsorTokenRequired from "./DeviceModal/SponsorTokenRequired.vue";
@ -162,7 +151,14 @@ import { defineComponent } from "vue";
export default defineComponent({
name: "ModbusProxyModal",
components: { JsonModal, Modbus, FormRow, PropertyField, SponsorTokenRequired, SelectGroup },
components: {
JsonModal,
ModbusProxyConnection,
FormRow,
PropertyField,
SponsorTokenRequired,
SelectGroup,
},
props: {
isSponsor: Boolean,
},
@ -170,10 +166,6 @@ export default defineComponent({
data() {
return {
ASCII_DIAGRAM,
MODBUS_PROXY_READONLY,
MODBUS_CONNECTION,
MODBUS_PROTOCOL,
MODBUS_TYPE,
};
},
computed: {
@ -188,12 +180,6 @@ export default defineComponent({
formId(index: number, name: string) {
return `modbusproxy-connection-${index}-${name}`;
},
getModbus(s: ModbusProxySettings) {
if (s.device) {
return MODBUS_TYPE.RS485_SERIAL;
}
return s.rtu ? MODBUS_TYPE.RS485_TCPIP : MODBUS_TYPE.TCPIP;
},
getReadonlyHelp(readonly = MODBUS_PROXY_READONLY.FALSE): string {
return this.$t(`config.modbusproxy.readonly.help.${readonly}`);
},
@ -203,41 +189,12 @@ export default defineComponent({
port: highestPort + 1,
readonly: MODBUS_PROXY_READONLY.FALSE,
settings: {
uri: ":502",
baudrate: 9600,
comset: "8N1" as MODBUS_COMSET,
uri: `:${DEFAULT_PORT}`,
baudrate: DEFAULT_BAUDRATE,
comset: DEFAULT_COMSET,
},
});
},
getHost(uri?: string) {
return uri?.split(":")[0] || "";
},
getPort(uri?: string) {
return uri?.split(":")[1] || "";
},
updateHost(newHost: string, settings: ModbusProxySettings) {
const port = this.getPort(settings.uri);
settings.uri = `${newHost}:${port}`;
},
updatePort(newPort: string | number, settings: ModbusProxySettings) {
const host = this.getHost(settings.uri);
settings.uri = `${host}:${newPort}`;
},
updateModbus(settings: ModbusProxySettings, modbus: MODBUS_TYPE) {
switch (modbus) {
case MODBUS_TYPE.RS485_SERIAL:
settings.uri = undefined;
settings.rtu = undefined;
break;
case MODBUS_TYPE.RS485_TCPIP:
case MODBUS_TYPE.TCPIP:
settings.device = undefined;
settings.baudrate = undefined;
settings.comset = undefined;
settings.rtu = modbus === MODBUS_TYPE.RS485_TCPIP;
break;
}
},
},
});
</script>

View file

@ -87,6 +87,35 @@ test.describe("modbusproxy", async () => {
await expect(modal).not.toContainText("Connection #1");
});
test("modbusproxy serial defaults", async ({ page }) => {
await start();
await page.goto("/#/config");
const modbusproxyCard = page.getByTestId("modbusproxy");
await modbusproxyCard.getByRole("button", { name: "edit" }).click();
const modal = page.getByTestId("modbusproxy-modal");
await expectModalVisible(modal);
await modal.getByRole("button", { name: "Add proxy connection" }).click();
const deviceBox = modal.getByTestId("device-box");
await expect(deviceBox.getByLabel("Network")).toBeChecked();
await expect(deviceBox.getByLabel("Port")).toHaveValue("502");
// switch to RS485
await deviceBox.getByText("RS485", { exact: true }).click();
await expect(deviceBox.getByLabel("Baud rate")).toHaveValue("9600");
await expect(deviceBox.getByLabel("ComSet")).toHaveValue("8N1");
await deviceBox.getByLabel("Baud rate").selectOption("19200");
// switch to Network and back: defaults restored
await deviceBox.getByText("Network").click();
await deviceBox.getByText("RS485", { exact: true }).click();
await expect(deviceBox.getByLabel("Baud rate")).toHaveValue("9600");
await expect(deviceBox.getByLabel("ComSet")).toHaveValue("8N1");
});
test("modbusproxy via db (yaml to json migration)", async ({ page }) => {
await start(undefined, CONFIG_MODBUSPROXY_MIGRATE);
await page.goto("/#/config");