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"
)
// 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 {
Network Network
Ocpp ocpp.Config
@ -79,13 +86,6 @@ func (c Hems) Redacted() any {
var _ api.Redactor = (*Mqtt)(nil)
func masked(s any) string {
if s != "" {
return "***"
}
return ""
}
type Mqtt struct {
mqtt.Config `mapstructure:",squash"`
Topic string `json:"topic"`
@ -97,12 +97,12 @@ func (m Mqtt) Redacted() any {
Config: mqtt.Config{
Broker: m.Broker,
User: m.User,
Password: masked(m.Password),
Password: util.Masked(m.Password),
ClientID: m.ClientID,
Insecure: m.Insecure,
CaCert: masked(m.CaCert),
ClientCert: masked(m.ClientCert),
ClientKey: masked(m.ClientKey),
CaCert: util.Masked(m.CaCert),
ClientCert: util.Masked(m.ClientCert),
ClientKey: util.Masked(m.ClientKey),
},
Topic: m.Topic,
}
@ -124,10 +124,10 @@ func (c Influx) Redacted() any {
return Influx{
URL: c.URL,
Database: c.Database,
Token: masked(c.Token),
Token: util.Masked(c.Token),
Org: c.Org,
User: c.User,
Password: masked(c.Password),
Password: util.Masked(c.Password),
Insecure: c.Insecure,
}
}
@ -147,7 +147,7 @@ type MessagingEventTemplate struct {
Title, Msg string
}
func (c Messaging) Configured() bool {
func (c Messaging) IsConfigured() bool {
return len(c.Services) > 0 || len(c.Events) > 0
}

View file

@ -6,9 +6,10 @@
'round-box--error': error,
'round-box--warning': warning,
'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">
<slot name="icon" />
</div>
@ -33,8 +34,10 @@
<shopicon-regular-adjust size="s"></shopicon-regular-adjust>
</button>
</div>
<hr class="my-3 divide" />
<slot name="tags" />
<div v-if="$slots.tags">
<hr class="my-3 divide" />
<slot name="tags" />
</div>
</div>
</template>
@ -67,7 +70,7 @@ export default {
}
let title = `${this.$t("config.main.name")}: <span class='font-monospace'>${this.name}</span>`;
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>`;
},
@ -105,6 +108,8 @@ export default {
list-style-type: none;
border-radius: 1rem;
padding: 1rem 1.5rem;
}
.root--with-tags {
min-height: 8rem;
}
.root--unconfigured {

View file

@ -1,26 +1,153 @@
<template>
<YamlModal
<JsonModal
id="eebusModal"
:title="$t('config.eebus.title')"
:description="$t('config.eebus.description')"
docs="/docs/reference/configuration/eebus"
:defaultYaml="defaultYaml"
removeKey="eebus"
endpoint="/config/eebus"
state-key="eebus.config"
data-testid="eebus-modal"
:no-buttons="fromYaml"
:confirm-remove="$t('config.eebus.removeConfirm')"
@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>
<script>
import YamlModal from "./YamlModal.vue";
import defaultYaml from "./defaultYaml/eebus.yaml?raw";
<script lang="ts">
import type { PropType } from "vue";
// 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 {
name: "EebusModal",
components: { YamlModal },
components: {
JsonModal,
FormRow,
PropertyField,
PropertyCertField,
PropertyCollapsible,
},
props: {
status: {
type: Object as PropType<EebusStatus>,
default: () => ({}),
},
fromYaml: Boolean,
},
emits: ["changed"],
data() {
return { defaultYaml: defaultYaml.trim() };
methods: {
formId(s: string) {
return `eebus-${s}`;
},
},
};
</script>

View file

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

View file

@ -88,6 +88,7 @@ export default {
saveMethod: { type: String, default: "post" },
storeValuesInArray: Boolean,
size: { type: String },
confirmRemove: String,
},
emits: ["changed", "open"],
data() {
@ -121,7 +122,13 @@ export default {
await this.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) {
this.serverValues = this.transformReadValues(this.serverValues);
}
@ -155,6 +162,9 @@ export default {
this.saving = false;
},
async remove() {
if (this.confirmRemove && !window.confirm(this.confirmRemove)) {
return;
}
this.removing = true;
this.error = "";
try {

View file

@ -1,6 +1,6 @@
<template>
<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">
<button
type="button"
@ -26,6 +26,7 @@ export default {
props: {
id: String,
modelValue: String,
required: Boolean,
},
emits: ["update:modelValue"],
computed: {

View file

@ -220,7 +220,9 @@ export default {
return this.property === "icon";
},
textarea() {
return ["accessToken", "refreshToken", "identifiers"].includes(this.property);
return (
this.array || ["accessToken", "refreshToken", "identifiers"].includes(this.property)
);
},
boolean() {
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;
shm?: ShmConfig;
sponsor?: Sponsor;
eebus?: any;
eebus?: Eebus;
modbusproxy?: ModbusProxy[];
messaging?: any;
interval?: number;
@ -440,6 +440,29 @@ export enum MODBUS_PROTOCOL {
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 = {
port: number;
readonly: MODBUS_PROXY_READONLY;

View file

@ -210,33 +210,6 @@
<DeviceTags :tags="influxTags" />
</template>
</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
:title="`${$t('config.circuits.title')}`"
editable
@ -278,19 +251,6 @@
<DeviceTags :tags="modbusproxyTags" />
</template>
</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
:title="$t('config.hems.title')"
editable
@ -306,6 +266,37 @@
</DeviceCard>
</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" />
<h2 class="my-4 mt-5">{{ $t("config.section.system") }}</h2>
@ -387,7 +378,11 @@
:extMeters="extMeters"
@changed="yamlChanged"
/>
<EebusModal @changed="yamlChanged" />
<EebusModal
:status="eebus?.status"
:from-yaml="eebus?.fromYaml"
@changed="yamlChanged"
/>
<OcppModal :ocpp="ocpp" />
<BackupRestoreModal v-bind="backupRestoreProps" />
<PasswordModal update-mode />
@ -656,12 +651,6 @@ export default defineComponent({
vehicleOptions(): VehicleOption[] {
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() {
return store.state?.hems;
},
@ -698,30 +687,8 @@ export default defineComponent({
// @ts-expect-error: telemetry property exists but not in TypeScript definitions
return store.state?.telemetry === true;
},
eebusTags(): DeviceTags {
return { configured: { value: store.state?.eebus || false } };
},
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;
eebus() {
return store.state?.eebus;
},
modbusproxyTags(): DeviceTags {
const config = store.state?.modbusproxy || [];

View file

@ -8,6 +8,7 @@ import (
"strings"
"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/config"
"go.yaml.in/yaml/v4"
@ -120,3 +121,18 @@ func deviceHeader[T any](dev config.Device[T]) string {
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/server"
"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/network"
"github.com/evcc-io/evcc/server/updater"
@ -176,10 +177,7 @@ func runRoot(cmd *cobra.Command, args []string) {
ocppCS := ocpp.Instance()
ocppCS.SetUpdated(func() {
// republish when OCPP state updates
valueChan <- util.Param{Key: keys.Ocpp, Val: struct {
Config ocpp.Config `json:"config"`
Status ocpp.Status `json:"status"`
}{
valueChan <- util.Param{Key: keys.Ocpp, Val: globalconfig.ConfigStatus{
Config: conf.Ocpp,
Status: ocpp.GetStatus(),
}}
@ -329,35 +327,29 @@ func runRoot(cmd *cobra.Command, args []string) {
}
// 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.Influx, Val: conf.Influx}
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.Mqtt, Val: conf.Mqtt}
valueChan <- util.Param{Key: keys.Network, Val: conf.Network}
valueChan <- util.Param{Key: keys.Ocpp, Val: struct {
Config ocpp.Config `json:"config"`
Status ocpp.Status `json:"status"`
}{
valueChan <- util.Param{Key: keys.Ocpp, Val: globalconfig.ConfigStatus{
Config: conf.Ocpp,
Status: ocpp.GetStatus(),
}}
valueChan <- util.Param{Key: keys.Sponsor, Val: struct {
Status sponsor.Status `json:"status"`
FromYaml bool `json:"fromYaml"`
}{
Status: sponsor.GetStatus(),
valueChan <- util.Param{Key: keys.Sponsor, Val: globalconfig.ConfigStatus{
Status: sponsor.RedactedStatus(),
FromYaml: fromYaml.sponsor,
}}
valueChan <- util.Param{Key: keys.Hems, Val: struct {
Config globalconfig.Hems `json:"config"`
Status *hemsapi.Status `json:"status,omitempty"`
FromYaml bool `json:"fromYaml"`
}{
Config: conf.HEMS,
valueChan <- util.Param{Key: keys.Hems, Val: globalconfig.ConfigStatus{
Config: conf.HEMS.Redacted(),
Status: hemsapi.GetStatus(hems),
FromYaml: fromYaml.hems,
}}

View file

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

View file

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

View file

@ -209,6 +209,7 @@
"dragList": "Ponovo naručiva lista",
"experimental": "Eksperimentalni",
"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",
"invalidFileSelected": "Odabrana neispravna datoteka",
"noFileSelected": "Nije odabrana nijedna datoteka.",
@ -247,8 +248,7 @@
"downloadCsv": "Preuzmi CSV",
"eventsRecorded": "Zabilježeno je {count} događaja ograničenja mreže.",
"lastEvent": "Najnoviji prije {timeAgo}.",
"title": "Vanjski limit",
"yamlConfigured": "Napomena: Konfigurisano putem evcc.yaml. Uklonite unos iz datoteke da biste ovdje omogućili uređivanje."
"title": "Vanjski limit"
},
"icon": {
"change": "promjena",
@ -344,6 +344,7 @@
"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}."
},
"main": {},
"meter": {
"save": "Sačuvaj"
},

View file

@ -209,6 +209,7 @@
"dragList": "Seznam umožňující úpravy pořadí",
"experimental": "Experimentální nastavení",
"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í",
"invalidFileSelected": "Zvolen neplatný soubor",
"noFileSelected": "Nebyl zvolen žádný soubor.",
@ -247,8 +248,7 @@
"downloadCsv": "Stáhnout CSV",
"eventsRecorded": "Zaznamenáno {count} událostí omezení mřížky.",
"lastEvent": "Nejnovější {timeAgo}.",
"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."
"title": "Vnější limit"
},
"icon": {
"change": "Změnit",
@ -390,8 +390,7 @@
"unconfigured": "není konfigurováno",
"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í**.",
"welcomeBannerTitle": "Pojďme nakonfigurovat váš systém!",
"yaml": "Zařízení z evcc.yaml nelze upravovat."
"welcomeBannerTitle": "Pojďme nakonfigurovat váš systém!"
},
"messaging": {
"description": "Dostávejte notifikace o průběhu nabíjení.",

View file

@ -209,6 +209,7 @@
"dragList": "Rækkefølgen kan ændres",
"experimental": "Eksperimentel",
"forceSave": "Gem alligevel",
"fromYamlHint": "Bemærk: Er konfigureret via evcc.yaml. Fjern indtastningen fra filen for at aktivere redigering her.",
"hideAdvancedSettings": "Skjul avancerede indstillinger",
"invalidFileSelected": "Ugyldig fil er valgt",
"noFileSelected": "Ingen fil valgt.",
@ -247,8 +248,7 @@
"downloadCsv": "Download CSV",
"eventsRecorded": "Registrerede {count} netbegrænsningshændelser.",
"lastEvent": "Sidst {timeAgo}.",
"title": "Ekstern grænse",
"yamlConfigured": "Bemærk: Er konfigureret via evcc.yaml. Fjern indtastningen fra filen for at aktivere redigering her."
"title": "Ekstern grænse"
},
"icon": {
"change": "ændre",
@ -390,8 +390,7 @@
"unconfigured": "ikke konfigureret",
"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**.",
"welcomeBannerTitle": "Lad os konfigurere dit system!",
"yaml": "Enheder fra evcc.yaml kan ikke ændres her."
"welcomeBannerTitle": "Lad os konfigurere dit system!"
},
"messaging": {
"description": "Modtag beskeder om dine opladningssessioner.",

View file

@ -172,7 +172,23 @@
"loading": "Lade YAML-Editor …"
},
"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"
},
"experimental": {
@ -209,6 +225,7 @@
"dragList": "Sortierbare Liste",
"experimental": "Experimentell",
"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",
"invalidFileSelected": "Ungültige Datei ausgewählt",
"noFileSelected": "Keine Datei ausgewählt.",
@ -247,8 +264,7 @@
"downloadCsv": "CSV herunterladen",
"eventsRecorded": "{count} Netzbegrenzungen gespeichert.",
"lastEvent": "Zuletzt {timeAgo}.",
"title": "Externe Begrenzung",
"yamlConfigured": "Hinweis: Konfiguriert über evcc.yaml. Entferne den Eintrag aus der Datei, um die Bearbeitung hier zu aktivieren."
"title": "Externe Begrenzung"
},
"icon": {
"change": "ändern",
@ -390,8 +406,7 @@
"unconfigured": "nicht konfiguriert",
"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**.",
"welcomeBannerTitle": "Los geht's mit der Konfiguration!",
"yaml": "Geräte aus evcc.yaml sind nicht editierbar."
"welcomeBannerTitle": "Los geht's mit der Konfiguration!"
},
"messaging": {
"description": "Benachrichtigungen über Ladevorgänge und andere Ereignisse erhalten.",
@ -543,6 +558,7 @@
"integrations": "Integrationen",
"loadpoints": "Laden & Heizen",
"meter": "PV & Batterie",
"services": "Dienste",
"system": "System",
"vehicles": "Fahrzeuge"
},

View file

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

View file

@ -172,7 +172,23 @@
"loading": "Loading YAML editor…"
},
"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"
},
"experimental": {
@ -209,6 +225,7 @@
"dragList": "Reorderable list",
"experimental": "Experimental",
"forceSave": "Save anyway",
"fromYamlHint": "Note: Configured via evcc.yaml. Remove the entry from the file to enable editing here.",
"hideAdvancedSettings": "Hide advanced settings",
"invalidFileSelected": "Invalid file selected",
"noFileSelected": "No file selected.",
@ -247,8 +264,7 @@
"downloadCsv": "Download CSV",
"eventsRecorded": "Recorded {count} grid limitation events.",
"lastEvent": "Most recent {timeAgo}.",
"title": "External Limit",
"yamlConfigured": "Note: Configured via evcc.yaml. Remove the entry from the file to enable editing here."
"title": "External Limit"
},
"icon": {
"change": "change",
@ -390,8 +406,7 @@
"unconfigured": "not configured",
"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**.",
"welcomeBannerTitle": "Let's configure your system!",
"yaml": "Devices from evcc.yaml are not editable."
"welcomeBannerTitle": "Let's configure your system!"
},
"messaging": {
"description": "Receive messages about your charging sessions.",
@ -543,6 +558,7 @@
"integrations": "Integrations",
"loadpoints": "Charging & Heating",
"meter": "Solar & Battery",
"services": "Services",
"system": "System",
"vehicles": "Vehicles"
},

View file

@ -209,6 +209,7 @@
"dragList": "Lista reordenable",
"experimental": "Experimental",
"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",
"invalidFileSelected": "Se ha seleccionado un archivo no válido",
"noFileSelected": "No hay ningún archivo seleccionado.",
@ -247,8 +248,7 @@
"downloadCsv": "Descargar CSV",
"eventsRecorded": "Se registraron {count} eventos de limitación de la red.",
"lastEvent": "Más reciente {timeAgo}.",
"title": "Límite externo",
"yamlConfigured": "Nota: Configurado a través de evcc.yaml. Elimina la entrada del archivo para poder editar aquí."
"title": "Límite externo"
},
"icon": {
"change": "cambio",
@ -390,8 +390,7 @@
"unconfigured": "no configurado",
"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**.",
"welcomeBannerTitle": "¡Configuremos su sistema!",
"yaml": "Los dispositivos de evcc.yaml no se pueden editar."
"welcomeBannerTitle": "¡Configuremos su sistema!"
},
"messaging": {
"description": "Recibe mensajes sobre tus sesiones de carga.",

View file

@ -209,6 +209,7 @@
"dragList": "Järjestettävä lista",
"experimental": "Kokeellinen",
"forceSave": "Tallenna joka tapauksessa",
"fromYamlHint": "Huomio: Asetus on evcc.yaml-tiedostosta. Poista asetus tiedostosta, jotta voisit muokata sitä täällä.",
"hideAdvancedSettings": "Piilota edistyneet asetukset",
"invalidFileSelected": "Valittu virheellinen tiedosto",
"noFileSelected": "Ei valittua tiedostoa.",
@ -247,8 +248,7 @@
"downloadCsv": "Lataa CSV-taulukko",
"eventsRecorded": "Tallennettu {count} verkon rajoitustapahtumaa.",
"lastEvent": "Viimeisin {timeAgo}.",
"title": "Ulkoinen raja",
"yamlConfigured": "Huomio: Asetus on evcc.yaml-tiedostosta. Poista asetus tiedostosta, jotta voisit muokata sitä täällä."
"title": "Ulkoinen raja"
},
"icon": {
"change": "vaihda",
@ -390,8 +390,7 @@
"unconfigured": "ei määritetty",
"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**.",
"welcomeBannerTitle": "Määritetään järjestelmäsi!",
"yaml": "evcc.yaml-asetuksissa määritettyjä laitteita ei voi muokata."
"welcomeBannerTitle": "Määritetään järjestelmäsi!"
},
"messaging": {
"description": "Vastaanota viestejä koskien lataustapahtumiasi.",

View file

@ -209,6 +209,7 @@
"dragList": "Liste à ordonner",
"experimental": "Expérimental",
"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",
"invalidFileSelected": "Fichier sélectionné non valide",
"noFileSelected": "Aucun fichier sélectionné.",
@ -247,8 +248,7 @@
"downloadCsv": "Télécharger CSV",
"eventsRecorded": "Enregistrement de {count} événements liés à la limitation de la grille.",
"lastEvent": "Le plus récent {timeAgo}.",
"title": "Limite externe",
"yamlConfigured": "Remarque : configuré via evcc.yaml. Supprimez l'entrée du fichier pour pouvoir effectuer des modifications ici."
"title": "Limite externe"
},
"icon": {
"change": "changer",
@ -390,8 +390,7 @@
"unconfigured": "non configuré",
"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**.",
"welcomeBannerTitle": "Configurons votre système !",
"yaml": "Un appareil provenant du evcc.yaml n’est pas modifiable."
"welcomeBannerTitle": "Configurons votre système !"
},
"messaging": {
"description": "Recevoir des messages à propos de vos sessions de charge.",

View file

@ -209,6 +209,7 @@
"dragList": "Preurediv popis",
"experimental": "Eksperimentalno",
"forceSave": "Svejedno spremi",
"fromYamlHint": "Napomena: Konfigurirano putem evcc.yaml. Uklonite unos iz datoteke kako biste ovdje omogućili uređivanje.",
"hideAdvancedSettings": "Sakrij napredne postavke",
"invalidFileSelected": "Neispravna datoteka",
"noFileSelected": "Datoteka nije odabrana.",
@ -247,8 +248,7 @@
"downloadCsv": "Preuzmi CSV",
"eventsRecorded": "Zabilježeno je {count} događaja ograničenja mreže.",
"lastEvent": "Najnoviji {timeAgo}",
"title": "Vanjski limit",
"yamlConfigured": "Napomena: Konfigurirano putem evcc.yaml. Uklonite unos iz datoteke kako biste ovdje omogućili uređivanje."
"title": "Vanjski limit"
},
"icon": {
"change": "promjeni",
@ -390,8 +390,7 @@
"unconfigured": "nije postavljeno",
"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**.",
"welcomeBannerTitle": "Konfigurirajmo vaš sustav!",
"yaml": "U evcc.yaml datoteci definirani uređaji se ne mogu uređivati."
"welcomeBannerTitle": "Konfigurirajmo vaš sustav!"
},
"messaging": {
"description": "Primajte poruke o svojim sesijama punjenja.",

View file

@ -209,6 +209,7 @@
"dragList": "Átrendezhető lista",
"experimental": "Kísérleti",
"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",
"invalidFileSelected": "Érvénytelen fájl kiválasztva",
"noFileSelected": "Nincs fájl kiválasztva.",
@ -247,8 +248,7 @@
"downloadCsv": "CSV letöltése",
"eventsRecorded": "{count} rácskorlátozási esemény rögzítve.",
"lastEvent": "Legutóbbi {timeAgo}.",
"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."
"title": "Külső határ"
},
"icon": {
"change": "változtatás",
@ -390,8 +390,7 @@
"unconfigured": "nincs konfigurálva",
"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**.",
"welcomeBannerTitle": "Konfiguráljuk a rendszerét!",
"yaml": "Az evcc.yaml fájlban szereplő eszközök nem szerkeszthetők."
"welcomeBannerTitle": "Konfiguráljuk a rendszerét!"
},
"messaging": {
"description": "Értesítési üzenetek beállítása a töltési folyamatokról.",

View file

@ -209,6 +209,7 @@
"dragList": "Elenco riordinabile",
"experimental": "Sperimentale",
"forceSave": "Salva comunque",
"fromYamlHint": "Nota: configurato tramite evcc.yaml. Rimuovere la voce dal file per abilitare la modifica qui.",
"hideAdvancedSettings": "Nascondi impostazioni avanzate",
"invalidFileSelected": "File non valido selezionato",
"noFileSelected": "Nessun file selezionato.",
@ -247,8 +248,7 @@
"downloadCsv": "Scarica CSV",
"eventsRecorded": "Registrati {count} eventi di limitazione della griglia.",
"lastEvent": "Più recente {timeAgo}.",
"title": "HEMSLimite esterno",
"yamlConfigured": "Nota: configurato tramite evcc.yaml. Rimuovere la voce dal file per abilitare la modifica qui."
"title": "HEMSLimite esterno"
},
"icon": {
"change": "cambiamento",
@ -390,8 +390,7 @@
"unconfigured": "non configurato",
"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**.",
"welcomeBannerTitle": "Configuriamo il tuo sistema!",
"yaml": "I dispositivi di evcc.yaml non sono modificabili."
"welcomeBannerTitle": "Configuriamo il tuo sistema!"
},
"messaging": {
"description": "Ricevi messaggi sulle sessioni di ricarica.",

View file

@ -209,6 +209,7 @@
"dragList": "Sortéirbar Lëscht",
"experimental": "Experimentell",
"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",
"invalidFileSelected": "Ongülteg Datei ausgewielt",
"noFileSelected": "Keng Datei ausgewielt.",
@ -247,8 +248,7 @@
"downloadCsv": "CSV eroflueden",
"eventsRecorded": "{count} gespäichert Limitatiounen vum Netz.",
"lastEvent": "Méi rezent {timeAgo}.",
"title": "Äusserlech Grenz",
"yamlConfigured": "Bemierkung: Konfiguréiert iwwer evcc.yaml. Läscht d'Astellungen aus der Datei fir d'Ännerung hei z'erméiglechen."
"title": "Äusserlech Grenz"
},
"icon": {
"change": "änneren",
@ -390,8 +390,7 @@
"unconfigured": "net konfiguréiert",
"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**.",
"welcomeBannerTitle": "Loosst eis de System konfiguréieren!",
"yaml": "Geräter aus der evcc.yaml Datei si net editiérbar."
"welcomeBannerTitle": "Loosst eis de System konfiguréieren!"
},
"messaging": {
"description": "Notifikatiounen iwwer Är Opluedsessioune kréien.",

View file

@ -209,6 +209,7 @@
"dragList": "Galima sukeisti vietomis",
"experimental": "Eksperimentinis",
"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",
"invalidFileSelected": "Pasirinktas netinkamas failas",
"noFileSelected": "Nepasirinktas failas.",
@ -247,8 +248,7 @@
"downloadCsv": "Atsisiųsti CSV failą",
"eventsRecorded": "Užfiksuota {count} tinklo galios apribojimo įvykių.",
"lastEvent": "Naujausias {timeAgo}.",
"title": "Išorinė riba",
"yamlConfigured": "Pastaba: Sukonfigūruota naudojant evcc.yaml. Pašalinkite įrašą iš failo, kad būtų galima redaguoti čia."
"title": "Išorinė riba"
},
"icon": {
"change": "pakeisti",
@ -390,8 +390,7 @@
"unconfigured": "nesukonfigūruota",
"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į**.",
"welcomeBannerTitle": "Sukonfigūruokime jūsų sistemą!",
"yaml": "Įtaisas iš evcc.yaml , čia konfigūruoti neina."
"welcomeBannerTitle": "Sukonfigūruokime jūsų sistemą!"
},
"messaging": {
"description": "Gaukite pranešimus apie įkrovimo sesijas.",

View file

@ -209,6 +209,7 @@
"dragList": "Sorteerbare lijst",
"experimental": "Experimenteel",
"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",
"invalidFileSelected": "Ongeldig bestand geselecteerd",
"noFileSelected": "Geen bestand geselecteerd.",
@ -247,8 +248,7 @@
"downloadCsv": "Download CSV",
"eventsRecorded": "{count} gebeurtenissen met rasterbeperkingen geregistreerd.",
"lastEvent": "Meest recent {timeAgo}.",
"title": "Externe limiet",
"yamlConfigured": "Opmerking: Geconfigureerd via evcc.yaml. Verwijder de betreffende regels uit het bestand om hier bewerkingen mogelijk te maken."
"title": "Externe limiet"
},
"icon": {
"change": "wijzig",
@ -390,8 +390,7 @@
"unconfigured": "Niet geconfigureerd",
"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**.",
"welcomeBannerTitle": "Start met het configureren van uw systeem!",
"yaml": "Apparaten geconfigureerd in evcc.yaml kunnen hier niet worden bewerkt."
"welcomeBannerTitle": "Start met het configureren van uw systeem!"
},
"messaging": {
"description": "Ontvang berichten over je oplaadsessies.",

View file

@ -209,6 +209,7 @@
"dragList": "Omordningsbar liste",
"experimental": "Eksperimentell",
"forceSave": "Lagre likevel",
"fromYamlHint": "Merk: Konfigurert via evcc.yaml. Fjern oppføringen fra filen for å aktivere redigering her.",
"hideAdvancedSettings": "Skjul avanserte innstillinger",
"invalidFileSelected": "Ugyldig fil valgt",
"noFileSelected": "Ingen fil valgt.",
@ -247,8 +248,7 @@
"downloadCsv": "Last ned CSV",
"eventsRecorded": "Registrerte {count} hendelser med rutenettbegrensning.",
"lastEvent": "Siste {timeAgo}.",
"title": "Ekstern grense",
"yamlConfigured": "Merk: Konfigurert via evcc.yaml. Fjern oppføringen fra filen for å aktivere redigering her."
"title": "Ekstern grense"
},
"icon": {
"change": "endring",
@ -390,8 +390,7 @@
"unconfigured": "ikke konfigurert",
"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**.",
"welcomeBannerTitle": "La oss konfigurere systemet ditt!",
"yaml": "Enheter fra evcc.yaml kan ikke redigeres."
"welcomeBannerTitle": "La oss konfigurere systemet ditt!"
},
"messaging": {
"description": "Motta meldinger om ladingsøktene dine.",

View file

@ -209,6 +209,7 @@
"dragList": "Lista do ponownego zamówienia",
"experimental": "Eksperymentalny",
"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",
"invalidFileSelected": "Wybrano nieprawidłowy plik",
"noFileSelected": "Nie wybrano pliku.",
@ -247,8 +248,7 @@
"downloadCsv": "Pobierz CSV",
"eventsRecorded": "Zarejestrowano {count} zdarzeń związanych z ograniczeniami sieci.",
"lastEvent": "Najnowsze {timeAgo}.",
"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."
"title": "Limit zewnętrzny"
},
"icon": {
"change": "zmień",
@ -390,8 +390,7 @@
"unconfigured": "nie skonfigurowane",
"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**.",
"welcomeBannerTitle": "Skonfigurujmy Twój system!",
"yaml": "Urządzenia z pliku evcc.yaml nie mogą być edytowane."
"welcomeBannerTitle": "Skonfigurujmy Twój system!"
},
"messaging": {
"description": "Otrzymuj wiadomości o sesjach ładowania.",

View file

@ -209,6 +209,7 @@
"dragList": "Lista reordenável",
"experimental": "Experimental",
"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",
"invalidFileSelected": "Ficheiro selecionado inválido",
"noFileSelected": "Nenhum ficheiro selecionado.",
@ -247,8 +248,7 @@
"downloadCsv": "Descarregar CSV",
"eventsRecorded": "Registou {count} eventos de limitação da rede.",
"lastEvent": "Mais recente {timeAgo}.",
"title": "Limite externo",
"yamlConfigured": "Nota: Configurado através do evcc.yaml. Remova a entrada do ficheiro para permitir a edição aqui."
"title": "Limite externo"
},
"icon": {
"change": "alterar",
@ -390,8 +390,7 @@
"unconfigured": "não configurado",
"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**.",
"welcomeBannerTitle": "Configuremos agora o seu sistema!",
"yaml": "Os dispositivos de evcc.yaml não são editáveis."
"welcomeBannerTitle": "Configuremos agora o seu sistema!"
},
"messaging": {
"description": "Receber mensagens sobre as sessões de carregamento.",

View file

@ -209,6 +209,7 @@
"dragList": "Listă reordonabilă",
"experimental": "Experimental",
"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",
"invalidFileSelected": "Fișier invalid selectat",
"noFileSelected": "Nu a fost selectat niciun fișier.",
@ -247,8 +248,7 @@
"downloadCsv": "Descărcați CSV",
"eventsRecorded": "Au fost înregistrate {count} evenimente de limitare a grilei.",
"lastEvent": "Cel mai recent {timeAgo}.",
"title": "Limită externă",
"yamlConfigured": "Notă: Configurat prin evcc.yaml. Ștergeți intrarea din fișier pentru a activa editarea aici."
"title": "Limită externă"
},
"icon": {
"change": "schimbare",
@ -390,8 +390,7 @@
"unconfigured": "neconfigurat",
"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**.",
"welcomeBannerTitle": "Să configurăm sistemul tău!",
"yaml": "Dispozitivele din evcc.yaml nu pot fi editate."
"welcomeBannerTitle": "Să configurăm sistemul tău!"
},
"messaging": {
"description": "Primiți mesaje despre sesiunile dvs. de încărcare.",

View file

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

View file

@ -209,6 +209,7 @@
"dragList": "Seznam, ki ga je mogoče prerazporediti",
"experimental": "Eksperimentalno",
"forceSave": "Vseeno shrani",
"fromYamlHint": "Opomba: Konfigurirano prek evcc.yaml. Odstranite vnos iz datoteke, da omogočite urejanje tukaj.",
"hideAdvancedSettings": "Skrij napredne nastavitve",
"invalidFileSelected": "Izbrana neveljavna datoteka",
"noFileSelected": "Izbrana ni nobena datoteka.",
@ -247,8 +248,7 @@
"downloadCsv": "Prenesi CSV",
"eventsRecorded": "Zabeleženo {count} dogodkov omejitve omrežja.",
"lastEvent": "Najnovejše {timeAgo}.",
"title": "Zunanja meja",
"yamlConfigured": "Opomba: Konfigurirano prek evcc.yaml. Odstranite vnos iz datoteke, da omogočite urejanje tukaj."
"title": "Zunanja meja"
},
"icon": {
"change": "sprememba",
@ -390,8 +390,7 @@
"unconfigured": "ni konfigurirano",
"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**.",
"welcomeBannerTitle": "Konfigurirajmo vaš sistem!",
"yaml": "Naprav iz datoteke evcc.yaml ni mogoče urejati."
"welcomeBannerTitle": "Konfigurirajmo vaš sistem!"
},
"messaging": {
"description": "Prejemanje sporočil o vaših sejah polnjenja.",

View file

@ -209,6 +209,7 @@
"dragList": "Sorterbar lista",
"experimental": "Experimentella funktioner",
"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",
"invalidFileSelected": "Ogiltig fil vald",
"noFileSelected": "Ingen fil vald.",
@ -247,8 +248,7 @@
"downloadCsv": "Hämta CSV",
"eventsRecorded": "Registrerade {count} händelser med nätbegränsning.",
"lastEvent": "Senaste {timeAgo}.",
"title": "Extern gräns",
"yamlConfigured": "Obs: Konfigurerad via evcc.yaml. Ta bort posten från filen för att aktivera redigering här."
"title": "Extern gräns"
},
"icon": {
"change": "ändra",
@ -390,8 +390,7 @@
"unconfigured": "ej konfigurerad",
"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**.",
"welcomeBannerTitle": "Låt oss konfigurera ditt system!",
"yaml": "Konfigurera i evcc.yaml. Kan inte ändras i UI."
"welcomeBannerTitle": "Låt oss konfigurera ditt system!"
},
"messaging": {
"description": "Få notiser om dina laddningar.",

View file

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

View file

@ -209,6 +209,7 @@
"dragList": "Yeniden sıralanabilir liste",
"experimental": "Deneysel",
"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”",
"invalidFileSelected": "Geçersiz dosya seçildi",
"noFileSelected": "Seçili dosya yok.",
@ -247,8 +248,7 @@
"downloadCsv": "CSV'yi indir",
"eventsRecorded": "{count} şebeke sınırlama etkinliği kaydedildi.",
"lastEvent": "En son {timeAgo}.",
"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."
"title": "Dış Limit"
},
"icon": {
"change": "değiştir",
@ -390,8 +390,7 @@
"unconfigured": "yapılandırılmadı",
"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ç.",
"welcomeBannerTitle": "Haydi sistemini yapılandıralım!",
"yaml": "evcc.yaml'daki cihazlar düzenlenemez."
"welcomeBannerTitle": "Haydi sistemini yapılandıralım!"
},
"messaging": {
"description": "Dolum oturumları hakkında bildirimler al.",

View file

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

View file

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

View file

@ -4,9 +4,7 @@ import (
"crypto/tls"
"errors"
"fmt"
"net"
"slices"
"strconv"
"strings"
"sync"
"time"
@ -34,7 +32,6 @@ import (
"github.com/enbility/spine-go/model"
"github.com/enbility/spine-go/spine"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/machine"
)
type Device interface {
@ -82,29 +79,33 @@ type EEBus struct {
mux sync.Mutex
log *util.Logger
Ski string
ski string
clients map[string][]Device
}
var Instance *EEBus
func GetStatus() any {
return struct {
Ski string `json:"ski"`
}{
Ski: Ski(),
}
}
func NewServer(other Config) (*EEBus, error) {
cc := Config{
URI: ":4712",
Port: 4712,
}
if err := mergo.Merge(&cc, other, mergo.WithOverride); err != nil {
return nil, err
}
log := util.NewLogger("eebus")
protectedID := machine.ProtectedID("evcc-eebus")
serial := fmt.Sprintf("%s-%0x", "EVCC", protectedID[:8])
if len(cc.ShipID) != 0 {
serial = cc.ShipID
serial := cc.ShipID
if serial == "" {
serial = createShipID()
}
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
}
_, 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(
BrandName, BrandName, Model, serial,
model.DeviceTypeTypeEnergyManagementSystem,
[]model.EntityTypeType{model.EntityTypeTypeCEM},
port, certificate, time.Second*4,
cc.Port, certificate, time.Second*4,
)
if err != nil {
return nil, err
@ -147,8 +137,8 @@ func NewServer(other Config) (*EEBus, error) {
}
c := &EEBus{
log: log,
Ski: ski,
log: util.NewLogger("eebus"),
ski: ski,
clients: make(map[string][]Device),
}
@ -226,7 +216,7 @@ func (c *EEBus) RegisterDevice(ski, ip string, device Device) error {
ski = shiputil.NormalizeSKI(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")
}

View file

@ -38,12 +38,13 @@ func TestEEBus(t *testing.T) {
})
require.NoError(t, err, "server")
require.NotEmpty(t, srv.Ski, "server ski")
server.Instance = srv
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")
eventC := make(chan api.EventType, 1)

View file

@ -1,6 +1,11 @@
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 (
BrandName string = "EVCC"
@ -11,17 +16,69 @@ const (
var DeviceCode = util.Getenv("EEBUS_DEVICE_CODE", "EVCC_HEMS_01")
type Certificate struct {
Public, Private string
Public string `json:"public"`
Private string `json:"private"`
}
type Config struct {
URI string
ShipID string
Interfaces []string
Certificate Certificate
URI_ string `mapstructure:"uri" json:"uri"` // TODO deprecated
Port int `json:"port"`
ShipID string `json:"shipid"`
Interfaces []string `json:"interfaces,omitempty"`
Certificate Certificate `json:"certificate"`
}
// Configured returns true if the EEbus server is configured
func (c Config) Configured() bool {
// IsConfigured returns true if the EEbus server is configured
func (c Config) IsConfigured() bool {
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
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.Tariffs: func() (any, any) { return map[string]any{}, globalconfig.Tariffs{} },
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.Shm: func() any { return new(shm.Config) },
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["delete"+key] = route{Method: "DELETE", Pattern: "/" + key, HandlerFunc: settingsDeleteJsonHandler(key, pub, fun())}

View file

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

View file

@ -9,6 +9,7 @@ import (
"strings"
"time"
"github.com/evcc-io/evcc/core/keys"
"github.com/evcc-io/evcc/server/db/settings"
"github.com/evcc-io/evcc/util/redact"
"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 {
return func(w http.ResponseWriter, r *http.Request) {
struc := newStruc()
@ -104,7 +111,9 @@ func settingsSetJsonHandler(key string, pub publisher, newStruc func() any) http
settings.SetJson(key, struc)
setConfigDirty()
pub(key, struc)
if allowPub(key) {
pub(key, struc)
}
jsonWrite(w, true)
}
@ -115,7 +124,9 @@ func settingsDeleteJsonHandler(key string, pub publisher, struc any) http.Handle
settings.SetString(key, "")
setConfigDirty()
pub(key, struc)
if allowPub(key) {
pub(key, struc)
}
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
const ocppCard = page.getByTestId("ocpp");
await expect(ocppCard).toContainText(["Configured", "no"].join(""));
await ocppCard.getByRole("button", { name: "edit" }).click();
const ocppModal = page.getByTestId("ocpp-modal");
@ -44,7 +43,6 @@ test.describe("ocpp", () => {
// Navigate back to evcc config page
await page.goto("/#/config");
await expect(ocppCard).toContainText(["Connections", "0/0", "Detected", "1"].join(""));
await ocppCard.getByRole("button", { name: "edit" }).click();
await expectModalVisible(ocppModal);
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"`
}
// GetStatus returns the sponsorship status
func GetStatus() Status {
// RedactedStatus returns the sponsorship status
func RedactedStatus() Status {
mu.RLock()
defer mu.RUnlock()