diff --git a/README.md b/README.md index 3150a206b8..75ddae3530 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ evcc is an extensible EV Charge Controller and home energy management system. Fe - simple and clean user interface - wide range of supported [chargers](https://docs.evcc.io/docs/devices/chargers): - - ABL eMH1, Alfen (Eve), Bender (CC612/613), cFos (PowerBrain), Daheimladen, Ebee (Wallbox), Ensto (Chago Wallbox), [EVSEWifi/ smartWB](https://www.evse-wifi.de), Garo (GLB, GLB+, LS4), go-eCharger, HardyBarth (eCB1, cPH1, cPH2), Heidelberg (Energy Control), Innogy (eBox), Juice (Charger Me), KEBA/BMW, Mennekes (Amedio, Amtron Premium/Xtra, Amtron ChargeConrol), older NRGkicks (before 2022/2023), [openWB (includes Pro)](https://openwb.de/), Optec (Mobility One), PC Electric (includes Garo), Siemens, TechniSat (Technivolt), [Tinkerforge Warp Charger](https://www.warp-charger.com), Ubitricity (Heinz), Vestel, Wallbe, Webasto (Live), Mobile Charger Connect and many more + - ABL eMH1, Alfen (Eve), Bender (CC612/613), cFos (PowerBrain), Daheimladen, Ebee (Wallbox), Ensto (Chago Wallbox), [EVSEWifi/ smartWB](https://www.evse-wifi.de), Garo (GLB, GLB+, LS4), go-eCharger, HardyBarth (eCB1, cPH1, cPH2), Heidelberg (Energy Control), Innogy (eBox), Juice (Charger Me), KEBA/BMW, Mennekes (Amedio, Amtron Premium/Xtra, Amtron ChargeConrol), older NRGkicks (before 2022/2023), NRGKick Gen2,[openWB (includes Pro)](https://openwb.de/), Optec (Mobility One), PC Electric (includes Garo), Siemens, TechniSat (Technivolt), [Tinkerforge Warp Charger](https://www.warp-charger.com), Ubitricity (Heinz), Vestel, Wallbe, Webasto (Live), Mobile Charger Connect and many more - experimental EEBus support (Elli, PMCC) - experimental OCPP support - Build-your-own: Phoenix Contact (includes ESL Walli), [EVSE DIN](http://evracing.cz/simple-evse-wallbox) diff --git a/assets/js/api.js b/assets/js/api.js index 45859733fc..cd7fbd4d9e 100644 --- a/assets/js/api.js +++ b/assets/js/api.js @@ -5,11 +5,27 @@ const { protocol, hostname, port, pathname } = window.location; const base = protocol + "//" + hostname + (port ? ":" + port : "") + pathname; +// override the way axios serializes arrays in query parameters (a=1&a=2&a=3 instead of a[]=1&a[]=2&a[]=3) +function customParamsSerializer(params) { + const queryString = Object.keys(params) + .filter((key) => params[key] !== null) + .map((key) => { + const value = params[key]; + if (Array.isArray(value)) { + return value.map((v) => `${encodeURIComponent(key)}=${encodeURIComponent(v)}`).join("&"); + } + return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`; + }) + .join("&"); + return queryString; +} + const api = axios.create({ baseURL: base + "api/", headers: { Accept: "application/json", }, + paramsSerializer: customParamsSerializer, }); // global error handling diff --git a/assets/js/components/MultiSelect.vue b/assets/js/components/MultiSelect.vue new file mode 100644 index 0000000000..1173215103 --- /dev/null +++ b/assets/js/components/MultiSelect.vue @@ -0,0 +1,126 @@ + + + diff --git a/assets/js/router.js b/assets/js/router.js index b0b993f663..2bdaaab6e1 100644 --- a/assets/js/router.js +++ b/assets/js/router.js @@ -56,6 +56,13 @@ export default function setupRouter(i18n) { path: "/log", component: () => import("./views/Log.vue"), beforeEnter: ensureAuth, + props: (route) => { + const { areas, level } = route.query; + return { + areas: areas ? areas.split(",") : undefined, + level, + }; + }, }, ], }); diff --git a/assets/js/views/Log.vue b/assets/js/views/Log.vue index a22632570b..5e7a911a8e 100644 --- a/assets/js/views/Log.vue +++ b/assets/js/views/Log.vue @@ -47,29 +47,26 @@
- + {{ areasLabel }} +

@@ -112,14 +109,15 @@ import "@h2d2/shopicons/es/regular/download"; import TopHeader from "../components/TopHeader.vue"; import Play from "../components/MaterialIcon/Play.vue"; import Record from "../components/MaterialIcon/Record.vue"; +import MultiSelect from "../components/MultiSelect.vue"; import api from "../api"; import store from "../store"; -const LEVELS = ["FATAL", "ERROR", "WARN", "INFO", "DEBUG", "TRACE"]; -const DEFAULT_LEVEL = "DEBUG"; +const LEVELS = ["fatal", "error", "warn", "info", "debug", "trace"]; +const DEFAULT_LEVEL = "debug"; const DEFAULT_COUNT = 1000; -const levelMatcher = new RegExp(`\\[.*?\\] (${LEVELS.join("|")})`); +const levelMatcher = new RegExp(`\\[.*?\\] (${LEVELS.map((l) => l.toUpperCase()).join("|")})`); export default { name: "Log", @@ -127,14 +125,17 @@ export default { TopHeader, Play, Record, + MultiSelect, + }, + props: { + areas: { type: Array, default: () => [] }, + level: { type: String, default: DEFAULT_LEVEL }, }, data() { return { lines: [], - areas: [], + availableAreas: [], search: "", - level: DEFAULT_LEVEL, - area: "", timeout: null, levels: LEVELS, busy: false, @@ -168,6 +169,18 @@ export default { return { key, className, line }; }); }, + areaOptions() { + return this.availableAreas.map((area) => ({ name: area, value: area })); + }, + areasLabel() { + if (this.areas.length === 0) { + return this.$t("log.areas"); + } else if (this.areas.length === 1) { + return this.areas[0]; + } else { + return this.$t("log.nAreas", { count: this.areas.length }); + } + }, showMoreButton() { return this.lines.length === DEFAULT_COUNT; }, @@ -179,9 +192,9 @@ export default { if (this.level) { params.append("level", this.level); } - if (this.area) { - params.append("area", this.area); - } + this.areas.forEach((area) => { + params.append("area", area); + }); params.append("format", "txt"); return `./api/system/log?${params.toString()}`; }, @@ -189,6 +202,14 @@ export default { return this.timeout !== null; }, }, + watch: { + selectedAreas() { + this.updateLogs(); + }, + level() { + this.updateLogs(); + }, + }, methods: { async updateLogs(showAll) { // prevent concurrent requests @@ -198,8 +219,8 @@ export default { this.busy = true; const response = await api.get("/system/log", { params: { - level: this.level?.toLocaleLowerCase() || null, - area: this.area || null, + level: this.level || null, + area: this.areas.length ? this.areas : null, count: showAll ? null : DEFAULT_COUNT, }, }); @@ -232,7 +253,7 @@ export default { async updateAreas() { try { const response = await api.get("/system/log/areas"); - this.areas = response.data?.result || []; + this.availableAreas = response.data?.result || []; } catch (e) { console.error(e); } @@ -260,6 +281,22 @@ export default { this.startInterval(); } }, + updateQuery({ level, areas }) { + let newLevel = level || this.level; + let newAreas = areas || this.areas; + // reset to default level + if (newLevel === DEFAULT_LEVEL) newLevel = undefined; + newAreas = newAreas.length ? newAreas.join(",") : undefined; + this.$router.push({ + query: { level: newLevel, areas: newAreas }, + }); + }, + changeLevel(event) { + this.updateQuery({ level: event.target.value }); + }, + changeAreas(areas) { + this.updateQuery({ areas }); + }, }, }; diff --git a/charger/easee.go b/charger/easee.go index d710a25352..063c670737 100644 --- a/charger/easee.go +++ b/charger/easee.go @@ -208,8 +208,7 @@ func (c *Easee) chargerSite(charger string) (easee.Site, error) { // connect creates an HTTP connection to the signalR hub func (c *Easee) connect(ts oauth2.TokenSource) func() (signalr.Connection, error) { - bo := backoff.NewExponentialBackOff() - bo.MaxInterval = time.Minute + bo := backoff.NewExponentialBackOff(backoff.WithMaxInterval(time.Minute)) return func() (conn signalr.Connection, err error) { defer func() { diff --git a/charger/eebus.go b/charger/eebus.go index 8e8ae5ace8..76fef4e854 100644 --- a/charger/eebus.go +++ b/charger/eebus.go @@ -111,13 +111,6 @@ func NewEEBus(ski string, hasMeter, hasChargedEnergy, vasVW bool) (api.Charger, return c, nil } -func (c *EEBus) setEvEntity(entity spineapi.EntityRemoteInterface) { - c.mux.Lock() - defer c.mux.Unlock() - - c.ev = entity -} - func (c *EEBus) evEntity() spineapi.EntityRemoteInterface { c.mux.RLock() defer c.mux.RUnlock() @@ -139,15 +132,18 @@ var _ eebus.Device = (*EEBus)(nil) // UseCaseEvent implements the eebus.Device interface func (c *EEBus) UseCaseEvent(device spineapi.DeviceRemoteInterface, entity spineapi.EntityRemoteInterface, event eebusapi.EventType) { - switch event { + c.mux.Lock() + defer c.mux.Unlock() + // EV + switch event { case evcc.EvConnected: - c.setEvEntity(entity) + c.ev = entity c.reconnect = true c.currentLimit = -1 case evcc.EvDisconnected: - c.setEvEntity(nil) + c.ev = nil c.currentLimit = -1 } } @@ -561,6 +557,9 @@ var _ api.CurrentGetter = (*EEBus)(nil) // GetMaxCurrent implements the api.CurrentGetter interface func (c *EEBus) GetMaxCurrent() (float64, error) { + c.mux.RLock() + defer c.mux.RUnlock() + if c.currentLimit == -1 { return 0, api.ErrNotAvailable } diff --git a/charger/hardybarth-salia.go b/charger/hardybarth-salia.go index 9a8ecb3893..2dc8adbe1e 100644 --- a/charger/hardybarth-salia.go +++ b/charger/hardybarth-salia.go @@ -132,9 +132,9 @@ func NewSalia(uri string, cache time.Duration) (api.Charger, error) { } func (wb *Salia) heartbeat() { - bo := backoff.NewExponentialBackOff() - bo.InitialInterval = 5 * time.Second - bo.MaxInterval = time.Minute + bo := backoff.NewExponentialBackOff( + backoff.WithInitialInterval(5*time.Second), + backoff.WithMaxInterval(time.Minute)) for range time.Tick(30 * time.Second) { if err := backoff.Retry(func() error { diff --git a/charger/nrggen2.go b/charger/nrggen2.go new file mode 100644 index 0000000000..5afcc25094 --- /dev/null +++ b/charger/nrggen2.go @@ -0,0 +1,333 @@ +package charger + +import ( + "encoding/binary" + "fmt" + "math" + + "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/util" + "github.com/evcc-io/evcc/util/modbus" + "github.com/evcc-io/evcc/util/sponsor" + "github.com/spf13/cast" + "github.com/volkszaehler/mbmd/encoding" +) + +// https://www.nrgkick.com/wp-content/uploads/2024/07/local_api_docu_simulate.html + +// NRGKickGen2 charger implementation +type NRGKickGen2 struct { + conn *modbus.Connection +} + +const ( + // All register use LittleEndian + // Read only (0x03) + nrgKickGen2Serial = 0 // 11 regs + nrgKickGen2ModelType = 11 // 16 regs + nrgKickGen2MaxPhases = 36 + nrgKickGen2SoftwareVersionSM = 122 // 8 regs + // Read (0x03) / Write (0x06, 0x16) Registers + nrgKickGen2ChargingCurrent = 194 // A, factor 10 + nrgKickGen2Enabled = 195 + nrgKickGen2Phases = 198 + // Read only (0x03) + nrgKickGen2TotalChargedEnergy = 199 // Wh, 4 regs + nrgKickGen2ChargedEnergy = 203 // Wh, 2 regs + nrgKickGen2TotalActivePower = 210 // W, 2 regs, factor 1000 + nrgKickGen2PhaseVoltages = 217 // factor 100 + nrgKickGen2PhaseCurrents = 220 // factor 1000 + nrgKickGen2RegStatus = 251 + nrgKickGen2RegRelais = 253 + nrgKickGen2RegRCD = 255 + nrgKickGen2RegWarning = 256 + nrgKickGen2RegError = 257 +) + +func init() { + registry.Add("nrggen2", NewNRGKickGen2FromConfig) +} + +//go:generate go run ../cmd/tools/decorate.go -f decorateNRGKickGen2 -b *NRGKickGen2 -r api.Charger -t "api.PhaseSwitcher,Phases1p3p,func(int) error" + +// NewNRGKickGen2FromConfig creates a NRGKickGen2 charger from generic config +func NewNRGKickGen2FromConfig(other map[string]interface{}) (api.Charger, error) { + cc := struct { + modbus.TcpSettings `mapstructure:",squash"` + Phases1p3p bool + }{ + TcpSettings: modbus.TcpSettings{ + ID: 1, // default + }, + Phases1p3p: false, + } + + if err := util.DecodeOther(other, &cc); err != nil { + return nil, err + } + + nrg, err := NewNRGKickGen2(cc.URI, cc.ID) + if err != nil { + return nil, err + } + + var phasesS func(int) error + if cc.Phases1p3p { + // user could have an adapter plug which doesn't support 3 phases + if b, err := nrg.conn.ReadHoldingRegisters(nrgKickGen2MaxPhases, 1); err == nil { + if maxPhases := encoding.Uint16(b); maxPhases > 1 { + phasesS = nrg.phases1p3p + } + } + } + + return decorateNRGKickGen2(nrg, phasesS), nil +} + +// NewNRGKickGen2 creates NRGKickGen2 charger +func NewNRGKickGen2(uri string, slaveID uint8) (*NRGKickGen2, error) { + conn, err := modbus.NewConnection(uri, "", "", 0, modbus.Tcp, slaveID) + if err != nil { + return nil, err + } + + if !sponsor.IsAuthorized() { + return nil, api.ErrSponsorRequired + } + + log := util.NewLogger("nrggen2") + conn.Logger(log.TRACE) + + nrg := &NRGKickGen2{ + conn: conn, + } + + return nrg, nil +} + +// Status implements the api.Charger interface +func (nrg *NRGKickGen2) Status() (api.ChargeStatus, error) { + b, err := nrg.conn.ReadHoldingRegisters(nrgKickGen2RegStatus, 1) + if err != nil { + return api.StatusNone, err + } + + // 0 - "UNKNOWN", + // 1 - "STANDBY", + // 2 - "CONNECTED", + // 3 - "CHARGING", + // 6 - "ERROR", + // 7 - "WAKEUP" + switch status := binary.BigEndian.Uint16(b); status { + case 0: + return api.StatusNone, nil + case 1: + return api.StatusA, nil + case 2: + return api.StatusB, nil + case 3: + return api.StatusC, nil + case 6: + // 0 - "NO_ERROR", + // 1 - "GENERAL_ERROR", + // 2 - "32A_ATTACHMENT_ON_16A_UNIT", + // 3 - "VOLTAGE_DROP_DETECTED", + // 4 - "UNPLUG_DETECTION_TRIGGERED", + // 5 - "TYPE2_NOT_AUTHORIZED", + // 16 - "RESIDUAL_CURRENT_DETECTED", + // 32 - "CP_SIGNAL_VOLTAGE_ERROR", + // 33 - "CP_SIGNAL_IMPERMISSIBLE", + // 34 - "EV_DIODE_FAULT", + // 48 - "PE_SELF_TEST_FAILED", + // 49 - "RCD_SELF_TEST_FAILED", + // 50 - "RELAY_SELF_TEST_FAILED", + // 51 - "PE_AND_RCD_SELF_TEST_FAILED", + // 52 - "PE_AND_RELAY_SELF_TEST_FAILED", + // 53 - "RCD_AND_RELAY_SELF_TEST_FAILED", + // 54 - "PE_AND_RCD_AND_RELAY_SELF_TEST_FAILED", + // 64 - "SUPPLY_VOLTAGE_ERROR", + // 65 - "PHASE_SHIFT_ERROR", + // 66 - "OVERVOLTAGE_DETECTED", + // 67 - "UNDERVOLTAGE_DETECTED", + // 68 - "OVERVOLTAGE_WITHOUT_PE_DETECTED", + // 69 - "UNDERVOLTAGE_WITHOUT_PE_DETECTED", + // 70 - "UNDERFREQUENCY_DETECTED", + // 71 - "OVERFREQUENCY_DETECTED", + // 72 - "UNKNOWN_FREQUENCY_TYPE", + // 73 - "UNKNOWN_GRID_TYPE", + // 80 - "GENERAL_OVERTEMPERATURE", + // 81 - "HOUSING_OVERTEMPERATURE", + // 82 - "ATTACHMENT_OVERTEMPERATURE", + // 83 - "DOMESTIC_PLUG_OVERTEMPERATURE", + // x - "UNKNOWN" + b, err := nrg.conn.ReadHoldingRegisters(nrgKickGen2RegError, 1) + if err != nil { + return api.StatusNone, err + } + return api.StatusF, fmt.Errorf("%d", binary.BigEndian.Uint16(b)) + case 7: + return api.StatusB, nil + default: + return api.StatusNone, fmt.Errorf("unhandled status type") + } +} + +// Enabled implements the api.Charger interface +func (nrg *NRGKickGen2) Enabled() (bool, error) { + b, err := nrg.conn.ReadHoldingRegisters(nrgKickGen2Enabled, 1) + if err != nil { + return false, err + } + + // 0 = no charge pause, 1 = charge pause + return binary.BigEndian.Uint16(b) == 0, nil +} + +// Enable implements the api.Charger interface +func (nrg *NRGKickGen2) Enable(enable bool) error { + _, err := nrg.conn.WriteSingleRegister(nrgKickGen2Enabled, cast.ToUint16(!enable)) + return err +} + +// MaxCurrent implements the api.Charger interface +func (nrg *NRGKickGen2) MaxCurrent(current int64) error { + return nrg.MaxCurrentMillis(float64(current)) +} + +var _ api.ChargerEx = (*NRGKickGen2)(nil) + +// MaxCurrentMillis implements the api.ChargerEx interface +func (nrg *NRGKickGen2) MaxCurrentMillis(current float64) error { + if current < 6 { + return fmt.Errorf("invalid current %.1f", current) + } + + _, err := nrg.conn.WriteSingleRegister(nrgKickGen2ChargingCurrent, uint16(math.Trunc(current*10))) + return err +} + +func (nrg *NRGKickGen2) GetMaxCurrent() (float64, error) { + b, err := nrg.conn.ReadHoldingRegisters(nrgKickGen2ChargingCurrent, 1) + if err != nil { + return 0, err + } + + return float64(binary.BigEndian.Uint16(b)) / 10, nil +} + +var _ api.Meter = (*NRGKickGen2)(nil) + +// CurrentPower implements the api.Meter interface +func (nrg *NRGKickGen2) CurrentPower() (float64, error) { + b, err := nrg.conn.ReadHoldingRegisters(nrgKickGen2TotalActivePower, 2) + if err != nil { + return 0, err + } + + return float64(encoding.Int32LswFirst(b)) * 1e-3, nil +} + +var _ api.MeterEnergy = (*NRGKickGen2)(nil) + +// TotalEnergy implements the api.MeterEnergy interface +func (nrg *NRGKickGen2) TotalEnergy() (float64, error) { + b, err := nrg.conn.ReadHoldingRegisters(nrgKickGen2TotalChargedEnergy, 4) + if err != nil { + return 0, err + } + + return float64(encoding.Uint64LswFirst(b)) * 1e-3, nil +} + +var _ api.PhaseCurrents = (*NRGKickGen2)(nil) + +// Currents implements the api.PhaseCurrents interface +func (nrg *NRGKickGen2) Currents() (float64, float64, float64, error) { + b, err := nrg.conn.ReadHoldingRegisters(nrgKickGen2PhaseCurrents, 3) + if err != nil { + return 0, 0, 0, err + } + + var res [3]float64 + for i := range res { + res[i] = float64(binary.BigEndian.Uint16(b[2*i:])) * 1e-3 + } + + return res[0], res[1], res[2], nil +} + +var _ api.PhaseVoltages = (*NRGKickGen2)(nil) + +// Currents implements the api.PhaseVoltages interface +func (nrg *NRGKickGen2) Voltages() (float64, float64, float64, error) { + b, err := nrg.conn.ReadHoldingRegisters(nrgKickGen2PhaseVoltages, 3) + if err != nil { + return 0, 0, 0, err + } + + var res [3]float64 + for i := range res { + res[i] = float64(binary.BigEndian.Uint16(b[2*i:])) * 1e-2 + } + + return res[0], res[1], res[2], nil +} + +var _ api.ChargeRater = (*NRGKickGen2)(nil) + +func (nrg *NRGKickGen2) ChargedEnergy() (float64, error) { + b, err := nrg.conn.ReadHoldingRegisters(nrgKickGen2ChargedEnergy, 2) + if err != nil { + return 0, err + } + + return float64(encoding.Uint32LswFirst(b)) * 1e-3, nil +} + +// Phases1p3p implements the api.PhaseSwitcher interface +func (nrg *NRGKickGen2) phases1p3p(phases int) error { + // this can return an error, if phase switching isn't activated via the App + _, err := nrg.conn.WriteSingleRegister(nrgKickGen2Phases, uint16(phases)) + return err +} + +var _ api.PhaseGetter = (*NRGKickGen2)(nil) + +func (nrg *NRGKickGen2) GetPhases() (int, error) { + b, err := nrg.conn.ReadHoldingRegisters(nrgKickGen2Phases, 1) + if err != nil { + return 0, err + } + + return int(binary.BigEndian.Uint16(b)), nil +} + +var _ api.Diagnosis = (*NRGKickGen2)(nil) + +// Diagnose implements the api.Diagnosis interface +func (nrg *NRGKickGen2) Diagnose() { + if b, err := nrg.conn.ReadHoldingRegisters(nrgKickGen2Serial, 11); err == nil { + fmt.Printf("\tSerial:\t%s\n", bytesAsString(b)) + } + if b, err := nrg.conn.ReadHoldingRegisters(nrgKickGen2ModelType, 16); err == nil { + fmt.Printf("\tModel:\t%s\n", bytesAsString(b)) + } + if b, err := nrg.conn.ReadHoldingRegisters(nrgKickGen2SoftwareVersionSM, 8); err == nil { + fmt.Printf("\tSmartModule Version:\t%s\n", bytesAsString(b)) + } + if b, err := nrg.conn.ReadHoldingRegisters(nrgKickGen2RegStatus, 1); err == nil { + fmt.Printf("\tStatus:\t%d\n", binary.BigEndian.Uint16(b)) + } + if b, err := nrg.conn.ReadHoldingRegisters(nrgKickGen2RegRelais, 1); err == nil { + fmt.Printf("\tRelais Switching:\t%d\n", binary.BigEndian.Uint16(b)) + } + if b, err := nrg.conn.ReadHoldingRegisters(nrgKickGen2RegRCD, 1); err == nil { + fmt.Printf("\tRCD:\t%d\n", binary.BigEndian.Uint16(b)) + } + if b, err := nrg.conn.ReadHoldingRegisters(nrgKickGen2RegWarning, 1); err == nil { + fmt.Printf("\tWarning:\t%d\n", binary.BigEndian.Uint16(b)) + } + if b, err := nrg.conn.ReadHoldingRegisters(nrgKickGen2RegError, 1); err == nil { + fmt.Printf("\tError:\t%d\n", binary.BigEndian.Uint16(b)) + } +} diff --git a/charger/nrggen2_decorators.go b/charger/nrggen2_decorators.go new file mode 100644 index 0000000000..c2db71ca97 --- /dev/null +++ b/charger/nrggen2_decorators.go @@ -0,0 +1,35 @@ +package charger + +// Code generated by github.com/evcc-io/evcc/cmd/tools/decorate.go. DO NOT EDIT. + +import ( + "github.com/evcc-io/evcc/api" +) + +func decorateNRGKickGen2(base *NRGKickGen2, phaseSwitcher func(int) error) api.Charger { + switch { + case phaseSwitcher == nil: + return base + + case phaseSwitcher != nil: + return &struct { + *NRGKickGen2 + api.PhaseSwitcher + }{ + NRGKickGen2: base, + PhaseSwitcher: &decorateNRGKickGen2PhaseSwitcherImpl{ + phaseSwitcher: phaseSwitcher, + }, + } + } + + return nil +} + +type decorateNRGKickGen2PhaseSwitcherImpl struct { + phaseSwitcher func(int) error +} + +func (impl *decorateNRGKickGen2PhaseSwitcherImpl) Phases1p3p(p0 int) error { + return impl.phaseSwitcher(p0) +} diff --git a/charger/ocpp.go b/charger/ocpp.go index 81663f86e5..324e8d8962 100644 --- a/charger/ocpp.go +++ b/charger/ocpp.go @@ -239,6 +239,11 @@ func NewOCPP(id string, connector int, idtag string, c.idtag = *opt.Value c.log.DEBUG.Printf("overriding default `idTag` with Alfen-specific value: %s", c.idtag) } + + case ocpp.KeyEvBoxSupportedMeasurands: + if meterValues == "" { + meterValues = *opt.Value + } } if err != nil { diff --git a/charger/ocpp/const.go b/charger/ocpp/const.go index 23adf372ae..872f0fd679 100644 --- a/charger/ocpp/const.go +++ b/charger/ocpp/const.go @@ -17,4 +17,5 @@ const ( // Vendor specific keys KeyAlfenPlugAndChargeIdentifier = "PlugAndChargeIdentifier" + KeyEvBoxSupportedMeasurands = "evb_SupportedMeasurands" ) diff --git a/charger/pulsatrix.go b/charger/pulsatrix.go index 5ff0ee4422..87c69a548f 100644 --- a/charger/pulsatrix.go +++ b/charger/pulsatrix.go @@ -35,11 +35,11 @@ import ( "time" "github.com/cenkalti/backoff/v4" + "github.com/coder/websocket" "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/request" "github.com/evcc-io/evcc/util/sponsor" - "nhooyr.io/websocket" ) // pulsatrix charger implementation @@ -124,10 +124,10 @@ func (c *Pulsatrix) connectWs() error { // ReconnectWs reconnects to a pulsatrix SECC websocket func (c *Pulsatrix) reconnectWs() { - bo := backoff.NewExponentialBackOff() - bo.InitialInterval = time.Second - bo.MaxInterval = 1 * time.Minute - bo.MaxElapsedTime = 0 * time.Second // retry forever; default is 15 min + bo := backoff.NewExponentialBackOff( + backoff.WithInitialInterval(time.Second), + backoff.WithMaxInterval(time.Minute), + backoff.WithMaxElapsedTime(0)) // retry forever; default is 15 min if err := backoff.Retry(c.connectWs, bo); err != nil { c.log.ERROR.Println(err) } diff --git a/cmd/charger.go b/cmd/charger.go index 06f3cba49a..ae6c2f9cbd 100644 --- a/cmd/charger.go +++ b/cmd/charger.go @@ -19,7 +19,7 @@ var chargerCmd = &cobra.Command{ func init() { rootCmd.AddCommand(chargerCmd) - chargerCmd.Flags().IntP(flagCurrent, "i", 0, flagCurrentDescription) + chargerCmd.Flags().Float64P(flagCurrent, "i", 0, flagCurrentDescription) //lint:ignore SA1019 as Title is safe on ascii chargerCmd.Flags().BoolP(flagEnable, "e", false, strings.Title(flagEnable)) //lint:ignore SA1019 as Title is safe on ascii @@ -60,13 +60,19 @@ func runCharger(cmd *cobra.Command, args []string) { if flag := cmd.Flags().Lookup(flagCurrent); flag.Changed { flagUsed = true - current, err := strconv.ParseInt(flag.Value.String(), 10, 64) + current, err := strconv.ParseFloat(flag.Value.String(), 64) if err != nil { log.ERROR.Fatalln(err) } - if err := v.MaxCurrent(current); err != nil { - log.ERROR.Println("set current:", err) + if vv, ok := v.(api.ChargerEx); ok { + if err := vv.MaxCurrentMillis(current); err != nil { + log.ERROR.Println("set current:", err) + } + } else { + if err := v.MaxCurrent(int64(current)); err != nil { + log.ERROR.Println("set current:", err) + } } } diff --git a/cmd/check_config.go b/cmd/check_config.go index 6ba015ad14..088ae24649 100644 --- a/cmd/check_config.go +++ b/cmd/check_config.go @@ -11,7 +11,10 @@ import ( var checkconfig = &cobra.Command{ Use: "checkconfig", Short: "Check config file for errors", - Run: runConfigCheck, + Long: `Check the (specified or default) config file for errors. Note that + checkconfig only checks the config file for parsing errors and does + not check that individual device configurations are valid.`, + Run: runConfigCheck, } func init() { diff --git a/core/helper.go b/core/helper.go index 846287e24d..5986c28e89 100644 --- a/core/helper.go +++ b/core/helper.go @@ -18,9 +18,7 @@ var ( // bo returns an exponential backoff for reading meter power quickly func bo() *backoff.ExponentialBackOff { - bo := backoff.NewExponentialBackOff() - bo.MaxElapsedTime = time.Second - return bo + return backoff.NewExponentialBackOff(backoff.WithMaxElapsedTime(time.Second)) } // powerToCurrent is a helper function to convert power to per-phase current diff --git a/core/keys/site.go b/core/keys/site.go index 07254c0db8..3d5417a86c 100644 --- a/core/keys/site.go +++ b/core/keys/site.go @@ -29,11 +29,13 @@ const ( TariffPriceLoadpoints = "tariffPriceLoadpoints" Vehicles = "vehicles" Circuits = "circuits" + Ext = "ext" // meters GridMeter = "gridMeter" PvMeters = "pvMeters" BatteryMeters = "batteryMeters" + ExtMeters = "extMeters" AuxMeters = "auxMeters" // battery settings diff --git a/core/loadpoint.go b/core/loadpoint.go index be6bb03e00..643f0e38ff 100644 --- a/core/loadpoint.go +++ b/core/loadpoint.go @@ -1369,10 +1369,7 @@ func (lp *Loadpoint) pvMaxCurrent(mode api.ChargeMode, sitePower float64, batter // UpdateChargePowerAndCurrents updates charge meter power and currents for load management func (lp *Loadpoint) UpdateChargePowerAndCurrents() { - bo := backoff.NewExponentialBackOff() - bo.MaxElapsedTime = time.Second - - if power, err := backoff.RetryWithData(lp.chargeMeter.CurrentPower, bo); err == nil { + if power, err := backoff.RetryWithData(lp.chargeMeter.CurrentPower, bo()); err == nil { lp.Lock() lp.chargePower = power // update value if no error lp.Unlock() @@ -1409,7 +1406,7 @@ func (lp *Loadpoint) UpdateChargePowerAndCurrents() { lp.publish(keys.ChargeCurrents, lp.chargeCurrents) return nil - }, bo); err != nil { + }, bo()); err != nil { lp.log.ERROR.Printf("charge currents: %v", err) } } diff --git a/core/site.go b/core/site.go index 6a5a135059..f663c4e62d 100644 --- a/core/site.go +++ b/core/site.go @@ -82,6 +82,7 @@ type Site struct { gridMeter api.Meter // Grid usage meter pvMeters []api.Meter // PV generation meters batteryMeters []api.Meter // Battery charging meters + extMeters []api.Meter // External meters - for monitoring only auxMeters []api.Meter // Auxiliary meters // battery settings @@ -112,6 +113,7 @@ type MetersConfig struct { GridMeterRef string `mapstructure:"grid"` // Grid usage meter PVMetersRef []string `mapstructure:"pv"` // PV meter BatteryMetersRef []string `mapstructure:"battery"` // Battery charging meter + ExtMetersRef []string `mapstructure:"ext"` // Meters used only for monitoring AuxMetersRef []string `mapstructure:"aux"` // Auxiliary meters } @@ -209,6 +211,15 @@ func (site *Site) Boot(log *util.Logger, loadpoints []*Loadpoint, tariffs *tarif site.log.WARN.Println("battery configured but residualPower is missing or <= 0 (add residualPower: 100 to site), see https://docs.evcc.io/en/docs/reference/configuration/site#residualpower") } + // Meters used only for monitoring + for _, ref := range site.Meters.ExtMetersRef { + dev, err := config.Meters().ByName(ref) + if err != nil { + return err + } + site.extMeters = append(site.extMeters, dev.Instance()) + } + // auxiliary meters for _, ref := range site.Meters.AuxMetersRef { dev, err := config.Meters().ByName(ref) @@ -258,6 +269,9 @@ func (site *Site) restoreMetersAndTitle() { if v, err := settings.String(keys.BatteryMeters); err == nil && v != "" { site.Meters.BatteryMetersRef = append(site.Meters.BatteryMetersRef, filterConfigurable(strings.Split(v, ","))...) } + if v, err := settings.String(keys.ExtMeters); err == nil && v != "" { + site.Meters.ExtMetersRef = append(site.Meters.ExtMetersRef, filterConfigurable(strings.Split(v, ","))...) + } if v, err := settings.String(keys.AuxMeters); err == nil && v != "" { site.Meters.AuxMetersRef = append(site.Meters.AuxMetersRef, filterConfigurable(strings.Split(v, ","))...) } @@ -469,6 +483,39 @@ func (site *Site) updatePvMeters() { site.publish(keys.Pv, mm) } +// updateExtMeters updates ext meters. All measurements are optional. +func (site *Site) updateExtMeters() { + if len(site.extMeters) == 0 { + return + } + + mm := make([]meterMeasurement, len(site.extMeters)) + + for i, meter := range site.extMeters { + // ext power + power, err := backoff.RetryWithData(meter.CurrentPower, bo()) + if err != nil { + site.log.ERROR.Printf("ext meter %d power: %v", i+1, err) + } + + // ext energy + var energy float64 + if m, ok := meter.(api.MeterEnergy); err == nil && ok { + energy, err = m.TotalEnergy() + if err != nil { + site.log.ERROR.Printf("ext meter %d energy: %v", i+1, err) + } + } + + mm[i] = meterMeasurement{ + Power: power, + Energy: energy, + } + } + + // Publishing will be done in separate PR +} + // updateBatteryMeters updates battery meters. Power is retried, other measurements are optional. func (site *Site) updateBatteryMeters() error { if len(site.batteryMeters) == 0 { @@ -615,6 +662,7 @@ func (site *Site) updateMeters() error { if err := site.updateBatteryMeters(); err != nil { return err } + site.updateExtMeters() return site.updateGridMeter() } diff --git a/go.mod b/go.mod index cb29bf5ee9..6968b318b4 100644 --- a/go.mod +++ b/go.mod @@ -18,15 +18,16 @@ require ( github.com/bogosj/tesla v1.3.1 github.com/cenkalti/backoff/v4 v4.3.0 github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21 + github.com/coder/websocket v1.8.12 github.com/containrrr/shoutrrr v0.8.0 github.com/coreos/go-oidc/v3 v3.10.0 github.com/denisbrodbeck/machineid v1.0.1 github.com/dmarkham/enumer v1.5.10 github.com/dylanmei/iso8601 v0.1.0 github.com/eclipse/paho.mqtt.golang v1.4.3 - github.com/enbility/eebus-go v0.6.1 - github.com/enbility/ship-go v0.5.2 - github.com/enbility/spine-go v0.6.1 + github.com/enbility/eebus-go v0.6.2 + github.com/enbility/ship-go v0.5.3 + github.com/enbility/spine-go v0.6.2 github.com/evcc-io/tesla-proxy-client v0.0.0-20240221194046-4168b3759701 github.com/fatih/structs v1.1.0 github.com/glebarez/sqlite v1.11.0 @@ -105,7 +106,6 @@ require ( google.golang.org/protobuf v1.34.2 gopkg.in/yaml.v3 v3.0.1 gorm.io/gorm v1.25.10 - nhooyr.io/websocket v1.8.11 ) require ( @@ -193,16 +193,11 @@ require ( modernc.org/mathutil v1.6.0 // indirect modernc.org/memory v1.8.0 // indirect modernc.org/sqlite v1.30.1 // indirect + nhooyr.io/websocket v1.8.17 // indirect ) replace gopkg.in/yaml.v3 => github.com/andig/yaml v0.0.0-20240531135838-1ff5761ab467 replace github.com/grid-x/modbus => github.com/evcc-io/modbus v0.0.0-20240503125516-9fd99fe0e438 -replace github.com/enbility/eebus-go => github.com/enbility/eebus-go v0.0.0-20240807063658-b851a17d9f25 - -replace github.com/enbility/spine-go => github.com/enbility/spine-go v0.0.0-20240806132249-c994673d74e4 - -replace github.com/enbility/ship-go => github.com/enbility/ship-go v0.0.0-20240806195332-a545a1063e94 - replace github.com/lorenzodonini/ocpp-go => github.com/evcc-io/ocpp-go v0.0.0-20240730071053-d69e53b0fce9 diff --git a/go.sum b/go.sum index 7c28715edf..f21842df7c 100644 --- a/go.sum +++ b/go.sum @@ -84,6 +84,8 @@ github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21 h1:tuij github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21/go.mod h1:po7NpZ/QiTKzBKyrsEAxwnTamCoh8uDk/egRpQ7siIc= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= +github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= +github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= github.com/containrrr/shoutrrr v0.8.0 h1:mfG2ATzIS7NR2Ec6XL+xyoHzN97H8WPjir8aYzJUSec= github.com/containrrr/shoutrrr v0.8.0/go.mod h1:ioyQAyu1LJY6sILuNyKaQaw+9Ttik5QePU8atnAdO2o= github.com/coreos/go-oidc/v3 v3.10.0 h1:tDnXHnLyiTVyT/2zLDGj09pFPkhND8Gl8lnTRhoEaJU= @@ -121,12 +123,12 @@ github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFP github.com/eclipse/paho.mqtt.golang v1.4.3 h1:2kwcUGn8seMUfWndX0hGbvH8r7crgcJguQNCyp70xik= github.com/eclipse/paho.mqtt.golang v1.4.3/go.mod h1:CSYvoAlsMkhYOXh/oKyxa8EcBci6dVkLCbo5tTC1RIE= github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= -github.com/enbility/eebus-go v0.0.0-20240807063658-b851a17d9f25 h1:FVgRp1riElkVOQNazirRcWp+st/lefSoxY1NuLc/CKE= -github.com/enbility/eebus-go v0.0.0-20240807063658-b851a17d9f25/go.mod h1:XulY17uTjq65MWG4LQh28C/D6ogXBUa1e8nNNKssPg4= -github.com/enbility/ship-go v0.0.0-20240806195332-a545a1063e94 h1:DKPRgPnl3PywWpJ5KfXIrpe92WF3qeZJ0ntnyirmPH4= -github.com/enbility/ship-go v0.0.0-20240806195332-a545a1063e94/go.mod h1:jewJWYQ10jNhsnhS1C4jESx3CNmDa5HNWZjBhkTug5Y= -github.com/enbility/spine-go v0.0.0-20240806132249-c994673d74e4 h1:cHXwGD/jkB740sPDKEU6DxRPeZt29MV50EmZY3Ovp6c= -github.com/enbility/spine-go v0.0.0-20240806132249-c994673d74e4/go.mod h1:pRGS+C5rZ5rhxTAA1whU8fC9p7lH5ixyut++yEZe470= +github.com/enbility/eebus-go v0.6.2 h1:/NO3KEboFnVnTGNMKoIKyICE8wK83vG1kcZSbhzXM1U= +github.com/enbility/eebus-go v0.6.2/go.mod h1:G9MOCaHFHx9nkOQkZfQvtkINB/wt5T7KuH/UaAQCSys= +github.com/enbility/ship-go v0.5.3 h1:P8eA/WDz3hq18zEKzqCB697OA1sVFiZkTF51Cx4tyGU= +github.com/enbility/ship-go v0.5.3/go.mod h1:jewJWYQ10jNhsnhS1C4jESx3CNmDa5HNWZjBhkTug5Y= +github.com/enbility/spine-go v0.6.2 h1:uxEUGLcaaA3PzkaTYTe4Ic64PElFlMQcfcES0O4Dk1c= +github.com/enbility/spine-go v0.6.2/go.mod h1:6AbRXzd0fLVGFJdT60YQACe1WskwdjiznCfljQ+Ud6s= github.com/enbility/zeroconf/v2 v2.0.0-20240210101930-d0004078577b h1:sg3c6LJ4eWffwtt9SW0lgcIX4Oh274vwdJnNFNNrDco= github.com/enbility/zeroconf/v2 v2.0.0-20240210101930-d0004078577b/go.mod h1:BjzRRiYX6mWdOgku1xxDE+NsV8PijTby7Q7BkYVdfDU= github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= @@ -943,8 +945,8 @@ modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= -nhooyr.io/websocket v1.8.11 h1:f/qXNc2/3DpoSZkHt1DQu6rj4zGC8JmkkLkWss0MgN0= -nhooyr.io/websocket v1.8.11/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c= +nhooyr.io/websocket v1.8.17 h1:KEVeLJkUywCKVsnLIDlD/5gtayKp8VoCkksHCGGfT9Y= +nhooyr.io/websocket v1.8.17/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c= pgregory.net/rapid v1.1.0 h1:CMa0sjHSru3puNx+J0MIAuiiEV4N0qj8/cMWGBBCsjw= pgregory.net/rapid v1.1.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= diff --git a/i18n/da.toml b/i18n/da.toml index f3d398185d..1cbc843cf5 100644 --- a/i18n/da.toml +++ b/i18n/da.toml @@ -364,8 +364,10 @@ areaLabel = "Filtrer per område" areas = "Alle områder" download = "Hent hele log filen" levelLabel = "Filtrer på log niveau" +nAreas = "{count} områder" noResults = "Ingen matchende logposter" search = "Søg" +selectAll = "vælg alt" showAll = "Vis alle poster" title = "Log filer" update = "Opdater automatisk" diff --git a/i18n/de.toml b/i18n/de.toml index 3af110a2b6..654bac129c 100644 --- a/i18n/de.toml +++ b/i18n/de.toml @@ -10,8 +10,8 @@ legendBottomName = "Priorisiere die Hausbatterie" legendBottomSubline = "bis sie {soc} erreicht hat." legendMiddleName = "Priorisiere Fahrzeugladen," legendMiddleSubline = "wenn Hausbatterie über {soc} ist." -legendTopAutostart = "Starte automatisch," -legendTopName = "Batterieunterstütztes Fahrzeugladen," +legendTopAutostart = "Starte automatisch" +legendTopName = "Batterieunterstütztes Fahrzeugladen" legendTopSubline = "wenn Hausbatterie über {soc} ist." modalTitle = "Hausbatterie" usageTab = "Batterienutzung" @@ -103,7 +103,7 @@ titleAdd = "Netzzähler hinzufügen" titleEdit = "Netzzähler bearbeiten" [config.hems] -description = "Connect evcc to another home energy management system." +description = "evcc mit einem anderen Hausenergiemanagementsystem verbinden." title = "HEMS" [config.influx] @@ -158,7 +158,7 @@ labelCheckInsecure = "Erlaube unsichere Verbindungen" labelClientId = "Client ID" labelInsecure = "Zertifikatüberprüfung" labelPassword = "Passwort" -labelTopic = "Topic" +labelTopic = "Thema" labelUser = "Benutzer" publishing = "Veröffentlichen" title = "MQTT" @@ -174,6 +174,10 @@ title = "Netzwerk" [config.options] +[config.options.boolean] +no = "nein" +yes = "ja" + [config.options.endianness] big = "big-endian" little = "little-endian" @@ -237,7 +241,7 @@ generic = "Weitere Integrationen" offline = "Generisches Fahrzeug" online = "Fahrzeuge mit Schnittstelle" save = "Speichern" -scooter = "Tretroller" +scooter = "Elektroroller" template = "Hersteller" titleAdd = "Fahrzeug hinzufügen" titleEdit = "Fahrzeug bearbeiten" @@ -250,7 +254,7 @@ greenEnergy = "Sonne" greenEnergySub1 = "über evcc geladen" greenEnergySub2 = "seit Oktober 2022" greenShare = "Sonnenanteil" -greenShareSub1 = "der Leistung kommt" +greenShareSub1 = "Strom bereitgestellt durch" greenShareSub2 = "von PV und Speicher" power = "Ladeleistung" powerSub1 = "{activeClients} von {totalClients} Nutzern" @@ -280,12 +284,12 @@ tabTitle = "Meine Daten" total = "gesamt" [footer.sponsor] -becomeSponsor = "Sponsor werden" +becomeSponsor = "Werden Sie Sponsor" becomeSponsorExtended = "Unterstütze uns direkt. Es gibt auch Sticker." confetti = "Lust auf Konfetti?" confettiPromise = "Es gibt auch Sticker und digitales Konfetti" sticker = "… oder evcc Sticker?" -supportUs = "Unsere Mission: Sonne tanken zum Standard machen. Hilf uns und unterstütze evcc finanziell." +supportUs = "Unsere Mission: Sonne tanken zum Standard machen. Zusammen mit Ihrer finanziellen Unterstützung, können dir es ermöglichen." thanks = "Vielen Dank, {sponsor}! Dein Beitrag hilft, evcc weiterzuentwickeln." titleNoSponsor = "Unterstütze uns" titleSponsor = "Du bist Unterstützer" @@ -348,8 +352,10 @@ areaLabel = "Nach Bereich filtern" areas = "Alle Bereiche" download = "Komplettes Log herunterladen" levelLabel = "Nach Log-Level filtern" +nAreas = "{count} Bereiche" noResults = "Keine passenden Einträge gefunden." search = "Suchen" +selectAll = "alle wählen" showAll = "Alle Einträge anzeigen" title = "Logs" update = "Aktualisieren" @@ -422,7 +428,7 @@ currents = "Ladestrom" default = "default" disclaimerHint = "Hinweis:" onlyForSocBasedCharging = "Diese Optionen sind nur für Fahrzeuge mit bekanntem Ladestand verfügbar." -smartCostCheap = "Günstiges Netzladen" +smartCostCheap = "günstige Netzladung" smartCostClean = "Grünes Netzladen" title = "Einstellungen {0}" vehicle = "Fahrzeug" @@ -455,7 +461,7 @@ minpv = "Min+PV" now = "Schnell" off = "Aus" pv = "PV" -smart = "Smart" +smart = "Klever" [main.provider] login = "anmelden" diff --git a/i18n/en.toml b/i18n/en.toml index 655bbf33f6..5ca4eb1094 100644 --- a/i18n/en.toml +++ b/i18n/en.toml @@ -350,8 +350,10 @@ areaLabel = "Filter by area" areas = "All areas" download = "Download complete log" levelLabel = "Filter by log level" +nAreas = "{count} areas" noResults = "No matching log entries." search = "Search" +selectAll = "select all" showAll = "Show all entries" title = "Logs" update = "Auto update" diff --git a/i18n/fr.toml b/i18n/fr.toml index 5446aad4cd..5ea5bb773a 100644 --- a/i18n/fr.toml +++ b/i18n/fr.toml @@ -18,7 +18,7 @@ modalTitle = "Batterie domestique" usageTab = "Utilisation batterie" [batterySettings.bufferStart] -above = "lorsque chargé à plus que {soc}" +above = "lorsque chargé à plus que {soc}." full = "quand chargé à {soc}." low = "quand partiellement pleine" medium = "quand à moitié pleine" @@ -246,7 +246,7 @@ generic = "Autres intégrations" offline = "Véhicule générique" online = "Véhicules avec API en ligne" save = "Enregistrer" -scooter = "Scooter" +scooter = "Trottinette" template = "Fabricant" titleAdd = "Ajouter le véhicule" titleEdit = "Modifier le véhicule" @@ -369,8 +369,10 @@ areaLabel = "Filtrer par zone" areas = "Toutes les zones" download = "Télécharger les journaux complets" levelLabel = "Filtrer par niveau" +nAreas = "{count} zones" noResults = "Aucune entrée de journal ne correspond." search = "Rechercher" +selectAll = "sélectionner tout" showAll = "Voir toutes les entrées" title = "Journaux" update = "Mise à jour automatique" diff --git a/i18n/hu.toml b/i18n/hu.toml index 30c28a544a..8c176cab2c 100644 --- a/i18n/hu.toml +++ b/i18n/hu.toml @@ -5,20 +5,22 @@ control = "Akkumulátor vezérlés" discharge = "Kisütés megakadályoza gyorstöltési és tervezett töltési üzemmódban." disclaimerHint = "Megjegyzés:" disclaimerText = "Ezek a beállítások csak a szolár üzemmódot érintik. A töltés módja ennek megfelelően kerül beállításra." -legendBottomName = "otthoni prioritás" -legendBottomSubline = "nincs töltésre használva" -legendMiddleName = "jármű először" -legendMiddleSubline = "otthon másodszor" +gridChargeTab = "Hálózati töltés" +legendBottomName = "Otthoni akkumulátoros töltés priorizálása" +legendBottomSubline = "ameddig eléri {soc}-ot." +legendMiddleName = "Jármű töltésének priorizálása" +legendMiddleSubline = "amikor az otthoni akkumulátor {soc} felett van." legendTitle = "Hogyan használjuk fel a napenergiát?" -legendTopAutostart = "automatikusan indul" -legendTopName = "energiatárolóval támogatott töltés" -legendTopSubline = "megszakítások nélkül" +legendTopAutostart = "Automatikusan indul" +legendTopName = "Energiatárolóval támogatott töltés" +legendTopSubline = "amikor az otthoni akkumulátor {soc} felett van." modalTitle = "Otthoni Akkumulátor" +usageTab = "Akkumulátor használat" [batterySettings.bufferStart] -above = "amikor {soc} felett van" -full = "amikor {soc}-on van" -never = "csak ha van elég többlet" +above = "amikor {soc} felett van." +full = "amikor {soc}-on van." +never = "csak ha van elég többlet." [config] @@ -54,6 +56,7 @@ co2 = "Hálózat CO₂" configured = "Konfigurálva" currency = "Valuta" current = "Jelenlegi" +currentRange = "Áram" enabled = "Engedélyezve" energy = "Energia" feedinPrice = "Kötelező átvételi ár" @@ -65,6 +68,7 @@ phaseCurrents = "Áram L1..L3" phasePowers = "Teljesítmény L1..L3" phaseVoltages = "Feszültség L1..L3" power = "Teljesítmény" +powerRange = "Teljesítmény" range = "Hatótáv" soc = "SoC" socLimit = "Limit" @@ -204,7 +208,7 @@ labelToken = "Szponzor token" title = "Szponzoráció" [config.system] -logs = "Naplók" +logs = "Napló" restart = "Újraindítás" restartRequiredDescription = "Kérlek indítsd újra a hatás eléréséhez." restartRequiredMessage = "A Konfiguráció megváltozott." @@ -349,7 +353,7 @@ levelLabel = "Szűrés a napló szintje alapján" noResults = "Nincs egyező napló bejegyzés." search = "Keresés" showAll = "Az összes bejegyzés megjelenítése" -title = "Naplók" +title = "Napló" update = "Automatikus frissítés" [loginModal] @@ -388,9 +392,11 @@ batteryCharge = "Energiatároló töltés" batteryDischarge = "Energiatároló kisütés" batteryHold = "Energiatároló (lezárva)" batteryTooltip = "{energy} / {total} ({soc})" +cheapBatteryGridCharge = "olcsó hálózati energia" +cleanBatteryGridCharge = "tiszta hálózati energia" gridImport = "Hálózatból import" homePower = "Fogyasztás" -loadpoints = "Töltő| Töltő | {count} töltők" +loadpoints = "Töltő| Töltő | {count} töltő" noEnergy = "Nincs mérési adat" pvExport = "Hálózatba export" pvProduction = "Napelem termelés" @@ -441,7 +447,7 @@ label = "Min. töltés %" [main.loadpointSettings.phasesConfigured] label = "Fázis" -no1p3pSupport = "Hogyan van csatlakoztatva a töltőd?" +no1p3pSupport = "Milyen módon van csatlakoztatva a töltőd?" phases_0 = "auto kapcsolás" phases_1 = "1 fázis" phases_1_hint = "({min}-tól {max}-ig)" diff --git a/i18n/lb.toml b/i18n/lb.toml index cd8ffb8e47..1bd93bd778 100644 --- a/i18n/lb.toml +++ b/i18n/lb.toml @@ -32,35 +32,183 @@ description = "Sécherstellen, datt d'Zomm vun all den Luedpunkten déi mam Circ title = "Management vun der Belaaschtung" [config.control] -description = "Normalerweis sinn d'Standardwäerter gutt. Verännert se nëmmen wann Dir wësst wat Dir maacht." +description = "Normalerweis sinn d'Standardwäerter gutt. Veränner se nëmmen wann s du weess wat s du mëss." +descriptionInterval = "Kontrollschläifen-Update-Zyklus a Sekonnen. Definéiert wéi dacks evcc Meter-Daten liest, d'Luedekraaft upasst an d'UI aktualiséiert. Kuerz Intervalle (<30s) kënnen Oszillatiounen an ongewollt Verhalen verursaachen." +descriptionMaxGridSupply = "Nëmme relevant fir Hybrid-Invertere, déi net fäeg sinn d'Totalitéit vum DC-Stroum iwwer AC an d'Haus ze liwweren. Dëse Szenario kann zu ongewolltem Netzverbrauch féieren, well evcc unhëllt datt d'Totalitéit vum DC-Stroum verfügbar ass. Benotzt ee Wäert vu mindestens 50 W fir dat ze verhënneren." +descriptionResidualPower = "Verschiibt d'Funktioun vun der Kontrollschläifen. Wann s du eng Hausbatterie hues, ass et recommandéiert ee Wäert vun 100 W ze setzen. Esou kritt d'Batterie eng liicht Prioritéit par Rapport zum Netz." +labelInterval = "Aktualiséierungsintervall" +labelMaxGridSupply = "Max. Netzbezuch beim Lueden" +labelResidualPower = "Residuell Leeschtung" +title = "Kontrollverhalen" [config.deviceValue] +broker = "Broker" +bucket = "Eemer" +capacity = "Capacitéit" +chargeStatus = "Status" +chargeStatusA = "net verbonnen" +chargeStatusB = "verbonnen" +chargeStatusC = "luet op" +chargeStatusE = "keng Leeschtung" +chargeStatusF = "Feeler" +chargedEnergy = "Opgelueden" +co2 = "Netz CO₂" +configured = "Konfiguréiert" +currency = "Wärung" +current = "Stroum" +currentRange = "Stroum" +enabled = "Ageschalt" +energy = "Energie" +feedinPrice = "Präis fir anzespeisen" +gridPrice = "Netz Präis" +no = "nee" +odometer = "Kilometerzäler" +org = "Organisatioun" +phaseCurrents = "Stroum L1..L3" +phasePowers = "Leeschtung L1..L3" +phaseVoltages = "Spannung L1..L3" +power = "Leeschtung" +powerRange = "Leeschtung" +range = "Autonomie" +soc = "SoC" +socLimit = "SoC Limitt" +temp = "Temperatur" +topic = "Sujet" +url = "URL" +yes = "jo" + +[config.eebus] +description = "Konfiguratioun déi et evcc erméiglecht fir mat aneren EEBus-Geräter ze kommunizéieren." +title = "EEBus" [config.form] +example = "Beispill" +optional = "optionell" [config.general] +cancel = "Ofbriechen" +docsLink = "D'Dokumentatioun kucken." +experimental = "Experimentell" +off = "aus" +on = "un" +password = "Passwuert" +remove = "Ewechhuelen" +save = "Späicheren" +telemetry = "Telemetrie" +title = "Titel" [config.grid] +title = "Netzzieler" +titleAdd = "Netzzieler derbäi fügen" +titleEdit = "Netzzieler beaarbechten" + +[config.hems] +description = "evcc mat engem aneren Heem Energiemanagement System verbannen." +title = "HEMS" + +[config.influx] +description = "Schreift Lueddaten an aner Moossen an InfluxDB. Benotzt Grafana oder aner Tools fir d'Donnéeën ze visualiséieren." +descriptionToken = "Kuckt d'InfluxDB Dokumentatioun fir ze léieren wéi een eng erstellt. https://docs.influxdata.com/influxdb/v2/admin/" +labelBucket = "Eemer" +labelDatabase = "Datebank" +labelOrg = "Organisatioun" +labelPassword = "Passwuert" +labelToken = "API-Token" +labelUrl = "URL" +labelUser = "Benotzer" +title = "InfluxDB" +v1Support = "Brauchs du Ënnerstëtzung fir InfluxDB 1.x?" +v2Support = "Zréck op InfluxDB 2.x" [config.main] +addLoadpoint = "Luedpunkt derbäi setzen" +addPvBattery = "PV oder Batterie derbäi setzen" +addVehicle = "Gefier derbäi setzen" +configured = "konfiguréiert" +edit = "beaarbechten" +title = "Konfiguratioun" +unconfigured = "net konfiguréiert" +vehicles = "Meng Gefierer" +yaml = "Konfiguratioun an der evcc.yaml Datei fonnt. Net iwwer d'UI editiérbar." + +[config.messaging] +description = "Notifikatiounen iwwer Är Opluedsessioune kréien." +title = "Notifikatiounen" [config.meter] +cancel = "Ofbriechen" +delete = "Läschen" +save = "Späicheren" +template = "Hiersteller" +titleChoice = "Wat wëlls du derbäi setzen?" +validateSave = "Überpréiwen & späicheren" + +[config.modbusproxy] +description = "Erlaabt méi Clienten Zougank zu engem eenzegen Modbus Apparat ze hunn." +title = "Modbus-Proxy" + +[config.mqtt] +authentication = "Authentifizéierung" +description = "Mat engem MQTT-Broker verbanne fir Daten mat anere Systemer op Ärem Netz auszetauschen." +descriptionClientId = "Auteur vun de Messagen. Wann eidel `evcc-[rand]` benotzt gëtt." +descriptionTopic = "Eidel loosse fir d'Verëffentlechung ze deaktivéieren." +labelBroker = "Broker" +labelCheckInsecure = "Onsécher Verbindungen erlaben" +labelClientId = "Client ID" +labelInsecure = "Validatioun vum Certificat" +labelPassword = "Passwuert" +labelTopic = "Thema" +labelUser = "Benotzer" +publishing = "Verëffentlechen" +title = "MQTT" + +[config.network] +descriptionHost = "Benotzt de Suffix .local fir mDNS z'aktivéieren. Ass relevant fir d'Entdeckung vun der mobiler App an e puer OCPP Luedgeräter." +descriptionPort = "Port fir de Web Interface an d'API. Du muss d'Browser-URL updaten, wann s du dëst änners." +descriptionSchema = "Beaflosst nëmme wéi d'URL'en generéiert ginn. Wann s du HTTPS auswiels, da gëtt d'Verschlësselung net aktivéiert." +labelHost = "Hostname" +labelPort = "Port" +labelSchema = "Schema" +title = "Netzwierk" [config.options] +[config.options.boolean] +no = "nee" +yes = "jo" + [config.options.endianness] +big = "big-endian" +little = "little-endian" [config.options.schema] +http = "HTTP (unverschlësselt)" +https = "HTTPS (verschlësselt)" [config.pv] +titleAdd = "Zieler (PV) derbäi fügen" +titleEdit = "Zieler (PV) beaarbechten" [config.section] +general = "Allgemeng" +grid = "Netzuschloss" +integrations = "Integratiounen" +loadpoints = "Luedpunkte" +meter = "PV & Batterie" +system = "System" +vehicles = "Gefierer" [config.sponsor] +addToken = "Sponsortoken aginn" +changeToken = "Sponsortoken änneren" +description = "De Sponsoring Modell hëlleft eis de Projet z'erhalen an nohalteg nei a spannend Fonctiounen z'entwéckelen. Als Sponsor kriss du Zougank zu all Wallbox-Implementatiounen." +descriptionToken = "Du kriss den Token vun {url}. Mir bidden och ee Test-Token un fir ze testen." +error = "De Sponsortoken ass ongülteg." +labelToken = "Sponsor-Token" title = "Patronage" [config.system] -logs = "Logs" +logs = "Logge" restart = "Neistart" restartRequiredDescription = "Wgl. neistarten fir d'Ännerungen ze gesinn." restartRequiredMessage = "Configuratioun geännert." @@ -68,7 +216,7 @@ restartingDescription = "Wgl. waarden…" restartingMessage = "evcc gëtt nei gestart." [config.tariffs] -description = "Definéiert Är Energietariffer fir d'Käschte vun Ären Opluedsessiounen ze berechnen." +description = "Definéier d'Stroumtariffer fir d'Käschte vun den Opluedsessiounen ze berechnen." title = "Tariffer" [config.title] @@ -91,7 +239,7 @@ generic = "Aner Integratiounen" offline = "Genereschen Auto" online = "Autoe mat online API" save = "Späicheren" -scooter = "Scooter" +scooter = "Trottinett" template = "Hiersteller" titleAdd = "Auto derbäi fügen" titleEdit = "Auto beaarbechten" @@ -103,25 +251,31 @@ validateSave = "Validéieren & späicheren" greenEnergy = "Solarenergie" greenEnergySub1 = "opgelueden mat evcc" greenEnergySub2 = "zënter Oktober 2022" -greenShare = "Solar deelen" -greenShareSub1 = "Kraaft ass geliwwert vun" +greenShare = "Sonnenundeel" +greenShareSub1 = "Stroum gëtt bereetgestallt vun" greenShareSub2 = "Solar- & Batteriespäicher" -power = "Luedekraaft" +power = "Luedeleeschtung" powerSub1 = "{activeClients} vu {totalClients} Participanten" powerSub2 = "gëtt elo gelueden…" tabTitle = "Live Gemeinschaft" [footer.savings] +co2Saved = "{value} agespuert" +co2Title = "CO₂ Emissiounen" +configurePriceCo2 = "Léiert wéi een de Präis an d'CO₂-Daten konfiguréiert." footerLong = "{percent} Solarenergie" footerShort = "{percent} Solar" modalTitle = "Iwwersiicht vum geluedenen Stroum" +moneySaved = "{value} gespuert" percentGrid = "{grid} kWh Stroumnetz" percentSelf = "{self} kWh Solar" percentTitle = "Solarenergie" -periodLabel = "Period:" +periodLabel = "Zäitraum:" priceFeedIn = "{feedInPrice} fidderen" priceGrid = "{gridPrice} Stroumnetz" priceTitle = "Energiepräis" +referenceGrid = "Netz" +referenceLabel = "Referenzdaten:" savingsComparedToGrid = "verglach mam Stroumnetz" savingsTitle = "Spueren" savingsTotalEnergy = "{total} kWh gelueden" @@ -129,16 +283,24 @@ since = "zënter {since}" tabTitle = "Meng Daten" [footer.savings.period] +30d = "déi lescht 30 Deeg" +365d = "lescht 365 Deeg" +total = "gesamt" [footer.sponsor] -becomeSponsor = "Gitt e Sponsor" +becomeSponsor = "Gitt ee Sponsor" +becomeSponsorExtended = "Ënnerstëtzt eis direkt fir Stickeren ze kréien." confetti = "Prett fir de Konfetti?" -confettiPromise = "Dir kritt Stickeren an digitale Konfetti" +confettiPromise = "Du kriss Stickeren an digitale Konfetti" sticker = "… oder evcc Stickeren?" -supportUs = "Eis Missioun: Solarladung de Standard maachen. Hëllef evcc andeems Dir bezuelt wat et Iech wäert ass." +supportUs = "Eis Missioun: Solarlueden zum Standard maachen. Zesumme mat Ärer finanzieller Ënnerstëtzung kënne mir dëst méiglech maachen." thanks = "Merci, {sponsor}! Däi Bäitrag hëlleft evcc weider ze entwéckelen." titleNoSponsor = "Ënnerstëtzt eis" -titleSponsor = "Dir sidd e Supporter" +titleSponsor = "Du bass ee Supporter" +titleTrial = "Testmodus" +titleVictron = "Ënnerstëtzt vu Victron Energy" +trial = "Du bass am Testmodus a kanns all Funktiounen benotzen. Mir géifen eis driwwer freeën, wann s du Sponsor géifs ginn." +victron = "Du benotz evcc mat Victron Energy Hardware an hues esou Zougang zu all Funktiounen." [footer.telemetry] optIn = "Ech wëll meng Donnéeën och bäidroen." @@ -163,7 +325,10 @@ about = "Iwwer" blog = "Blog" docs = "Dokumentatioun" github = "GitHub" -login = "Gefier Login" +login = "Logine vun de Gefierer" +logout = "Ofmellen" +nativeSettings = "Server änneren" +needHelp = "Brauchs du Hëllef?" sessions = "Opluedsessiounen" settings = "Astellungen" @@ -173,46 +338,114 @@ dark = "Design: donkel" light = "Design: Liicht" [help] +discussionsButton = "GitHub Diskussiounen" +documentationButton = "Dokumentatioun" +issueButton = "Bug mellen" +issueDescription = "Hues du ee komescht oder falscht Verhalen identifizéiert?" +logsButton = "Logs ukucken" +logsDescription = "Kontrolléier d'Logbicher op Feeler." +modalTitle = "Brauchs du Hëllef?" +primaryActions = "Funktionnéiert eppes net esou wéi et soll? Dëst si gutt Plazen fir Hëllef ze kréien." +restartButton = "Nei starten" +restartDescription = "Hues du schonns probéiert d'Gerät aus- an erëm unzemaachen?" +secondaryActions = "Nach ëmmer keng Léisung fonnt? Hei sinn e puer weider Méiglechkeeten." [help.restart] +cancel = "Ofbriechen" +confirm = "Jo, nei starten!" +description = "Ënner normalen Ëmstänn dierft ee Neistart net néideg sinn. Mell wgl. de Feeler, wann s du evcc reegelméisseg nei starte muss." +disclaimer = "Hiweis: evcc wäert gestoppt ginn a verléisst sech drop datt de Service nei start." +modalTitle = "Bass du sécher datt s du nei starte wëlls?" [log] +areaLabel = "No Beräich filteren" +areas = "All Beräicher" +download = "Komplette Log eroflueden" +levelLabel = "No Log-Level filteren" +nAreas = "{count} Beräicher" +noResults = "Keng passend Entréeë fonnt." +search = "Sichen" +selectAll = "alles auswielen" +showAll = "All Entréeë uweisen" +title = "Logge" +update = "Automatesch Aktualiséieren" [loginModal] +cancel = "Ofbriechen" +error = "Login feelgeschloen: " +iframeHint = "Evcc an engem neien Tab opmaachen." +iframeIssue = "D'Passwuert ass richteg, mee de Browser schéngt den Authentifikatiouns-Cookie ofgeleent ze hunn. Dëst ka geschéien wann s du evcc an engem iframe iwwer HTTP verwenns." +invalid = "Passwuert ass ongültig." +login = "Umellen" +password = "Passwuert" +reset = "Passwuert zerécksetzen?" +title = "Authentifizéierung" [main] vehicles = "Parking" [main.chargingPlan] +active = "Aktiv" +arrivalTab = "Arrivée" +day = "Dag" +departureTab = "Depart" +goal = "Luedzil" +modalTitle = "Luedplanifikatioun" +none = "keen" +remove = "Ewechhuelen" +time = "Zäit" +title = "Planifikatioun" +titleMinSoc = "Min. Ladung" +titleTargetCharge = "Depart" +unsavedChanges = "Net gespäichert Ännerungen leie vir. Elo uwennen?" +update = "Uwennen" [main.energyflow] battery = "Batterie" batteryCharge = "Batterie gelueden" batteryDischarge = "Batterie Entladung" +batteryHold = "Batterie (gespäert)" +batteryTooltip = "{energy} vun {total} ({soc})" +cheapBatteryGridCharge = "Gënschtege Stroum vum Netz" +cleanBatteryGridCharge = "grénge Stroum vum Netz" gridImport = "Stroumnetz Import" homePower = "Verbrauch" -loadpoints = "Opluedpunkt | Opluedpunkt | {count} Opluedstatiounen" +loadpoints = "Wallbox | Wallbox | {count} Wallboxen" noEnergy = "Keng Moossdaten" pvExport = "Stroumnetz Export" pvProduction = "Produktioun" selfConsumption = "eegene Konsum" [main.heatingStatus] +charging = "Wiermen…" +waitForVehicle = "Prett. Waarden op d'Heizung..." [main.loadpoint] +avgPrice = "⌀ Präis" charged = "Belaascht" +co2 = "⌀ CO₂" duration = "Dauer" -fallbackName = "Opluedpunkt" +fallbackName = "Luedpunkt" power = "Leeschtung" +price = "Σ Präis" +remaining = "Reschtzäit" +remoteDisabledHard = "{source}: Deaktivéiert" +remoteDisabledSoft = "{source}: Adaptatiivt PV-Lueden deaktivéiert" +solar = "Sonn" [main.loadpointSettings] -currents = "Ladestroum" +currents = "Luedstroum" default = "Standard" disclaimerHint = "Notiz:" +onlyForSocBasedCharging = "Dës Optioune sinn nëmme fir Gefierer mat bekannte Luedstatiounen verfügbar." +smartCostCheap = "Bëlleg vum Reseau oplueden" +smartCostClean = "Grengt Luede vun Netz" title = "Astellungen {0}" vehicle = "Gefier" [main.loadpointSettings.limitSoc] +description = "Luedlimitt déi benotzt gëtt, wann dëst Gefier verbonnen ass." +label = "Standard Luedlimitt" [main.loadpointSettings.maxCurrent] label = "Max. Stroum" @@ -221,11 +454,12 @@ label = "Max. Stroum" label = "Min. Stroum" [main.loadpointSettings.minSoc] -description = "Fir Noutfäll. D'Gefier gëtt „séier” gelueden op {0} vun all verfügbare Solarenergie, a geet dann mat nëmmen dem Solariwwerschoss weider." +description = "D'Gefier gëtt \"séier\" op {0} am Solarmodus gelueden. Da geet et weider mat Solariwwerschëss. Dëst ass nëtzlech fir eng Minimum-Reechwäit och wärend méi däischter Deeg ze garantéieren." label = "Minimum Staat vun charge" [main.loadpointSettings.phasesConfigured] label = "Phasen" +no1p3pSupport = "Wéi ass deng Wallbox ugeschloss?" phases_0 = "automatesch wiesselen" phases_1 = "1 Phase" phases_1_hint = "({min} bis {max})" @@ -235,8 +469,9 @@ phases_3_hint = "({min} bis {max})" [main.mode] minpv = "Min+PV" now = "Schnell" -off = "Off" +off = "Aus" pv = "PV" +smart = "Clever" [main.provider] login = "umellen" @@ -244,21 +479,33 @@ logout = "ausloggen" [main.targetCharge] activate = "Aktivéieren" +co2Limit = "CO₂-Grenz vu {co2}" +costLimitIgnore = "Den agestellte {limit} gëtt an dësem Zäitraum ignoréiert." +currentPlan = "Aktiv Planifikatioun" descriptionEnergy = "Bis wéini soll {targetEnergy} an d'Gefier gelueden ginn?" descriptionSoc = "Wéini soll d'Gefier op {targetSoc} gelueden ginn?" inactiveLabel = "Zilzäit" modalTitle = "Zielzäit setzen" -planDuration = "Opluedzäit" -planPeriodLabel = "Period" +notReachableInTime = "Zilzäit gëtt {overrun} spéider erreecht." +onlyInPvMode = "Luedplanifikatioun ass nëmmen am PV-Modus aktiv." +planDuration = "Dauer vum Oplueden" +planPeriodLabel = "Zäitraum" planPeriodValue = "{Start} bis {end}" planUnknown = "nach net bekannt" +preview = "Virschau vun der Planifikatioun" +priceLimit = "Präisgrenz vu {price}" +remove = "Ewechhuelen" setTargetTime = "keen" +targetIsAboveLimit = "Déi konfiguréiert Luedlimitt vu {limit} gëtt wärend dëser Period ignoréiert." +targetIsAboveVehicleLimit = "D'Limitt vum Gefier ass méi kleng wéi d'Luedzil." targetIsInThePast = "Déi gewielt Zäit ass an der Vergaangenheet." -targetIsTooFarInTheFuture = "Mir wäerten de Plang upassen soubal mir méi iwwer d'Zukunft wëssen." +targetIsTooFarInTheFuture = "Mir wäerten d'Planifikatioun upassen soubal mir méi iwwer d'Zukunft wëssen." title = "Zilzäit" today = "haut" tomorrow = "muer" update = "Aktualiséieren" +vehicleCapacityDocs = "Léier wéi du et konfiguréiers." +vehicleCapacityRequired = "D'Kapazitéit vun der Batterie vum Gefier ass néideg fir d'Luedzäit anzeschätzen." [main.targetChargePlan] chargeDuration = "Opluedzäit" @@ -268,15 +515,20 @@ timeRange = "{day} {range} Auer" unknownPrice = "nach onbekannt" [main.targetEnergy] -label = "Limite" +label = "Luedlimitt" noLimit = "keen" [main.vehicle] +addVehicle = "Gefier derbäi fügen" changeVehicle = "Gefier änneren" detectionActive = "Erkennung vum Gefier ..." fallbackName = "Gefier" +moreActions = "Weider Aktiounen" none = "Keng Gefier" -targetSoc = "Limite" +notReachable = "D'Gefier war net erreechbar. Probéiert evcc nei ze starten." +targetSoc = "Luedlimitt" +temp = "Temperatur" +tempLimit = "Temp. Limitt" unknown = "Gäscht Gefier" vehicleSoc = "Staat vun charge" @@ -285,77 +537,134 @@ charging = "oplueden" connected = "verbonne" disconnected = "deconnectéiert" ready = "bereet" -vehicleLimit = "Gefier Limite: {soc}" +vehicleLimit = "Limitt vum Gefier: {soc}" [main.vehicleStatus] charging = "Laden ..." +cheapEnergyCharging = "Gënschteg Energie verfügbar." +cheapEnergyNextStart = "Gënschteg Energie an {duration}." +cheapEnergySet = "Präislimitt gesat." +cleanEnergyCharging = "Gréng Energie verfügbar." +cleanEnergyNextStart = "Gréng Energie an {duration}." +cleanEnergySet = "CO₂-Limitt gesetzt." +climating = "Virklimatiséierung erkannt." connected = "Verbonne." disconnected = "Deconnectéiert." +finished = "Ofgeschloss." minCharge = "Charge op d'mannst op {soc}." -pvDisable = "Net genuch Iwwerschoss. Pausen an {remaining}…" -pvEnable = "Iwwerschoss verfügbar. Vun {remaining}…" -scale1p = "Reduktioun op eenzeg Phase am {remaining}…" -scale3p = "Erhéijung op dräi Phase am {remaining}…" -targetChargeActive = "Zilladung aktiv ..." -targetChargePlanned = "Zilladung fänkt um {time} un." -targetChargeWaitForVehicle = "Ziel Charge prett. Waart op Gefier ..." -vehicleLimitReached = "Gefier Limit {soc} erreecht." +pvDisable = "Net genuch Iwwerschoss. Paus a {remaining}…" +pvEnable = "Iwwerschoss verfügbar. Starte geschwënn." +scale1p = "Reduktioun op eng eenzeg Phase." +scale3p = "Erhéije geschwënn op dräi Phasen." +targetChargeActive = "Luedplanung aktiv. Ageschate Schluss an {duration}." +targetChargePlanned = "Luedpladung fänkt un an {time} un." +targetChargeWaitForVehicle = "Luedplang ass prett. Waarden op d'Gefier..." +unknown = "" +vehicleLimit = "Limitt vum Gefier." +vehicleLimitReached = "Limitt vum Gefier erreecht." waitForVehicle = "Prett. Waart op Gefier ..." +welcome = "Kuerz initial Ladung fir d'Bestätegue vun der Verbindung." [notifications] -dismissAll = "Alles entloossen" +dismissAll = "Notifikatiounen ewechuelen" +logs = "Vollständege Log ukucken" modalTitle = "Notifikatiounen" [offline] +configurationError = "Feeler beim Starten. Iwwerpréif deng Konfiguratioun a start nei." message = "Keng Verbindung mam Server." reload = "Reload?" +restart = "Neistart" +restartNeeded = "Noutwendeg fir Ännerungen z'applizéieren." +restarting = "Server ass geschwënn erëm verfügbar." [passwordModal] +description = "Definéiert e Passwuert fir d'Konfiguratiounsastellungen ze schützen. Et kann een den Haaptbildschierm och ouni Login nach ëmmer benotzen." +empty = "Passwuert duerf net eidel sinn" +error = "Feeler: " +labelCurrent = "Aktuellt Passwuert" +labelNew = "Neit Passwuert" +labelRepeat = "Neit Passwuert widderhuelen" +newPassword = "Passwuert erstellen" +noMatch = "Passwierder stëmmen net iwwerteneen" +titleNew = "Administrator Passwuert erstellen" +titleUpdate = "Administrator Passwuert änneren" +updatePassword = "Passwuert änneren" [session] cancel = "Ofbriechen" +co2 = "CO₂" +date = "Zäitraum" delete = "Läschen" finished = "Enn Zäit" +meter = "Zielerstand" meterstart = "Éischt Meter Liesung" meterstop = "Last Meter Liesung" odometer = "Kilometerzähler" +price = "Präis" started = "Startzeit" title = "Charging Sessioun" [sessions] -date = "Period" +avgPower = "⌀ Leeschtung" +avgPrice = "⌀ Präis" +chargeDuration = "Lueddauer" +co2 = "⌀ CO₂" +csvMonth = "Download {month} CSV" +csvTotal = "Gesamt CSV eroflueden" +date = "Ufank" downloadCsv = "Als CSV eroflueden" energy = "Belaascht" -loadpoint = "Opluedpunkt" -reallyDelete = "Wëllt Dir dës Sessioun wierklech läschen?" +loadpoint = "Luedpunkt" +noData = "Nach keng Opluedsessiounen fir dëse Mount." +price = "Σ Präis" +reallyDelete = "Wëlls du dës Sessioun wierklech läschen?" +solar = "Sonn" title = "Opluedsessiounen" +total = "Total" vehicle = "Gefier" [sessions.csv] chargedenergy = "Energie (kWh)" +chargeduration = "Oplueddauer" +co2perkwh = "CO₂/kWh" created = "Erstallt" finished = "Fäerdeg" -identifier = "Identifier" +identifier = "Identifikatioun" loadpoint = "Opluedpunkt" -meterstart = "Meter Start (kWh)" -meterstop = "Meter Stop (kWh)" +meterstart = "Zielerstand am Ufank (kWh)" +meterstop = "Zielerstand zum Schluss (kWh)" odometer = "Kilometerstand (km)" +price = "Präis" +priceperkwh = "Präis/kWh" +solarpercentage = "Sonn (%)" vehicle = "Gefier" [sessions.filter] +allLoadpoints = "All Opluedpunkten" +allVehicles = "All Gefierer" +filter = "Filteren" [settings] -title = "Astellungen" +title = "Duerstellung" [settings.fullscreen] +enter = "Vollbild starten" +exit = "Aus dem Vollbild erausgoen" +label = "Vollbild" [settings.hiddenFeatures] +label = "Experimentell" +value = "Experimentell UI-Funktiounen weisen." [settings.language] auto = "Automatesch" label = "Sprooch" [settings.sponsorToken] +expires = "Däi Sponsor Token leeft aus an {inXDays}. {getNewToken} an update et hei." +getNew = "Huel dir een neit" +hint = "N.b.: Mir wäerten dëst an Zukunft automatiséieren." [settings.telemetry] label = "Telemetrie" @@ -372,15 +681,29 @@ label = "Eenheeten" mi = "Meilen" [smartCost] +activeHours = "{charging} vun {total}" +activeHoursLabel = "Aktiv Stonnen" +applyToAll = "Iwwerall uwennen?" +batteryDescription = "Luet d'Hausbatterie aus dem Netz." +cheapTitle = "Gënschtegt Oplueden vum Netz" +cleanTitle = "Gréngt Opluede vun Netz" +co2Label = "CO₂-Emissioun" +co2Limit = "CO₂-Grenz" +loadpointDescription = "Aktivéiert iwwerganksméisseg Schnelloplueden am PV-Modus." +modalTitle = "Smart Opluede vum Netz" +none = "keent" +priceLabel = "Energiepräis" +priceLimit = "Präisgrenz" +saved = "Gepäichert." [startupError] configFile = "Konfiguratiounsdatei benotzt:" configuration = "Configuratioun" -description = "Kuckt w.e.g. Är Konfiguratiounsdatei. Wann d'Fehlermeldung Iech net hëlleft, kuckt op eis {0}." +description = "Iwwerpréift dKonfiguratiounsdatei. Wann d'Fehlermeldung net hëlleft fir eng Léisung ze fannen, kuckt an eisem {0}." discussions = "GitHub Diskussiounen" -fixAndRestart = "Fir w.e.g. de Problem fixéieren an de Server nei starten." -hint = "Notiz: Et kéint och sinn datt Dir e defekten Apparat hutt (Inverter, Meter, ...). Kontrolléiert Är Netzwierkverbindungen." -lineError = "Mir hunn e Feeler am {0} fonnt." +fixAndRestart = "Wgl. de Problem behiewen an de Server nei starten." +hint = "Notiz: Et kéint och sinn datt s du ee defekten Apparat hues (Inverter, Meter, ...). Kontrolléier deng Netzwierkverbindungen." +lineError = "An {0} gouf ee Feeler fonnt." lineErrorLink = "Linn {0}" restartButton = "Neistart" title = "Startup Feeler" diff --git a/i18n/pt.toml b/i18n/pt.toml index 9138a8da49..cb1abb0617 100644 --- a/i18n/pt.toml +++ b/i18n/pt.toml @@ -5,22 +5,24 @@ control = "Controle de bateria" discharge = "Prevenir a descarga no modo rápido e o carregamento planejado." disclaimerHint = "Nota:" disclaimerText = "Estas configurações só afetam o modo solar. O comportamento de carregamento é ajustado de acordo." -legendBottomName = "prioridade doméstica" -legendBottomSubline = "não usado para carregar" -legendMiddleName = "veículo primeiro" -legendMiddleSubline = "segunda casa" +gridChargeTab = "Carregamento de rede" +legendBottomName = "“Dar prioridade ao carregamento da bateria da casa”" +legendBottomSubline = "até atingir {soc}." +legendMiddleName = "“Dar prioridade ao carregamento do veículo”" +legendMiddleSubline = "quando a bateria doméstica está acima de {soc}." legendTitle = "Como a energia solar deve ser usada?" -legendTopAutostart = "inicio automatico" -legendTopName = "carregamento suportado por bateria" -legendTopSubline = "sem interrupções" +legendTopAutostart = "Iniciar automaticamente" +legendTopName = "Carregamento de veículos com a bateria" +legendTopSubline = "quando a bateria da casa está acima de {soc}." modalTitle = "Bateria Doméstica" +usageTab = "Utilização da bateria" [batterySettings.bufferStart] -above = "quando acima {soc}" -full = "quando estiver a {soc}" +above = "quando acima {soc}." +full = "quando estiver a {soc}." low = "quando um pouco cheio" medium = "quando meio cheio" -never = "apenas com excesso suficiente" +never = "apenas com excesso suficiente." [config] @@ -57,6 +59,7 @@ co2 = "Rede CO₂" configured = "Configurado" currency = "Curência" current = "Corrente" +currentRange = "Corrente" enabled = "Ativado" energy = "Energia" feedinPrice = "Preço de alimentação" @@ -68,9 +71,10 @@ phaseCurrents = "Corrente L1..L3" phasePowers = "Energia L1..L3" phaseVoltages = "Voltagem L1..L3" power = "Energia" +powerRange = "Potência" range = "Distância" soc = "Nível de carga" -socLimit = "Alvo de carga" +socLimit = "Limitação do SoC" temp = "Temperatura" topic = "Tema" url = "URL" @@ -172,6 +176,10 @@ title = "Rede" [config.options] +[config.options.boolean] +no = "não" +yes = "sim" + [config.options.endianness] big = "big-endian" little = "little-endian" @@ -357,8 +365,10 @@ areaLabel = "Filtrar por área" areas = "Todas as áreas" download = "Baixe o registo completo" levelLabel = "Filtrar por nível de registro" +nAreas = "{count} áreas" noResults = "Sem entradas de registo correspondentes." search = "Procurar" +selectAll = "selecionar tudo" showAll = "Mostrar todas as entradas" title = "Registos" update = "Atualização automática" @@ -399,6 +409,8 @@ batteryCharge = "Carrego de bateria" batteryDischarge = "Descarrego de bateria" batteryHold = "Bateria (suspensa)" batteryTooltip = "{energy} de {total} ({soc})" +cheapBatteryGridCharge = "energia de rede barata" +cleanBatteryGridCharge = "energia de rede verde" gridImport = "Consumo de rede" homePower = "Consumo" loadpoints = "Carregador | Carregador | {count} Carregadores" @@ -464,6 +476,7 @@ minpv = "Mín+Sol" now = "Rápido" off = "Off" pv = "Sol" +smart = "Smart" [main.provider] login = "iniciar sessão" @@ -478,7 +491,7 @@ descriptionEnergy = "Até quando {targetEnergy} deve ser carregado no veículo?" descriptionSoc = "Quando o veículo deve ser carregado até {targetSoc}?" inactiveLabel = "Tempo alvo" modalTitle = "Definir hora-alvo" -notReachableInTime = "O objetivo não é alcançável no tempo. Final estimado: {endTime}." +notReachableInTime = "O objetivo será alcançado {overrun} mais tarde." onlyInPvMode = "O plano de carregamento só funciona no modo solar." planDescription = "Entrar um tempo de partida, e evcc irá carregar o veículo o mais rentável ou ambientalmente amigável possível." planDuration = "Tempo de carga" @@ -491,7 +504,7 @@ remove = "Remover" setPlan = "Defina um plano de carregamento" setTargetTime = "sem" targetIsAboveLimit = "O limite de carregamento configurado de {limit} será ignorado durante este período." -targetIsAboveVehicleLimit = "Aumente o limite do veículo ({limit}) para alcançar o objetivo de carregamento." +targetIsAboveVehicleLimit = "O limite do veículo está abaixo do objetivo de carregamento." targetIsInThePast = "Escolha um momento no futuro, Marty." targetIsTooFarInTheFuture = "Vamos ajustar o plano assim que soubermos mais sobre o futuro." title = "Tempo alvo" @@ -535,22 +548,29 @@ vehicleLimit = "Limite de veículo: {soc}" [main.vehicleStatus] charging = "Carregando..." -cheapEnergyCharging = "Carregando energia barata: {price} (limit {limit})" -cleanEnergyCharging = "Carregando energia limpa: {co2} (limit {limit})" +cheapEnergyCharging = "Energia barata disponível." +cheapEnergyNextStart = "Energia barata em {duration}." +cheapEnergySet = "Limite de preço definido." +cleanEnergyCharging = "Energia verde disponível." +cleanEnergyNextStart = "Energia verde em {duration}." +cleanEnergySet = "Limite de CO₂ definido." climating = "Pré-condicionamento detectado." connected = "Ligado." disconnected = "Não conectado." +finished = "Concluído." minCharge = "Carga mínima até {soc}" -pvDisable = "Excesso insuficiente. Pausa em {remaining}..." -pvEnable = "Excesso disponível. Carga em {remaining}..." -scale1p = "Reduzindo para carregamento monofásico em {remaining}…" -scale3p = "Aumentando para carregamento trifásico em {remaining}…" -targetChargeActive = "Plano de carregamento activo..." -targetChargePlanned = "O plano de carregamento começa a {time}." +pvDisable = "Excedente insuficiente. Pausa em breve." +pvEnable = "Excesso disponível. Carga em breve." +scale1p = "Reduzindo para carregamento monofásico em breve." +scale3p = "Aumentando para carregamento trifásico em breve." +targetChargeActive = "Plano de carregamento ativo. Fim estimado em {duration}." +targetChargePlanned = "O plano de carregamento começa em {duration}." targetChargeWaitForVehicle = "Plano de carregamento pronto. À espera do veículo..." unknown = "" -vehicleLimitReached = "Limite de veículo {soc} atingido." +vehicleLimit = "“Limite dos veículos.”" +vehicleLimitReached = "Limite de veículo atingido." waitForVehicle = "Pronto. Esperando pelo veículo..." +welcome = "Carga inicial curta para confirmar a ligação." [notifications] dismissAll = "Recusar tudos" @@ -558,6 +578,7 @@ logs = "Ver registos completos" modalTitle = "Notícias" [offline] +configurationError = "Erro durante a inicialização. Verifique a sua configuração e reinicie." message = "Não conectado a um servidor." reload = "Iniciar de novo?" restart = "Reiniciar" diff --git a/i18n/sv.toml b/i18n/sv.toml index 6089eb0eb2..11fba50704 100644 --- a/i18n/sv.toml +++ b/i18n/sv.toml @@ -364,8 +364,10 @@ areaLabel = "Filtrera per område" areas = "Alla områden" download = "Ladda hem alla loggar" levelLabel = "Filtrera på loggnivå" +nAreas = "{count} områden" noResults = "Inga matchande loggposter." search = "Sök" +selectAll = "välj alla" showAll = "Visa allt" title = "Loggar" update = "Autouppdatera" diff --git a/meter/dsmr.go b/meter/dsmr.go index 561d7b7326..fb730958a0 100644 --- a/meter/dsmr.go +++ b/meter/dsmr.go @@ -139,8 +139,7 @@ func NewDsmr(uri, energy string, timeout time.Duration) (api.Meter, error) { // based on https://github.com/basvdlei/gotsmart/blob/master/gotsmart.go func (m *Dsmr) run(conn net.Conn, done chan struct{}) { log := util.NewLogger("dsmr") - bo := backoff.NewExponentialBackOff() - bo.MaxInterval = 5 * time.Minute + bo := backoff.NewExponentialBackOff(backoff.WithMaxInterval(5 * time.Minute)) handle := func(op string, err error) { log.ERROR.Printf("%s: %v", op, err) diff --git a/meter/goodwe/server.go b/meter/goodwe/server.go index 80c0ce8cf8..d2f1d1665a 100644 --- a/meter/goodwe/server.go +++ b/meter/goodwe/server.go @@ -60,9 +60,9 @@ func (m *Server) GetInverter(ip string) *util.Monitor[Inverter] { } func (m *Server) readData() { - bo := backoff.NewExponentialBackOff() - bo.MaxInterval = time.Second - bo.MaxElapsedTime = 10 * time.Second + bo := backoff.NewExponentialBackOff( + backoff.WithMaxInterval(time.Second), + backoff.WithMaxElapsedTime(10*time.Second)) for { mu.RLock() diff --git a/meter/rct.go b/meter/rct.go index 2030785450..5fe094bfe3 100644 --- a/meter/rct.go +++ b/meter/rct.go @@ -87,9 +87,9 @@ func NewRCT(uri, usage string, cache time.Duration, capacity func() float64) (ap return nil, err } - bo := backoff.NewExponentialBackOff() - bo.InitialInterval = 10 * time.Millisecond - bo.MaxElapsedTime = time.Second + bo := backoff.NewExponentialBackOff( + backoff.WithInitialInterval(10*time.Millisecond), + backoff.WithMaxElapsedTime(time.Second)) m := &RCT{ usage: strings.ToLower(usage), diff --git a/provider/socket.go b/provider/socket.go index f66171cda8..d9ccb1902c 100644 --- a/provider/socket.go +++ b/provider/socket.go @@ -8,12 +8,12 @@ import ( "sync" "time" + "github.com/coder/websocket" "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/provider/pipeline" "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/request" "github.com/evcc-io/evcc/util/transport" - "nhooyr.io/websocket" ) const retryDelay = 5 * time.Second diff --git a/provider/socket_test.go b/provider/socket_test.go index 09f63180ca..757d72e4f0 100644 --- a/provider/socket_test.go +++ b/provider/socket_test.go @@ -8,8 +8,8 @@ import ( "testing" "time" + "github.com/coder/websocket" "github.com/stretchr/testify/require" - "nhooyr.io/websocket" ) func TestSocketProvider(t *testing.T) { diff --git a/server/socket.go b/server/socket.go index 7ecf3a4a21..cf48617226 100644 --- a/server/socket.go +++ b/server/socket.go @@ -8,8 +8,8 @@ import ( "sync" "time" + "github.com/coder/websocket" "github.com/evcc-io/evcc/util" - "nhooyr.io/websocket" ) const ( diff --git a/tariff/amber.go b/tariff/amber.go index b85a83e6f5..416e6df735 100644 --- a/tariff/amber.go +++ b/tariff/amber.go @@ -82,7 +82,6 @@ func NewAmberFromConfig(other map[string]interface{}) (api.Tariff, error) { func (t *Amber) run(done chan error) { var once sync.Once - bo := newBackoff() tick := time.NewTicker(time.Minute) for ; true; <-tick.C { @@ -91,7 +90,7 @@ func (t *Amber) run(done chan error) { if err := backoff.Retry(func() error { return backoffPermanentError(t.GetJSON(uri, &res)) - }, bo); err != nil { + }, bo()); err != nil { once.Do(func() { done <- err }) t.log.ERROR.Println(err) diff --git a/tariff/awattar.go b/tariff/awattar.go index eeae54cfc2..9acef9d67c 100644 --- a/tariff/awattar.go +++ b/tariff/awattar.go @@ -55,7 +55,7 @@ func NewAwattarFromConfig(other map[string]interface{}) (api.Tariff, error) { func (t *Awattar) run(done chan error) { var once sync.Once - bo := newBackoff() + client := request.NewHelper(t.log) tick := time.NewTicker(time.Hour) @@ -64,7 +64,7 @@ func (t *Awattar) run(done chan error) { if err := backoff.Retry(func() error { return backoffPermanentError(client.GetJSON(t.uri, &res)) - }, bo); err != nil { + }, bo()); err != nil { once.Do(func() { done <- err }) t.log.ERROR.Println(err) diff --git a/tariff/edf-tempo.go b/tariff/edf-tempo.go index 88d3d3a2c1..c31465d3f8 100644 --- a/tariff/edf-tempo.go +++ b/tariff/edf-tempo.go @@ -102,7 +102,6 @@ func (t *EdfTempo) RefreshToken(_ *oauth2.Token) (*oauth2.Token, error) { func (t *EdfTempo) run(done chan error) { var once sync.Once - bo := newBackoff() tick := time.NewTicker(time.Hour) for ; true; <-tick.C { @@ -125,7 +124,7 @@ func (t *EdfTempo) run(done chan error) { if err := backoff.Retry(func() error { return backoffPermanentError(t.GetJSON(uri, &res)) - }, bo); err != nil { + }, bo()); err != nil { once.Do(func() { done <- err }) t.log.ERROR.Println(err) diff --git a/tariff/electricitymaps.go b/tariff/electricitymaps.go index de5d7a860b..6483bef41d 100644 --- a/tariff/electricitymaps.go +++ b/tariff/electricitymaps.go @@ -79,7 +79,7 @@ func NewElectricityMapsFromConfig(other map[string]interface{}) (api.Tariff, err func (t *ElectricityMaps) run(done chan error) { var once sync.Once - bo := newBackoff() + uri := fmt.Sprintf("%s/carbon-intensity/forecast?zone=%s", t.uri, t.zone) tick := time.NewTicker(time.Hour) @@ -88,7 +88,7 @@ func (t *ElectricityMaps) run(done chan error) { if err := backoff.Retry(func() error { return backoffPermanentError(t.GetJSON(uri, &res)) - }, bo); err != nil { + }, bo()); err != nil { if res.Error != "" { err = errors.New(res.Error) } diff --git a/tariff/elering.go b/tariff/elering.go index 0c0768b9c6..e49e154073 100644 --- a/tariff/elering.go +++ b/tariff/elering.go @@ -60,7 +60,6 @@ func NewEleringFromConfig(other map[string]interface{}) (api.Tariff, error) { func (t *Elering) run(done chan error) { var once sync.Once client := request.NewHelper(t.log) - bo := newBackoff() tick := time.NewTicker(time.Hour) for ; true; <-tick.C { @@ -73,7 +72,7 @@ func (t *Elering) run(done chan error) { if err := backoff.Retry(func() error { return backoffPermanentError(client.GetJSON(uri, &res)) - }, bo); err != nil { + }, bo()); err != nil { once.Do(func() { done <- err }) t.log.ERROR.Println(err) diff --git a/tariff/energinet.go b/tariff/energinet.go index 516fe32727..a5170f1da6 100644 --- a/tariff/energinet.go +++ b/tariff/energinet.go @@ -59,7 +59,6 @@ func NewEnerginetFromConfig(other map[string]interface{}) (api.Tariff, error) { func (t *Energinet) run(done chan error) { var once sync.Once client := request.NewHelper(t.log) - bo := newBackoff() tick := time.NewTicker(time.Hour) for ; true; <-tick.C { @@ -73,7 +72,7 @@ func (t *Energinet) run(done chan error) { if err := backoff.Retry(func() error { return backoffPermanentError(client.GetJSON(uri, &res)) - }, bo); err != nil { + }, bo()); err != nil { once.Do(func() { done <- err }) t.log.ERROR.Println(err) diff --git a/tariff/entsoe.go b/tariff/entsoe.go index 7b08687ea6..714117a3b0 100644 --- a/tariff/entsoe.go +++ b/tariff/entsoe.go @@ -85,8 +85,6 @@ func NewEntsoeFromConfig(other map[string]interface{}) (api.Tariff, error) { func (t *Entsoe) run(done chan error) { var once sync.Once - bo := newBackoff() - // Data updated by ESO every half hour, but we only need data every hour to stay current. tick := time.NewTicker(time.Hour) for ; true; <-tick.C { @@ -127,7 +125,7 @@ func (t *Entsoe) run(done chan error) { default: return backoff.Permanent(errors.New("invalid document name: " + doc.XMLName.Local)) } - }, bo); err != nil { + }, bo()); err != nil { once.Do(func() { done <- err }) t.log.ERROR.Println(err) diff --git a/tariff/groupe-e.go b/tariff/groupe-e.go index d29222b8fe..c708e38014 100644 --- a/tariff/groupe-e.go +++ b/tariff/groupe-e.go @@ -39,7 +39,7 @@ func NewGroupeEFromConfig(other map[string]interface{}) (api.Tariff, error) { func (t *GroupeE) run(done chan error) { var once sync.Once - bo := newBackoff() + client := request.NewHelper(t.log) tick := time.NewTicker(time.Hour) @@ -55,7 +55,7 @@ func (t *GroupeE) run(done chan error) { if err := backoff.Retry(func() error { return backoffPermanentError(client.GetJSON(uri, &res)) - }, bo); err != nil { + }, bo()); err != nil { once.Do(func() { done <- err }) t.log.ERROR.Println(err) diff --git a/tariff/gruenstromindex.go b/tariff/gruenstromindex.go index 13c1de193a..d954a8f409 100644 --- a/tariff/gruenstromindex.go +++ b/tariff/gruenstromindex.go @@ -89,7 +89,7 @@ func NewGrünStromIndexFromConfig(other map[string]interface{}) (api.Tariff, err func (t *GrünStromIndex) run(done chan error) { var once sync.Once client := request.NewHelper(t.log) - bo := newBackoff() + uri := fmt.Sprintf("https://api.corrently.io/v2.0/gsi/prediction?zip=%s", t.zip) tick := time.NewTicker(time.Hour) @@ -98,7 +98,7 @@ func (t *GrünStromIndex) run(done chan error) { err := backoff.Retry(func() error { return backoffPermanentError(client.GetJSON(uri, &res)) - }, bo) + }, bo()) if err == nil && res.Err { if s, ok := res.Message.(string); ok { diff --git a/tariff/helper.go b/tariff/helper.go index 4b7332b189..1da0ee4190 100644 --- a/tariff/helper.go +++ b/tariff/helper.go @@ -11,11 +11,11 @@ import ( "github.com/evcc-io/evcc/util/request" ) -func newBackoff() backoff.BackOff { - bo := backoff.NewExponentialBackOff() - bo.InitialInterval = time.Second - bo.MaxElapsedTime = time.Minute - return bo +func bo() backoff.BackOff { + return backoff.NewExponentialBackOff( + backoff.WithInitialInterval(time.Second), + backoff.WithMaxElapsedTime(time.Minute), + ) } // backoffPermanentError returns a permanent error in case of HTTP 400 diff --git a/tariff/ngeso.go b/tariff/ngeso.go index 93b751b143..9df5b24d5c 100644 --- a/tariff/ngeso.go +++ b/tariff/ngeso.go @@ -57,7 +57,6 @@ func NewNgesoFromConfig(other map[string]interface{}) (api.Tariff, error) { func (t *Ngeso) run(done chan error) { var once sync.Once client := request.NewHelper(t.log) - bo := newBackoff() // Use national results by default. var tReq ngeso.CarbonForecastRequest @@ -79,7 +78,7 @@ func (t *Ngeso) run(done chan error) { res, err := backoff.RetryWithData(func() (ngeso.CarbonForecastResponse, error) { res, err := tReq.DoRequest(client) return res, backoffPermanentError(err) - }, bo) + }, bo()) if err != nil { once.Do(func() { done <- err }) diff --git a/tariff/octopus.go b/tariff/octopus.go index 2620c5e89e..0525ea6666 100644 --- a/tariff/octopus.go +++ b/tariff/octopus.go @@ -84,7 +84,6 @@ func NewOctopusFromConfig(other map[string]interface{}) (api.Tariff, error) { func (t *Octopus) run(done chan error) { var once sync.Once client := request.NewHelper(t.log) - bo := newBackoff() var restQueryUri string @@ -115,7 +114,7 @@ func (t *Octopus) run(done chan error) { if err := backoff.Retry(func() error { return backoffPermanentError(client.GetJSON(restQueryUri, &res)) - }, bo); err != nil { + }, bo()); err != nil { once.Do(func() { done <- err }) t.log.ERROR.Println(err) diff --git a/tariff/pun.go b/tariff/pun.go index 8d73ba66e7..1225928261 100644 --- a/tariff/pun.go +++ b/tariff/pun.go @@ -70,7 +70,6 @@ func NewPunFromConfig(other map[string]interface{}) (api.Tariff, error) { func (t *Pun) run(done chan error) { var once sync.Once - bo := newBackoff() tick := time.NewTicker(time.Hour) for ; true; <-tick.C { @@ -81,7 +80,7 @@ func (t *Pun) run(done chan error) { today, err = t.getData(time.Now()) return err - }, bo); err != nil { + }, bo()); err != nil { once.Do(func() { done <- err }) t.log.ERROR.Println(err) @@ -91,7 +90,7 @@ func (t *Pun) run(done chan error) { res, err := backoff.RetryWithData(func() (api.Rates, error) { res, err := t.getData(time.Now().AddDate(0, 0, 1)) return res, backoffPermanentError(err) - }, bo) + }, bo()) if err != nil { once.Do(func() { done <- err }) t.log.ERROR.Println(err) diff --git a/tariff/smartenergy.go b/tariff/smartenergy.go index 033b0e2965..17b0116f7b 100644 --- a/tariff/smartenergy.go +++ b/tariff/smartenergy.go @@ -49,7 +49,6 @@ func NewSmartEnergyFromConfig(other map[string]interface{}) (api.Tariff, error) func (t *SmartEnergy) run(done chan error) { var once sync.Once client := request.NewHelper(t.log) - bo := newBackoff() tick := time.NewTicker(time.Hour) for ; true; <-tick.C { @@ -57,7 +56,7 @@ func (t *SmartEnergy) run(done chan error) { if err := backoff.Retry(func() error { return backoffPermanentError(client.GetJSON(smartenergy.URI, &res)) - }, bo); err != nil { + }, bo()); err != nil { once.Do(func() { done <- err }) t.log.ERROR.Println(err) diff --git a/tariff/tariff.go b/tariff/tariff.go index 8986614ab7..1a1d77c18f 100644 --- a/tariff/tariff.go +++ b/tariff/tariff.go @@ -80,7 +80,6 @@ func NewConfigurableFromConfig(other map[string]interface{}) (api.Tariff, error) func (t *Tariff) run(forecastG func() (string, error), done chan error) { var once sync.Once - bo := newBackoff() tick := time.NewTicker(time.Hour) for ; true; <-tick.C { @@ -97,7 +96,7 @@ func (t *Tariff) run(forecastG func() (string, error), done chan error) { data[i].Price = t.totalPrice(r.Price) } return nil - }, bo); err != nil { + }, bo()); err != nil { once.Do(func() { done <- err }) t.log.ERROR.Println(err) diff --git a/tariff/tibber.go b/tariff/tibber.go index 9f9348d409..e339cad471 100644 --- a/tariff/tibber.go +++ b/tariff/tibber.go @@ -72,7 +72,6 @@ func NewTibberFromConfig(other map[string]interface{}) (api.Tariff, error) { func (t *Tibber) run(done chan error) { var once sync.Once - bo := newBackoff() v := map[string]interface{}{ "id": graphql.ID(t.homeID), @@ -94,7 +93,7 @@ func (t *Tibber) run(done chan error) { ctx, cancel := context.WithTimeout(context.Background(), request.Timeout) defer cancel() return t.client.Query(ctx, &res, v) - }, bo); err != nil { + }, bo()); err != nil { once.Do(func() { done <- err }) t.log.ERROR.Println(err) diff --git a/templates/definition/charger/elli-charger-pro.yaml b/templates/definition/charger/elli-charger-pro.yaml index d46854e590..7a190f4fe9 100644 --- a/templates/definition/charger/elli-charger-pro.yaml +++ b/templates/definition/charger/elli-charger-pro.yaml @@ -35,5 +35,3 @@ params: - preset: eebus render: | {{ include "eebus" . }} - meter: true - chargedEnergy: false diff --git a/templates/definition/charger/evbox-livo.yaml b/templates/definition/charger/evbox-livo.yaml new file mode 100644 index 0000000000..ae4f43fade --- /dev/null +++ b/templates/definition/charger/evbox-livo.yaml @@ -0,0 +1,14 @@ +template: livo +products: + - brand: EVBox + description: + generic: Livo +requirements: + evcc: ["eebus"] + description: + de: Das Gerät benötigt eine feste IP Adresse. + en: The device requires a fixed IP addres. +params: + - preset: eebus +render: | + {{ include "eebus" . }} diff --git a/templates/definition/charger/fronius-wattpilot.yaml b/templates/definition/charger/fronius-wattpilot.yaml index 90f7eac343..15d3d8efd2 100644 --- a/templates/definition/charger/fronius-wattpilot.yaml +++ b/templates/definition/charger/fronius-wattpilot.yaml @@ -1,4 +1,5 @@ template: fronius-wattpilot +deprecated: true products: - brand: Fronius description: diff --git a/templates/definition/charger/nrggen2.yaml b/templates/definition/charger/nrggen2.yaml new file mode 100644 index 0000000000..6b0b2538d1 --- /dev/null +++ b/templates/definition/charger/nrggen2.yaml @@ -0,0 +1,25 @@ +template: nrggen2 +products: + - brand: NRGKick + description: + generic: Gen2 +requirements: + evcc: ["sponsorship"] +capabilities: ["1p3p", "mA"] +params: + - name: modbus + choice: ["tcpip"] + id: 1 + - name: phases1p3p + type: bool + default: false + advanced: true + help: + de: Aktiviert Phasenumschaltung. Erweiterte Funktion "Phasenumschaltung" muss in der NRGkick App aktiviert sein. + en: Activates phase switching. Extended feature "Phase Switching" must be activated in the NRGKick app. +render: | + type: nrggen2 + {{- include "modbus" . }} + {{- if ne .phases1p3p "false" }} + phases1p3p: true + {{- end }} diff --git a/templates/definition/charger/ocpp-fronius-wattpilot.yaml b/templates/definition/charger/ocpp-fronius-wattpilot.yaml new file mode 100644 index 0000000000..11657c6484 --- /dev/null +++ b/templates/definition/charger/ocpp-fronius-wattpilot.yaml @@ -0,0 +1,9 @@ +template: ocpp-fronius-wattpilot +products: + - brand: Fronius + description: + generic: Wattpilot (OCPP) +params: + - preset: ocpp +render: | + {{ include "ocpp" . }} diff --git a/templates/definition/charger/victron-evcs.yaml b/templates/definition/charger/victron-evcs.yaml index 56c6f69d4e..690ecee885 100644 --- a/templates/definition/charger/victron-evcs.yaml +++ b/templates/definition/charger/victron-evcs.yaml @@ -2,7 +2,7 @@ template: victron-evcs products: - brand: Victron description: - generic: EV charging station + generic: EV Charging Station requirements: evcc: ["sponsorship"] description: diff --git a/templates/definition/charger/victron.yaml b/templates/definition/charger/victron.yaml index 7dd26adc51..fdf3911491 100644 --- a/templates/definition/charger/victron.yaml +++ b/templates/definition/charger/victron.yaml @@ -2,7 +2,7 @@ template: victron products: - brand: Victron description: - generic: EV Charging Station + generic: EV Charging Station (via GX) requirements: evcc: ["sponsorship"] description: diff --git a/templates/definition/meter/fronius-gen24.yaml b/templates/definition/meter/fronius-gen24.yaml index 2c2ae464a1..10f8f60f0d 100644 --- a/templates/definition/meter/fronius-gen24.yaml +++ b/templates/definition/meter/fronius-gen24.yaml @@ -6,6 +6,7 @@ products: - brand: Fronius description: generic: Primo GEN24 Plus +capabilities: ["battery-control"] params: - name: usage choice: ["grid", "pv", "battery"] @@ -59,11 +60,95 @@ render: | uri: {{ .host }}:{{ .port }} id: 1 value: 160:4:DCW # mppt 4 discharge + energy: + source: sunspec + uri: {{ .host }}:{{ .port }} + id: 1 + value: 160:4:DCWH # mppt 4 (discharge) + scale: 0.001 soc: source: sunspec uri: {{ .host }}:{{ .port }} id: 1 - value: 124:ChaState + value: 124:0:ChaState + batterymode: # model 124 + source: switch + switch: + - case: 1 # normal + set: + source: sequence + set: + - source: const + value: 0 # off + set: + source: sunspec + uri: {{ .host }}:{{ .port }} + id: 1 + value: 124:0:ChaGriSet + - source: const + value: 0 + set: + source: sunspec + uri: {{ .host }}:{{ .port }} + id: 1 + value: 124:0:StorCtl_Mod + - source: const + value: 100 # % + set: + source: sunspec + uri: {{ .host }}:{{ .port }} + id: 1 + value: 124:0:OutWRte + - case: 2 # hold + set: + source: sequence + set: + - source: const + value: 0 # off + set: + source: sunspec + uri: {{ .host }}:{{ .port }} + id: 1 + value: 124:0:ChaGriSet + - source: const + value: 2 + set: + source: sunspec + uri: {{ .host }}:{{ .port }} + id: 1 + value: 124:0:StorCtl_Mod + - source: const + value: 0 # % + set: + source: sunspec + uri: {{ .host }}:{{ .port }} + id: 1 + value: 124:0:OutWRte + - case: 3 # charge + set: + source: sequence + set: + - source: const + value: 1 # off + set: + source: sunspec + uri: {{ .host }}:{{ .port }} + id: 1 + value: 124:0:ChaGriSet + - source: const + value: 2 + set: + source: sunspec + uri: {{ .host }}:{{ .port }} + id: 1 + value: 124:0:StorCtl_Mod + - source: const + value: -100 # % + set: + source: sunspec + uri: {{ .host }}:{{ .port }} + id: 1 + value: 124:0:OutWRte {{- if .capacity }} capacity: {{ .capacity }} # kWh {{- end }} diff --git a/templates/definition/meter/goodwe-hybrid.yaml b/templates/definition/meter/goodwe-hybrid.yaml index a25984222c..59dde85ffd 100644 --- a/templates/definition/meter/goodwe-hybrid.yaml +++ b/templates/definition/meter/goodwe-hybrid.yaml @@ -11,6 +11,11 @@ params: choice: ["rs485", "tcpip", "udp"] baudrate: 9600 id: 247 + - name: battery + default: 1 + validvalues: + - 1 + - 2 - name: capacity advanced: true render: | @@ -75,16 +80,17 @@ render: | source: modbus {{- include "modbus" . | indent 2 }} register: # manual non-sunspec register configuration - address: 35182 # Battery1 Power + address: {{ if eq .battery "1" }}35182{{ else }}35265{{ end }} # Battery1/2 Power type: holding decode: int32 soc: source: modbus {{- include "modbus" . | indent 2 }} register: # manual non-sunspec register configuration - address: 37007 # SOC + address: {{ if eq .battery "1" }}37007{{ else }}39005{{ end }} # Battery1/2 Soc type: holding decode: uint16 + {{- if eq .battery "1" }} energy: source: modbus {{- include "modbus" . | indent 2 }} @@ -93,6 +99,7 @@ render: | type: holding decode: uint32 scale: 0.1 + {{- end }} batterymode: source: watchdog timeout: 30s diff --git a/util/modbus/connection.go b/util/modbus/connection.go index 672a44ffd2..9036d838f0 100644 --- a/util/modbus/connection.go +++ b/util/modbus/connection.go @@ -1,18 +1,15 @@ package modbus import ( - "sync" "time" - "github.com/grid-x/modbus" "github.com/volkszaehler/mbmd/meters" ) // Connection is a logical modbus connection per slave ID sharing a physical connection type Connection struct { + *logger meters.Connection - mu sync.Mutex - logger *logger logical meters.Logger delay time.Duration } @@ -42,72 +39,80 @@ func (c *Connection) Timeout(timeout time.Duration) { } } -func (c *Connection) Logger(l modbus.Logger) { - c.mu.Lock() - defer c.mu.Unlock() +func (c *Connection) exec(fun func() ([]byte, error)) ([]byte, error) { + return c.WithLogger(c.logical, func() ([]byte, error) { + time.Sleep(c.delay) - c.logical = l -} - -func (c *Connection) prepare() { - c.mu.Lock() - defer c.mu.Unlock() - - time.Sleep(c.delay) - c.logger.Logger(c.logical) + b, err := fun() + if err != nil { + c.Connection.Close() + } + return b, err + }) } func (c *Connection) ReadCoils(address, quantity uint16) ([]byte, error) { - c.prepare() - return c.ModbusClient().ReadCoils(address, quantity) + return c.exec(func() ([]byte, error) { + return c.ModbusClient().ReadCoils(address, quantity) + }) } func (c *Connection) WriteSingleCoil(address, value uint16) ([]byte, error) { - c.prepare() - return c.ModbusClient().WriteSingleCoil(address, value) + return c.exec(func() ([]byte, error) { + return c.ModbusClient().WriteSingleCoil(address, value) + }) } func (c *Connection) ReadInputRegisters(address, quantity uint16) ([]byte, error) { - c.prepare() - return c.ModbusClient().ReadInputRegisters(address, quantity) + return c.exec(func() ([]byte, error) { + return c.ModbusClient().ReadInputRegisters(address, quantity) + }) } func (c *Connection) ReadHoldingRegisters(address, quantity uint16) ([]byte, error) { - c.prepare() - return c.ModbusClient().ReadHoldingRegisters(address, quantity) + return c.exec(func() ([]byte, error) { + return c.ModbusClient().ReadHoldingRegisters(address, quantity) + }) } func (c *Connection) WriteSingleRegister(address, value uint16) ([]byte, error) { - c.prepare() - return c.ModbusClient().WriteSingleRegister(address, value) + return c.exec(func() ([]byte, error) { + return c.ModbusClient().WriteSingleRegister(address, value) + }) } func (c *Connection) WriteMultipleRegisters(address, quantity uint16, value []byte) ([]byte, error) { - c.prepare() - return c.ModbusClient().WriteMultipleRegisters(address, quantity, value) + return c.exec(func() ([]byte, error) { + return c.ModbusClient().WriteMultipleRegisters(address, quantity, value) + }) } func (c *Connection) ReadDiscreteInputs(address, quantity uint16) (results []byte, err error) { - c.prepare() - return c.ModbusClient().ReadDiscreteInputs(address, quantity) + return c.exec(func() ([]byte, error) { + return c.ModbusClient().ReadDiscreteInputs(address, quantity) + }) } func (c *Connection) WriteMultipleCoils(address, quantity uint16, value []byte) (results []byte, err error) { - c.prepare() - return c.ModbusClient().WriteMultipleCoils(address, quantity, value) + return c.exec(func() ([]byte, error) { + return c.ModbusClient().WriteMultipleCoils(address, quantity, value) + }) } func (c *Connection) ReadWriteMultipleRegisters(readAddress, readQuantity, writeAddress, writeQuantity uint16, value []byte) (results []byte, err error) { - c.prepare() - return c.ModbusClient().ReadWriteMultipleRegisters(readAddress, readQuantity, writeAddress, writeQuantity, value) + return c.exec(func() ([]byte, error) { + return c.ModbusClient().ReadWriteMultipleRegisters(readAddress, readQuantity, writeAddress, writeQuantity, value) + }) } func (c *Connection) MaskWriteRegister(address, andMask, orMask uint16) (results []byte, err error) { - c.prepare() - return c.ModbusClient().MaskWriteRegister(address, andMask, orMask) + return c.exec(func() ([]byte, error) { + return c.ModbusClient().MaskWriteRegister(address, andMask, orMask) + }) } func (c *Connection) ReadFIFOQueue(address uint16) (results []byte, err error) { - c.prepare() - return c.ModbusClient().ReadFIFOQueue(address) + return c.exec(func() ([]byte, error) { + return c.ModbusClient().ReadFIFOQueue(address) + }) } diff --git a/util/modbus/log.go b/util/modbus/log.go index 59495eb20b..08043e7cd9 100644 --- a/util/modbus/log.go +++ b/util/modbus/log.go @@ -2,6 +2,7 @@ package modbus import ( "sync" + "time" "github.com/grid-x/modbus" "github.com/volkszaehler/mbmd/meters" @@ -12,17 +13,22 @@ type logger struct { logger meters.Logger } -func (l *logger) Logger(logger modbus.Logger) { +func (l *logger) WithLogger(logger modbus.Logger, fun func() ([]byte, error)) ([]byte, error) { l.mu.Lock() defer l.mu.Unlock() - l.logger = logger + if l.logger != logger { + // small delay when switching logger/ slave id to mimic mbmd behavior + time.Sleep(10 * time.Millisecond) + l.logger = logger + } + + return fun() } +// Printf implements modbus.Logger interface. +// Must always be called while being wrapped in WithLogger, hence the lock is held. func (l *logger) Printf(format string, v ...interface{}) { - l.mu.RLock() - defer l.mu.RUnlock() - if l.logger != nil { l.logger.Printf(format, v...) } diff --git a/util/templates/defaults.yaml b/util/templates/defaults.yaml index 32c24a7d8a..c3dc3885d0 100644 --- a/util/templates/defaults.yaml +++ b/util/templates/defaults.yaml @@ -128,30 +128,6 @@ params: advanced: true type: duration example: 5m - - name: cloud - description: - de: evcc Cloud - en: evcc Cloud - help: - de: | - Anstatt auf die Online API des Fahrzeugherstellers von der lokalen Installation zuzugreifen, - wird dies über die evcc Cloud gemacht. Die Zugangsdaten und VIN werden sicher und verschlüsselt - übertragen und in der Cloud nicht gespeichert. - - Die Vorteile sind: - - Falls sich etwas an der Online API des Fahrzeugherstellers ändert, können Updates für alle Benutzer viel schneller zur Verfügung gestellt werden - - Man muss nicht auf eine neue Version von evcc warten und dann ein lokales Update von evcc durchführen - en: | - Instead of connecting to the vehicle manufacturers online API from your local installation, - this will use the evcc Cloud instead. Your credentials and VIN will be transfered encrypted and - securely and won't be stored in the cloud. - - The benefits are: - - If the vehicles online API changes, we can update this for all users more quickly - - No waiting for a new evcc version in this case and then requiring an update - requirements: - evcc: ["sponsorship"] - type: bool - name: timeout description: de: Zeitüberschreitung @@ -376,12 +352,8 @@ presets: - name: stationid type: string help: - en: | - Station ID of the charging point. Only required if multiple OCPP charging stations are set up to assign them correctly. A single OCPP charging station can also be automatically assigned. - Note: In exceptional cases, it may be necessary to manually append this ID to the OCPP URL of the charging station in the form ws://:8887/. Most charging stations automatically add the ID internally. - de: | - Station ID des Ladepunktes. Nur erforderlich wenn mehrere OCPP-Ladestationen eingerichtet sind um diese korrekt zuzuweisen. Eine einzelne OCPP-Ladestation kann auch automatisch zugeordnet werden. - Hinweis: In Ausnahmefällen kann es erforderlich sein, diese ID manuell an die OCPP-URL der Ladestation in der Form ws://:8887/ anzuhängen. Die meisten Ladestationen fügen die ID intern automatisch hinzu. + en: "Station ID of the charging point. Only required if multiple OCPP charging stations are set up to assign them correctly. A single OCPP charging station can also be automatically assigned. Note: In exceptional cases, it may be necessary to manually append this ID to the OCPP URL of the charging station in the form ws://:8887/. Most charging stations automatically add the ID internally." + de: "Station ID des Ladepunktes. Nur erforderlich wenn mehrere OCPP-Ladestationen eingerichtet sind um diese korrekt zuzuweisen. Eine einzelne OCPP-Ladestation kann auch automatisch zugeordnet werden. Hinweis: In Ausnahmefällen kann es erforderlich sein, diese ID manuell an die OCPP-URL der Ladestation in der Form ws://:8887/ anzuhängen. Die meisten Ladestationen fügen die ID intern automatisch hinzu." example: EVB-P12354 - name: connector help: @@ -395,16 +367,8 @@ presets: de: Immer eine Remote-Transaktion starten (RemoteStartTransaction) sobald ein Fahrzeug angeschlossen wird en: Always start a remote transaction (RemoteStartTransaction) as soon as a vehicle is connected help: - de: | - Diese Option nur aktivieren wenn keinerlei Möglichkeit besteht Transaktionen seitens des Ladepunktes zu initiieren! - Das ist nur der Fall wenn z. B. kein RFID-Lesegerät vorhanden ist und Ladevorgänge grundsätzlich einzeln per App freigeschaltet werden müssten. - Normalerweise sollte der Ladepunkt am Gerät immer so konfiguriert werden, dass entweder eine RFID-Karte zur Freischaltung verwendet wird oder der Ladepunkt auf "Autostart", "Freies Laden" o.ä. eingestellt ist. - Zunächst die Dokumentation und die Konfigurationsmöglichkeiten des Ladepunktes prüfen, ggf. beim Hersteller nachfragen! - en: | - Only enable this option if there is no way to initiate transactions from the charger side! - This is only the case if e.g. no RFID reader is available and charging processes would have to be released individually via app. - Normally, the charger should always be configured at the device so that either an RFID card is used for activation or the charger is set to "Autostart", "Free Charging" or similar. - First check the documentation and configuration possibilities of the charger, ask the manufacturer if necessary! + de: Diese Option nur aktivieren wenn keinerlei Möglichkeit besteht Transaktionen seitens des Ladepunktes zu initiieren! Das ist nur der Fall wenn z. B. kein RFID-Lesegerät vorhanden ist und Ladevorgänge grundsätzlich einzeln per App freigeschaltet werden müssten. Normalerweise sollte der Ladepunkt am Gerät immer so konfiguriert werden, dass entweder eine RFID-Karte zur Freischaltung verwendet wird oder der Ladepunkt auf "Autostart", "Freies Laden" o.ä. eingestellt ist. Zunächst die Dokumentation und die Konfigurationsmöglichkeiten des Ladepunktes prüfen, ggf. beim Hersteller nachfragen! + en: Only enable this option if there is no way to initiate transactions from the charger side! This is only the case if e.g. no RFID reader is available and charging processes would have to be released individually via app. Normally, the charger should always be configured at the device so that either an RFID card is used for activation or the charger is set to "Autostart", "Free Charging" or similar. First check the documentation and configuration possibilities of the charger, ask the manufacturer if necessary! - name: idtag advanced: true type: string @@ -412,10 +376,8 @@ presets: en: Identifier used for authentication of external transactions (RemoteStartTransaction) de: Identifikator zur Authentifizierung von externen Transaktionen (RemoteStartTransaction) help: - de: | - Diese Option ist nur in Ausnahmefällen erforderlich wenn der Ladepunkt für die Annahme externer Transaktionen einen spezifischen Token erfordert. - en: | - This option is only required in exceptional cases if the charger requires a specific token for accepting external transactions. + de: Diese Option ist nur in Ausnahmefällen erforderlich wenn der Ladepunkt für die Annahme externer Transaktionen einen spezifischen Token erfordert. + en: This option is only required in exceptional cases if the charger requires a specific token for accepting external transactions. example: evcc - name: connecttimeout advanced: true diff --git a/util/templates/includes/ocpp.tpl b/util/templates/includes/ocpp.tpl index 4c5787bd16..99d60ecc47 100644 --- a/util/templates/includes/ocpp.tpl +++ b/util/templates/includes/ocpp.tpl @@ -9,6 +9,9 @@ connector: {{ .connector }} {{- if .idtag }} idtag: {{ .idtag }} {{- end }} +{{- if ne .remotestart "false"}} +remotestart: {{ .remotestart }} +{{- end }} {{- if ne .connecttimeout "5m" }} connecttimeout: {{ .connecttimeout }} {{- end }} diff --git a/util/templates/merge.go b/util/templates/merge.go new file mode 100644 index 0000000000..fa9efdfc49 --- /dev/null +++ b/util/templates/merge.go @@ -0,0 +1,55 @@ +package templates + +import ( + "reflect" + "strings" +) + +// https://github.com/peterbourgon/mergemap + +const mergeMaxDepth = 100 + +var matchKey = strings.EqualFold + +// mergeMaps recursively merges other into target using matchKey for key comparison +func mergeMaps(other map[string]any, target map[string]any) error { + // return mergo.Map(&target, other, mergo.WithOverride) + // return util.DecodeOther(other, target) + merge(target, other, 0) + return nil +} + +func merge(dst, src map[string]any, depth int) map[string]any { + if depth > mergeMaxDepth { + panic("too deep!") + } + for key, srcVal := range src { + for k := range dst { + if matchKey(k, key) { + // overwrite key + key = k + + srcMap, srcMapOk := mapify(srcVal) + dstMap, dstMapOk := mapify(dst[k]) + if srcMapOk && dstMapOk { + srcVal = merge(dstMap, srcMap, depth+1) + } + break + } + } + dst[key] = srcVal + } + return dst +} + +func mapify(i any) (map[string]any, bool) { + value := reflect.ValueOf(i) + if value.Kind() == reflect.Map { + m := map[string]any{} + for _, k := range value.MapKeys() { + m[k.String()] = value.MapIndex(k).Interface() + } + return m, true + } + return map[string]any{}, false +} diff --git a/util/templates/merge_test.go b/util/templates/merge_test.go new file mode 100644 index 0000000000..b8e878872a --- /dev/null +++ b/util/templates/merge_test.go @@ -0,0 +1,32 @@ +package templates + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestMergeMaps(t *testing.T) { + target := map[string]any{ + "foo": "bar", + "nested": map[string]any{ + "bar": "baz", + }, + } + other := map[string]any{ + "Foo": 1, + "Nested": map[string]any{ + "Bar": 2, + }, + "baz": 3, + } + + require.NoError(t, mergeMaps(other, target)) + require.Equal(t, map[string]any{ + "foo": 1, + "nested": map[string]any{ + "bar": 2, + }, + "baz": 3, + }, target) +} diff --git a/util/templates/render_instance.go b/util/templates/render_instance.go index 3c6577f5c8..441129f40b 100644 --- a/util/templates/render_instance.go +++ b/util/templates/render_instance.go @@ -3,6 +3,7 @@ package templates import ( "errors" "fmt" + "os" "github.com/evcc-io/evcc/util" "gopkg.in/yaml.v3" @@ -35,6 +36,10 @@ func RenderInstance(class Class, other map[string]interface{}) (*Instance, error return nil, util.NewConfigError(err) } + if os.Getenv("EVCC_TEMPLATE_RENDER") == cc.Template { + fmt.Println(string(b)) + } + var instance Instance if err := yaml.Unmarshal(b, &instance); err != nil { return nil, fmt.Errorf("%w:\n%s", err, string(b)) diff --git a/util/templates/template.go b/util/templates/template.go index b982a8f291..c2e1d09b9a 100644 --- a/util/templates/template.go +++ b/util/templates/template.go @@ -9,7 +9,6 @@ import ( "strings" "text/template" - "github.com/evcc-io/evcc/util" "github.com/go-sprout/sprout" ) @@ -267,9 +266,9 @@ func (t *Template) RenderProxyWithValues(values map[string]interface{}, lang str } // RenderResult renders the result template to instantiate the proxy -func (t *Template) RenderResult(renderMode int, other map[string]interface{}) ([]byte, map[string]interface{}, error) { +func (t *Template) RenderResult(renderMode int, other map[string]any) ([]byte, map[string]any, error) { values := t.Defaults(renderMode) - if err := util.DecodeOther(other, &values); err != nil { + if err := mergeMaps(other, values); err != nil { return nil, values, err } diff --git a/vehicle/tesla.go b/vehicle/tesla.go index fd5525968f..6a0c8c74c1 100644 --- a/vehicle/tesla.go +++ b/vehicle/tesla.go @@ -111,6 +111,8 @@ func NewTeslaFromConfig(other map[string]interface{}) (api.Vehicle, error) { } tcc.SetBaseUrl(cc.CommandProxy) + log.INFO.Printf("!! tesla proxy for %s: %s", vehicle.DisplayName, cc.CommandProxy) + v := &Tesla{ embed: &cc.embed, Provider: tesla.NewProvider(vehicle, cc.Cache),