EEBUS: configure by default (#26944)

This commit is contained in:
Maschga 2026-02-01 11:59:02 +01:00 • committed by GitHub
parent ca7bbba0a4
commit 768656e0c3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
52 changed files with 712 additions and 324 deletions

View file

@ -18,6 +18,13 @@ import (
"github.com/evcc-io/evcc/util/modbus" "github.com/evcc-io/evcc/util/modbus"
) )
// ConfigStatus for publishing config, status and source to UI and external systems
type ConfigStatus struct {
Config any `json:"config,omitempty"`
Status any `json:"status,omitempty"`
FromYaml bool `json:"fromYaml,omitempty"`
}
type All struct { type All struct {
Network Network Network Network
Ocpp ocpp.Config Ocpp ocpp.Config
@ -79,13 +86,6 @@ func (c Hems) Redacted() any {
var _ api.Redactor = (*Mqtt)(nil) var _ api.Redactor = (*Mqtt)(nil)
func masked(s any) string {
if s != "" {
return "***"
}
return ""
}
type Mqtt struct { type Mqtt struct {
mqtt.Config `mapstructure:",squash"` mqtt.Config `mapstructure:",squash"`
Topic string `json:"topic"` Topic string `json:"topic"`
@ -97,12 +97,12 @@ func (m Mqtt) Redacted() any {
Config: mqtt.Config{ Config: mqtt.Config{
Broker: m.Broker, Broker: m.Broker,
User: m.User, User: m.User,
Password: masked(m.Password), Password: util.Masked(m.Password),
ClientID: m.ClientID, ClientID: m.ClientID,
Insecure: m.Insecure, Insecure: m.Insecure,
CaCert: masked(m.CaCert), CaCert: util.Masked(m.CaCert),
ClientCert: masked(m.ClientCert), ClientCert: util.Masked(m.ClientCert),
ClientKey: masked(m.ClientKey), ClientKey: util.Masked(m.ClientKey),
}, },
Topic: m.Topic, Topic: m.Topic,
} }
@ -124,10 +124,10 @@ func (c Influx) Redacted() any {
return Influx{ return Influx{
URL: c.URL, URL: c.URL,
Database: c.Database, Database: c.Database,
Token: masked(c.Token), Token: util.Masked(c.Token),
Org: c.Org, Org: c.Org,
User: c.User, User: c.User,
Password: masked(c.Password), Password: util.Masked(c.Password),
Insecure: c.Insecure, Insecure: c.Insecure,
} }
} }
@ -147,7 +147,7 @@ type MessagingEventTemplate struct {
Title, Msg string Title, Msg string
} }
func (c Messaging) Configured() bool { func (c Messaging) IsConfigured() bool {
return len(c.Services) > 0 || len(c.Events) > 0 return len(c.Services) > 0 || len(c.Events) > 0
} }

View file

@ -6,9 +6,10 @@
'round-box--error': error, 'round-box--error': error,
'round-box--warning': warning, 'round-box--warning': warning,
'root--unconfigured': unconfigured, 'root--unconfigured': unconfigured,
'root--with-tags': $slots.tags,
}" }"
> >
<div class="d-flex align-items-center mb-2"> <div class="d-flex align-items-center" :class="{ 'mb-2': $slots.tags }">
<div class="icon me-2"> <div class="icon me-2">
<slot name="icon" /> <slot name="icon" />
</div> </div>
@ -33,8 +34,10 @@
<shopicon-regular-adjust size="s"></shopicon-regular-adjust> <shopicon-regular-adjust size="s"></shopicon-regular-adjust>
</button> </button>
</div> </div>
<hr class="my-3 divide" /> <div v-if="$slots.tags">
<slot name="tags" /> <hr class="my-3 divide" />
<slot name="tags" />
</div>
</div> </div>
</template> </template>
@ -67,7 +70,7 @@ export default {
} }
let title = `${this.$t("config.main.name")}: <span class='font-monospace'>${this.name}</span>`; let title = `${this.$t("config.main.name")}: <span class='font-monospace'>${this.name}</span>`;
if (!this.editable) { if (!this.editable) {
title += `<div class="mt-1">${this.$t("config.main.yaml")}</div>`; title += `<div class="mt-1">${this.$t("config.general.fromYamlHint")}</div>`;
} }
return `<div class="text-start">${title}</div>`; return `<div class="text-start">${title}</div>`;
}, },
@ -105,6 +108,8 @@ export default {
list-style-type: none; list-style-type: none;
border-radius: 1rem; border-radius: 1rem;
padding: 1rem 1.5rem; padding: 1rem 1.5rem;
}
.root--with-tags {
min-height: 8rem; min-height: 8rem;
} }
.root--unconfigured { .root--unconfigured {

View file

@ -1,26 +1,153 @@
<template> <template>
<YamlModal <JsonModal
id="eebusModal" id="eebusModal"
:title="$t('config.eebus.title')" :title="$t('config.eebus.title')"
:description="$t('config.eebus.description')" :description="$t('config.eebus.description')"
docs="/docs/reference/configuration/eebus" docs="/docs/reference/configuration/eebus"
:defaultYaml="defaultYaml"
removeKey="eebus"
endpoint="/config/eebus" endpoint="/config/eebus"
state-key="eebus.config"
data-testid="eebus-modal"
:no-buttons="fromYaml"
:confirm-remove="$t('config.eebus.removeConfirm')"
@changed="$emit('changed')" @changed="$emit('changed')"
/> >
<template #default="{ values }: { values: EebusConfig }">
<p v-if="fromYaml" class="text-muted">
{{ $t("config.general.fromYamlHint") }}
</p>
<FormRow
v-if="values.shipid"
:id="formId('shipid-display')"
:label="$t('config.eebus.shipid')"
:help="$t('config.eebus.shipidExplain')"
>
<input
:id="formId('shipid-display')"
:value="values.shipid"
readonly
class="form-control text-muted"
/>
</FormRow>
<FormRow
v-if="status.ski"
:id="formId('ski-display')"
:label="$t('config.eebus.ski')"
:help="$t('config.eebus.skiExplain')"
>
<input
:id="formId('ski-display')"
:value="status.ski"
readonly
class="form-control text-muted"
/>
</FormRow>
<PropertyCollapsible v-if="!fromYaml">
<template #advanced>
<div class="alert alert-danger">
{{ $t("config.eebus.descriptionAdvanced") }}
</div>
<FormRow
:id="formId('shipid')"
:label="$t('config.eebus.shipid')"
:help="$t('config.eebus.shipidHelp')"
>
<PropertyField
:id="formId('shipid')"
v-model="values.shipid"
type="String"
required
/>
</FormRow>
<FormRow
:id="formId('port')"
:label="$t('config.eebus.port')"
:help="$t('config.eebus.portHelp')"
optional
>
<PropertyField
:id="formId('port')"
v-model="values.port"
property="port"
type="Int"
/>
</FormRow>
<FormRow
:id="formId('interfaces')"
:label="$t('config.eebus.interfaces')"
:help="$t('config.eebus.interfacesHelp')"
optional
example="eth0"
>
<PropertyField
:id="formId('interfaces')"
v-model="values.interfaces"
type="List"
/>
</FormRow>
<h6>{{ $t("config.eebus.certificate.title") }}</h6>
<FormRow
:id="formId('certificate-public')"
:label="$t('config.eebus.certificate.public')"
>
<PropertyCertField
:id="formId('certificate-public')"
:model-value="values.certificate?.public"
required
@update:model-value="
values.certificate ? (values.certificate.public = $event) : ''
"
/>
</FormRow>
<FormRow
:id="formId('certificate-private')"
:label="$t('config.eebus.certificate.private')"
>
<PropertyCertField
:id="formId('certificate-private')"
:model-value="values.certificate?.private"
required
@update:model-value="
values.certificate ? (values.certificate.private = $event) : ''
"
/>
</FormRow>
</template>
</PropertyCollapsible>
</template>
</JsonModal>
</template> </template>
<script> <script lang="ts">
import YamlModal from "./YamlModal.vue"; import type { PropType } from "vue";
import defaultYaml from "./defaultYaml/eebus.yaml?raw"; // eslint-disable-next-line @typescript-eslint/no-unused-vars
import type { EebusConfig, EebusStatus } from "@/types/evcc";
import JsonModal from "./JsonModal.vue";
import FormRow from "./FormRow.vue";
import PropertyField from "./PropertyField.vue";
import PropertyCertField from "./PropertyCertField.vue";
import PropertyCollapsible from "./PropertyCollapsible.vue";
export default { export default {
name: "EebusModal", name: "EebusModal",
components: { YamlModal }, components: {
JsonModal,
FormRow,
PropertyField,
PropertyCertField,
PropertyCollapsible,
},
props: {
status: {
type: Object as PropType<EebusStatus>,
default: () => ({}),
},
fromYaml: Boolean,
},
emits: ["changed"], emits: ["changed"],
data() { methods: {
return { defaultYaml: defaultYaml.trim() }; formId(s: string) {
return `eebus-${s}`;
},
}, },
}; };
</script> </script>

View file

@ -31,7 +31,7 @@
</a> </a>
</div> </div>
<p v-if="fromYaml" class="text-muted"> <p v-if="fromYaml" class="text-muted">
{{ $t("config.hems.yamlConfigured") }} {{ $t("config.general.fromYamlHint") }}
</p> </p>
</template> </template>
</YamlModal> </YamlModal>

View file

@ -88,6 +88,7 @@ export default {
saveMethod: { type: String, default: "post" }, saveMethod: { type: String, default: "post" },
storeValuesInArray: Boolean, storeValuesInArray: Boolean,
size: { type: String }, size: { type: String },
confirmRemove: String,
}, },
emits: ["changed", "open"], emits: ["changed", "open"],
data() { data() {
@ -121,7 +122,13 @@ export default {
await this.load(); await this.load();
}, },
async load() { async load() {
this.serverValues = this.stateKey ? store.state[this.stateKey] : store.state; if (this.stateKey) {
// Support nested keys like "eebus.config"
const keys = this.stateKey.split(".");
this.serverValues = keys.reduce((obj, key) => obj?.[key], store.state);
} else {
this.serverValues = store.state;
}
if (this.transformReadValues) { if (this.transformReadValues) {
this.serverValues = this.transformReadValues(this.serverValues); this.serverValues = this.transformReadValues(this.serverValues);
} }
@ -155,6 +162,9 @@ export default {
this.saving = false; this.saving = false;
}, },
async remove() { async remove() {
if (this.confirmRemove && !window.confirm(this.confirmRemove)) {
return;
}
this.removing = true; this.removing = true;
this.error = ""; this.error = "";
try { try {

View file

@ -1,6 +1,6 @@
<template> <template>
<div> <div>
<textarea :id="id" v-model="value" class="form-control" rows="3" /> <textarea :id="id" v-model="value" class="form-control" rows="3" :required="required" />
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">
<button <button
type="button" type="button"
@ -26,6 +26,7 @@ export default {
props: { props: {
id: String, id: String,
modelValue: String, modelValue: String,
required: Boolean,
}, },
emits: ["update:modelValue"], emits: ["update:modelValue"],
computed: { computed: {

View file

@ -220,7 +220,9 @@ export default {
return this.property === "icon"; return this.property === "icon";
}, },
textarea() { textarea() {
return ["accessToken", "refreshToken", "identifiers"].includes(this.property); return (
this.array || ["accessToken", "refreshToken", "identifiers"].includes(this.property)
);
}, },
boolean() { boolean() {
return this.type === "Bool"; return this.type === "Bool";

View file

@ -1,12 +0,0 @@
#shipid: EVCC-1234567890abcdef
#interfaces:
# - eth0
#certificate:
# public: |
# -----BEGIN CERTIFICATE-----
# 1234567890abcdef==
# -----END CERTIFICATE-----
# private: |
# -----BEGIN EC PRIVATE KEY-----
# 1234567890abcdef
# -----END EC PRIVATE KEY-----

View file

@ -84,7 +84,7 @@ export interface State {
hems?: Hems; hems?: Hems;
shm?: ShmConfig; shm?: ShmConfig;
sponsor?: Sponsor; sponsor?: Sponsor;
eebus?: any; eebus?: Eebus;
modbusproxy?: ModbusProxy[]; modbusproxy?: ModbusProxy[];
messaging?: any; messaging?: any;
interval?: number; interval?: number;
@ -440,6 +440,29 @@ export enum MODBUS_PROTOCOL {
RTU = "rtu", RTU = "rtu",
} }
export type Certificate = {
public: string;
private: string;
};
export type Eebus = {
config: EebusConfig;
status: EebusStatus;
fromYaml?: boolean;
};
export type EebusConfig = {
uri: string;
port: number;
shipid: string;
interfaces?: string[];
certificate?: Certificate;
};
export type EebusStatus = {
ski: string;
};
export type ModbusProxy = { export type ModbusProxy = {
port: number; port: number;
readonly: MODBUS_PROXY_READONLY; readonly: MODBUS_PROXY_READONLY;

View file

@ -210,33 +210,6 @@
<DeviceTags :tags="influxTags" /> <DeviceTags :tags="influxTags" />
</template> </template>
</DeviceCard> </DeviceCard>
<DeviceCard
:title="$t('config.eebus.title')"
editable
:error="hasClassError('eebus')"
:unconfigured="isUnconfigured(eebusTags)"
data-testid="eebus"
@edit="openModal('eebusModal')"
>
<template #icon><EebusIcon /></template>
<template #tags>
<DeviceTags :tags="eebusTags" />
</template>
</DeviceCard>
<DeviceCard
:title="$t('config.ocpp.title')"
editable
:error="hasClassError('ocpp')"
:unconfigured="isUnconfigured(ocppTags)"
data-testid="ocpp"
@edit="openModal('ocppModal')"
>
<template #icon><OcppIcon /></template>
<template #tags>
<DeviceTags :tags="ocppTags" />
</template>
</DeviceCard>
<DeviceCard <DeviceCard
:title="`${$t('config.circuits.title')}`" :title="`${$t('config.circuits.title')}`"
editable editable
@ -278,19 +251,6 @@
<DeviceTags :tags="modbusproxyTags" /> <DeviceTags :tags="modbusproxyTags" />
</template> </template>
</DeviceCard> </DeviceCard>
<DeviceCard
:title="$t('config.shm.cardTitle')"
editable
:error="hasClassError('shm')"
:unconfigured="isUnconfigured(shmTags)"
data-testid="shm"
@edit="openModal('shmModal')"
>
<template #icon><ShmIcon /></template>
<template #tags>
<DeviceTags :tags="shmTags" />
</template>
</DeviceCard>
<DeviceCard <DeviceCard
:title="$t('config.hems.title')" :title="$t('config.hems.title')"
editable editable
@ -306,6 +266,37 @@
</DeviceCard> </DeviceCard>
</div> </div>
<h2 class="my-4 mt-5">{{ $t("config.section.services") }}</h2>
<div class="p-0 config-list">
<DeviceCard
:title="$t('config.ocpp.title')"
editable
:error="hasClassError('ocpp')"
data-testid="ocpp"
@edit="openModal('ocppModal')"
>
<template #icon><OcppIcon /></template>
</DeviceCard>
<DeviceCard
:title="$t('config.shm.cardTitle')"
editable
:error="hasClassError('shm')"
data-testid="shm"
@edit="openModal('shmModal')"
>
<template #icon><ShmIcon /></template>
</DeviceCard>
<DeviceCard
:title="$t('config.eebus.title')"
editable
:error="hasClassError('eebus')"
data-testid="eebus"
@edit="openModal('eebusModal')"
>
<template #icon><EebusIcon /></template>
</DeviceCard>
</div>
<hr class="my-5" /> <hr class="my-5" />
<h2 class="my-4 mt-5">{{ $t("config.section.system") }}</h2> <h2 class="my-4 mt-5">{{ $t("config.section.system") }}</h2>
@ -387,7 +378,11 @@
:extMeters="extMeters" :extMeters="extMeters"
@changed="yamlChanged" @changed="yamlChanged"
/> />
<EebusModal @changed="yamlChanged" /> <EebusModal
:status="eebus?.status"
:from-yaml="eebus?.fromYaml"
@changed="yamlChanged"
/>
<OcppModal :ocpp="ocpp" /> <OcppModal :ocpp="ocpp" />
<BackupRestoreModal v-bind="backupRestoreProps" /> <BackupRestoreModal v-bind="backupRestoreProps" />
<PasswordModal update-mode /> <PasswordModal update-mode />
@ -656,12 +651,6 @@ export default defineComponent({
vehicleOptions(): VehicleOption[] { vehicleOptions(): VehicleOption[] {
return this.vehicles.map((v) => ({ key: v.name, name: v.config?.title || v.name })); return this.vehicles.map((v) => ({ key: v.name, name: v.config?.title || v.name }));
}, },
shmTags(): DeviceTags {
const { vendorId, deviceId } = store.state?.shm || {};
// TODO: use incoming SEMP connections to determin configured/active status
const value = !!vendorId || !!deviceId;
return { configured: { value } };
},
hems() { hems() {
return store.state?.hems; return store.state?.hems;
}, },
@ -698,30 +687,8 @@ export default defineComponent({
// @ts-expect-error: telemetry property exists but not in TypeScript definitions // @ts-expect-error: telemetry property exists but not in TypeScript definitions
return store.state?.telemetry === true; return store.state?.telemetry === true;
}, },
eebusTags(): DeviceTags { eebus() {
return { configured: { value: store.state?.eebus || false } }; return store.state?.eebus;
},
ocppTags(): DeviceTags {
const ocpp = store.state?.ocpp;
const stations = ocpp?.status?.stations || [];
if (stations.length === 0) {
return { configured: { value: false } };
}
const connected = stations.filter((s) => s.status === "connected").length;
const configured = stations.filter((s) => s.status === "configured").length;
const detected = stations.filter((s) => s.status === "unknown").length;
const total = connected + configured;
const tags: Record<string, any> = {
connections: { value: `${connected}/${total}` },
};
if (detected > 0) {
tags["detected"] = { value: detected };
}
return tags;
}, },
modbusproxyTags(): DeviceTags { modbusproxyTags(): DeviceTags {
const config = store.state?.modbusproxy || []; const config = store.state?.modbusproxy || [];

View file

@ -8,6 +8,7 @@ import (
"strings" "strings"
"github.com/evcc-io/evcc/cmd/shutdown" "github.com/evcc-io/evcc/cmd/shutdown"
"github.com/evcc-io/evcc/server/db/settings"
"github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/config" "github.com/evcc-io/evcc/util/config"
"go.yaml.in/yaml/v4" "go.yaml.in/yaml/v4"
@ -120,3 +121,18 @@ func deviceHeader[T any](dev config.Device[T]) string {
return name return name
} }
// migrateYamlToJson converts a settings value from yaml to json if needed
func migrateYamlToJson[T any](key string, res *T) error {
if settings.IsJson(key) {
return settings.Json(key, res)
}
if err := settings.Yaml(key, new(T), res); err != nil {
return err
}
settings.SetJson(key, res)
return nil
}

34
cmd/helper_test.go Normal file
View file

@ -0,0 +1,34 @@
package cmd
import (
"testing"
"github.com/evcc-io/evcc/server/db"
"github.com/evcc-io/evcc/server/db/settings"
"github.com/stretchr/testify/require"
)
func TestMigrateYaml(t *testing.T) {
require.NoError(t, db.NewInstance("sqlite", ":memory:"))
const key = "foo"
type T struct {
Key string
Val []string
}
expect := T{
Key: "foo",
Val: []string{"a", "b"},
}
require.NoError(t, settings.SetYaml(key, expect))
require.False(t, settings.IsJson(key))
var res T
require.NoError(t, migrateYamlToJson(key, &res))
require.True(t, settings.IsJson(key))
require.Equal(t, expect, res)
}

View file

@ -20,6 +20,7 @@ import (
"github.com/evcc-io/evcc/push" "github.com/evcc-io/evcc/push"
"github.com/evcc-io/evcc/server" "github.com/evcc-io/evcc/server"
"github.com/evcc-io/evcc/server/db" "github.com/evcc-io/evcc/server/db"
"github.com/evcc-io/evcc/server/eebus"
"github.com/evcc-io/evcc/server/mcp" "github.com/evcc-io/evcc/server/mcp"
"github.com/evcc-io/evcc/server/network" "github.com/evcc-io/evcc/server/network"
"github.com/evcc-io/evcc/server/updater" "github.com/evcc-io/evcc/server/updater"
@ -176,10 +177,7 @@ func runRoot(cmd *cobra.Command, args []string) {
ocppCS := ocpp.Instance() ocppCS := ocpp.Instance()
ocppCS.SetUpdated(func() { ocppCS.SetUpdated(func() {
// republish when OCPP state updates // republish when OCPP state updates
valueChan <- util.Param{Key: keys.Ocpp, Val: struct { valueChan <- util.Param{Key: keys.Ocpp, Val: globalconfig.ConfigStatus{
Config ocpp.Config `json:"config"`
Status ocpp.Status `json:"status"`
}{
Config: conf.Ocpp, Config: conf.Ocpp,
Status: ocpp.GetStatus(), Status: ocpp.GetStatus(),
}} }}
@ -329,35 +327,29 @@ func runRoot(cmd *cobra.Command, args []string) {
} }
// publish initial settings // publish initial settings
valueChan <- util.Param{Key: keys.EEBus, Val: conf.EEBus.Configured()} valueChan <- util.Param{Key: keys.EEBus, Val: globalconfig.ConfigStatus{
Config: conf.EEBus.Redacted(),
Status: eebus.GetStatus(),
FromYaml: fromYaml.eebus,
}}
valueChan <- util.Param{Key: keys.Shm, Val: conf.SHM} valueChan <- util.Param{Key: keys.Shm, Val: conf.SHM}
valueChan <- util.Param{Key: keys.Influx, Val: conf.Influx} valueChan <- util.Param{Key: keys.Influx, Val: conf.Influx}
valueChan <- util.Param{Key: keys.Interval, Val: conf.Interval} valueChan <- util.Param{Key: keys.Interval, Val: conf.Interval}
valueChan <- util.Param{Key: keys.Messaging, Val: conf.Messaging.Configured()} valueChan <- util.Param{Key: keys.Messaging, Val: conf.Messaging.IsConfigured()}
valueChan <- util.Param{Key: keys.ModbusProxy, Val: conf.ModbusProxy} valueChan <- util.Param{Key: keys.ModbusProxy, Val: conf.ModbusProxy}
valueChan <- util.Param{Key: keys.Mqtt, Val: conf.Mqtt} valueChan <- util.Param{Key: keys.Mqtt, Val: conf.Mqtt}
valueChan <- util.Param{Key: keys.Network, Val: conf.Network} valueChan <- util.Param{Key: keys.Network, Val: conf.Network}
valueChan <- util.Param{Key: keys.Ocpp, Val: struct { valueChan <- util.Param{Key: keys.Ocpp, Val: globalconfig.ConfigStatus{
Config ocpp.Config `json:"config"`
Status ocpp.Status `json:"status"`
}{
Config: conf.Ocpp, Config: conf.Ocpp,
Status: ocpp.GetStatus(), Status: ocpp.GetStatus(),
}} }}
valueChan <- util.Param{Key: keys.Sponsor, Val: struct { valueChan <- util.Param{Key: keys.Sponsor, Val: globalconfig.ConfigStatus{
Status sponsor.Status `json:"status"` Status: sponsor.RedactedStatus(),
FromYaml bool `json:"fromYaml"`
}{
Status: sponsor.GetStatus(),
FromYaml: fromYaml.sponsor, FromYaml: fromYaml.sponsor,
}} }}
valueChan <- util.Param{Key: keys.Hems, Val: struct { valueChan <- util.Param{Key: keys.Hems, Val: globalconfig.ConfigStatus{
Config globalconfig.Hems `json:"config"` Config: conf.HEMS.Redacted(),
Status *hemsapi.Status `json:"status,omitempty"`
FromYaml bool `json:"fromYaml"`
}{
Config: conf.HEMS,
Status: hemsapi.GetStatus(hems), Status: hemsapi.GetStatus(hems),
FromYaml: fromYaml.hems, FromYaml: fromYaml.hems,
}} }}

View file

@ -73,7 +73,7 @@ var conf = globalconfig.All{
Topic: "evcc", Topic: "evcc",
}, },
EEBus: eebus.Config{ EEBus: eebus.Config{
URI: ":4712", Port: 4712,
}, },
Database: globalconfig.DB{ Database: globalconfig.DB{
Type: "sqlite", Type: "sqlite",
@ -84,6 +84,7 @@ var conf = globalconfig.All{
var fromYaml struct { var fromYaml struct {
sponsor bool sponsor bool
hems bool hems bool
eebus bool
} }
var nameRE = regexp.MustCompile(`^[a-zA-Z0-9_.:-]+$`) var nameRE = regexp.MustCompile(`^[a-zA-Z0-9_.:-]+$`)
@ -164,7 +165,7 @@ func configureCircuits(conf *[]config.Named) error {
children := slices.Clone(*conf) children := slices.Clone(*conf)
// TODO: check for circular references // TODO check for circular references
NEXT: NEXT:
for i, cc := range children { for i, cc := range children {
if cc.Name == "" { if cc.Name == "" {
@ -780,14 +781,23 @@ func configureOCPP(cfg *ocpp.Config, externalUrl string) {
func configureEEBus(conf *eebus.Config) error { func configureEEBus(conf *eebus.Config) error {
// migrate settings // migrate settings
if settings.Exists(keys.EEBus) { if settings.Exists(keys.EEBus) {
*conf = eebus.Config{} if err := migrateYamlToJson(keys.EEBus, conf); err != nil {
if err := settings.Yaml(keys.EEBus, new(map[string]any), &conf); err != nil {
return err return err
} }
} else if conf.IsConfigured() {
fromYaml.eebus = true
} }
if !conf.Configured() { if !conf.IsConfigured() {
return nil cc, err := eebus.DefaultConfig(conf)
if err != nil {
return err
}
*conf = *cc
if err := settings.SetJson(keys.EEBus, conf); err != nil {
return err
}
} }
var err error var err error
@ -968,33 +978,11 @@ func configureDevices(conf globalconfig.All) error {
return joinErrors(errs...) return joinErrors(errs...)
} }
// migrateYamlToJson converts a settings value from yaml to json if needed
func migrateYamlToJson[T any](key string) error {
var err error
if settings.IsJson(key) {
// already JSON, nothing to do
return nil
}
var data T
if err := settings.Yaml(key, new(T), &data); err == nil {
settings.SetJson(key, data)
log.INFO.Printf("migrated %s setting to JSON", key)
}
return err
}
func configureModbusProxy(conf *[]globalconfig.ModbusProxy) error { func configureModbusProxy(conf *[]globalconfig.ModbusProxy) error {
if settings.Exists(keys.ModbusProxy) { if settings.Exists(keys.ModbusProxy) {
// TODO: delete if not needed any more if err := migrateYamlToJson(keys.ModbusProxy, conf); err != nil {
if err := migrateYamlToJson[[]globalconfig.ModbusProxy](keys.ModbusProxy); err != nil {
return err return err
} }
if err := settings.Json(keys.ModbusProxy, &conf); err != nil {
return fmt.Errorf("failed to read modbusproxy setting: %w", err)
}
} }
// prevent panic // prevent panic

View file

@ -209,6 +209,7 @@
"dragList": "Списък за повторна поръчка", "dragList": "Списък за повторна поръчка",
"experimental": "Експериментален", "experimental": "Експериментален",
"forceSave": "Запази все пак", "forceSave": "Запази все пак",
"fromYamlHint": "Забележка: Конфигурира се чрез evcc.yaml. Премахнете записа от файла, за да активирате редактирането тук.",
"hideAdvancedSettings": "Скрий разширените настройки", "hideAdvancedSettings": "Скрий разширените настройки",
"invalidFileSelected": "Избраният файл е невалиден", "invalidFileSelected": "Избраният файл е невалиден",
"noFileSelected": "Няма избран файл.", "noFileSelected": "Няма избран файл.",
@ -247,8 +248,7 @@
"downloadCsv": "Изтегли CSV", "downloadCsv": "Изтегли CSV",
"eventsRecorded": "Записани {count} събития за ограничение на мрежата.", "eventsRecorded": "Записани {count} събития за ограничение на мрежата.",
"lastEvent": "Най-скорошно {timeAgo}.", "lastEvent": "Най-скорошно {timeAgo}.",
"title": "Външен лимит", "title": "Външен лимит"
"yamlConfigured": "Забележка: Конфигурира се чрез evcc.yaml. Премахнете записа от файла, за да активирате редактирането тук."
}, },
"icon": { "icon": {
"change": "промени", "change": "промени",
@ -390,8 +390,7 @@
"unconfigured": "не е конфигуриран", "unconfigured": "не е конфигуриран",
"vehicles": "Моите Автомобили", "vehicles": "Моите Автомобили",
"welcomeBannerText": "Започнете с създаването на поне един **зарядно устройство**, **нагревател**, **мрежа**, **соларна система**, **батерия** или **допълнителен електромер**. Ако искате само да тествате, изберете **демонстрационно устройство**..", "welcomeBannerText": "Започнете с създаването на поне един **зарядно устройство**, **нагревател**, **мрежа**, **соларна система**, **батерия** или **допълнителен електромер**. Ако искате само да тествате, изберете **демонстрационно устройство**..",
"welcomeBannerTitle": "Да конфигурираме вашата система!", "welcomeBannerTitle": "Да конфигурираме вашата система!"
"yaml": "Устройствата от evcc.yaml не могат да се редактират."
}, },
"messaging": { "messaging": {
"description": "Получавайте съобщения за вашите сесии на зареждане.", "description": "Получавайте съобщения за вашите сесии на зареждане.",

View file

@ -209,6 +209,7 @@
"dragList": "Ponovo naručiva lista", "dragList": "Ponovo naručiva lista",
"experimental": "Eksperimentalni", "experimental": "Eksperimentalni",
"forceSave": "Sačuvaj svejedno", "forceSave": "Sačuvaj svejedno",
"fromYamlHint": "Napomena: Konfigurisano putem evcc.yaml. Uklonite unos iz datoteke da biste ovdje omogućili uređivanje.",
"hideAdvancedSettings": "Sakrij napredne postavke", "hideAdvancedSettings": "Sakrij napredne postavke",
"invalidFileSelected": "Odabrana neispravna datoteka", "invalidFileSelected": "Odabrana neispravna datoteka",
"noFileSelected": "Nije odabrana nijedna datoteka.", "noFileSelected": "Nije odabrana nijedna datoteka.",
@ -247,8 +248,7 @@
"downloadCsv": "Preuzmi CSV", "downloadCsv": "Preuzmi CSV",
"eventsRecorded": "Zabilježeno je {count} događaja ograničenja mreže.", "eventsRecorded": "Zabilježeno je {count} događaja ograničenja mreže.",
"lastEvent": "Najnoviji prije {timeAgo}.", "lastEvent": "Najnoviji prije {timeAgo}.",
"title": "Vanjski limit", "title": "Vanjski limit"
"yamlConfigured": "Napomena: Konfigurisano putem evcc.yaml. Uklonite unos iz datoteke da biste ovdje omogućili uređivanje."
}, },
"icon": { "icon": {
"change": "promjena", "change": "promjena",
@ -344,6 +344,7 @@
"thresholdDisableHelpZero": "Zaustavite kada se za {delay} ne može zadovoljiti minimalna potrebna snaga.", "thresholdDisableHelpZero": "Zaustavite kada se za {delay} ne može zadovoljiti minimalna potrebna snaga.",
"thresholdEnableHelpZero": "­ Počnite kada se minimalna potrebna snaga može zadovoljiti za {delay}." "thresholdEnableHelpZero": "­ Počnite kada se minimalna potrebna snaga može zadovoljiti za {delay}."
}, },
"main": {},
"meter": { "meter": {
"save": "Sačuvaj" "save": "Sačuvaj"
}, },

View file

@ -209,6 +209,7 @@
"dragList": "Seznam umožňující úpravy pořadí", "dragList": "Seznam umožňující úpravy pořadí",
"experimental": "Experimentální nastavení", "experimental": "Experimentální nastavení",
"forceSave": "Vynutit uložení", "forceSave": "Vynutit uložení",
"fromYamlHint": "Poznámka: Konfigurace se provádí prostřednictvím souboru evcc.yaml. Chcete-li zde povolit úpravy, odstraňte záznam ze souboru.",
"hideAdvancedSettings": "Skrýt pokročilá nastavení", "hideAdvancedSettings": "Skrýt pokročilá nastavení",
"invalidFileSelected": "Zvolen neplatný soubor", "invalidFileSelected": "Zvolen neplatný soubor",
"noFileSelected": "Nebyl zvolen žádný soubor.", "noFileSelected": "Nebyl zvolen žádný soubor.",
@ -247,8 +248,7 @@
"downloadCsv": "Stáhnout CSV", "downloadCsv": "Stáhnout CSV",
"eventsRecorded": "Zaznamenáno {count} událostí omezení mřížky.", "eventsRecorded": "Zaznamenáno {count} událostí omezení mřížky.",
"lastEvent": "Nejnovější {timeAgo}.", "lastEvent": "Nejnovější {timeAgo}.",
"title": "Vnější limit", "title": "Vnější limit"
"yamlConfigured": "Poznámka: Konfigurace se provádí prostřednictvím souboru evcc.yaml. Chcete-li zde povolit úpravy, odstraňte záznam ze souboru."
}, },
"icon": { "icon": {
"change": "Změnit", "change": "Změnit",
@ -390,8 +390,7 @@
"unconfigured": "není konfigurováno", "unconfigured": "není konfigurováno",
"vehicles": "Má vozidla", "vehicles": "Má vozidla",
"welcomeBannerText": "Začněte vytvořením alespoň jednoho **nabíječe**, **ohřívače**, **sítě**, **solárního panelu**, **baterie** nebo **dalšího měřiče**. Pokud chcete pouze otestovat, vyberte **demo zařízení**.", "welcomeBannerText": "Začněte vytvořením alespoň jednoho **nabíječe**, **ohřívače**, **sítě**, **solárního panelu**, **baterie** nebo **dalšího měřiče**. Pokud chcete pouze otestovat, vyberte **demo zařízení**.",
"welcomeBannerTitle": "Pojďme nakonfigurovat váš systém!", "welcomeBannerTitle": "Pojďme nakonfigurovat váš systém!"
"yaml": "Zařízení z evcc.yaml nelze upravovat."
}, },
"messaging": { "messaging": {
"description": "Dostávejte notifikace o průběhu nabíjení.", "description": "Dostávejte notifikace o průběhu nabíjení.",

View file

@ -209,6 +209,7 @@
"dragList": "Rækkefølgen kan ændres", "dragList": "Rækkefølgen kan ændres",
"experimental": "Eksperimentel", "experimental": "Eksperimentel",
"forceSave": "Gem alligevel", "forceSave": "Gem alligevel",
"fromYamlHint": "Bemærk: Er konfigureret via evcc.yaml. Fjern indtastningen fra filen for at aktivere redigering her.",
"hideAdvancedSettings": "Skjul avancerede indstillinger", "hideAdvancedSettings": "Skjul avancerede indstillinger",
"invalidFileSelected": "Ugyldig fil er valgt", "invalidFileSelected": "Ugyldig fil er valgt",
"noFileSelected": "Ingen fil valgt.", "noFileSelected": "Ingen fil valgt.",
@ -247,8 +248,7 @@
"downloadCsv": "Download CSV", "downloadCsv": "Download CSV",
"eventsRecorded": "Registrerede {count} netbegrænsningshændelser.", "eventsRecorded": "Registrerede {count} netbegrænsningshændelser.",
"lastEvent": "Sidst {timeAgo}.", "lastEvent": "Sidst {timeAgo}.",
"title": "Ekstern grænse", "title": "Ekstern grænse"
"yamlConfigured": "Bemærk: Er konfigureret via evcc.yaml. Fjern indtastningen fra filen for at aktivere redigering her."
}, },
"icon": { "icon": {
"change": "ændre", "change": "ændre",
@ -390,8 +390,7 @@
"unconfigured": "ikke konfigureret", "unconfigured": "ikke konfigureret",
"vehicles": "Mine køretøjer", "vehicles": "Mine køretøjer",
"welcomeBannerText": "Start med at oprette mindst én **lader**, **varmer**, **elnet**, **sol**, **batteri** eller **ekstra måler**. Hvis du blot vil teste, kan du vælge en **demoenhed**.", "welcomeBannerText": "Start med at oprette mindst én **lader**, **varmer**, **elnet**, **sol**, **batteri** eller **ekstra måler**. Hvis du blot vil teste, kan du vælge en **demoenhed**.",
"welcomeBannerTitle": "Lad os konfigurere dit system!", "welcomeBannerTitle": "Lad os konfigurere dit system!"
"yaml": "Enheder fra evcc.yaml kan ikke ændres her."
}, },
"messaging": { "messaging": {
"description": "Modtag beskeder om dine opladningssessioner.", "description": "Modtag beskeder om dine opladningssessioner.",

View file

@ -172,7 +172,23 @@
"loading": "Lade YAML-Editor …" "loading": "Lade YAML-Editor …"
}, },
"eebus": { "eebus": {
"description": "Grundkonfiguration für die Kommunikation mit anderen EEBus-Geräten.", "certificate": {
"private": "Privater Schlüssel",
"public": "Öffentliches Zertifikat",
"title": "Zertifikate"
},
"description": "Konfiguration zur Kommunikation mit EEBus-kompatiblen Geräten wie Wallboxen oder dem Steuergerät des Netzbetreibers. Alle notwendigen Initialisierungen und die Zertifikatsgenerierung erfolgen automatisch beim ersten Start.",
"descriptionAdvanced": "Keine Änderungen erforderlich. Bei Änderung ist Neukoppelung aller Geräte erforderlich.",
"interfaces": "Schnittstellen",
"interfacesHelp": "Begrenzt die Netzwerkschnittstellen, die EEBus nutzen soll, um Kommunikationsprobleme zu vermeiden. Feld leer lassen, um alle Schnittstellen zu verwenden. Ein Eintrag pro Zeile.",
"port": "Port",
"portHelp": "Der zu verwendende Port.",
"removeConfirm": "Die gesamte EEBus-Konfiguration wird entfernt. Neue Zertifikate und Kennungen werden beim nächsten Start generiert. Bist du sicher?",
"shipid": "SHIP-ID",
"shipidExplain": "Permanente Gerätekennung zur Identifikation im EEBus-Netzwerk.",
"shipidHelp": "Diese SHIP-ID ist mit den unten stehenden Zertifikaten verknüpft.",
"ski": "SKI",
"skiExplain": "Eindeutige Sicherheitskennung zum Koppeln von EEBus-Geräten.",
"title": "EEBus" "title": "EEBus"
}, },
"experimental": { "experimental": {
@ -209,6 +225,7 @@
"dragList": "Sortierbare Liste", "dragList": "Sortierbare Liste",
"experimental": "Experimentell", "experimental": "Experimentell",
"forceSave": "Trotzdem speichern", "forceSave": "Trotzdem speichern",
"fromYamlHint": "Hinweis: Konfiguriert über evcc.yaml. Entferne den Eintrag aus der Datei, um die Bearbeitung hier zu aktivieren.",
"hideAdvancedSettings": "Erweiterte Einstellungen ausblenden", "hideAdvancedSettings": "Erweiterte Einstellungen ausblenden",
"invalidFileSelected": "Ungültige Datei ausgewählt", "invalidFileSelected": "Ungültige Datei ausgewählt",
"noFileSelected": "Keine Datei ausgewählt.", "noFileSelected": "Keine Datei ausgewählt.",
@ -247,8 +264,7 @@
"downloadCsv": "CSV herunterladen", "downloadCsv": "CSV herunterladen",
"eventsRecorded": "{count} Netzbegrenzungen gespeichert.", "eventsRecorded": "{count} Netzbegrenzungen gespeichert.",
"lastEvent": "Zuletzt {timeAgo}.", "lastEvent": "Zuletzt {timeAgo}.",
"title": "Externe Begrenzung", "title": "Externe Begrenzung"
"yamlConfigured": "Hinweis: Konfiguriert über evcc.yaml. Entferne den Eintrag aus der Datei, um die Bearbeitung hier zu aktivieren."
}, },
"icon": { "icon": {
"change": "ändern", "change": "ändern",
@ -390,8 +406,7 @@
"unconfigured": "nicht konfiguriert", "unconfigured": "nicht konfiguriert",
"vehicles": "Meine Fahrzeuge", "vehicles": "Meine Fahrzeuge",
"welcomeBannerText": "Lege zunächst eine **Wallbox**, **Heizung**, **Netzzähler**, **Solar**, **Batterie** oder **zusätzlichen Zähler** an. Falls du nur testen möchtest, wähle ein **Demogerät**.", "welcomeBannerText": "Lege zunächst eine **Wallbox**, **Heizung**, **Netzzähler**, **Solar**, **Batterie** oder **zusätzlichen Zähler** an. Falls du nur testen möchtest, wähle ein **Demogerät**.",
"welcomeBannerTitle": "Los geht's mit der Konfiguration!", "welcomeBannerTitle": "Los geht's mit der Konfiguration!"
"yaml": "Geräte aus evcc.yaml sind nicht editierbar."
}, },
"messaging": { "messaging": {
"description": "Benachrichtigungen über Ladevorgänge und andere Ereignisse erhalten.", "description": "Benachrichtigungen über Ladevorgänge und andere Ereignisse erhalten.",
@ -543,6 +558,7 @@
"integrations": "Integrationen", "integrations": "Integrationen",
"loadpoints": "Laden & Heizen", "loadpoints": "Laden & Heizen",
"meter": "PV & Batterie", "meter": "PV & Batterie",
"services": "Dienste",
"system": "System", "system": "System",
"vehicles": "Fahrzeuge" "vehicles": "Fahrzeuge"
}, },

View file

@ -209,6 +209,7 @@
"dragList": "Λίστα με αναδιάταξη", "dragList": "Λίστα με αναδιάταξη",
"experimental": "Πειραματικό", "experimental": "Πειραματικό",
"forceSave": "Αποθήκευση ούτως ή άλλως", "forceSave": "Αποθήκευση ούτως ή άλλως",
"fromYamlHint": "Σημείωση: Διαμορφώνεται μέσω του αρχείου evcc.yaml. Διαγράψτε την καταχώριση από το αρχείο για να ενεργοποιήσετε την επεξεργασία εδώ.",
"hideAdvancedSettings": "Απόκρυψη ρυθμίσεων για προχωρημένους", "hideAdvancedSettings": "Απόκρυψη ρυθμίσεων για προχωρημένους",
"invalidFileSelected": "Επιλέχθηκε μη έγκυρο αρχείο", "invalidFileSelected": "Επιλέχθηκε μη έγκυρο αρχείο",
"noFileSelected": "Δεν έχει επιλεγεί αρχείο.", "noFileSelected": "Δεν έχει επιλεγεί αρχείο.",
@ -247,8 +248,7 @@
"downloadCsv": "Λήψη CSV", "downloadCsv": "Λήψη CSV",
"eventsRecorded": "Καταγράφηκαν {count} συμβάντα περιορισμού πλέγματος.", "eventsRecorded": "Καταγράφηκαν {count} συμβάντα περιορισμού πλέγματος.",
"lastEvent": "Πιο πρόσφατο {timeAgo}.", "lastEvent": "Πιο πρόσφατο {timeAgo}.",
"title": "Εξωτερικό όριο", "title": "Εξωτερικό όριο"
"yamlConfigured": "Σημείωση: Διαμορφώνεται μέσω του αρχείου evcc.yaml. Διαγράψτε την καταχώριση από το αρχείο για να ενεργοποιήσετε την επεξεργασία εδώ."
}, },
"icon": { "icon": {
"change": "αλλαγή", "change": "αλλαγή",
@ -390,8 +390,7 @@
"unconfigured": "δεν έχει ρυθμιστεί", "unconfigured": "δεν έχει ρυθμιστεί",
"vehicles": "Τα Οχήματά μου", "vehicles": "Τα Οχήματά μου",
"welcomeBannerText": "Ξεκινήστε δημιουργώντας τουλάχιστον έναν **φορτιστή**, **θερμαντήρα**, **πλέγμα**, **ηλιακό**, **μπαταρία** ή **επιπλέον μετρητή**. Αν θέλετε απλώς να δοκιμάσετε, επιλέξτε μια **συσκευή επίδειξης**.", "welcomeBannerText": "Ξεκινήστε δημιουργώντας τουλάχιστον έναν **φορτιστή**, **θερμαντήρα**, **πλέγμα**, **ηλιακό**, **μπαταρία** ή **επιπλέον μετρητή**. Αν θέλετε απλώς να δοκιμάσετε, επιλέξτε μια **συσκευή επίδειξης**.",
"welcomeBannerTitle": "Ας διαμορφώσουμε το σύστημά σας!", "welcomeBannerTitle": "Ας διαμορφώσουμε το σύστημά σας!"
"yaml": "Συσκευές που ορίζονται στο evcc.yaml δεν είναι εδώ προς επεξεργασία."
}, },
"messaging": { "messaging": {
"description": "Λάβετε μηνύματα σχετικά με τις συνεδρίες φόρτισης.", "description": "Λάβετε μηνύματα σχετικά με τις συνεδρίες φόρτισης.",

View file

@ -172,7 +172,23 @@
"loading": "Loading YAML editor…" "loading": "Loading YAML editor…"
}, },
"eebus": { "eebus": {
"description": "Configuration that enables evcc to communicate with other EEBus devices.", "certificate": {
"private": "Private key",
"public": "Public certificate",
"title": "Certificates"
},
"description": "Configuration that enables evcc to communicate with EEBus compatible devices like chargers or a control unit of your grid operator. All relevant initialization and certificate generation is done automatically on first start.",
"descriptionAdvanced": "No changes required. Only perform changes if you really know what you're doing. If you change either the SHIP-id or the certificates, you'll need to pair your devices again.",
"interfaces": "Interfaces",
"interfacesHelp": "Limit the network interfaces that EEBus should use to avoid communication problems. Leave the field blank to use all interfaces. One entry per line.",
"port": "Port",
"portHelp": "The port to be used.",
"removeConfirm": "All EEBus configuration will be removed. New certificates and identifiers will be generated on next start. Are you sure?",
"shipid": "SHIP-ID",
"shipidExplain": "Permanent device identifier for identification in the EEBus network.",
"shipidHelp": "This SHIP-ID is linked to the certificates below.",
"ski": "SKI",
"skiExplain": "Unique security identifier for pairing EEBus devices.",
"title": "EEBus" "title": "EEBus"
}, },
"experimental": { "experimental": {
@ -209,6 +225,7 @@
"dragList": "Reorderable list", "dragList": "Reorderable list",
"experimental": "Experimental", "experimental": "Experimental",
"forceSave": "Save anyway", "forceSave": "Save anyway",
"fromYamlHint": "Note: Configured via evcc.yaml. Remove the entry from the file to enable editing here.",
"hideAdvancedSettings": "Hide advanced settings", "hideAdvancedSettings": "Hide advanced settings",
"invalidFileSelected": "Invalid file selected", "invalidFileSelected": "Invalid file selected",
"noFileSelected": "No file selected.", "noFileSelected": "No file selected.",
@ -247,8 +264,7 @@
"downloadCsv": "Download CSV", "downloadCsv": "Download CSV",
"eventsRecorded": "Recorded {count} grid limitation events.", "eventsRecorded": "Recorded {count} grid limitation events.",
"lastEvent": "Most recent {timeAgo}.", "lastEvent": "Most recent {timeAgo}.",
"title": "External Limit", "title": "External Limit"
"yamlConfigured": "Note: Configured via evcc.yaml. Remove the entry from the file to enable editing here."
}, },
"icon": { "icon": {
"change": "change", "change": "change",
@ -390,8 +406,7 @@
"unconfigured": "not configured", "unconfigured": "not configured",
"vehicles": "My Vehicles", "vehicles": "My Vehicles",
"welcomeBannerText": "Start with creating at least one **charger**, **heater**, **grid**, **solar**, **battery** or **additional meter**. If you just want to test, pick a **demo device**.", "welcomeBannerText": "Start with creating at least one **charger**, **heater**, **grid**, **solar**, **battery** or **additional meter**. If you just want to test, pick a **demo device**.",
"welcomeBannerTitle": "Let's configure your system!", "welcomeBannerTitle": "Let's configure your system!"
"yaml": "Devices from evcc.yaml are not editable."
}, },
"messaging": { "messaging": {
"description": "Receive messages about your charging sessions.", "description": "Receive messages about your charging sessions.",
@ -543,6 +558,7 @@
"integrations": "Integrations", "integrations": "Integrations",
"loadpoints": "Charging & Heating", "loadpoints": "Charging & Heating",
"meter": "Solar & Battery", "meter": "Solar & Battery",
"services": "Services",
"system": "System", "system": "System",
"vehicles": "Vehicles" "vehicles": "Vehicles"
}, },

View file

@ -209,6 +209,7 @@
"dragList": "Lista reordenable", "dragList": "Lista reordenable",
"experimental": "Experimental", "experimental": "Experimental",
"forceSave": "Guardar de todos modos", "forceSave": "Guardar de todos modos",
"fromYamlHint": "Nota: Configurado a través de evcc.yaml. Elimina la entrada del archivo para poder editar aquí.",
"hideAdvancedSettings": "Ocultar configuración avanzada", "hideAdvancedSettings": "Ocultar configuración avanzada",
"invalidFileSelected": "Se ha seleccionado un archivo no válido", "invalidFileSelected": "Se ha seleccionado un archivo no válido",
"noFileSelected": "No hay ningún archivo seleccionado.", "noFileSelected": "No hay ningún archivo seleccionado.",
@ -247,8 +248,7 @@
"downloadCsv": "Descargar CSV", "downloadCsv": "Descargar CSV",
"eventsRecorded": "Se registraron {count} eventos de limitación de la red.", "eventsRecorded": "Se registraron {count} eventos de limitación de la red.",
"lastEvent": "Más reciente {timeAgo}.", "lastEvent": "Más reciente {timeAgo}.",
"title": "Límite externo", "title": "Límite externo"
"yamlConfigured": "Nota: Configurado a través de evcc.yaml. Elimina la entrada del archivo para poder editar aquí."
}, },
"icon": { "icon": {
"change": "cambio", "change": "cambio",
@ -390,8 +390,7 @@
"unconfigured": "no configurado", "unconfigured": "no configurado",
"vehicles": "Mis vehículos", "vehicles": "Mis vehículos",
"welcomeBannerText": "Empiece por crear al menos un **cargador**, **calentador**, **red**, **solar**, **batería** o **contador adicional**. Si solo desea realizar una prueba, elija un **dispositivo de demostración**.", "welcomeBannerText": "Empiece por crear al menos un **cargador**, **calentador**, **red**, **solar**, **batería** o **contador adicional**. Si solo desea realizar una prueba, elija un **dispositivo de demostración**.",
"welcomeBannerTitle": "¡Configuremos su sistema!", "welcomeBannerTitle": "¡Configuremos su sistema!"
"yaml": "Los dispositivos de evcc.yaml no se pueden editar."
}, },
"messaging": { "messaging": {
"description": "Recibe mensajes sobre tus sesiones de carga.", "description": "Recibe mensajes sobre tus sesiones de carga.",

View file

@ -209,6 +209,7 @@
"dragList": "Järjestettävä lista", "dragList": "Järjestettävä lista",
"experimental": "Kokeellinen", "experimental": "Kokeellinen",
"forceSave": "Tallenna joka tapauksessa", "forceSave": "Tallenna joka tapauksessa",
"fromYamlHint": "Huomio: Asetus on evcc.yaml-tiedostosta. Poista asetus tiedostosta, jotta voisit muokata sitä täällä.",
"hideAdvancedSettings": "Piilota edistyneet asetukset", "hideAdvancedSettings": "Piilota edistyneet asetukset",
"invalidFileSelected": "Valittu virheellinen tiedosto", "invalidFileSelected": "Valittu virheellinen tiedosto",
"noFileSelected": "Ei valittua tiedostoa.", "noFileSelected": "Ei valittua tiedostoa.",
@ -247,8 +248,7 @@
"downloadCsv": "Lataa CSV-taulukko", "downloadCsv": "Lataa CSV-taulukko",
"eventsRecorded": "Tallennettu {count} verkon rajoitustapahtumaa.", "eventsRecorded": "Tallennettu {count} verkon rajoitustapahtumaa.",
"lastEvent": "Viimeisin {timeAgo}.", "lastEvent": "Viimeisin {timeAgo}.",
"title": "Ulkoinen raja", "title": "Ulkoinen raja"
"yamlConfigured": "Huomio: Asetus on evcc.yaml-tiedostosta. Poista asetus tiedostosta, jotta voisit muokata sitä täällä."
}, },
"icon": { "icon": {
"change": "vaihda", "change": "vaihda",
@ -390,8 +390,7 @@
"unconfigured": "ei määritetty", "unconfigured": "ei määritetty",
"vehicles": "Minun ajoneuvot", "vehicles": "Minun ajoneuvot",
"welcomeBannerText": "Aloita luomalla vähintään yksi **laturi**, **lämmitin**, **sähköverkko**, **aurinkopaneeli**, **akku** tai **lisämittari**. Jos haluat vain testata, valitse **esittelylaite**.", "welcomeBannerText": "Aloita luomalla vähintään yksi **laturi**, **lämmitin**, **sähköverkko**, **aurinkopaneeli**, **akku** tai **lisämittari**. Jos haluat vain testata, valitse **esittelylaite**.",
"welcomeBannerTitle": "Määritetään järjestelmäsi!", "welcomeBannerTitle": "Määritetään järjestelmäsi!"
"yaml": "evcc.yaml-asetuksissa määritettyjä laitteita ei voi muokata."
}, },
"messaging": { "messaging": {
"description": "Vastaanota viestejä koskien lataustapahtumiasi.", "description": "Vastaanota viestejä koskien lataustapahtumiasi.",

View file

@ -209,6 +209,7 @@
"dragList": "Liste à ordonner", "dragList": "Liste à ordonner",
"experimental": "Expérimental", "experimental": "Expérimental",
"forceSave": "Enregistrer quand même", "forceSave": "Enregistrer quand même",
"fromYamlHint": "Remarque : configuré via evcc.yaml. Supprimez l'entrée du fichier pour pouvoir effectuer des modifications ici.",
"hideAdvancedSettings": "Masquer les paramètres avancés", "hideAdvancedSettings": "Masquer les paramètres avancés",
"invalidFileSelected": "Fichier sélectionné non valide", "invalidFileSelected": "Fichier sélectionné non valide",
"noFileSelected": "Aucun fichier sélectionné.", "noFileSelected": "Aucun fichier sélectionné.",
@ -247,8 +248,7 @@
"downloadCsv": "Télécharger CSV", "downloadCsv": "Télécharger CSV",
"eventsRecorded": "Enregistrement de {count} événements liés à la limitation de la grille.", "eventsRecorded": "Enregistrement de {count} événements liés à la limitation de la grille.",
"lastEvent": "Le plus récent {timeAgo}.", "lastEvent": "Le plus récent {timeAgo}.",
"title": "Limite externe", "title": "Limite externe"
"yamlConfigured": "Remarque : configuré via evcc.yaml. Supprimez l'entrée du fichier pour pouvoir effectuer des modifications ici."
}, },
"icon": { "icon": {
"change": "changer", "change": "changer",
@ -390,8 +390,7 @@
"unconfigured": "non configuré", "unconfigured": "non configuré",
"vehicles": "Mes véhicules", "vehicles": "Mes véhicules",
"welcomeBannerText": "Commencez par créer au moins un **chargeur**, un **chauffage**, un **réseau**, un **système solaire**, une **batterie** ou un **compteur supplémentaire**. Si vous souhaitez simplement tester, choisissez un **appareil de démonstration**.", "welcomeBannerText": "Commencez par créer au moins un **chargeur**, un **chauffage**, un **réseau**, un **système solaire**, une **batterie** ou un **compteur supplémentaire**. Si vous souhaitez simplement tester, choisissez un **appareil de démonstration**.",
"welcomeBannerTitle": "Configurons votre système !", "welcomeBannerTitle": "Configurons votre système !"
"yaml": "Un appareil provenant du evcc.yaml n’est pas modifiable."
}, },
"messaging": { "messaging": {
"description": "Recevoir des messages à propos de vos sessions de charge.", "description": "Recevoir des messages à propos de vos sessions de charge.",

View file

@ -209,6 +209,7 @@
"dragList": "Preurediv popis", "dragList": "Preurediv popis",
"experimental": "Eksperimentalno", "experimental": "Eksperimentalno",
"forceSave": "Svejedno spremi", "forceSave": "Svejedno spremi",
"fromYamlHint": "Napomena: Konfigurirano putem evcc.yaml. Uklonite unos iz datoteke kako biste ovdje omogućili uređivanje.",
"hideAdvancedSettings": "Sakrij napredne postavke", "hideAdvancedSettings": "Sakrij napredne postavke",
"invalidFileSelected": "Neispravna datoteka", "invalidFileSelected": "Neispravna datoteka",
"noFileSelected": "Datoteka nije odabrana.", "noFileSelected": "Datoteka nije odabrana.",
@ -247,8 +248,7 @@
"downloadCsv": "Preuzmi CSV", "downloadCsv": "Preuzmi CSV",
"eventsRecorded": "Zabilježeno je {count} događaja ograničenja mreže.", "eventsRecorded": "Zabilježeno je {count} događaja ograničenja mreže.",
"lastEvent": "Najnoviji {timeAgo}", "lastEvent": "Najnoviji {timeAgo}",
"title": "Vanjski limit", "title": "Vanjski limit"
"yamlConfigured": "Napomena: Konfigurirano putem evcc.yaml. Uklonite unos iz datoteke kako biste ovdje omogućili uređivanje."
}, },
"icon": { "icon": {
"change": "promjeni", "change": "promjeni",
@ -390,8 +390,7 @@
"unconfigured": "nije postavljeno", "unconfigured": "nije postavljeno",
"vehicles": "Moja vozila", "vehicles": "Moja vozila",
"welcomeBannerText": "Počnite s izradom barem jednog **punjača**, **grijača**, **mreže**, **solarne ploče**, **baterije** ili **dodatnog brojila**. Ako samo želite testirati, odaberite **demo uređaj**.", "welcomeBannerText": "Počnite s izradom barem jednog **punjača**, **grijača**, **mreže**, **solarne ploče**, **baterije** ili **dodatnog brojila**. Ako samo želite testirati, odaberite **demo uređaj**.",
"welcomeBannerTitle": "Konfigurirajmo vaš sustav!", "welcomeBannerTitle": "Konfigurirajmo vaš sustav!"
"yaml": "U evcc.yaml datoteci definirani uređaji se ne mogu uređivati."
}, },
"messaging": { "messaging": {
"description": "Primajte poruke o svojim sesijama punjenja.", "description": "Primajte poruke o svojim sesijama punjenja.",

View file

@ -209,6 +209,7 @@
"dragList": "Átrendezhető lista", "dragList": "Átrendezhető lista",
"experimental": "Kísérleti", "experimental": "Kísérleti",
"forceSave": "Mentés mindenképpen", "forceSave": "Mentés mindenképpen",
"fromYamlHint": "Megjegyzés: Az evcc.yaml fájlban konfigurálható. A szerkesztés engedélyezéséhez távolítsa el a bejegyzést a fájlból.",
"hideAdvancedSettings": "Speciális beállítások elrejtése", "hideAdvancedSettings": "Speciális beállítások elrejtése",
"invalidFileSelected": "Érvénytelen fájl kiválasztva", "invalidFileSelected": "Érvénytelen fájl kiválasztva",
"noFileSelected": "Nincs fájl kiválasztva.", "noFileSelected": "Nincs fájl kiválasztva.",
@ -247,8 +248,7 @@
"downloadCsv": "CSV letöltése", "downloadCsv": "CSV letöltése",
"eventsRecorded": "{count} rácskorlátozási esemény rögzítve.", "eventsRecorded": "{count} rácskorlátozási esemény rögzítve.",
"lastEvent": "Legutóbbi {timeAgo}.", "lastEvent": "Legutóbbi {timeAgo}.",
"title": "Külső határ", "title": "Külső határ"
"yamlConfigured": "Megjegyzés: Az evcc.yaml fájlban konfigurálható. A szerkesztés engedélyezéséhez távolítsa el a bejegyzést a fájlból."
}, },
"icon": { "icon": {
"change": "változtatás", "change": "változtatás",
@ -390,8 +390,7 @@
"unconfigured": "nincs konfigurálva", "unconfigured": "nincs konfigurálva",
"vehicles": "Járműveim", "vehicles": "Járműveim",
"welcomeBannerText": "Kezdje azzal, hogy létrehoz legalább egy **töltőt**, **fűtőt**, **hálózatot**, **napelemet**, **akkumulátort** vagy **kiegészítő mérőt**. Ha csak tesztelni szeretne, válasszon egy **demo eszközt**.", "welcomeBannerText": "Kezdje azzal, hogy létrehoz legalább egy **töltőt**, **fűtőt**, **hálózatot**, **napelemet**, **akkumulátort** vagy **kiegészítő mérőt**. Ha csak tesztelni szeretne, válasszon egy **demo eszközt**.",
"welcomeBannerTitle": "Konfiguráljuk a rendszerét!", "welcomeBannerTitle": "Konfiguráljuk a rendszerét!"
"yaml": "Az evcc.yaml fájlban szereplő eszközök nem szerkeszthetők."
}, },
"messaging": { "messaging": {
"description": "Értesítési üzenetek beállítása a töltési folyamatokról.", "description": "Értesítési üzenetek beállítása a töltési folyamatokról.",

View file

@ -209,6 +209,7 @@
"dragList": "Elenco riordinabile", "dragList": "Elenco riordinabile",
"experimental": "Sperimentale", "experimental": "Sperimentale",
"forceSave": "Salva comunque", "forceSave": "Salva comunque",
"fromYamlHint": "Nota: configurato tramite evcc.yaml. Rimuovere la voce dal file per abilitare la modifica qui.",
"hideAdvancedSettings": "Nascondi impostazioni avanzate", "hideAdvancedSettings": "Nascondi impostazioni avanzate",
"invalidFileSelected": "File non valido selezionato", "invalidFileSelected": "File non valido selezionato",
"noFileSelected": "Nessun file selezionato.", "noFileSelected": "Nessun file selezionato.",
@ -247,8 +248,7 @@
"downloadCsv": "Scarica CSV", "downloadCsv": "Scarica CSV",
"eventsRecorded": "Registrati {count} eventi di limitazione della griglia.", "eventsRecorded": "Registrati {count} eventi di limitazione della griglia.",
"lastEvent": "Più recente {timeAgo}.", "lastEvent": "Più recente {timeAgo}.",
"title": "HEMSLimite esterno", "title": "HEMSLimite esterno"
"yamlConfigured": "Nota: configurato tramite evcc.yaml. Rimuovere la voce dal file per abilitare la modifica qui."
}, },
"icon": { "icon": {
"change": "cambiamento", "change": "cambiamento",
@ -390,8 +390,7 @@
"unconfigured": "non configurato", "unconfigured": "non configurato",
"vehicles": "I miei veicoli", "vehicles": "I miei veicoli",
"welcomeBannerText": "Inizia creando almeno un **caricabatterie**, un **riscaldatore**, una **rete**, un **impianto solare**, una **batteria** o un **contatore aggiuntivo**. Se desideri solo effettuare un test, scegli un **dispositivo demo**.", "welcomeBannerText": "Inizia creando almeno un **caricabatterie**, un **riscaldatore**, una **rete**, un **impianto solare**, una **batteria** o un **contatore aggiuntivo**. Se desideri solo effettuare un test, scegli un **dispositivo demo**.",
"welcomeBannerTitle": "Configuriamo il tuo sistema!", "welcomeBannerTitle": "Configuriamo il tuo sistema!"
"yaml": "I dispositivi di evcc.yaml non sono modificabili."
}, },
"messaging": { "messaging": {
"description": "Ricevi messaggi sulle sessioni di ricarica.", "description": "Ricevi messaggi sulle sessioni di ricarica.",

View file

@ -209,6 +209,7 @@
"dragList": "Sortéirbar Lëscht", "dragList": "Sortéirbar Lëscht",
"experimental": "Experimentell", "experimental": "Experimentell",
"forceSave": "Trotzdem späicheren", "forceSave": "Trotzdem späicheren",
"fromYamlHint": "Bemierkung: Konfiguréiert iwwer evcc.yaml. Läscht d'Astellungen aus der Datei fir d'Ännerung hei z'erméiglechen.",
"hideAdvancedSettings": "Fortgeschratten Astellunge verstoppen", "hideAdvancedSettings": "Fortgeschratten Astellunge verstoppen",
"invalidFileSelected": "Ongülteg Datei ausgewielt", "invalidFileSelected": "Ongülteg Datei ausgewielt",
"noFileSelected": "Keng Datei ausgewielt.", "noFileSelected": "Keng Datei ausgewielt.",
@ -247,8 +248,7 @@
"downloadCsv": "CSV eroflueden", "downloadCsv": "CSV eroflueden",
"eventsRecorded": "{count} gespäichert Limitatiounen vum Netz.", "eventsRecorded": "{count} gespäichert Limitatiounen vum Netz.",
"lastEvent": "Méi rezent {timeAgo}.", "lastEvent": "Méi rezent {timeAgo}.",
"title": "Äusserlech Grenz", "title": "Äusserlech Grenz"
"yamlConfigured": "Bemierkung: Konfiguréiert iwwer evcc.yaml. Läscht d'Astellungen aus der Datei fir d'Ännerung hei z'erméiglechen."
}, },
"icon": { "icon": {
"change": "änneren", "change": "änneren",
@ -390,8 +390,7 @@
"unconfigured": "net konfiguréiert", "unconfigured": "net konfiguréiert",
"vehicles": "Meng Gefierer", "vehicles": "Meng Gefierer",
"welcomeBannerText": "Fänkt un mat der Erstelle vun op d'mannst engem **Ladegerät**, **Heizer**, **Netz**, **Solarpanel**, **Batterie** oder **zousätzlechen Zähler**. Wann Dir just testen wëllt, wielt e **Demonstratiounsapparat**.", "welcomeBannerText": "Fänkt un mat der Erstelle vun op d'mannst engem **Ladegerät**, **Heizer**, **Netz**, **Solarpanel**, **Batterie** oder **zousätzlechen Zähler**. Wann Dir just testen wëllt, wielt e **Demonstratiounsapparat**.",
"welcomeBannerTitle": "Loosst eis de System konfiguréieren!", "welcomeBannerTitle": "Loosst eis de System konfiguréieren!"
"yaml": "Geräter aus der evcc.yaml Datei si net editiérbar."
}, },
"messaging": { "messaging": {
"description": "Notifikatiounen iwwer Är Opluedsessioune kréien.", "description": "Notifikatiounen iwwer Är Opluedsessioune kréien.",

View file

@ -209,6 +209,7 @@
"dragList": "Galima sukeisti vietomis", "dragList": "Galima sukeisti vietomis",
"experimental": "Eksperimentinis", "experimental": "Eksperimentinis",
"forceSave": "Vistiek įrašyti", "forceSave": "Vistiek įrašyti",
"fromYamlHint": "Pastaba: Sukonfigūruota naudojant evcc.yaml. Pašalinkite įrašą iš failo, kad būtų galima redaguoti čia.",
"hideAdvancedSettings": "Slėpti išplėstinius nustatymus", "hideAdvancedSettings": "Slėpti išplėstinius nustatymus",
"invalidFileSelected": "Pasirinktas netinkamas failas", "invalidFileSelected": "Pasirinktas netinkamas failas",
"noFileSelected": "Nepasirinktas failas.", "noFileSelected": "Nepasirinktas failas.",
@ -247,8 +248,7 @@
"downloadCsv": "Atsisiųsti CSV failą", "downloadCsv": "Atsisiųsti CSV failą",
"eventsRecorded": "Užfiksuota {count} tinklo galios apribojimo įvykių.", "eventsRecorded": "Užfiksuota {count} tinklo galios apribojimo įvykių.",
"lastEvent": "Naujausias {timeAgo}.", "lastEvent": "Naujausias {timeAgo}.",
"title": "Išorinė riba", "title": "Išorinė riba"
"yamlConfigured": "Pastaba: Sukonfigūruota naudojant evcc.yaml. Pašalinkite įrašą iš failo, kad būtų galima redaguoti čia."
}, },
"icon": { "icon": {
"change": "pakeisti", "change": "pakeisti",
@ -390,8 +390,7 @@
"unconfigured": "nesukonfigūruota", "unconfigured": "nesukonfigūruota",
"vehicles": "Mano automobiliai", "vehicles": "Mano automobiliai",
"welcomeBannerText": "Pradžiai sukonfigūruokite bent vieną **įkroviklį**, **šildytuvą**, **tinklo skaitiklį**, **saulės jėgainę** ar **papildomą skaitiklį**. Jei norite tik pasibandyti, pasirinkite **demonstracinį įrenginį**.", "welcomeBannerText": "Pradžiai sukonfigūruokite bent vieną **įkroviklį**, **šildytuvą**, **tinklo skaitiklį**, **saulės jėgainę** ar **papildomą skaitiklį**. Jei norite tik pasibandyti, pasirinkite **demonstracinį įrenginį**.",
"welcomeBannerTitle": "Sukonfigūruokime jūsų sistemą!", "welcomeBannerTitle": "Sukonfigūruokime jūsų sistemą!"
"yaml": "Įtaisas iš evcc.yaml , čia konfigūruoti neina."
}, },
"messaging": { "messaging": {
"description": "Gaukite pranešimus apie įkrovimo sesijas.", "description": "Gaukite pranešimus apie įkrovimo sesijas.",

View file

@ -209,6 +209,7 @@
"dragList": "Sorteerbare lijst", "dragList": "Sorteerbare lijst",
"experimental": "Experimenteel", "experimental": "Experimenteel",
"forceSave": "Toch opslaan", "forceSave": "Toch opslaan",
"fromYamlHint": "Opmerking: Geconfigureerd via evcc.yaml. Verwijder de betreffende regels uit het bestand om hier bewerkingen mogelijk te maken.",
"hideAdvancedSettings": "Verberg geavanceerde instellingen", "hideAdvancedSettings": "Verberg geavanceerde instellingen",
"invalidFileSelected": "Ongeldig bestand geselecteerd", "invalidFileSelected": "Ongeldig bestand geselecteerd",
"noFileSelected": "Geen bestand geselecteerd.", "noFileSelected": "Geen bestand geselecteerd.",
@ -247,8 +248,7 @@
"downloadCsv": "Download CSV", "downloadCsv": "Download CSV",
"eventsRecorded": "{count} gebeurtenissen met rasterbeperkingen geregistreerd.", "eventsRecorded": "{count} gebeurtenissen met rasterbeperkingen geregistreerd.",
"lastEvent": "Meest recent {timeAgo}.", "lastEvent": "Meest recent {timeAgo}.",
"title": "Externe limiet", "title": "Externe limiet"
"yamlConfigured": "Opmerking: Geconfigureerd via evcc.yaml. Verwijder de betreffende regels uit het bestand om hier bewerkingen mogelijk te maken."
}, },
"icon": { "icon": {
"change": "wijzig", "change": "wijzig",
@ -390,8 +390,7 @@
"unconfigured": "Niet geconfigureerd", "unconfigured": "Niet geconfigureerd",
"vehicles": "Mijn voertuigen", "vehicles": "Mijn voertuigen",
"welcomeBannerText": "Begin met het aanmaken van minstens één **oplader**, **verwarmer**, **netstroom**, **zonnepanelen**, **thuisbatterij** of **extra meter**. Als je gewoon wilt testen, kies dan een **demo-apparaat**.", "welcomeBannerText": "Begin met het aanmaken van minstens één **oplader**, **verwarmer**, **netstroom**, **zonnepanelen**, **thuisbatterij** of **extra meter**. Als je gewoon wilt testen, kies dan een **demo-apparaat**.",
"welcomeBannerTitle": "Start met het configureren van uw systeem!", "welcomeBannerTitle": "Start met het configureren van uw systeem!"
"yaml": "Apparaten geconfigureerd in evcc.yaml kunnen hier niet worden bewerkt."
}, },
"messaging": { "messaging": {
"description": "Ontvang berichten over je oplaadsessies.", "description": "Ontvang berichten over je oplaadsessies.",

View file

@ -209,6 +209,7 @@
"dragList": "Omordningsbar liste", "dragList": "Omordningsbar liste",
"experimental": "Eksperimentell", "experimental": "Eksperimentell",
"forceSave": "Lagre likevel", "forceSave": "Lagre likevel",
"fromYamlHint": "Merk: Konfigurert via evcc.yaml. Fjern oppføringen fra filen for å aktivere redigering her.",
"hideAdvancedSettings": "Skjul avanserte innstillinger", "hideAdvancedSettings": "Skjul avanserte innstillinger",
"invalidFileSelected": "Ugyldig fil valgt", "invalidFileSelected": "Ugyldig fil valgt",
"noFileSelected": "Ingen fil valgt.", "noFileSelected": "Ingen fil valgt.",
@ -247,8 +248,7 @@
"downloadCsv": "Last ned CSV", "downloadCsv": "Last ned CSV",
"eventsRecorded": "Registrerte {count} hendelser med rutenettbegrensning.", "eventsRecorded": "Registrerte {count} hendelser med rutenettbegrensning.",
"lastEvent": "Siste {timeAgo}.", "lastEvent": "Siste {timeAgo}.",
"title": "Ekstern grense", "title": "Ekstern grense"
"yamlConfigured": "Merk: Konfigurert via evcc.yaml. Fjern oppføringen fra filen for å aktivere redigering her."
}, },
"icon": { "icon": {
"change": "endring", "change": "endring",
@ -390,8 +390,7 @@
"unconfigured": "ikke konfigurert", "unconfigured": "ikke konfigurert",
"vehicles": "Mine kjøretøy", "vehicles": "Mine kjøretøy",
"welcomeBannerText": "Begynn med å opprette minst én **lader**, **varmeapparat**, **nett**, **solcelle**, **batteri** eller **ekstra måler**. Hvis du bare vil teste, velger du en **demoenhet**.", "welcomeBannerText": "Begynn med å opprette minst én **lader**, **varmeapparat**, **nett**, **solcelle**, **batteri** eller **ekstra måler**. Hvis du bare vil teste, velger du en **demoenhet**.",
"welcomeBannerTitle": "La oss konfigurere systemet ditt!", "welcomeBannerTitle": "La oss konfigurere systemet ditt!"
"yaml": "Enheter fra evcc.yaml kan ikke redigeres."
}, },
"messaging": { "messaging": {
"description": "Motta meldinger om ladingsøktene dine.", "description": "Motta meldinger om ladingsøktene dine.",

View file

@ -209,6 +209,7 @@
"dragList": "Lista do ponownego zamówienia", "dragList": "Lista do ponownego zamówienia",
"experimental": "Eksperymentalny", "experimental": "Eksperymentalny",
"forceSave": "Zapisz mimo to", "forceSave": "Zapisz mimo to",
"fromYamlHint": "Uwaga: Konfiguracja odbywa się za pomocą pliku evcc.yaml. Aby umożliwić edycję w tym miejscu, usuń wpis z pliku.",
"hideAdvancedSettings": "Ukryj ustawienia zaawansowane", "hideAdvancedSettings": "Ukryj ustawienia zaawansowane",
"invalidFileSelected": "Wybrano nieprawidłowy plik", "invalidFileSelected": "Wybrano nieprawidłowy plik",
"noFileSelected": "Nie wybrano pliku.", "noFileSelected": "Nie wybrano pliku.",
@ -247,8 +248,7 @@
"downloadCsv": "Pobierz CSV", "downloadCsv": "Pobierz CSV",
"eventsRecorded": "Zarejestrowano {count} zdarzeń związanych z ograniczeniami sieci.", "eventsRecorded": "Zarejestrowano {count} zdarzeń związanych z ograniczeniami sieci.",
"lastEvent": "Najnowsze {timeAgo}.", "lastEvent": "Najnowsze {timeAgo}.",
"title": "Limit zewnętrzny", "title": "Limit zewnętrzny"
"yamlConfigured": "Uwaga: Konfiguracja odbywa się za pomocą pliku evcc.yaml. Aby umożliwić edycję w tym miejscu, usuń wpis z pliku."
}, },
"icon": { "icon": {
"change": "zmień", "change": "zmień",
@ -390,8 +390,7 @@
"unconfigured": "nie skonfigurowane", "unconfigured": "nie skonfigurowane",
"vehicles": "Moje pojazdy", "vehicles": "Moje pojazdy",
"welcomeBannerText": "Zacznij od utworzenia co najmniej jednej **ładowarki**, **grzejnika**, **sieci**, **panelu słonecznego**, **baterii** lub **dodatkowego licznika**. Jeśli chcesz tylko przetestować, wybierz **urządzenie demonstracyjne**.", "welcomeBannerText": "Zacznij od utworzenia co najmniej jednej **ładowarki**, **grzejnika**, **sieci**, **panelu słonecznego**, **baterii** lub **dodatkowego licznika**. Jeśli chcesz tylko przetestować, wybierz **urządzenie demonstracyjne**.",
"welcomeBannerTitle": "Skonfigurujmy Twój system!", "welcomeBannerTitle": "Skonfigurujmy Twój system!"
"yaml": "Urządzenia z pliku evcc.yaml nie mogą być edytowane."
}, },
"messaging": { "messaging": {
"description": "Otrzymuj wiadomości o sesjach ładowania.", "description": "Otrzymuj wiadomości o sesjach ładowania.",

View file

@ -209,6 +209,7 @@
"dragList": "Lista reordenável", "dragList": "Lista reordenável",
"experimental": "Experimental", "experimental": "Experimental",
"forceSave": "Guardar na mesma", "forceSave": "Guardar na mesma",
"fromYamlHint": "Nota: Configurado através do evcc.yaml. Remova a entrada do ficheiro para permitir a edição aqui.",
"hideAdvancedSettings": "Ocultar definições avançadas", "hideAdvancedSettings": "Ocultar definições avançadas",
"invalidFileSelected": "Ficheiro selecionado inválido", "invalidFileSelected": "Ficheiro selecionado inválido",
"noFileSelected": "Nenhum ficheiro selecionado.", "noFileSelected": "Nenhum ficheiro selecionado.",
@ -247,8 +248,7 @@
"downloadCsv": "Descarregar CSV", "downloadCsv": "Descarregar CSV",
"eventsRecorded": "Registou {count} eventos de limitação da rede.", "eventsRecorded": "Registou {count} eventos de limitação da rede.",
"lastEvent": "Mais recente {timeAgo}.", "lastEvent": "Mais recente {timeAgo}.",
"title": "Limite externo", "title": "Limite externo"
"yamlConfigured": "Nota: Configurado através do evcc.yaml. Remova a entrada do ficheiro para permitir a edição aqui."
}, },
"icon": { "icon": {
"change": "alterar", "change": "alterar",
@ -390,8 +390,7 @@
"unconfigured": "não configurado", "unconfigured": "não configurado",
"vehicles": "Os meus Veículos", "vehicles": "Os meus Veículos",
"welcomeBannerText": "Comece por criar pelo menos um **carregador**, **aquecedor**, **rede**, **solar**, **bateria** ou **medidor adicional**. Se quiser apenas testar, escolha um **dispositivo de demonstração**.", "welcomeBannerText": "Comece por criar pelo menos um **carregador**, **aquecedor**, **rede**, **solar**, **bateria** ou **medidor adicional**. Se quiser apenas testar, escolha um **dispositivo de demonstração**.",
"welcomeBannerTitle": "Configuremos agora o seu sistema!", "welcomeBannerTitle": "Configuremos agora o seu sistema!"
"yaml": "Os dispositivos de evcc.yaml não são editáveis."
}, },
"messaging": { "messaging": {
"description": "Receber mensagens sobre as sessões de carregamento.", "description": "Receber mensagens sobre as sessões de carregamento.",

View file

@ -209,6 +209,7 @@
"dragList": "Listă reordonabilă", "dragList": "Listă reordonabilă",
"experimental": "Experimental", "experimental": "Experimental",
"forceSave": "Salvați oricum", "forceSave": "Salvați oricum",
"fromYamlHint": "Notă: Configurat prin evcc.yaml. Ștergeți intrarea din fișier pentru a activa editarea aici.",
"hideAdvancedSettings": "Ascunde setările avansate", "hideAdvancedSettings": "Ascunde setările avansate",
"invalidFileSelected": "Fișier invalid selectat", "invalidFileSelected": "Fișier invalid selectat",
"noFileSelected": "Nu a fost selectat niciun fișier.", "noFileSelected": "Nu a fost selectat niciun fișier.",
@ -247,8 +248,7 @@
"downloadCsv": "Descărcați CSV", "downloadCsv": "Descărcați CSV",
"eventsRecorded": "Au fost înregistrate {count} evenimente de limitare a grilei.", "eventsRecorded": "Au fost înregistrate {count} evenimente de limitare a grilei.",
"lastEvent": "Cel mai recent {timeAgo}.", "lastEvent": "Cel mai recent {timeAgo}.",
"title": "Limită externă", "title": "Limită externă"
"yamlConfigured": "Notă: Configurat prin evcc.yaml. Ștergeți intrarea din fișier pentru a activa editarea aici."
}, },
"icon": { "icon": {
"change": "schimbare", "change": "schimbare",
@ -390,8 +390,7 @@
"unconfigured": "neconfigurat", "unconfigured": "neconfigurat",
"vehicles": "Masinile mele", "vehicles": "Masinile mele",
"welcomeBannerText": "Începeți prin a crea cel puțin un **încărcător**, **încălzitor**, **rețea**, **sistem solar**, **baterie** sau **contor suplimentar**. Dacă doriți doar să testați, alegeți un **dispozitiv demo**.", "welcomeBannerText": "Începeți prin a crea cel puțin un **încărcător**, **încălzitor**, **rețea**, **sistem solar**, **baterie** sau **contor suplimentar**. Dacă doriți doar să testați, alegeți un **dispozitiv demo**.",
"welcomeBannerTitle": "Să configurăm sistemul tău!", "welcomeBannerTitle": "Să configurăm sistemul tău!"
"yaml": "Dispozitivele din evcc.yaml nu pot fi editate."
}, },
"messaging": { "messaging": {
"description": "Primiți mesaje despre sesiunile dvs. de încărcare.", "description": "Primiți mesaje despre sesiunile dvs. de încărcare.",

View file

@ -209,6 +209,7 @@
"dragList": "Список, который можно переупорядочить", "dragList": "Список, который можно переупорядочить",
"experimental": "Экспериментальный", "experimental": "Экспериментальный",
"forceSave": "Сохранить в любом случае", "forceSave": "Сохранить в любом случае",
"fromYamlHint": "Примечание: Настраивается через evcc.yaml. Удалите запись из файла, чтобы включить редактирование здесь.",
"hideAdvancedSettings": "Скрыть расширенные настройки", "hideAdvancedSettings": "Скрыть расширенные настройки",
"invalidFileSelected": "Выбран недействительный файл", "invalidFileSelected": "Выбран недействительный файл",
"noFileSelected": "Файл не выбран.", "noFileSelected": "Файл не выбран.",
@ -247,8 +248,7 @@
"downloadCsv": "Скачать CSV", "downloadCsv": "Скачать CSV",
"eventsRecorded": "Зарегистрировано {count} событий ограничения сетки.", "eventsRecorded": "Зарегистрировано {count} событий ограничения сетки.",
"lastEvent": "Последнее {timeAgo}.", "lastEvent": "Последнее {timeAgo}.",
"title": "Внешний предел", "title": "Внешний предел"
"yamlConfigured": "Примечание: Настраивается через evcc.yaml. Удалите запись из файла, чтобы включить редактирование здесь."
}, },
"icon": { "icon": {
"change": "изменение", "change": "изменение",
@ -390,8 +390,7 @@
"unconfigured": "не настроен", "unconfigured": "не настроен",
"vehicles": "Мои транспортные средства", "vehicles": "Мои транспортные средства",
"welcomeBannerText": "Начните с создания хотя бы одного **зарядного устройства**, **нагревателя**, **сети**, **солнечной батареи**, **аккумулятора** или **дополнительного счетчика**. Если вы просто хотите протестировать, выберите **демонстрационное устройство**.", "welcomeBannerText": "Начните с создания хотя бы одного **зарядного устройства**, **нагревателя**, **сети**, **солнечной батареи**, **аккумулятора** или **дополнительного счетчика**. Если вы просто хотите протестировать, выберите **демонстрационное устройство**.",
"welcomeBannerTitle": "Давайте настроим вашу систему!", "welcomeBannerTitle": "Давайте настроим вашу систему!"
"yaml": "Устройства из evcc.yaml не подлежат редактированию."
}, },
"messaging": { "messaging": {
"description": "Получайте сообщения о ваших сеансах зарядки.", "description": "Получайте сообщения о ваших сеансах зарядки.",

View file

@ -209,6 +209,7 @@
"dragList": "Seznam, ki ga je mogoče prerazporediti", "dragList": "Seznam, ki ga je mogoče prerazporediti",
"experimental": "Eksperimentalno", "experimental": "Eksperimentalno",
"forceSave": "Vseeno shrani", "forceSave": "Vseeno shrani",
"fromYamlHint": "Opomba: Konfigurirano prek evcc.yaml. Odstranite vnos iz datoteke, da omogočite urejanje tukaj.",
"hideAdvancedSettings": "Skrij napredne nastavitve", "hideAdvancedSettings": "Skrij napredne nastavitve",
"invalidFileSelected": "Izbrana neveljavna datoteka", "invalidFileSelected": "Izbrana neveljavna datoteka",
"noFileSelected": "Izbrana ni nobena datoteka.", "noFileSelected": "Izbrana ni nobena datoteka.",
@ -247,8 +248,7 @@
"downloadCsv": "Prenesi CSV", "downloadCsv": "Prenesi CSV",
"eventsRecorded": "Zabeleženo {count} dogodkov omejitve omrežja.", "eventsRecorded": "Zabeleženo {count} dogodkov omejitve omrežja.",
"lastEvent": "Najnovejše {timeAgo}.", "lastEvent": "Najnovejše {timeAgo}.",
"title": "Zunanja meja", "title": "Zunanja meja"
"yamlConfigured": "Opomba: Konfigurirano prek evcc.yaml. Odstranite vnos iz datoteke, da omogočite urejanje tukaj."
}, },
"icon": { "icon": {
"change": "sprememba", "change": "sprememba",
@ -390,8 +390,7 @@
"unconfigured": "ni konfigurirano", "unconfigured": "ni konfigurirano",
"vehicles": "Moja vozila", "vehicles": "Moja vozila",
"welcomeBannerText": "Začnite z ustvarjanjem vsaj enega **polnilnika**, **grelca**, **omrežja**, **sončne energije**, **baterije** ali **dodatnega merilnika**. Če želite samo preizkusiti, izberite **demo napravo**.", "welcomeBannerText": "Začnite z ustvarjanjem vsaj enega **polnilnika**, **grelca**, **omrežja**, **sončne energije**, **baterije** ali **dodatnega merilnika**. Če želite samo preizkusiti, izberite **demo napravo**.",
"welcomeBannerTitle": "Konfigurirajmo vaš sistem!", "welcomeBannerTitle": "Konfigurirajmo vaš sistem!"
"yaml": "Naprav iz datoteke evcc.yaml ni mogoče urejati."
}, },
"messaging": { "messaging": {
"description": "Prejemanje sporočil o vaših sejah polnjenja.", "description": "Prejemanje sporočil o vaših sejah polnjenja.",

View file

@ -209,6 +209,7 @@
"dragList": "Sorterbar lista", "dragList": "Sorterbar lista",
"experimental": "Experimentella funktioner", "experimental": "Experimentella funktioner",
"forceSave": "Spara ändå", "forceSave": "Spara ändå",
"fromYamlHint": "Obs: Konfigurerad via evcc.yaml. Ta bort posten från filen för att aktivera redigering här.",
"hideAdvancedSettings": "Dölj avancerade inställningar", "hideAdvancedSettings": "Dölj avancerade inställningar",
"invalidFileSelected": "Ogiltig fil vald", "invalidFileSelected": "Ogiltig fil vald",
"noFileSelected": "Ingen fil vald.", "noFileSelected": "Ingen fil vald.",
@ -247,8 +248,7 @@
"downloadCsv": "Hämta CSV", "downloadCsv": "Hämta CSV",
"eventsRecorded": "Registrerade {count} händelser med nätbegränsning.", "eventsRecorded": "Registrerade {count} händelser med nätbegränsning.",
"lastEvent": "Senaste {timeAgo}.", "lastEvent": "Senaste {timeAgo}.",
"title": "Extern gräns", "title": "Extern gräns"
"yamlConfigured": "Obs: Konfigurerad via evcc.yaml. Ta bort posten från filen för att aktivera redigering här."
}, },
"icon": { "icon": {
"change": "ändra", "change": "ändra",
@ -390,8 +390,7 @@
"unconfigured": "ej konfigurerad", "unconfigured": "ej konfigurerad",
"vehicles": "Mina fordon", "vehicles": "Mina fordon",
"welcomeBannerText": "Börja med att skapa minst en **laddare**, **värmare**, **nät**, **solcell**, **batteri** eller **extra mätare**. Om du bara vill testa, välj en **demoenhet**.", "welcomeBannerText": "Börja med att skapa minst en **laddare**, **värmare**, **nät**, **solcell**, **batteri** eller **extra mätare**. Om du bara vill testa, välj en **demoenhet**.",
"welcomeBannerTitle": "Låt oss konfigurera ditt system!", "welcomeBannerTitle": "Låt oss konfigurera ditt system!"
"yaml": "Konfigurera i evcc.yaml. Kan inte ändras i UI."
}, },
"messaging": { "messaging": {
"description": "Få notiser om dina laddningar.", "description": "Få notiser om dina laddningar.",

View file

@ -154,6 +154,7 @@
"delete": "நீக்கு", "delete": "நீக்கு",
"docsLink": "ஆவணங்களைக் காண்க.", "docsLink": "ஆவணங்களைக் காண்க.",
"experimental": "சோதனை", "experimental": "சோதனை",
"fromYamlHint": "Evcc.yaml இல் கட்டமைக்கப்பட்டுள்ளது. இடைமுகம் இல் திருத்த முடியாது.",
"hideAdvancedSettings": "மேம்பட்ட அமைப்புகளை மறைக்க", "hideAdvancedSettings": "மேம்பட்ட அமைப்புகளை மறைக்க",
"invalidFileSelected": "தவறான கோப்பு தேர்ந்தெடுக்கப்பட்டது", "invalidFileSelected": "தவறான கோப்பு தேர்ந்தெடுக்கப்பட்டது",
"noFileSelected": "எந்த கோப்பும் தேர்ந்தெடுக்கப்படவில்லை.", "noFileSelected": "எந்த கோப்பும் தேர்ந்தெடுக்கப்படவில்லை.",
@ -314,8 +315,7 @@
"name": "பெயர்", "name": "பெயர்",
"title": "உள்ளமைவு", "title": "உள்ளமைவு",
"unconfigured": "கட்டமைக்கப்படவில்லை", "unconfigured": "கட்டமைக்கப்படவில்லை",
"vehicles": "என் வாகனங்கள்", "vehicles": "என் வாகனங்கள்"
"yaml": "Evcc.yaml இல் கட்டமைக்கப்பட்டுள்ளது. இடைமுகம் இல் திருத்த முடியாது."
}, },
"messaging": { "messaging": {
"description": "உங்கள் சார்சிங் அமர்வுகள் பற்றிய செய்திகளைப் பெறுங்கள்.", "description": "உங்கள் சார்சிங் அமர்வுகள் பற்றிய செய்திகளைப் பெறுங்கள்.",

View file

@ -209,6 +209,7 @@
"dragList": "Yeniden sıralanabilir liste", "dragList": "Yeniden sıralanabilir liste",
"experimental": "Deneysel", "experimental": "Deneysel",
"forceSave": "Yine de kaydet", "forceSave": "Yine de kaydet",
"fromYamlHint": "Not: evcc.yaml aracılığıyla yapılandırıldı. Burada düzenlemeyi etkinleştirmek için dosyadan öğeyi kaldır.",
"hideAdvancedSettings": "“Gelişmiş ayarları gizle”", "hideAdvancedSettings": "“Gelişmiş ayarları gizle”",
"invalidFileSelected": "Geçersiz dosya seçildi", "invalidFileSelected": "Geçersiz dosya seçildi",
"noFileSelected": "Seçili dosya yok.", "noFileSelected": "Seçili dosya yok.",
@ -247,8 +248,7 @@
"downloadCsv": "CSV'yi indir", "downloadCsv": "CSV'yi indir",
"eventsRecorded": "{count} şebeke sınırlama etkinliği kaydedildi.", "eventsRecorded": "{count} şebeke sınırlama etkinliği kaydedildi.",
"lastEvent": "En son {timeAgo}.", "lastEvent": "En son {timeAgo}.",
"title": "Dış Limit", "title": "Dış Limit"
"yamlConfigured": "Not: evcc.yaml aracılığıyla yapılandırıldı. Burada düzenlemeyi etkinleştirmek için dosyadan öğeyi kaldır."
}, },
"icon": { "icon": {
"change": "değiştir", "change": "değiştir",
@ -390,8 +390,7 @@
"unconfigured": "yapılandırılmadı", "unconfigured": "yapılandırılmadı",
"vehicles": "Araçlarım", "vehicles": "Araçlarım",
"welcomeBannerText": "Önce bir **doldurma cihazı**, **ısıtıcı**, **elektrik sayacı**, **güneş enerjisi**, **batarya** veya **ek sayaç** oluştur. Sadece denemek istiyorsan, bir **demo cihazı** seç.", "welcomeBannerText": "Önce bir **doldurma cihazı**, **ısıtıcı**, **elektrik sayacı**, **güneş enerjisi**, **batarya** veya **ek sayaç** oluştur. Sadece denemek istiyorsan, bir **demo cihazı** seç.",
"welcomeBannerTitle": "Haydi sistemini yapılandıralım!", "welcomeBannerTitle": "Haydi sistemini yapılandıralım!"
"yaml": "evcc.yaml'daki cihazlar düzenlenemez."
}, },
"messaging": { "messaging": {
"description": "Dolum oturumları hakkında bildirimler al.", "description": "Dolum oturumları hakkında bildirimler al.",

View file

@ -209,6 +209,7 @@
"dragList": "Список, який можна перевпорядкувати", "dragList": "Список, який можна перевпорядкувати",
"experimental": "Експериментальний", "experimental": "Експериментальний",
"forceSave": "Зберегти все одно", "forceSave": "Зберегти все одно",
"fromYamlHint": "Примітка: Налаштовано через evcc.yaml. Видаліть запис із файлу, щоб дозволити редагування тут.",
"hideAdvancedSettings": "Приховати розширені налаштування", "hideAdvancedSettings": "Приховати розширені налаштування",
"invalidFileSelected": "Вибрано недійсний файл", "invalidFileSelected": "Вибрано недійсний файл",
"noFileSelected": "Не вибрано файл.", "noFileSelected": "Не вибрано файл.",
@ -247,8 +248,7 @@
"downloadCsv": "Завантажити CSV-файл", "downloadCsv": "Завантажити CSV-файл",
"eventsRecorded": "Записано {count} подій обмеження мережі.", "eventsRecorded": "Записано {count} подій обмеження мережі.",
"lastEvent": "Найновіші {timeAgo}.", "lastEvent": "Найновіші {timeAgo}.",
"title": "Зовнішнє обмеження", "title": "Зовнішнє обмеження"
"yamlConfigured": "Примітка: Налаштовано через evcc.yaml. Видаліть запис із файлу, щоб дозволити редагування тут."
}, },
"icon": { "icon": {
"change": "зміна", "change": "зміна",
@ -390,8 +390,7 @@
"unconfigured": "не налаштовано", "unconfigured": "не налаштовано",
"vehicles": "Мій транспорт", "vehicles": "Мій транспорт",
"welcomeBannerText": "Почніть зі створення принаймні одного **зарядного пристрою**, **обігрівача**, **мережевої**, **сонячної батареї**, **акумулятора** або **додаткового лічильника**. Якщо ви просто хочете протестувати, виберіть **демонстраційний пристрій**.", "welcomeBannerText": "Почніть зі створення принаймні одного **зарядного пристрою**, **обігрівача**, **мережевої**, **сонячної батареї**, **акумулятора** або **додаткового лічильника**. Якщо ви просто хочете протестувати, виберіть **демонстраційний пристрій**.",
"welcomeBannerTitle": "Давайте налаштуємо вашу систему!", "welcomeBannerTitle": "Давайте налаштуємо вашу систему!"
"yaml": "Пристрої з evcc.yaml не можна редагувати."
}, },
"messaging": { "messaging": {
"description": "Отримувати повідомлення про ваші сеанси зарядки.", "description": "Отримувати повідомлення про ваші сеанси зарядки.",

View file

@ -171,6 +171,7 @@
"delete": "删除", "delete": "删除",
"docsLink": "查看文档。", "docsLink": "查看文档。",
"experimental": "实验性", "experimental": "实验性",
"fromYamlHint": "evcc.yaml 中的设备不可编辑。",
"hideAdvancedSettings": "隐藏高级设置", "hideAdvancedSettings": "隐藏高级设置",
"off": "关", "off": "关",
"on": "开", "on": "开",
@ -306,8 +307,7 @@
"name": "名称", "name": "名称",
"title": "配置", "title": "配置",
"unconfigured": "未配置", "unconfigured": "未配置",
"vehicles": "我的车辆", "vehicles": "我的车辆"
"yaml": "evcc.yaml 中的设备不可编辑。"
}, },
"messaging": { "messaging": {
"description": "接收有关您充电会话及其他事件的消息。", "description": "接收有关您充电会话及其他事件的消息。",

View file

@ -4,9 +4,7 @@ import (
"crypto/tls" "crypto/tls"
"errors" "errors"
"fmt" "fmt"
"net"
"slices" "slices"
"strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
@ -34,7 +32,6 @@ import (
"github.com/enbility/spine-go/model" "github.com/enbility/spine-go/model"
"github.com/enbility/spine-go/spine" "github.com/enbility/spine-go/spine"
"github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/machine"
) )
type Device interface { type Device interface {
@ -82,29 +79,33 @@ type EEBus struct {
mux sync.Mutex mux sync.Mutex
log *util.Logger log *util.Logger
Ski string ski string
clients map[string][]Device clients map[string][]Device
} }
var Instance *EEBus var Instance *EEBus
func GetStatus() any {
return struct {
Ski string `json:"ski"`
}{
Ski: Ski(),
}
}
func NewServer(other Config) (*EEBus, error) { func NewServer(other Config) (*EEBus, error) {
cc := Config{ cc := Config{
URI: ":4712", Port: 4712,
} }
if err := mergo.Merge(&cc, other, mergo.WithOverride); err != nil { if err := mergo.Merge(&cc, other, mergo.WithOverride); err != nil {
return nil, err return nil, err
} }
log := util.NewLogger("eebus") serial := cc.ShipID
if serial == "" {
protectedID := machine.ProtectedID("evcc-eebus") serial = createShipID()
serial := fmt.Sprintf("%s-%0x", "EVCC", protectedID[:8])
if len(cc.ShipID) != 0 {
serial = cc.ShipID
} }
certificate, err := tls.X509KeyPair([]byte(cc.Certificate.Public), []byte(cc.Certificate.Private)) certificate, err := tls.X509KeyPair([]byte(cc.Certificate.Public), []byte(cc.Certificate.Private))
@ -112,22 +113,11 @@ func NewServer(other Config) (*EEBus, error) {
return nil, err return nil, err
} }
_, portValue, err := net.SplitHostPort(cc.URI)
if err != nil {
return nil, err
}
port, err := strconv.Atoi(portValue)
if err != nil {
return nil, err
}
// TODO: get the voltage from the site
configuration, err := eebusapi.NewConfiguration( configuration, err := eebusapi.NewConfiguration(
BrandName, BrandName, Model, serial, BrandName, BrandName, Model, serial,
model.DeviceTypeTypeEnergyManagementSystem, model.DeviceTypeTypeEnergyManagementSystem,
[]model.EntityTypeType{model.EntityTypeTypeCEM}, []model.EntityTypeType{model.EntityTypeTypeCEM},
port, certificate, time.Second*4, cc.Port, certificate, time.Second*4,
) )
if err != nil { if err != nil {
return nil, err return nil, err
@ -147,8 +137,8 @@ func NewServer(other Config) (*EEBus, error) {
} }
c := &EEBus{ c := &EEBus{
log: log, log: util.NewLogger("eebus"),
Ski: ski, ski: ski,
clients: make(map[string][]Device), clients: make(map[string][]Device),
} }
@ -226,7 +216,7 @@ func (c *EEBus) RegisterDevice(ski, ip string, device Device) error {
ski = shiputil.NormalizeSKI(ski) ski = shiputil.NormalizeSKI(ski)
c.log.TRACE.Printf("registering ski: %s", ski) c.log.TRACE.Printf("registering ski: %s", ski)
if ski == c.Ski { if ski == c.ski {
return errors.New("device ski can not be identical to host ski") return errors.New("device ski can not be identical to host ski")
} }

View file

@ -38,12 +38,13 @@ func TestEEBus(t *testing.T) {
}) })
require.NoError(t, err, "server") require.NoError(t, err, "server")
require.NotEmpty(t, srv.Ski, "server ski")
server.Instance = srv server.Instance = srv
go srv.Run() go srv.Run()
box, err := createControlbox(t.Context(), server.Instance.Ski, remotePort) require.NotEmpty(t, server.Ski(), "server ski")
box, err := createControlbox(t.Context(), server.Ski(), remotePort)
require.NoError(t, err, "controlbox") require.NoError(t, err, "controlbox")
eventC := make(chan api.EventType, 1) eventC := make(chan api.EventType, 1)

View file

@ -1,6 +1,11 @@
package eebus package eebus
import "github.com/evcc-io/evcc/util" import (
"fmt"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/machine"
)
const ( const (
BrandName string = "EVCC" BrandName string = "EVCC"
@ -11,17 +16,69 @@ const (
var DeviceCode = util.Getenv("EEBUS_DEVICE_CODE", "EVCC_HEMS_01") var DeviceCode = util.Getenv("EEBUS_DEVICE_CODE", "EVCC_HEMS_01")
type Certificate struct { type Certificate struct {
Public, Private string Public string `json:"public"`
Private string `json:"private"`
} }
type Config struct { type Config struct {
URI string URI_ string `mapstructure:"uri" json:"uri"` // TODO deprecated
ShipID string Port int `json:"port"`
Interfaces []string ShipID string `json:"shipid"`
Certificate Certificate Interfaces []string `json:"interfaces,omitempty"`
Certificate Certificate `json:"certificate"`
} }
// Configured returns true if the EEbus server is configured // IsConfigured returns true if the EEbus server is configured
func (c Config) Configured() bool { func (c Config) IsConfigured() bool {
return len(c.Certificate.Public) > 0 && len(c.Certificate.Private) > 0 return len(c.Certificate.Public) > 0 && len(c.Certificate.Private) > 0
} }
// Redacted implements the redactor interface used by the tee publisher
func (c Config) Redacted() any {
return Config{
URI_: c.URI_,
Port: c.Port,
ShipID: c.ShipID,
Interfaces: c.Interfaces,
Certificate: Certificate{
Public: c.Certificate.Public,
Private: util.Masked(c.Certificate.Private),
},
}
}
func createShipID() string {
protectedID := machine.ProtectedID("evcc-eebus")
return fmt.Sprintf("%s-%0x", "EVCC", protectedID[:8])
}
func DefaultConfig(conf *Config) (*Config, error) {
cert, err := CreateCertificate()
if err != nil {
return nil, err
}
public, private, err := GetX509KeyPair(cert)
if err != nil {
return nil, err
}
res := Config{
Port: 4712,
ShipID: createShipID(),
Certificate: Certificate{
Public: public,
Private: private,
},
}
return &res, nil
}
// Ski returns the EEbus server SKI
func Ski() string {
if Instance == nil {
return ""
}
return Instance.ski
}

View file

@ -297,7 +297,6 @@ func (s *HTTPd) RegisterSystemHandler(site *core.Site, pub publisher, cache *uti
// yaml handlers // yaml handlers
for key, fun := range map[string]func() (any, any){ for key, fun := range map[string]func() (any, any){
keys.EEBus: func() (any, any) { return map[string]any{}, eebus.Config{} },
keys.Hems: func() (any, any) { return map[string]any{}, config.Typed{} }, keys.Hems: func() (any, any) { return map[string]any{}, config.Typed{} },
keys.Tariffs: func() (any, any) { return map[string]any{}, globalconfig.Tariffs{} }, keys.Tariffs: func() (any, any) { return map[string]any{}, globalconfig.Tariffs{} },
keys.Messaging: func() (any, any) { return map[string]any{}, globalconfig.Messaging{} }, // has default keys.Messaging: func() (any, any) { return map[string]any{}, globalconfig.Messaging{} }, // has default
@ -316,6 +315,7 @@ func (s *HTTPd) RegisterSystemHandler(site *core.Site, pub publisher, cache *uti
keys.ModbusProxy: func() any { return new([]globalconfig.ModbusProxy) }, // slice keys.ModbusProxy: func() any { return new([]globalconfig.ModbusProxy) }, // slice
keys.Shm: func() any { return new(shm.Config) }, keys.Shm: func() any { return new(shm.Config) },
keys.Influx: func() any { return new(globalconfig.Influx) }, keys.Influx: func() any { return new(globalconfig.Influx) },
keys.EEBus: func() any { return new(eebus.Config) },
} { } {
routes["update"+key] = route{Method: "POST", Pattern: "/" + key, HandlerFunc: settingsSetJsonHandler(key, pub, fun)} routes["update"+key] = route{Method: "POST", Pattern: "/" + key, HandlerFunc: settingsSetJsonHandler(key, pub, fun)}
routes["delete"+key] = route{Method: "DELETE", Pattern: "/" + key, HandlerFunc: settingsDeleteJsonHandler(key, pub, fun())} routes["delete"+key] = route{Method: "DELETE", Pattern: "/" + key, HandlerFunc: settingsDeleteJsonHandler(key, pub, fun())}

View file

@ -4,6 +4,7 @@ import (
"encoding/json" "encoding/json"
"net/http" "net/http"
"github.com/evcc-io/evcc/api/globalconfig"
"github.com/evcc-io/evcc/core/keys" "github.com/evcc-io/evcc/core/keys"
"github.com/evcc-io/evcc/server/db/settings" "github.com/evcc-io/evcc/server/db/settings"
"github.com/evcc-io/evcc/util/sponsor" "github.com/evcc-io/evcc/util/sponsor"
@ -26,11 +27,8 @@ func updateSponsortokenHandler(pub publisher) func(w http.ResponseWriter, r *htt
return return
} }
pub(keys.Sponsor, struct { pub(keys.Sponsor, globalconfig.ConfigStatus{
Status sponsor.Status `json:"status"` Status: sponsor.RedactedStatus(),
FromYaml bool `json:"fromYaml"`
}{
Status: sponsor.GetStatus(),
FromYaml: false, FromYaml: false,
}) })
} }
@ -39,7 +37,7 @@ func updateSponsortokenHandler(pub publisher) func(w http.ResponseWriter, r *htt
settings.SetString(keys.SponsorToken, req.Token) settings.SetString(keys.SponsorToken, req.Token)
setConfigDirty() setConfigDirty()
jsonWrite(w, sponsor.GetStatus()) jsonWrite(w, sponsor.RedactedStatus())
} }
} }
@ -47,10 +45,7 @@ func deleteSponsorTokenHandler(pub publisher) func(w http.ResponseWriter, r *htt
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
settings.SetString(keys.SponsorToken, "") settings.SetString(keys.SponsorToken, "")
pub(keys.Sponsor, struct { pub(keys.Sponsor, globalconfig.ConfigStatus{
Status sponsor.Status `json:"status"`
FromYaml bool `json:"fromYaml"`
}{
Status: sponsor.Status{}, Status: sponsor.Status{},
FromYaml: false, FromYaml: false,
}) })

View file

@ -9,6 +9,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/evcc-io/evcc/core/keys"
"github.com/evcc-io/evcc/server/db/settings" "github.com/evcc-io/evcc/server/db/settings"
"github.com/evcc-io/evcc/util/redact" "github.com/evcc-io/evcc/util/redact"
"github.com/gorilla/mux" "github.com/gorilla/mux"
@ -80,6 +81,12 @@ func settingsSetYamlHandler(key string, other, struc any) http.HandlerFunc {
} }
} }
func allowPub(key string) bool {
// don't publish on update - would overwrite globalconfig.Info struct with config
// TODO come up with a general solution once all endpoinds use Info
return key != keys.EEBus
}
func settingsSetJsonHandler(key string, pub publisher, newStruc func() any) http.HandlerFunc { func settingsSetJsonHandler(key string, pub publisher, newStruc func() any) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
struc := newStruc() struc := newStruc()
@ -104,7 +111,9 @@ func settingsSetJsonHandler(key string, pub publisher, newStruc func() any) http
settings.SetJson(key, struc) settings.SetJson(key, struc)
setConfigDirty() setConfigDirty()
pub(key, struc) if allowPub(key) {
pub(key, struc)
}
jsonWrite(w, true) jsonWrite(w, true)
} }
@ -115,7 +124,9 @@ func settingsDeleteJsonHandler(key string, pub publisher, struc any) http.Handle
settings.SetString(key, "") settings.SetString(key, "")
setConfigDirty() setConfigDirty()
pub(key, struc) if allowPub(key) {
pub(key, struc)
}
jsonWrite(w, true) jsonWrite(w, true)
} }

View file

@ -0,0 +1,27 @@
interval: 0.1s
eebus:
shipid: EVCC_HEMS_01
certificate:
public: |
-----BEGIN CERTIFICATE-----
MIIBuzCCAWKgAwIBAgIQatuHkiaPT4obu8RHSmfOwzAKBggqhkjOPQQDAjA+MQsw
CQYDVQQGEwJERTENMAsGA1UEChMERVZDQzEJMAcGA1UECxMAMRUwEwYDVQQDDAxF
VkNDX0hFTVNfMDEwHhcNMjYwMTMwMTAzNjUzWhcNMzYwMTI4MTAzNjUzWjA+MQsw
CQYDVQQGEwJERTENMAsGA1UEChMERVZDQzEJMAcGA1UECxMAMRUwEwYDVQQDDAxF
VkNDX0hFTVNfMDEwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQ8Z40GPe8HzV17
ZhbBPkGBwhu8emdvzO5OWJUPSaX1R8rrWSaNwlh6COPG3fOVvUzCYcQbgmMFXK/o
nhuAslXTo0IwQDAOBgNVHQ8BAf8EBAMCB4AwDwYDVR0TAQH/BAUwAwEB/zAdBgNV
HQ4EFgQUdQegutt1VxBfwDAlPdBpvrp7ztowCgYIKoZIzj0EAwIDRwAwRAIgS2ia
A+q0Tiy7NLyEjuKCV8QCVPjxt2OnfbSb9ZlDT3YCIDkwJoLc4+lsfvidDimUB+/I
ZvF1asyp3PVL7BDSFHil
-----END CERTIFICATE-----
private: |
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIIVZ2cLeTiENyCchgpj7eyJ5JKP5qpgj8eHUbLxn8Du1oAoGCCqGSM49
AwEHoUQDQgAEPGeNBj3vB81de2YWwT5BgcIbvHpnb8zuTliVD0ml9UfK61kmjcJY
egjjxt3zlb1MwmHEG4JjBVyv6J4bgLJV0w==
-----END EC PRIVATE KEY-----
circuits:
- name: main

137
tests/config-eebus.spec.ts Normal file
View file

@ -0,0 +1,137 @@
import { test, expect } from "@playwright/test";
import { start, stop, baseUrl, restart } from "./evcc";
import { expectModalHidden, expectModalVisible } from "./utils";
test.use({ baseURL: baseUrl() });
test.afterEach(async () => {
await stop();
});
test.describe("eebus", async () => {
test("first start, auto-configured", async ({ page }) => {
await start();
await page.goto("/#/config");
await page.getByTestId("eebus").getByRole("button", { name: "edit" }).click();
const modal = page.getByTestId("eebus-modal");
await expectModalVisible(modal);
await expect(modal.getByLabel("SHIP-ID")).not.toBeEmpty();
await expect(modal.getByLabel("SKI")).not.toBeEmpty();
await page.getByRole("button", { name: "Show advanced settings" }).click();
await expect(modal.getByLabel("Port")).toHaveValue("4712");
await expect(modal.getByLabel("Interfaces")).toBeVisible();
await expect(modal.getByLabel("Public certificate")).not.toBeEmpty();
await expect(modal.getByLabel("Private key")).toHaveValue("***");
});
test("change values", async ({ page }) => {
await start();
await page.goto("/#/config");
const eebusCard = page.getByTestId("eebus");
await eebusCard.getByRole("button", { name: "edit" }).click();
const modal = page.getByTestId("eebus-modal");
await expectModalVisible(modal);
const shipidReadonly = modal.locator("#eebus-shipid-display");
const ski = modal.getByLabel("SKI");
const shipidEditable = modal.locator("#eebus-shipid");
const port = modal.getByLabel("Port");
const interfaces = modal.getByLabel("Interfaces");
const publicCertificate = modal.getByLabel("Public certificate");
const privateKey = modal.getByLabel("Private key");
await page.getByRole("button", { name: "Show advanced settings" }).click();
await shipidEditable.fill("EVCC-1234");
await port.fill("4321");
await interfaces.fill("eth0");
await publicCertificate.fill("public certificate");
await privateKey.fill("private key");
// validate connection
await modal.getByRole("button", { name: "Save" }).click();
await expectModalHidden(modal);
// restart button appears
const restartButton = page
.getByTestId("bottom-banner")
.getByRole("button", { name: "Restart" });
await expect(restartButton).toBeVisible();
await restart();
await page.reload();
await eebusCard.getByRole("button", { name: "edit" }).click();
await page.getByRole("button", { name: "Show advanced settings" }).click();
await expect(shipidReadonly).toHaveValue("EVCC-1234");
await expect(ski).not.toBeVisible();
await expect(shipidEditable).toHaveValue("EVCC-1234");
await expect(port).toHaveValue("4321");
await expect(interfaces).toHaveValue("eth0");
await expect(publicCertificate).toHaveValue("public certificate");
await expect(privateKey).toHaveValue("***");
});
test("delete: ski changes", async ({ page }) => {
await start();
await page.goto("/#/config");
const eebusCard = page.getByTestId("eebus");
await page.getByTestId("eebus").getByRole("button", { name: "edit" }).click();
const modal = page.getByTestId("eebus-modal");
await expectModalVisible(modal);
// remember ski
const ski = modal.getByLabel("SKI");
const skiValue = await ski.inputValue();
page.on("dialog", async (dialog) => await dialog.accept());
await modal.getByRole("button", { name: "Remove" }).click();
// restart button appears
const restartButton = page
.getByTestId("bottom-banner")
.getByRole("button", { name: "Restart" });
await expect(restartButton).toBeVisible();
await restart();
await page.reload();
await eebusCard.getByRole("button", { name: "edit" }).click();
await expect(ski).not.toHaveValue(skiValue);
});
test("configured in yaml", async ({ page }) => {
await start("config-eebus.evcc.yaml");
await page.goto("/#/config");
await page.getByTestId("eebus").getByRole("button", { name: "edit" }).click();
const modal = page.getByTestId("eebus-modal");
await expectModalVisible(modal);
// yaml configuration note is visible
await expect(modal.getByText("Configured via evcc.yaml", { exact: false })).toBeVisible();
// SHIP-ID readonly field is visible with configured value
await expect(modal.getByLabel("SHIP-ID")).toBeVisible();
await expect(modal.getByLabel("SHIP-ID")).toHaveValue("EVCC_HEMS_01");
await expect(modal.getByLabel("SHIP-ID")).toHaveAttribute("readonly");
// SKI readonly field is visible
await expect(modal.getByLabel("SKI")).toBeVisible();
await expect(modal.getByLabel("SKI")).toHaveAttribute("readonly");
// no advanced settings button
await expect(modal.getByRole("button", { name: "Show advanced settings" })).not.toBeVisible();
// no action buttons
await expect(modal.getByRole("button", { name: "Save" })).not.toBeVisible();
await expect(modal.getByRole("button", { name: "Remove" })).not.toBeVisible();
});
});

View file

@ -25,7 +25,6 @@ test.describe("ocpp", () => {
// Open OCPP card // Open OCPP card
const ocppCard = page.getByTestId("ocpp"); const ocppCard = page.getByTestId("ocpp");
await expect(ocppCard).toContainText(["Configured", "no"].join(""));
await ocppCard.getByRole("button", { name: "edit" }).click(); await ocppCard.getByRole("button", { name: "edit" }).click();
const ocppModal = page.getByTestId("ocpp-modal"); const ocppModal = page.getByTestId("ocpp-modal");
@ -44,7 +43,6 @@ test.describe("ocpp", () => {
// Navigate back to evcc config page // Navigate back to evcc config page
await page.goto("/#/config"); await page.goto("/#/config");
await expect(ocppCard).toContainText(["Connections", "0/0", "Detected", "1"].join(""));
await ocppCard.getByRole("button", { name: "edit" }).click(); await ocppCard.getByRole("button", { name: "edit" }).click();
await expectModalVisible(ocppModal); await expectModalVisible(ocppModal);
await expect(ocppModal).toContainText("Detected station IDs"); await expect(ocppModal).toContainText("Detected station IDs");

8
util/redact.go Normal file
View file

@ -0,0 +1,8 @@
package util
func Masked(s any) string {
if s != "" {
return "***"
}
return ""
}

View file

@ -119,8 +119,8 @@ type Status struct {
Token string `json:"token,omitempty"` Token string `json:"token,omitempty"`
} }
// GetStatus returns the sponsorship status // RedactedStatus returns the sponsorship status
func GetStatus() Status { func RedactedStatus() Status {
mu.RLock() mu.RLock()
defer mu.RUnlock() defer mu.RUnlock()