Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
1595193
remove yaml
Maschga Oct 1, 2025
d085c0b
rewrite
Maschga Oct 1, 2025
2390833
wip
Maschga Oct 1, 2025
a7627ed
fix ids
Maschga Oct 1, 2025
01086eb
wip
Maschga Oct 1, 2025
251c809
lint
Maschga Oct 1, 2025
98a7b93
read json
Maschga Oct 1, 2025
8b76300
Merge branch 'master' into config-ui/modbusproxy
Maschga Oct 1, 2025
2fcb73a
add ascii diagram to clarify configuratoin and flow
naltatis Oct 2, 2025
530626d
add props
Maschga Oct 2, 2025
1b23c68
wip
Maschga Oct 2, 2025
d3649b8
fix reference bugs
Maschga Oct 13, 2025
7f80980
Merge branch 'master' into config-ui/modbusproxy
Maschga Oct 13, 2025
5bb2b0d
split host and port
Maschga Oct 13, 2025
7ac6d99
Merge branch 'config-ui/modbusproxy' of https://github.com/Maschga/ev…
Maschga Oct 13, 2025
8688e41
use columns
Maschga Oct 13, 2025
16abc0a
Merge branch 'master' into config-ui/modbusproxy
Maschga Oct 18, 2025
2791de9
use top alignment; better remove button position; show divider
Maschga Oct 18, 2025
828889a
suport small devices
Maschga Oct 18, 2025
fc64d73
Merge branch 'master' into config-ui/modbusproxy
naltatis Nov 5, 2025
c7775e8
ui optimize; sponsor; wording; translations
naltatis Nov 5, 2025
ddf8fed
move readonly to left; reduce diff in modbus.vue; translation; defaul…
naltatis Nov 5, 2025
76e6411
readd translation texts
naltatis Nov 6, 2025
9e4f8c3
Merge branch 'master' into config-ui/modbusproxy
naltatis Nov 6, 2025
2b3a77c
add migration strategy
Maschga Nov 7, 2025
646a290
use enums; fix protocol/connection switch
Maschga Nov 7, 2025
da81e7a
fix; simplify
Maschga Nov 8, 2025
d1e255f
fix mapping
Maschga Nov 8, 2025
92f1f83
add tests
Maschga Nov 8, 2025
e6b89da
lint
Maschga Nov 8, 2025
1033606
Update cmd/setup.go
Maschga Nov 9, 2025
7b4758c
migration: remove mapping
Maschga Nov 9, 2025
93024ca
use afterEach
Maschga Nov 9, 2025
abb057f
improve locators; add restart
Maschga Nov 9, 2025
a452bab
use modbus; use enum; simplify
Maschga Nov 9, 2025
8755156
remove logs
Maschga Nov 9, 2025
2bc3c2c
restore deviceHint
Maschga Nov 9, 2025
7e0cd9e
Merge branch 'master' into config-ui/modbusproxy
Maschga Nov 9, 2025
07f09de
refactor/test migrate; small fixes; proper form ids
naltatis Nov 12, 2025
33d7fd2
Merge branch 'master' into config-ui/modbusproxy
naltatis Nov 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion api/globalconfig/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ type Go struct {
type ModbusProxy struct {
Port int
ReadOnly string `yaml:",omitempty" json:",omitempty"`
modbus.Settings `mapstructure:",squash" yaml:",inline,omitempty" json:",omitempty"`
modbus.Settings `mapstructure:",squash" yaml:",inline,omitempty" json:"Settings,omitempty"`
}

var _ api.Redactor = (*Hems)(nil)
Expand Down
98 changes: 74 additions & 24 deletions assets/js/components/Config/DeviceModal/Modbus.vue
Original file line number Diff line number Diff line change
Expand Up @@ -38,23 +38,35 @@
</label>
</div>
</FormRow>
<FormRow id="modbusId" :label="$t('config.modbus.id')">
<FormRow v-if="!isProxy" id="modbusId" :label="$t('config.modbus.id')">
<PropertyField
id="modbusId"
property="id"
type="Int"
class="me-2"
required
:model-value="id || defaultId || 1"
:model-value="id || defaultId"
@change="$emit('update:id', $event.target.value)"
/>
</FormRow>
<div v-if="connection === 'tcpip'">
<FormRow
id="modbusHost"
:label="$t('config.modbus.host')"
:help="$t('config.modbus.hostHint')"
v-if="isProxy"
id="modbusURI"
:label="$t('config.modbus.uri')"
example="192.0.2.2:502"
>
<PropertyField
id="modbusURI"
property="uri"
type="String"
class="me-2"
required
:model-value="host"
@change="$emit('update:host', $event.target.value)"
/>
</FormRow>
<FormRow v-else id="modbusHost" :label="$t('config.modbus.host')" example="192.0.2.2">
<PropertyField
id="modbusHost"
property="host"
Expand All @@ -72,7 +84,7 @@
type="Int"
class="me-2 w-50"
required
:model-value="port || defaultPort || 502"
:model-value="port || defaultPort"
@change="$emit('update:port', $event.target.value)"
/>
</FormRow>
Expand Down Expand Up @@ -117,11 +129,7 @@
</FormRow>
</div>
<div v-else>
<FormRow
id="modbusDevice"
:label="$t('config.modbus.device')"
:help="$t('config.modbus.deviceHint')"
>
<FormRow id="modbusDevice" :label="$t('config.modbus.device')" example="/dev/ttyUSB0">
<PropertyField
id="modbusDevice"
property="device"
Expand Down Expand Up @@ -152,11 +160,39 @@
class="me-2 w-50"
:choice="comsetOptions"
required
:model-value="comset || defaultComset || '8N1'"
:model-value="comset || defaultComset"
@change="$emit('update:comset', $event.target.value)"
/>
</FormRow>
</div>
<FormRow
v-if="isProxy"
id="serialConnectionReadonly"
label="Readonly"
:help="
readOnly === MODBUS_PROXY_READONLY.TRUE
? 'Write access is blocked without response.'
: readOnly === MODBUS_PROXY_READONLY.FALSE
? 'Write access is blocked with a modbus error as response.'
: readOnly === MODBUS_PROXY_READONLY.DENY
? 'Write access is forwarded.'
: 'Whether Modbus write accesses by third-party systems should be blocked.'
"
>
<SelectGroup
id="serialConnectionReadonly"
:model-value="readOnly || defaultReadOnly"
class="w-100"
:options="
Object.values(MODBUS_PROXY_READONLY).map((v) => ({
value: v,
name: v,
}))
"
transparent
@update:model-value="$emit('update:readOnly', $event.target.value)"
/>
</FormRow>
</template>

<script lang="ts">
Expand All @@ -165,13 +201,15 @@ import FormRow from "../FormRow.vue";
import PropertyField from "../PropertyField.vue";
import type { PropType } from "vue";
import type { ModbusCapability } from "./index";
import { MODBUS_BAUDRATE, MODBUS_COMSET, MODBUS_PROXY_READONLY } from "@/types/evcc";
import SelectGroup from "@/components/Helper/SelectGroup.vue";
type Modbus = "rs485serial" | "rs485tcpip" | "tcpip";
type ConnectionOption = "tcpip" | "serial";
type ProtocolOption = "tcp" | "rtu";

export default defineComponent({
name: "Modbus",
components: { FormRow, PropertyField },
components: { FormRow, PropertyField, SelectGroup },
props: {
capabilities: {
type: Array as PropType<ModbusCapability[]>,
Expand All @@ -184,10 +222,16 @@ export default defineComponent({
baudrate: [Number, String],
comset: String,
device: String,
defaultPort: Number,
defaultId: Number,
defaultComset: String,
defaultBaudrate: Number,
readOnly: String as PropType<MODBUS_PROXY_READONLY>,
defaultPort: { type: Number, default: 502 },
defaultId: { type: Number, default: 1 },
defaultComset: { type: String as PropType<MODBUS_COMSET>, default: "8N1" },
defaultBaudrate: { type: Number as PropType<MODBUS_BAUDRATE>, default: 1200 },
defaultReadOnly: {
type: String as PropType<MODBUS_PROXY_READONLY>,
default: MODBUS_PROXY_READONLY.DENY,
},
isProxy: Boolean,
},
emits: [
"update:modbus",
Expand All @@ -197,11 +241,15 @@ export default defineComponent({
"update:device",
"update:baudrate",
"update:comset",
"update:readOnly",
],
data(): { connection: ConnectionOption; protocol: ProtocolOption } {
data() {
return {
connection: "tcpip",
protocol: "tcp",
connection: "tcpip" as ConnectionOption,
protocol: "tcp" as ProtocolOption,
MODBUS_PROXY_READONLY,
MODBUS_BAUDRATE,
MODBUS_COMSET,
};
},
computed: {
Expand All @@ -218,14 +266,16 @@ export default defineComponent({
return this.connection === "tcpip" && this.capabilities.includes("rs485");
},
comsetOptions() {
return ["8N1", "8E1", "8N2"].map((v) => {
return Object.values(MODBUS_COMSET).map((v) => {
return { key: v, name: v };
});
},
baudrateOptions() {
return [1200, 9600, 19200, 38400, 57600, 115200].map((v) => {
return { key: v, name: `${v}` };
});
return Object.values(MODBUS_BAUDRATE)
.filter((v) => typeof v === "number")
.map((v) => {
return { key: v, name: `${v}` };
});
},
},
watch: {
Expand Down
28 changes: 18 additions & 10 deletions assets/js/components/Config/JsonModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ import GenericModal from "../Helper/GenericModal.vue";
import api from "@/api";
import { docsPrefix } from "@/i18n";
import store from "@/store";
import deepClone from "@/utils/deepClone";

export default {
name: "JsonModal",
Expand All @@ -84,15 +85,16 @@ export default {
transformReadValues: Function,
stateKey: String,
saveMethod: { type: String, default: "post" },
storeValuesInArray: Boolean,
},
emits: ["changed", "open"],
data() {
return {
saving: false,
removing: false,
error: "",
values: {},
serverValues: {},
values: this.storeValuesInArray ? [] : {},
serverValues: this.storeValuesInArray ? [] : {},
};
},
computed: {
Expand Down Expand Up @@ -121,7 +123,7 @@ export default {
if (this.transformReadValues) {
this.serverValues = this.transformReadValues(this.serverValues);
}
this.values = { ...this.serverValues };
this.values = deepClone(this.serverValues);
},
async save() {
this.saving = true;
Expand Down Expand Up @@ -163,13 +165,19 @@ export default {
this.removing = false;
},
trimValues(values) {
// extend to recursive when needed in the future
return Object.fromEntries(
Object.entries(values).map(([key, value]) => [
key,
typeof value === "string" ? value.trim() : value,
])
);
if (Array.isArray(values)) {
for (let index = 0; index < values.length; index++) {
values[index] = this.trimValues(values[index]);
}
return values;
} else {
return Object.fromEntries(
Object.entries(values).map(([key, value]) => [
key,
typeof value === "string" ? value.trim() : value,
])
);
}
},
},
};
Expand Down
110 changes: 100 additions & 10 deletions assets/js/components/Config/ModbusProxyModal.vue
Original file line number Diff line number Diff line change
@@ -1,27 +1,117 @@
<template>
<YamlModal
<JsonModal
id="modbusProxyModal"
:title="$t('config.modbusproxy.title')"
:description="$t('config.modbusproxy.description')"
docs="/docs/reference/configuration/modbusproxy"
:defaultYaml="defaultYaml"
endpoint="/config/modbusproxy"
removeKey="modbusproxy"
size="md"
state-key="modbusproxy"
:store-values-in-array="true"
disable-remove
data-testid="modbusproxy-modal"
@changed="$emit('changed')"
/>
>
<template #default="{ values }: { values: ModbusProxy[] }">
<div class="mb-3">
<pre class="text-monospace">{{ ASCII_DIAGRAM }}</pre>
<div v-for="(connection, index) in values">

Check failure on line 17 in assets/js/components/Config/ModbusProxyModal.vue

View workflow job for this annotation

GitHub Actions / UI

Elements in iteration expect to have 'v-bind:key' directives
<div class="d-none d-lg-block">
<hr class="mt-5" />
<h5>
<div class="inner mb-3">Connection #{{ index + 1 }}</div>
</h5>
</div>
<Modbus
:capabilities="['rs485', 'tcpip']"
:host="connection.Settings.URI"
:port="connection.Port"
:baudrate="connection.Settings.Baudrate"
:comset="connection.Settings.Comset"
:device="connection.Settings.Device"
:read-only="connection.ReadOnly"
:is-proxy="true"
/>
<div class="align-items-center d-flex mb-4">
<button
type="button"
class="btn btn-sm btn-outline-secondary border-0 ms-auto"
aria-label="Remove"
tabindex="0"
@click="values.splice(index, 1)"
>
<shopicon-regular-trash
size="s"
class="flex-shrink-0"
></shopicon-regular-trash>
</button>
</div>
</div>
<button
type="button"
class="d-flex btn btn-sm btn-outline-secondary border-0 align-items-center gap-2 evcc-gray"
data-testid="networkconnection-add"
tabindex="0"
@click="addConnection(values)"
>
<shopicon-regular-plus size="s" class="flex-shrink-0"></shopicon-regular-plus>
Add network connection
</button>
</div>
</template>
</JsonModal>
</template>

<script>
import YamlModal from "./YamlModal.vue";
import defaultYaml from "./defaultYaml/modbusproxy.yaml?raw";
<script lang="ts">
import "@h2d2/shopicons/es/regular/plus";
import "@h2d2/shopicons/es/regular/trash";
import JsonModal from "./JsonModal.vue";
import { MODBUS_PROXY_READONLY, type ModbusProxy } from "@/types/evcc";
import ASCII_DIAGRAM from "./modbus-diagram.txt?raw";
import Modbus from "./DeviceModal/Modbus.vue";
import deepClone from "@/utils/deepClone";

const DEFAULT_MODBUS_PROXY: ModbusProxy = {
Port: 502,
ReadOnly: MODBUS_PROXY_READONLY.DENY,
Settings: {},
};

export default {
name: "ModbusProxyModal",
components: { YamlModal },
components: { JsonModal, Modbus },
emits: ["changed"],
data() {
return { defaultYaml: defaultYaml.trim() };
return {
ASCII_DIAGRAM,
MODBUS_PROXY_READONLY,
DEFAULT_MODBUS_PROXY,
deepClone,
};
},
methods: {
addConnection(values: ModbusProxy[]) {
const newConnection = { ...deepClone(DEFAULT_MODBUS_PROXY) }; // Ensures reactivity with spread
values.push(newConnection);
},
},
};
</script>

<style scoped>
h5 {
position: relative;
display: flex;
top: -25px;
margin-bottom: -0.5rem;
padding: 0 0.5rem;
justify-content: center;
}
h5 .inner {
padding: 0 0.5rem;
background-color: var(--evcc-box);
font-weight: normal;
color: var(--evcc-gray);
text-transform: uppercase;
text-align: center;
}
</style>
7 changes: 0 additions & 7 deletions assets/js/components/Config/defaultYaml/modbusproxy.yaml

This file was deleted.

7 changes: 7 additions & 0 deletions assets/js/components/Config/modbus-diagram.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
┌────────┐
┌─ network ─> │ device │
┌────────┐ ┌──────┐ └────────┘
│ client │ --port--> │ evcc │
└────────┘ └──────┘ ┌────────┐
└─ serial --> │ device │
└────────┘
Loading
Loading