From fe6ef6dd7c64d81938ef958e5202bdda3db54136 Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Thu, 5 Mar 2026 19:40:35 +0100 Subject: [PATCH 0001/1506] Translations update from Hosted Weblate (#27521) --- i18n/da.json | 173 ++++++++++++++++++++++- i18n/de.json | 9 +- i18n/fi.json | 101 +++++++++++++- i18n/fr.json | 171 ++++++++++++++++++++++- i18n/lb.json | 179 ++++++++++++++++++++++-- i18n/lt.json | 169 +++++++++++++++++++++- i18n/nl.json | 186 ++++++++++++++++++++++--- i18n/pl.json | 101 ++++++++++++-- i18n/pt.json | 177 +++++++++++++++++++++-- i18n/ro.json | 202 +++++++++++++++++++++++++-- i18n/sv.json | 92 +++++++++++- i18n/ta.json | 386 ++++++++++++++++++++++++++++++++++++++++++++++----- i18n/tr.json | 204 +++++++++++++++++++++++++-- 13 files changed, 2010 insertions(+), 140 deletions(-) diff --git a/i18n/da.json b/i18n/da.json index 1bc0e1b0f..f4c70ee23 100644 --- a/i18n/da.json +++ b/i18n/da.json @@ -87,7 +87,7 @@ }, "heatpump": "Brugerdefineret varmepumpe", "sgready": "Brugerdefineret varmepumpe (sg-ready via plugins)", - "sgready-boost": "Brugerdefineret varmepumpe (SG-Ready-Boost, ikke længere understøttet)", + "sgready-boost": "Brugerdefineret varmepumpe (sg-ready-boost, ikke længere understøttet)", "sgready-relay": "Brugerdefineret varmepumpe (sg-ready via relæer)", "switchsocket": "Brugerdefineret stikkontakt" } @@ -105,6 +105,12 @@ "labelResidualPower": "Tilbageværende effekt", "title": "kontrol adfærd" }, + "currency": { + "description": "Bruges til at formatere energipriser, omkostninger og besparelser baseret på din tarif.", + "example": "Din opladningspris var {price}. Du har sparet {amount}.", + "label": "Møntfod", + "title": "Møntfod" + }, "deviceValue": { "amount": "Mængde", "broker": "Broker", @@ -128,12 +134,16 @@ "dimmed": "Reduceret", "enabled": "Aktiveret", "energy": "Energi", + "events": "Begivenheder", "feedinPrice": "Indfødningstarif", + "forecast": "Prognose", "gridPrice": "God pris", "heaterTempLimit": "Opvarmning begrænsning", "hemsActiveLimit": "Aktiv begrænsning", "hemsType": "Kommunikation", "identifier": "RFID-identifikator", + "max": "max", + "messengers": "Tjenester", "no": "nej", "odometer": "Kilometertæller", "org": "Organisation", @@ -143,6 +153,7 @@ "phases1p3p": "Fase-skift", "power": "Effekt", "powerRange": "Effekt", + "price": "Pris", "range": "Rækkevidde", "singlePhase": "Enkelt fase", "soc": "Oplader", @@ -214,6 +225,7 @@ "cancel": "Afbryd", "clear": "Slet", "close": "Luk", + "confirmSave": "Ændringer er ikke gemt. Skal de gemmes nu?", "copied": "Kopieret!", "copy": "Kopier", "customHelp": "Opret en brugerdefineret enhed ved hjælp af evcc's plugin-system.", @@ -229,6 +241,7 @@ "fromYamlHint": "Bemærk: Er konfigureret via evcc.yaml. Fjern indtastningen fra filen for at aktivere redigering her.", "hideAdvancedSettings": "Skjul avancerede indstillinger", "invalidFileSelected": "Ugyldig fil er valgt", + "legacy": "ældre", "noFileSelected": "Ingen fil valgt.", "off": "slukket", "on": "tændt", @@ -320,6 +333,8 @@ }, "defaultModeHelpKeep": "Gemmer den senest valgte tilstand.", "defaultModeLabel": "Standardtilstand", + "defaultsHint": "Standardtilstand, soladfærd og elektriske detaljer bruger fornuftige standardværdier.", + "defaultsHintLink": "Juster indstillinger", "delete": "Slet", "electricalSubtitle": "Ved tvivl, spørg din elektriker.", "electricalTitle": "Elektrisk", @@ -410,9 +425,66 @@ "welcomeBannerTitle": "Lad os konfigurere dit system!" }, "messaging": { - "description": "Modtag beskeder om dine opladningssessioner.", + "addMessenger": "Tilføj tjeneste", + "description": "Modtag beskeder om opladningssessioner.", + "event": { + "asleep": { + "messageDefault": "Ladefrigivelse: Køretøjet {vehicleName} lader ikke.", + "title": "Når der ventes på køretøj", + "titleDefault": "Køretøjet sover" + }, + "connect": { + "messageDefault": "Bil forbundet med {pvPower}kW PV", + "title": "Når bilen forbindes", + "titleDefault": "Bil er forbundet" + }, + "disconnect": { + "messageDefault": "Bilen er frakoblet efter {connectedDuration}", + "title": "Når bilen frakobles", + "titleDefault": "Bilen er frakoblet" + }, + "guest": { + "messageDefault": "Ukendt køretøj, er et gæstekøretøj forbundet?", + "title": "Når en ukendt bil forbindes", + "titleDefault": "Ukendt køretøj" + }, + "planoverrun": { + "messageDefault": "{vehicleTitle}: Tidsplanen overskrides.", + "title": "Når opladningsplanen overskrides", + "titleDefault": "Planen overskrides" + }, + "soc": { + "messageDefault": "Batteri er opladet til {vehicleSoc}%", + "title": "Opdatering af ladeniveau", + "titleDefault": "Opladningsniveau er opdateret" + }, + "start": { + "messageDefault": "Opladning er startet i {mode} mode.", + "title": "Når opladning starter", + "titleDefault": "Opladning er startet" + }, + "stop": { + "messageDefault": "Opladning er afsluttet {chargedEnergy}kWh på {chargeDuration}.", + "title": "Når opladning stopper", + "titleDefault": "Opladning er færdig" + } + }, + "eventMessage": "Besked", + "eventTitle": "Titel", + "events": "Hændelser", + "legacyWarning": "Ny konfiguration til notifikationer er tilgængelig! Fjern og gem din konfiguration her for at tage den nye guidede proces i brug.", + "messengers": "Tjenester", + "seePlaceholders": "Vis pladsholdere", "title": "Beskeder" }, + "messenger": { + "custom": "Bruger-defineret tjeneste", + "generic": "Standardtjeneste", + "primary": "Specifik tjeneste", + "template": "Tjeneste", + "titleAdd": "Tilføj tjeneste", + "titleEdit": "Juster tjeneste" + }, "meter": { "cancel": "Afbryd", "delete": "Slet", @@ -508,7 +580,8 @@ "labelInternalUrl": "Intern webadresse", "labelPort": "port", "labelSchema": "Skema", - "title": "Netværk" + "title": "Netværk", + "warningUrlPath": "URL’en kræver som regel ikke en sti. Er du sikker på, at det er rigtigt?" }, "ocpp": { "connectedChargers": "Tilsluttede ladere", @@ -636,6 +709,79 @@ "restartingDescription": "Vent venligst…", "restartingMessage": "Genstarter evcc." }, + "tariff": { + "addForecast": "Tilføj prognose", + "addTariff": "Tilføj tarif", + "co2": { + "description": "CO₂-intensitetsprognose for netstrøm. Til CO₂-optimeret opladning og beregning af emissionsbesparelser.", + "titleAdd": "Tilføj CO₂ prognose", + "titleEdit": "Juster CO₂ prognose" + }, + "co2Services": "CO₂ tjenester", + "customForecast": "Bruger-defineret prognose", + "customTariff": "Bruger-defineret tarif", + "description": "Konfigurér dine energitariffer og prognoser. Brug enhedsbaseret konfiguration til dynamisk styring eller YAML til statiske indstillinger.", + "feedIn": { + "description": "Kompenser for eksporteret energi til el-nettet. Benyttes for at beregne den aktuelle opladningspris.", + "titleAdd": "Tilføj eksport tarif til el-nettet", + "titleEdit": "Juster eksport tarif til el-nettet" + }, + "generic": "Generiske integrationer", + "grid": { + "description": "Elpris for strøm fra el-nettet. Bruges til beregning af faktiske opladningsomkostninger og prisoptimeret opladning af køretøjer, varmeenheder eller opladning af hjemmebatteriet fra el-nettet.", + "titleAdd": "Tilføj elnet import tarif", + "titleEdit": "Juster elnet import tarif" + }, + "legacyWarning": "Ny tarif konfiguration er tilgængelig. Fjern og gem dine tariffer her, for at benytte den nye guidede proces.", + "option": { + "co2": "Tilføj CO₂ prognose", + "feedIn": "Tilføj elnet eksport tarif", + "grid": "Tilføj elnet import tarif", + "planner": "Tilføj planlægnings prognose", + "solar": "Tilføj sol prognose" + }, + "planner": { + "description": "Avanceret indstilling. Normalt ikke nødvendig, da dynamiske eltariffer eller CO₂-prognoser bruges automatisk. Aktiverer en ekstra datakilde, som kun anvendes til ladeplanlægning – ikke til statistik eller prisberegninger.", + "titleAdd": "Tilføj planlægnings prognose", + "titleEdit": "Juster planlægningsprognose" + }, + "services": "Tjenester", + "solar": { + "description": "Prognose for energi produktion fra dit solcelleanlæg. Vises i brugergrænsefladen og vil blive brugt af optimeringsalgoritmer i fremtiden.", + "titleAdd": "Tilføj sol prognose", + "titleEdit": "Juster sol prognose" + }, + "template": "Udbyder", + "title": "Tariffer og prognoser", + "titleChoice": "Hvad ønsker du at tilføje?", + "type": { + "co2": "CO₂ Intensitet", + "feedIn": "Indfødningspris", + "grid": "Forbrugspris fra elnettet", + "planner": "Planlægning", + "solar": "Sol" + }, + "zones": { + "add": "Tilføj zone", + "allDays": "Alle dage", + "allMonths": "Alle måneder", + "allTimes": "Alle tidspunkter", + "cancel": "Fortryd", + "days": "Dage", + "edit": "Juster", + "hours": "Timer", + "months": "Måneder", + "price": "Pris", + "priceRequired": "Pris er nødvendig", + "remove": "Fjern zone", + "save": "Gem", + "selectAll": "Alle dage", + "timeFrom": "Fra", + "timeRangeError": "Starttidspunkt skal være før sluttidspunkt. For at krydse midnat, skal der oprettes to separate zoner.", + "timeTo": "Til", + "weekdays": "Ugedage" + } + }, "tariffs": { "description": "Definer dine energitakster for at beregne omkostningerne ved dine opladningssessioner.", "title": "Tariffer" @@ -652,7 +798,7 @@ "validation": { "failed": "mislykkedes", "label": "Status", - "running": "Validering…", + "running": "validering…", "success": "succes", "unknown": "ukendt", "validate": "validerer" @@ -942,10 +1088,13 @@ "battery": "Batteri", "batteryCharge": "Batteriet oplades", "batteryDischarge": "Batteriet aflades", - "batteryGridChargeActive": "Oplader fra elnettet", - "batteryGridChargeLimit": "Oplader fra elnettet når", + "batteryForecastEmpty": "tom {time}", + "batteryForecastFull": "fyldt {time}", + "batteryGridChargeActive": "Opladning fra elnettet: aktiv", + "batteryGridChargeLimit": "Opladning fra elnettet: når", "batteryHold": "Batteri (låst)", "batteryTooltip": "{energy} af {total} ({soc})", + "forecast": "Prognose: ", "forecastTooltip": "prognose: resterende solenergi produktion i dag", "gridImport": "Forbrug fra elnet", "homePower": "Forbrug", @@ -972,6 +1121,7 @@ "charged": "Opladet", "co2": "⌀ CO₂", "duration": "Varighed", + "emission": "Udledning", "fallbackName": "Ladepunkt", "finished": "Sluttidspunkt", "power": "Effekt", @@ -984,9 +1134,14 @@ "loadpointSettings": { "batteryBoost": { "description": "Hurtig opladning fra husbatteriet, indtil det er afladet til {limit}.", + "descriptionDisabled": "Vælg en grænse for at tillade hurtig opladning fra hjemmebatteriet.", + "disabled": "Deaktiveret", "label": "Batteri boost", "mode": "Kun tilgængelig i solar og min+solar mode.", - "once": "Boost er aktiveret for denne opladning." + "once": "Boost er aktiveret for denne opladning.", + "stateActive": "Batteriboost er aktiv", + "stateBelowLimit": "Batteriniveau er for lav til boost", + "stateReady": "Batteriboost er klar" }, "batteryUsage": "Husbatteri", "currents": "Ladestrøm", @@ -1092,6 +1247,9 @@ "vehicleStatus": { "awaitingAuthorization": "Venter på godkendelse.", "batteryBoost": "Boost fra batteri er aktiv.", + "batteryBoostBelowLimit": "Batteriniveau er for lav til boost.", + "batteryBoostDisabled": "Batteriboost er deaktiveret.", + "batteryBoostEnabled": "Boost indtil batteri er på {limit}.", "charging": "Oplader…", "cheapEnergyCharging": "Billig energi er tilgængelig.", "cheapEnergyNextStart": "Billig energi i {duration}.", @@ -1116,6 +1274,7 @@ "targetChargeWaitForVehicle": "Plan for opladning er klar. Venter på køretøj…", "vehicleLimit": "Køretøjets grænse", "vehicleLimitReached": "Køretøjets grænse er nået.", + "waitForAuthorization": "Forbundet. Venter på autorisation…", "waitForVehicle": "Parat. Venter på køretøj…", "welcome": "Kort indledende opladning for at bekræfte forbindelsen." }, diff --git a/i18n/de.json b/i18n/de.json index 8bb70a26a..fc3343fa7 100644 --- a/i18n/de.json +++ b/i18n/de.json @@ -86,9 +86,9 @@ "heating": "Benutzerdefinierte Heizung" }, "heatpump": "Benutzerdefinierte Wärmepumpe", - "sgready": "Benutzerdefinierte Wärmepumpe (SG-Ready über Plugins)", + "sgready": "Benutzerdefinierte Wärmepumpe (sg-ready über Plugins)", "sgready-boost": "Benutzerdefinierte Wärmepumpe (sg-ready-boost, veraltet)", - "sgready-relay": "Benutzerdefinierte Wärmepumpe (SG-Ready über Relais)", + "sgready-relay": "Benutzerdefinierte Wärmepumpe (sg-ready über Relais)", "switchsocket": "Benutzerdefinierte schaltbare Steckdose" } }, @@ -153,6 +153,7 @@ "phases1p3p": "Phasenumschaltung", "power": "Leistung", "powerRange": "Leistung", + "price": "Preis", "range": "Reichweite", "singlePhase": "Einphasig", "soc": "Ladestand", @@ -424,7 +425,7 @@ }, "messaging": { "addMessenger": "Dienst hinzufügen", - "description": "Benachrichtigungen über Ladevorgänge und andere Ereignisse erhalten.", + "description": "Benachrichtigungen über Ladevorgänge erhalten.", "event": { "asleep": { "messageDefault": "Ladefreigabe, Fahrzeug {vehicleName} lädt nicht.", @@ -621,7 +622,7 @@ }, "pv": { "titleAdd": "PV-Anlage hinzufügen", - "titleEdit": "PV-Anlage bearbeiten" + "titleEdit": "PV-Zähler bearbeiten" }, "section": { "additionalMeter": "Zusätzliche Zähler", diff --git a/i18n/fi.json b/i18n/fi.json index 8f9bdb4f7..d452e0482 100644 --- a/i18n/fi.json +++ b/i18n/fi.json @@ -105,6 +105,12 @@ "labelResidualPower": "Jäännösteho", "title": "Määrittele toimintaa" }, + "currency": { + "description": "Käytetään energian hinnan, tariffien mukaisten kulujen ja säästöjen ilmoittamiseen.", + "example": "Latauksesi oli {price} hintainen. Säästit {amount}.", + "label": "Valuutta", + "title": "Valuutta" + }, "deviceValue": { "amount": "Määrä", "broker": "Välittäjä (Broker)", @@ -130,11 +136,13 @@ "energy": "Energia", "events": "Tapahtumat", "feedinPrice": "Sähköverkkoon myynninhinta", + "forecast": "Ennuste", "gridPrice": "Verkosta ostonhinta", "heaterTempLimit": "Lämmityksen raja", "hemsActiveLimit": "Aktiivinen raja", "hemsType": "Kommunikaatiomenetelmä", "identifier": "RFID-tunniste", + "max": "maksimi", "messengers": "Palvelut", "no": "ei", "odometer": "Matkamittari", @@ -145,6 +153,7 @@ "phases1p3p": "Vaihekytkin", "power": "Teho", "powerRange": "Teho", + "price": "Hinta", "range": "Toimintasäde", "singlePhase": "Yksivaiheinen", "soc": "Lataus", @@ -324,6 +333,8 @@ }, "defaultModeHelpKeep": "Säilyttää viimeksi valitun lataus/käyttötilan.", "defaultModeLabel": "Oletustila", + "defaultsHint": "Vakiotila, aurinkovoiman käyttö ja sähköjärjestelmän yksityiskohdat käyttävät järkeviä vakioasetuksia.", + "defaultsHintLink": "Säädä asetuksia", "delete": "Poista", "electricalSubtitle": "Jos olet epävarma, kysy sähköasentajaltasi.", "electricalTitle": "Sähköinen", @@ -569,7 +580,8 @@ "labelInternalUrl": "Sisäinen URL", "labelPort": "Portti", "labelSchema": "Kaavio", - "title": "Verkko" + "title": "Verkko", + "warningUrlPath": "URL ei yleensä sisällä polkua. Oletko varma, että asetus on oikein?" }, "ocpp": { "connectedChargers": "Yhdistetyt laturit", @@ -696,6 +708,79 @@ "restartingDescription": "Ole hyvä ja odota…", "restartingMessage": "Käynnistetään uudelleen evcc." }, + "tariff": { + "addForecast": "Lisää ennuste", + "addTariff": "Lisää tariffi", + "co2": { + "description": "CO₂-päästökertoimen ennuste sähköverkon sähkölle. Käytetään CO₂-optimoituun lataukseen ja päästösäästöjen laskelmiin.", + "titleAdd": "Lisää CO₂-ennuste", + "titleEdit": "Muokkaa CO₂-ennustetta" + }, + "co2Services": "CO₂-palvelut", + "customForecast": "Itse määritelty ennuste", + "customTariff": "Itse määritelty tariffi", + "description": "Määritä energiatariffit ja ennusteet. Käytä laitekohtaisia asetuksia dynaamiseen määrittelyyn tai YAML-asetuksia vakioasetuksiin.", + "feedIn": { + "description": "Sähkön myyntihinta verkkoon viedylle sähkölle. Käytetään laskemaan tosiasiallisia latauskustannuksia.", + "titleAdd": "Lisää myyntihinta verkkoon myydylle sähkölle", + "titleEdit": "Muokkaa sähkönverkkoon myydyn sähkön hintaa" + }, + "generic": "Yleiset integraatiot", + "grid": { + "description": "Sähkön hinta verkossa. Käytetään latauskustannuksien määrittelyssä sekä hintaoptimoidussa auton latauksessa, lämmityksessä tai kotiakun latauksessa verkosta.", + "titleAdd": "Lisää sähkön ostohinta", + "titleEdit": "Muokkaa sähkön ostohintaa" + }, + "legacyWarning": "Uusi tariffiasetus saatavilla! Poista ja tallenna tariffisi täällä käyttäen uudistettua opastettua prosessia.", + "option": { + "co2": "Lisää sähkön CO₂-ennuste", + "feedIn": "Lisää sähkön myyntihinta", + "grid": "Lisää sähkön ostohinta", + "planner": "Lisää ennuste suunnitelmalle", + "solar": "Lisää aurinkotuotantoennuste" + }, + "planner": { + "description": "Lisäasetukset. Tavanomaisesti näitä ei tarvitse asettaa, sillä vaihtuva (pörssi)sähkönhintaa tai CO₂-ennustetta käytetään automaattisesti. Mahdollistaa lisätietolähteiden määrittämisen, joita voidaan käyttää latausoptimoinninsuunnitteluun, muttei kuitenkaan tilastoihin tai hintalaskelmiin.", + "titleAdd": "Lisää ennuste suunnitelmaan", + "titleEdit": "Muokkaa suunnitelman ennustetta" + }, + "services": "Palvelut", + "solar": { + "description": "Aurinkotuotantoennuste aurinkovoimalle. Näytetään käyttöliittymässä ja mahdollisesti käytetään tulevaisuudessa latausoptimointialgoritmiin.", + "titleAdd": "Lisää aurinkotuotantoennuste", + "titleEdit": "Muokkaa aurinkotuotantoennustetta" + }, + "template": "Lähde", + "title": "Tariffit & Ennusteet", + "titleChoice": "Mitä haluat lisätä?", + "type": { + "co2": "CO₂-intensiteetti", + "feedIn": "Syöttöhinta", + "grid": "Verkkosähkön hinta", + "planner": "Suunnitelma", + "solar": "Aurinko" + }, + "zones": { + "add": "Lisää hintajakso", + "allDays": "Kaikkina päivinä", + "allMonths": "Joka kuussa", + "allTimes": "Koko päivän", + "cancel": "Peruuta", + "days": "Päivät", + "edit": "Muokkaa", + "hours": "Tunnit", + "months": "Kuukaudet", + "price": "Hinta", + "priceRequired": "Hinta vaaditaan", + "remove": "Poista hintajakso", + "save": "Tallenna", + "selectAll": "Kaikkina päivinä", + "timeFrom": "Alkaen", + "timeRangeError": "Alkuaika pitää olla ennen päättymisaikaa. Jatkaaksesi keskiyönyli, luo kaksi erillistä aikajaksoa.", + "timeTo": "Päättyen", + "weekdays": "Viikonpäivät" + } + }, "tariffs": { "description": "Määrittele energiatariffisi, jotta latausistuntojesi kustannukset lasketaan.", "title": "Tariffit" @@ -1002,10 +1087,13 @@ "battery": "Akku", "batteryCharge": "Akunlataus", "batteryDischarge": "Akku purkautuu", - "batteryGridChargeActive": "lataaminen verkkosta aktiivinen", - "batteryGridChargeLimit": "milloin ladataan verkosta", + "batteryForecastEmpty": "varaus loppuu {time}", + "batteryForecastFull": "varaus täynnä {time}", + "batteryGridChargeActive": "Ladataan verkosta", + "batteryGridChargeLimit": "Milloin ladataan verkosta", "batteryHold": "Akku (lukittu)", "batteryTooltip": "{energy} / {total} ({soc})", + "forecast": "Ennuste: ", "forecastTooltip": "ennuste: jäljellä oleva aurinkotuotanto tänään", "gridImport": "Kulutus sähköverkosta", "homePower": "Kodin sähkönkulutus", @@ -1069,7 +1157,7 @@ "label": "Minimivirta" }, "minSoc": { - "description": "Ajoneuvo ladataan nopeasti {0} asti huolimatta aurinkoenergian ylijäämästä, tämän jälkeen vain ylijäämäenergialla. Tämä on hyödyllinen takaamaan minimi toimintamatka myös pimeinä päivinä.", + "description": "Ajoneuvo ladataan välittömästi {0} asti huolimatta aurinkoenergian ylijäämästä, tämän jälkeen vain ylijäämäenergialla. Tämä on hyödyllinen takaamaan minimi toimintamatka myös pimeinä päivinä.", "label": "Minimi lataustaso" }, "onlyForSocBasedCharging": "Nämä vaihtoehdot ovat käytettävissä vain ajoneuvoille, joiden lataustaso on tiedossa.", @@ -1089,7 +1177,7 @@ }, "mode": { "minpv": "Minimi+PV", - "now": "Nopea", + "now": "Välitön", "off": "Seis", "pv": "PV", "smart": "Älykäs" @@ -1185,6 +1273,7 @@ "targetChargeWaitForVehicle": "Lataussuunnitelma valmis. Odotetaan ajoneuvoa…", "vehicleLimit": "Ajoneuvon latausraja", "vehicleLimitReached": "Ajoneuvon raja saavutettu.", + "waitForAuthorization": "Yhdistetty. Odottaa valtuutusta…", "waitForVehicle": "Valmiina. Odotetaan ajoneuvoa…", "welcome": "Lyhyt aloitus lataus yhteyden vahvistamiseksi." }, @@ -1370,7 +1459,7 @@ "co2Label": "CO₂ päästö", "co2Limit": "CO₂ raja", "enable": "Kytke rajoitus", - "loadpointDescription": "Mahdollistaa nopean lataamisen väliaikaisesti aurinkosähkötilassa (PV).", + "loadpointDescription": "Mahdollistaa välittömän lataamisen väliaikaisesti aurinkosähkötilassa (PV) kun sähkö on halpaa.", "modalTitle": "Älykäs lataus verkosta", "none": "ei mitään", "priceLabel": "Energian hinta", diff --git a/i18n/fr.json b/i18n/fr.json index 1f53c1c44..13e389955 100644 --- a/i18n/fr.json +++ b/i18n/fr.json @@ -105,6 +105,12 @@ "labelResidualPower": "Puissance résiduelle", "title": "Comportement du contrôle" }, + "currency": { + "description": "Utilisé pour formater les prix de l'énergie, les coûts et les économies en fonction de votre tarif.", + "example": "Votre prix de recharge était de {price}. Vous avez économisé {amount}.", + "label": "Devise", + "title": "Devise" + }, "deviceValue": { "amount": "Montant", "broker": "Courtier", @@ -128,12 +134,16 @@ "dimmed": "Atténué", "enabled": "Activé", "energy": "Énergie", + "events": "Evénements", "feedinPrice": "Prix de vente", + "forecast": "Prévisions", "gridPrice": "Prix du réseau", "heaterTempLimit": "Limite du chauffage", "hemsActiveLimit": "Limite active", "hemsType": "Communication", "identifier": "Identifiant RFID", + "max": "max", + "messengers": "Services", "no": "non", "odometer": "Compteur kilométrique", "org": "Organisation", @@ -143,6 +153,7 @@ "phases1p3p": "Commutation de phase", "power": "Puissance", "powerRange": "Plage de puissance", + "price": "Prix", "range": "Autonomie", "singlePhase": "Monophasé", "soc": "Charge", @@ -214,6 +225,7 @@ "cancel": "Annuler", "clear": "Effacer", "close": "Fermer", + "confirmSave": "Il y a des modifications. Enregistrer maintenant ?", "copied": "Copié !", "copy": "Copier", "customHelp": "Créer un appareil personnalisé en utilisant le système de greffons d’evcc.", @@ -229,6 +241,7 @@ "fromYamlHint": "Remarque : configuré via evcc.yaml. Supprimez l'entrée du fichier pour pouvoir effectuer des modifications.", "hideAdvancedSettings": "Masquer les paramètres avancés", "invalidFileSelected": "Fichier sélectionné non valide", + "legacy": "historique", "noFileSelected": "Aucun fichier sélectionné.", "off": "désactivé", "on": "activé", @@ -320,6 +333,8 @@ }, "defaultModeHelpKeep": "Conserver le dernier mode sélectionné.", "defaultModeLabel": "Mode par défaut", + "defaultsHint": "Mode par défaut, comportement solaire et détails électriques utilisent des valeurs par défaut raisonnables.", + "defaultsHintLink": "Régler les paramètres", "delete": "Supprimer", "electricalSubtitle": "En cas de doute, demandez à votre électricien.", "electricalTitle": "Électrique", @@ -410,9 +425,66 @@ "welcomeBannerTitle": "Configurons votre système !" }, "messaging": { - "description": "Recevoir des messages à propos de vos sessions de charge.", + "addMessenger": "Ajouter un service", + "description": "Recevoir des notifications à propos de vos sessions de charge.", + "event": { + "asleep": { + "messageDefault": "Charge arrêtée, le véhicule {vehicleName} ne charge plus.", + "title": "En attente du véhicule", + "titleDefault": "Véhicule en veille" + }, + "connect": { + "messageDefault": "Véhicule connecté à {pvPower}kW PV", + "title": "Quand un véhicule se connecte", + "titleDefault": "Véhicule connecté" + }, + "disconnect": { + "messageDefault": "Véhicule déconnecté après {connectedDuration}", + "title": "Un véhicule se déconnecte", + "titleDefault": "Véhicule déconnecté" + }, + "guest": { + "messageDefault": "Véhicule inconnu, invité connecté ?", + "title": "Un véhicule inconnu se connecte", + "titleDefault": "Véhicule inconnu" + }, + "planoverrun": { + "messageDefault": "{vehicleTitle} : Le plan va être dépassé.", + "title": "Lorsque le plan de charge va être dépassé", + "titleDefault": "Plan dépassé" + }, + "soc": { + "messageDefault": "Batterie chargée à {vehicleSoc}%", + "title": "Mise à jour du niveau de charge", + "titleDefault": "Niveau de charge mis à jour" + }, + "start": { + "messageDefault": "Charge démarrée en mode {mode}.", + "title": "Démarrage de la charge", + "titleDefault": "Charge démarrée" + }, + "stop": { + "messageDefault": "Charge de {chargedEnergy}kWh terminée en {chargeDuration}.", + "title": "Fin de la charge", + "titleDefault": "Charge terminée" + } + }, + "eventMessage": "Message", + "eventTitle": "Titre", + "events": "Evénements", + "legacyWarning": "Un nouveau mode de configuration des notifications est disponible ! Supprimez la configuration ci-dessous et enregistrez pour utiliser le nouveau mode de configuration par assistant.", + "messengers": "Services", + "seePlaceholders": "voir les champs de saisie", "title": "Notifications" }, + "messenger": { + "custom": "Service défini par l'utilisateur", + "generic": "Service générique", + "primary": "Service spécifique", + "template": "Service", + "titleAdd": "Ajouter un service", + "titleEdit": "Modifier le service" + }, "meter": { "cancel": "Annuler", "delete": "Supprimer", @@ -508,7 +580,8 @@ "labelInternalUrl": "URL interne", "labelPort": "Port", "labelSchema": "Schéma", - "title": "Réseau" + "title": "Réseau", + "warningUrlPath": "L'URL n'a généralement pas besoin de chemin d'accès. Êtes-vous sûr que c'est correct ?" }, "ocpp": { "connectedChargers": "Chargeurs connectés", @@ -635,6 +708,79 @@ "restartingDescription": "Veuillez patienter…", "restartingMessage": "Redémarrage d’evcc en cours." }, + "tariff": { + "addForecast": "Ajouter une prévision", + "addTariff": "Ajouter un tarif", + "co2": { + "description": "Prévisions d'intensité de CO₂ pour l'électricité du réseau. Pour une recharge optimisée en termes de CO₂ et le calcul des économies d'émissions.", + "titleAdd": "Ajouter les prévisions de CO₂", + "titleEdit": "Modifier les prévisions de CO₂" + }, + "co2Services": "Services CO₂", + "customForecast": "Prévision définie par l'utilisateur", + "customTariff": "Tarif défini par l'utilisateur", + "description": "Configurez vos tarifs énergétiques et vos prévisions. Utilisez la configuration basée sur les appareils pour une gestion dynamique ou YAML pour les paramètres statiques.", + "feedIn": { + "description": "Compensation pour l'électricité exportée vers le réseau. Utilisée pour calculer les coûts de recharge réels.", + "titleAdd": "Ajouter un tarif d'exportation de réseau", + "titleEdit": "Modifier le tarif d'exportation vers le réseau" + }, + "generic": "Intégrations génériques", + "grid": { + "description": "Prix de l'électricité pour la consommation du réseau. Pour calculer les coûts de recharge réels et optimiser le prix de la recharge des véhicules, des appareils de chauffage ou de la recharge de votre batterie domestique à partir du réseau.", + "titleAdd": "Ajouter un tarif d'importation de réseau", + "titleEdit": "Modifier le tarif d'importation du réseau" + }, + "legacyWarning": "Nouvelle configuration tarifaire disponible ! Supprimez et enregistrez vos tarifs ici pour utiliser le nouveau processus guidé.", + "option": { + "co2": "Ajouter les prévisions de CO₂", + "feedIn": "Ajouter un tarif d'exportation vers le réseau", + "grid": "Ajouter un tarif d'importation du réseau", + "planner": "Ajouter prévisions du planificateur", + "solar": "Ajouter prévisions solaires" + }, + "planner": { + "description": "Paramètre avancé. Généralement superflu, car les tarifs d'électricité dynamiques et les prévisions de CO₂ sont utilisés automatiquement. Active une source de données supplémentaire qui n'est utilisée que pour la planification de la recharge, et non pour les statistiques et les calculs de prix.", + "titleAdd": "Ajouter prévisions du planificateur", + "titleEdit": "Modifier prévisions du planificateur" + }, + "services": "Services", + "solar": { + "description": "Prévisions de production solaire pour votre système photovoltaïque. Affichées dans l'interface, elles seront utilisées à l'avenir pour les algorithmes d'optimisation.", + "titleAdd": "Ajouter prévisions solaires", + "titleEdit": "Modifier les prévisions solaires" + }, + "template": "Fournisseur", + "title": "Tarifs & prévisions", + "titleChoice": "Que voulez-vous rajouter ?", + "type": { + "co2": "Intensité de CO₂", + "feedIn": "Prix de rachat", + "grid": "Prix de consommation du réseau", + "planner": "Planificateur", + "solar": "Solaire" + }, + "zones": { + "add": "Ajouter une zone", + "allDays": "Tous les jours", + "allMonths": "Tous les mois", + "allTimes": "Tout le temps", + "cancel": "Annuler", + "days": "Jours", + "edit": "Éditer", + "hours": "Heures", + "months": "Mois", + "price": "Prix", + "priceRequired": "Le prix est requis", + "remove": "Supprimer la zone", + "save": "Enregistrer", + "selectAll": "Tous les jours", + "timeFrom": "De", + "timeRangeError": "L'heure de début doit être antérieure à l'heure de fin. Pour couvrir minuit, créez deux zones distinctes.", + "timeTo": "À", + "weekdays": "Jours de la semaine" + } + }, "tariffs": { "description": "Définissez vos tarifs d'énergie pour calculer les coûts de vos sessions de charge.", "title": "Tarifs" @@ -651,7 +797,7 @@ "validation": { "failed": "échec", "label": "Statut", - "running": "En cours de validation…", + "running": "en cours de validation…", "success": "réussi", "unknown": "inconnu", "validate": "valider" @@ -941,10 +1087,13 @@ "battery": "Batterie", "batteryCharge": "Batterie en charge", "batteryDischarge": "Décharge de la batterie", - "batteryGridChargeActive": "charge réseau active", - "batteryGridChargeLimit": "charge réseau quand", + "batteryForecastEmpty": "vide {time}", + "batteryForecastFull": "plein {time}", + "batteryGridChargeActive": "Charge réseau : active", + "batteryGridChargeLimit": "Charge réseau : quand", "batteryHold": "Batterie (verrouillée)", "batteryTooltip": "{energy} sur {total} ({soc})", + "forecast": "Prévisions : ", "forecastTooltip": "prévision : production solaire restante aujourd’hui", "gridImport": "Utilisation du réseau", "homePower": "Consommation", @@ -971,6 +1120,7 @@ "charged": "Chargé", "co2": "CO₂ ⌀", "duration": "Durée", + "emission": "Emission", "fallbackName": "Point de chargement", "finished": "Heure de fin", "power": "Puissance", @@ -983,9 +1133,14 @@ "loadpointSettings": { "batteryBoost": { "description": "Chargement rapide à partir de la batterie domestique jusqu'à ce qu'elle soit déchargée à {limit}.", + "descriptionDisabled": "Sélectionnez une limite pour autoriser la charge rapide depuis la batterie résidentielle.", + "disabled": "Désactivé", "label": "Boost Batterie", "mode": "Uniquement disponible en mode solaire et min+solaire ».", - "once": "Boost actif pour cette session de charge." + "once": "Boost actif pour cette session de charge.", + "stateActive": "Boost batterie activé", + "stateBelowLimit": "Niveau de batterie trop bas pour le boost", + "stateReady": "Boost de batterie prêt" }, "batteryUsage": "Batterie domestique", "currents": "Courant de charge", @@ -1091,6 +1246,9 @@ "vehicleStatus": { "awaitingAuthorization": "En attente d’autorisation.", "batteryBoost": "Boost Batterie actif.", + "batteryBoostBelowLimit": "Niveau de batterie trop bas pour boost.", + "batteryBoostDisabled": "Boost batterie désactivé.", + "batteryBoostEnabled": "Boost batterie jusqu'à {limit}.", "charging": "En charge…", "cheapEnergyCharging": "Énergie bon marché disponible.", "cheapEnergyNextStart": "Énergie bon marché dans {duration}.", @@ -1115,6 +1273,7 @@ "targetChargeWaitForVehicle": "Plan de charge prêt. Attente du véhicule…", "vehicleLimit": "Limite du véhicule", "vehicleLimitReached": "Limite du véhicule atteinte.", + "waitForAuthorization": "Connecté. En attente d'autorisation…", "waitForVehicle": "Prêt. Attente du véhicule…", "welcome": "Courte charge initiale pour confirmer la connexion." }, diff --git a/i18n/lb.json b/i18n/lb.json index 1cc293dbe..4f5acc722 100644 --- a/i18n/lb.json +++ b/i18n/lb.json @@ -105,10 +105,16 @@ "labelResidualPower": "Residuell Leeschtung", "title": "Kontrollverhalen" }, + "currency": { + "description": "Gëtt benotzt fir d'Energiepräisser, Käschten a Spueren op Basis vun Ärem Tarif ze formatéieren.", + "example": "Ären Opluedpräis war {price}. Dir hutt {amount} gespuert.", + "label": "Wärung", + "title": "Wärung" + }, "deviceValue": { "amount": "Unzahl", "broker": "Broker", - "bucket": "Eemer", + "bucket": "Bucket", "capacity": "Capacitéit", "chargeStatus": "Status", "chargeStatusA": "net verbonnen", @@ -128,12 +134,16 @@ "dimmed": "Gedimmt", "enabled": "Ageschalt", "energy": "Energie", + "events": "Evenementer", "feedinPrice": "Präis fir anzespeisen", + "forecast": "Previsioun", "gridPrice": "Netz Präis", "heaterTempLimit": "Limitt vun der Heizung", "hemsActiveLimit": "Aktiivt Limit", "hemsType": "Communicatioun", "identifier": "RFID-Identifikatioun", + "max": "max", + "messengers": "Servicer", "no": "nee", "odometer": "Kilometerzäler", "org": "Organisatioun", @@ -143,6 +153,7 @@ "phases1p3p": "Phasenëmschaltung", "power": "Leeschtung", "powerRange": "Leeschtung", + "price": "Präis", "range": "Autonomie", "singlePhase": "Eephaseg", "soc": "Luedstand", @@ -214,6 +225,7 @@ "cancel": "Ofbriechen", "clear": "Läschen", "close": "Zoumaachen", + "confirmSave": "Et ginn net gespäichert Ännerungen. Elo späicheren?", "copied": "Kopéiert!", "copy": "Kopéieren", "customHelp": "Erstell ee benotzerdefinéiert Gerät mam evcc-Plugin-System.", @@ -229,6 +241,7 @@ "fromYamlHint": "Bemierkung: Konfiguréiert iwwer evcc.yaml. Läscht d'Astellungen aus der Datei fir d'Ännerung hei z'erméiglechen.", "hideAdvancedSettings": "Fortgeschratten Astellunge verstoppen", "invalidFileSelected": "Ongülteg Datei ausgewielt", + "legacy": "vereelzt", "noFileSelected": "Keng Datei ausgewielt.", "off": "aus", "on": "un", @@ -274,7 +287,7 @@ "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", + "labelBucket": "Bucket", "labelCheckInsecure": "Onsécher Verbindungen erlaben", "labelDatabase": "Datebank", "labelInsecure": "Zertifikatiwwerpréiwung", @@ -320,6 +333,8 @@ }, "defaultModeHelpKeep": "De leschte Modus deen ausgewielt gouf gëtt bäibehal.", "defaultModeLabel": "Standard-Modus", + "defaultsHint": "De Standardmodus, d'PV-Iwwerschossverhalen an d'elektresch Detailer verwenne raisonnabel Standardwäerter.", + "defaultsHintLink": "Astellungen upassen", "delete": "Läschen", "electricalSubtitle": "Wann onsécher sidd, da léiwer den Elektriker froen.", "electricalTitle": "Elektresch", @@ -387,7 +402,7 @@ "titleLabel": "Titel", "vehicleAutoDetection": "automatesch Erkennung", "vehicleHelpAutoDetection": "Automatesch dat plausibelst Gefier auswielen. Manuell Iwwersteierung méiglech.", - "vehicleHelpDefault": "Ëmmer unhuelen, daRF dëst Gefier hei luet. Auto-Erkennung deaktivéiert. Manuell Iwwersteierung méiglech.", + "vehicleHelpDefault": "Ëmmer unhuelen, datt dëst Gefier hei luet. Auto-Erkennung deaktivéiert. Manuell Iwwersteierung méiglech.", "vehicleInvalid": "Dat Gefier existéiert net", "vehicleLabel": "Standard-Gefier", "vehiclesTitle": "Gefierer" @@ -410,9 +425,66 @@ "welcomeBannerTitle": "Loosst eis de System konfiguréieren!" }, "messaging": { + "addMessenger": "Service derbäisetzen", "description": "Notifikatiounen iwwer Är Opluedsessioune kréien.", + "event": { + "asleep": { + "messageDefault": "Fräiloosse vum Oplueden, Gefier {vehicleName} luet net.", + "title": "Wann een op d'Gefier waart", + "titleDefault": "Gefier schléift" + }, + "connect": { + "messageDefault": "Auto ugeschloss mat {pvPower} kW PV", + "title": "Wann een Auto sech verbënnt", + "titleDefault": "Auto ass verbonnen" + }, + "disconnect": { + "messageDefault": "Auto no {connectedDuration} getrennt", + "title": "Wann een Auto getrennt gëtt", + "titleDefault": "Auto getrennt" + }, + "guest": { + "messageDefault": "Onbekanntent Gefier, Gaascht verbonnen?", + "title": "Wann en onbekannten Auto sech verbënnt", + "titleDefault": "Onbekannt Gefier" + }, + "planoverrun": { + "messageDefault": "{vehicleTitle}: De Plang gëtt iwwerschratt.", + "title": "Wann dat geplangten Oplueden iwwerschratt gëtt", + "titleDefault": "Plang iwwerschratt" + }, + "soc": { + "messageDefault": "Batterie op {vehicleSoc}% opgelueden", + "title": "Luedstandsaktualiséierung", + "titleDefault": "Luedstand aktualiséiert" + }, + "start": { + "messageDefault": "Oplueden am {mode} Modus ugefaangen.", + "title": "Wann d'Oplueden ufänkt", + "titleDefault": "Oplueden ugefaangen" + }, + "stop": { + "messageDefault": "{chargedEnergy} kWh fäerdeg opgeluede an {chargeDuration}.", + "title": "Wann d'Oplueden ophält", + "titleDefault": "Oplueden ofgeschloss" + } + }, + "eventMessage": "Noriicht", + "eventTitle": "Titel", + "events": "Evenementer", + "legacyWarning": "Nei Konfiguratioun vun den Notifikatiounen verfügbar! Späichert a läscht hei Är Konfiguratioun, fir den neie guidéierte Prozess ze verwennen.", + "messengers": "Servicer", + "seePlaceholders": "Plazhalter kucken", "title": "Notifikatiounen" }, + "messenger": { + "custom": "Benotzerdefinéierte Service", + "generic": "Allgemenge Service", + "primary": "Spezifesche Service", + "template": "Service", + "titleAdd": "Service derbäisetzen", + "titleEdit": "Service beaarbechten" + }, "meter": { "cancel": "Ofbriechen", "delete": "Läschen", @@ -442,8 +514,8 @@ "baudrate": "Baudrate", "comset": "ComSet", "connection": "Modbus Verbindung", - "connectionHintSerial": "Den Apparat ass direkt iwwer RS485 (oder en USB-zu-RS485-Adapter) ugeschloss.", - "connectionHintTcpip": "Den Apparat ass iwwer d'Netzwierk (LAN/WiFi) z'erreechen.", + "connectionHintSerial": "Den Apparat ass direkt per RS485 mat EVCC verbonnen.", + "connectionHintTcpip": "Den Apparat kann direkt per LAN/Wifi duerch evcc konfiguréiert ginn.", "connectionValueSerial": "RS485", "connectionValueTcpip": "Netzwierk", "device": "Numm vum Apparat", @@ -508,7 +580,8 @@ "labelInternalUrl": "Intern URL", "labelPort": "Port", "labelSchema": "Schema", - "title": "Netzwierk" + "title": "Netzwierk", + "warningUrlPath": "D'URL brauch normalerweis keen Zougankspad. Sidd Dir sécher, datt dat richteg ass?" }, "ocpp": { "connectedChargers": "Verbonne Wallboxen", @@ -635,6 +708,79 @@ "restartingDescription": "Wgl. waarden…", "restartingMessage": "evcc gëtt nei gestart." }, + "tariff": { + "addForecast": "Previsioun derbäisetzen", + "addTariff": "Tarif derbäisetzen", + "co2": { + "description": "CO₂-Intensitéitsprevisioun fir Netzstroum. Fir CO₂-optiméiert Opluedung an d'Berechnung vun Emissiounsspueren.", + "titleAdd": "CO₂-Previsioun derbäifügen", + "titleEdit": "CO₂-Previsioun upassen" + }, + "co2Services": "CO₂-Servicer", + "customForecast": "Benotzerdefinéiert Previsioun", + "customTariff": "Benotzerdefinéierten Tarif", + "description": "Konfiguréiert Är Energie-Tariffer a Previsiounen. Benotzt d'Apparat-baséiert Konfiguratioun fir eng dynamesch Gestioun oder YAML fir statesch Astellungen.", + "feedIn": { + "description": "Akommes fir Stroum, deen an d'Netz exportéiert gëtt. Gëtt benotzt fir déi tatsächlech Opluedpräisser ze berechnen.", + "titleAdd": "Tarif fir den Export an d'Netz derbäi fügen", + "titleEdit": "Tarif fir den Export an d'Netz upassen" + }, + "generic": "Generesch Integratiounen", + "grid": { + "description": "Stroumpräis fir Netzkonsum. Fir déi tatsächlech Opluedkäschten ze berechnen an dat präisoptiméiert Opluede vu Gefierer, Heizapparater oder d'Opluede vun der Hausbatterie iwwer d'Netz ze optiméieren.", + "titleAdd": "Netz-Import-Tarif derbäifügen", + "titleEdit": "Netz-Import-Tarif upassen" + }, + "legacyWarning": "Nei Tarifkonfiguratioun verfügbar! Läscht a séchert hei Är Tariffer, fir de neie guidéierten Prozess ze benotzen.", + "option": { + "co2": "CO₂-Previsiounen derbäifügen", + "feedIn": "Netzexport-Tarif derbäifügen", + "grid": "Netzimport-Tarif derbäifügen", + "planner": "Planner-Previsiounen derbäifügen", + "solar": "Solar-Previsiounen derbäifügen" + }, + "planner": { + "description": "Erweidert Astellung. Normalerweis net néideg, well déi dynamesch Stroumtariffer oder CO₂-Previsiounen automatesch benotzt ginn. Erlaabt eng zousätzlech Datequell, déi nëmmen fir d'Opluedplangung benotzt gëtt, net fir Statistiken a Präisberechnungen.", + "titleAdd": "Planner-Previsiounen derbäisetzen", + "titleEdit": "Planer-Previsiounen änneren" + }, + "services": "Servicer", + "solar": { + "description": "Previsioun vun der Solarproduktioun fir Äere PV-System. Am Interface ugewisen a gëtt an Zukunft fir Optimiséierungsalgorithmen benotzt.", + "titleAdd": "Solarprevisioun derbäifügen", + "titleEdit": "Solarprevisioun beaarbechten" + }, + "template": "Fournisseur", + "title": "Tariffer & Previsiounen", + "titleChoice": "Wat wëlls du derbäisetzen?", + "type": { + "co2": "CO₂-Intensitéit", + "feedIn": "Verkafspräis", + "grid": "Netzverbrauchspräis", + "planner": "Planner", + "solar": "Solar" + }, + "zones": { + "add": "Zone derbäifügen", + "allDays": "All Dag", + "allMonths": "All Mount", + "allTimes": "Déi ganzen Zäit", + "cancel": "Ofbriechen", + "days": "Deeg", + "edit": "Beaarbechten", + "hours": "Stonnen", + "months": "Méint", + "price": "Präis", + "priceRequired": "De Präis ass erfuerderlech", + "remove": "Zon ewechhuelen", + "save": "Späicheren", + "selectAll": "All Dag", + "timeFrom": "Vun", + "timeRangeError": "D'Startzäit muss virun der Ennzäit sinn. Fir d'Mëtternuecht ze iwwerbrécken, erstellt zwou getrennte Zonen.", + "timeTo": "Bis", + "weekdays": "Wochendeeg" + } + }, "tariffs": { "description": "Definéier d'Stroumtariffer fir d'Käschte vun den Opluedsessiounen ze berechnen.", "title": "Tariffer" @@ -651,7 +797,7 @@ "validation": { "failed": "feelgeschloen", "label": "Status", - "running": "Validéiert…", + "running": "validéiert…", "success": "erfollegräich", "unknown": "onbekannt", "validate": "validéieren" @@ -940,10 +1086,13 @@ "battery": "Batterie", "batteryCharge": "Batterie oplueden", "batteryDischarge": "Batterie entlueden", - "batteryGridChargeActive": "Netzlueden aktiv", - "batteryGridChargeLimit": "Netzluede wann", + "batteryForecastEmpty": "eidel {time}", + "batteryForecastFull": "voll {time}", + "batteryGridChargeActive": "Oluede vum Netz: aktiv", + "batteryGridChargeLimit": "Opluede vum Netz: wann", "batteryHold": "Batterie (gespäert)", "batteryTooltip": "{energy} vun {total} ({soc})", + "forecast": "Viraussoen: ", "forecastTooltip": "Prognose: Rescht Solarproduktioun fir haut", "gridImport": "Verbrauch vum Stroumnetz", "homePower": "Verbrauch", @@ -970,6 +1119,7 @@ "charged": "Opgelueden", "co2": "⌀ CO₂", "duration": "Dauer", + "emission": "Emissioun", "fallbackName": "Luedstatioun", "finished": "Fäerdeg um", "power": "Leeschtung", @@ -982,9 +1132,14 @@ "loadpointSettings": { "batteryBoost": { "description": "Schnell vun der Hausbatterie lueden, bis se op {limit} entlueden ass.", + "descriptionDisabled": "Wielt eng Limitt, fir et ze erlabe séier vun der Hausbatterie opzelueden.", + "disabled": "Deaktivéiert", "label": "Batterie Boost", "mode": "Nëmmen am PV- a Min+PV-Modus verfügbar.", - "once": "Boost ass dir dës Luedsessioun aktivéiert." + "once": "Boost ass dir dës Luedsessioun aktivéiert.", + "stateActive": "Batterieboost aktiv", + "stateBelowLimit": "Batterie ze niddereg fir de Boost", + "stateReady": "Batterieboost bereet" }, "batteryUsage": "Hausbatterie", "currents": "Luedstroum", @@ -1090,6 +1245,9 @@ "vehicleStatus": { "awaitingAuthorization": "Waarden op d'Autorisatioun.", "batteryBoost": "Batterie Boost ass aktiv.", + "batteryBoostBelowLimit": "Batterie ze niddereg fir de Boost.", + "batteryBoostDisabled": "Batterieboost deaktivéiert.", + "batteryBoostEnabled": "Boost bis d'Batterie bei {limit} ass.", "charging": "Luet…", "cheapEnergyCharging": "Gënschteg Energie verfügbar.", "cheapEnergyNextStart": "Gënschteg Energie an {duration}.", @@ -1114,6 +1272,7 @@ "targetChargeWaitForVehicle": "Luedplang ass prett. Waarden op d'Gefier…", "vehicleLimit": "Limitt vum Gefier", "vehicleLimitReached": "Limitt vum Gefier erreecht.", + "waitForAuthorization": "Verbonnen. Waarden op d'Autorisatioun…", "waitForVehicle": "Prett. Waarden op d'Gefier…", "welcome": "Kuerz Lueden fir d'Verbindung ze confirméieren." }, diff --git a/i18n/lt.json b/i18n/lt.json index 5dfb600ed..7530d25ec 100644 --- a/i18n/lt.json +++ b/i18n/lt.json @@ -105,6 +105,12 @@ "labelResidualPower": "Likutinė galia", "title": "Valdymo elgesys" }, + "currency": { + "description": "Naudojama energijos kainoms, išlaidoms ir sutaupymams formuoti pagal jūsų tarifus.", + "example": "Jūsų įkrovimo kaina buvo {price}. Sutaupėte {amount}.", + "label": "Valiuta", + "title": "Valiuta" + }, "deviceValue": { "amount": "Kiekis", "broker": "Broker sistema", @@ -130,11 +136,13 @@ "energy": "Energija", "events": "Įvykiai", "feedinPrice": "Tiekimo į tinklą kaina", + "forecast": "Prognozė", "gridPrice": "Energijos pirkimo kaina", "heaterTempLimit": "Šildymo limitas", "hemsActiveLimit": "Aktyvus limitas", "hemsType": "Komunikacija", "identifier": "RFID-Identifikatorius", + "max": "max", "messengers": "Paslaugos", "no": "ne", "odometer": "Odometras", @@ -145,6 +153,7 @@ "phases1p3p": "Fazių perjungimas", "power": "Galia", "powerRange": "Galia", + "price": "Kaina", "range": "Nuvažiuojamas atstumas", "singlePhase": "Vienfazis", "soc": "Įkrova", @@ -216,6 +225,7 @@ "cancel": "Atšaukti", "clear": "Išvalyti", "close": "Uždaryti", + "confirmSave": "Yra neišsaugotų pakeitimų. Išsaugoti dabar ?", "copied": "Nukopijuota!", "copy": "Kopijuoti", "customHelp": "Sukurkite vartotojo apibrėžtą įrenginį naudodami evcc įskiepių sistemą.", @@ -231,6 +241,7 @@ "fromYamlHint": "Pastaba: Sukonfigūruota naudojant evcc.yaml. Pašalinkite įrašą iš failo, kad būtų galima redaguoti čia.", "hideAdvancedSettings": "Slėpti išplėstinius nustatymus", "invalidFileSelected": "Pasirinktas netinkamas failas", + "legacy": "seni", "noFileSelected": "Nepasirinktas failas.", "off": "išjungta", "on": "Įjungta", @@ -322,6 +333,8 @@ }, "defaultModeHelpKeep": "Išsaugo paskutinį pasirinktą režimą.", "defaultModeLabel": "Numatytasis režimas", + "defaultsHint": "Numatytasis režimas, saulės energijos elgsena ir elektros duomenys naudoja apgalvotus numatytuosius nustatymus.", + "defaultsHintLink": "Koreguoti nustatymus", "delete": "Ištrinti", "electricalSubtitle": "Jei abejojate, pasitarkite su elektriku.", "electricalTitle": "Elektros parametrai", @@ -412,9 +425,66 @@ "welcomeBannerTitle": "Sukonfigūruokime jūsų sistemą!" }, "messaging": { + "addMessenger": "Pridėti paslaugą", "description": "Gaukite pranešimus apie įkrovimo sesijas.", + "event": { + "asleep": { + "messageDefault": "Įkrovimas leidžiamas, automobilis {vehicleName} nesikrauna.", + "title": "Laukiama automobilio", + "titleDefault": "Automobilis miega" + }, + "connect": { + "messageDefault": "Automobilis prijungtas ties {pvPower} kW PV", + "title": "Kai automobilis prisijungia", + "titleDefault": "Automobilis prijungtas" + }, + "disconnect": { + "messageDefault": "Automobilis atjungtas po {connectedDuration}", + "title": "Kai automobilis atsijungia", + "titleDefault": "Automobilis atjungtas" + }, + "guest": { + "messageDefault": "Nežinomas automobilis, prisijungė svečiai?", + "title": "Kai prisijungia nežinomas automobilis", + "titleDefault": "Nežinomas automobilis" + }, + "planoverrun": { + "messageDefault": "{vehicleTitle}: Suplanuotas įkrovimas vėluos.", + "title": "Kai suplanuotas įkrovimas vėluos", + "titleDefault": "Suplanuotas įkrovimas vėluos" + }, + "soc": { + "messageDefault": "Akumuliatorius įkrautas iki {vehicleSoc}%", + "title": "Įkrovimo lygio atnaujinimas", + "titleDefault": "Įkrovimo lygis atnaujintas" + }, + "start": { + "messageDefault": "Pradėtas įkrovimas {mode} režimu.", + "title": "Kai prasideda įkrovimas", + "titleDefault": "Įkrovimas prasidėjo" + }, + "stop": { + "messageDefault": "Įkrovimas baigtas, {chargedEnergy}kWh per {chargeDuration}.", + "title": "Kai įkrovimas sustoja", + "titleDefault": "Įkrovimas baigtas" + } + }, + "eventMessage": "Pranešimas", + "eventTitle": "Pavadinimas", + "events": "Įvykiai", + "legacyWarning": "Galima nauja pranešimų konfigūracija! Pašalinkite ir išsaugokite savo konfigūraciją čia, kad galėtumėte naudoti naują vedlio procesą.", + "messengers": "Paslaugos", + "seePlaceholders": "žr. vietos žymeklius", "title": "Pranešimai" }, + "messenger": { + "custom": "Vartotojo nustatyta paslauga", + "generic": "Bendroji paslauga", + "primary": "Specifinė paslauga", + "template": "Paslauga", + "titleAdd": "Pridėti Paslaugą", + "titleEdit": "Redaguoti Paslaugą" + }, "meter": { "cancel": "Atšaukti", "delete": "Ištrinti", @@ -510,7 +580,8 @@ "labelInternalUrl": "Vidinis URL", "labelPort": "Port", "labelSchema": "Schema", - "title": "Tinklas" + "title": "Tinklas", + "warningUrlPath": "URL paprastai nereikalauja kelio. Ar esate įsitikinę?" }, "ocpp": { "connectedChargers": "Prijungti įkrovikliai", @@ -638,6 +709,79 @@ "restartingDescription": "Palaukite…", "restartingMessage": "Paleidžiama iš naujo." }, + "tariff": { + "addForecast": "Pridėti prognozę", + "addTariff": "Pridėti tarifą", + "co2": { + "description": "Tinklo elektros energijos CO₂ intensyvumo prognozė. CO₂ optimizuotam įkrovimui ir išmetamųjų teršalų kiekio sumažėjimo apskaičiavimui.", + "titleAdd": "Pridėti CO₂ Prognozę", + "titleEdit": "Redaguoti CO₂ prognozę" + }, + "co2Services": "CO₂ Paslaugos", + "customForecast": "Naudotojo apibrėžta prognozė", + "customTariff": "Naudotojo apibrėžtas tarifas", + "description": "Konfigūruokite energijos tarifus ir prognozes. Dinaminiam valdymui naudokite įrenginiu pagrįstą konfigūraciją, o statiniams nustatymams – YAML.", + "feedIn": { + "description": "Kompensacija už į tinklą eksportuotą elektros energiją. Naudojama faktinėms įkrovimo išlaidoms apskaičiuoti.", + "titleAdd": "Pridėti energijos eksporto tarifą", + "titleEdit": "Redaguoti energijos eksporto tarifą" + }, + "generic": "Bendrinės integracijos", + "grid": { + "description": "Perkamos elektros energijos kaina. Skirta apskaičiuoti faktines įkrovimo išlaidas, taipogi automobilių, šildymo įrenginių ar kaupiklio įkrovimo iš tinklo optimizavimui pagal kainą.", + "titleAdd": "Pridėti Pirkimo Tarifą", + "titleEdit": "Redaguoti pirkimo tarifą" + }, + "legacyWarning": "Atnaujinta tarifų konfigūracija! Pašalinkite ir išsaugokite savo tarifus čia, kad galėtumėte naudotis nauju vedliu.", + "option": { + "co2": "Pridėti CO₂ prognozę", + "feedIn": "Pridėti eksporto tarifą", + "grid": "Pridėti pirkimo tarifą", + "planner": "Pridėti planuoklio prognozę", + "solar": "Pridėti Saulės prognozę" + }, + "planner": { + "description": "Išplėstinis nustatymas. Paprastai nereikalingas, nes dinaminiai elektros energijos tarifai arba CO₂ prognozės naudojami automatiškai. Įjungia papildomą duomenų šaltinį, kuris naudojamas tik įkrovimo planavimui, o ne statistikai ar kainų skaičiavimams.", + "titleAdd": "Pridėti planavimo prognozę", + "titleEdit": "Redaguoti planavimo prognozę" + }, + "services": "Paslaugos", + "solar": { + "description": "Jūsų saulės elektrinės energijos gamybos prognozė. Rodoma sąsajoje o ateityje bus naudojama optimizavimo algoritmams.", + "titleAdd": "Pridėti Saulės Prognozę", + "titleEdit": "Redaguoti Saulės Prognozę" + }, + "template": "Tiekėjas", + "title": "Tarifai ir Prognozės", + "titleChoice": "Ką norite pridėti?", + "type": { + "co2": "CO₂ Intensyvumas", + "feedIn": "Eksporto kaina", + "grid": "Pirkimo kaina", + "planner": "Planuoklis", + "solar": "Saulės" + }, + "zones": { + "add": "Pridėti regioną", + "allDays": "Visos dienos", + "allMonths": "Visi mėnesiai", + "allTimes": "Visi laikai", + "cancel": "Atšaukti", + "days": "Dienos", + "edit": "Redaguoti", + "hours": "Valandos", + "months": "Mėnesiai", + "price": "Kaina", + "priceRequired": "Reikia kainos", + "remove": "Pašalinti regioną", + "save": "Išsaugoti", + "selectAll": "Visos dienos", + "timeFrom": "Nuo", + "timeRangeError": "Pradžios laikas turi būti ankstesnis nei pabaigos laikas. Norėdami įtraukti vidurnaktį, sukurkite du atskirus periodus.", + "timeTo": "Iki", + "weekdays": "Darbo dienos" + } + }, "tariffs": { "description": "Nustatykite savo energijos tarifus, kad apskaičiuotumėte įkrovimo sesijų išlaidas.", "title": "Tarifai" @@ -944,10 +1088,13 @@ "battery": "Kaupiklis", "batteryCharge": "Kaupiklis įkraunamas", "batteryDischarge": "Kaupiklis iškraunamas", - "batteryGridChargeActive": "įkraunama iš tinklo", - "batteryGridChargeLimit": "įkrauti iš tinklo, kai", + "batteryForecastEmpty": "tuščias {time}", + "batteryForecastFull": "pilnas {time}", + "batteryGridChargeActive": "Įkrovimas iš tinklo: aktyvus", + "batteryGridChargeLimit": "Įkrauti iš tinklo: kai", "batteryHold": "Kaupiklis (užblokuotas)", "batteryTooltip": "{energy} iš {total} ({soc})", + "forecast": "Prognozė: ", "forecastTooltip": "prognozė: likusi saulės energijos gamyba šiandien", "gridImport": "Iš tinklo", "homePower": "Namo suvartojimas", @@ -974,6 +1121,7 @@ "charged": "Įkrauta", "co2": "⌀ CO₂", "duration": "Trukmė", + "emission": "Emisija", "fallbackName": "Įkroviklis", "finished": "Bus baigta", "power": "Galia", @@ -986,9 +1134,14 @@ "loadpointSettings": { "batteryBoost": { "description": "Greitas įkrovimas iš namų kaupiklio, kol jis išsikraus iki {limit}.", + "descriptionDisabled": "Pasirinkite limitą greitam įkrovimui iš namų kaupiklio.", + "disabled": "Išjungtas", "label": "Kaupikliu paspartintas įkrovimas", "mode": "Galimas tik Saulė ir Min+Saulė režimuose.", - "once": "Paspartinimas aktyvuotas šiai įkrovimo sesijai." + "once": "Paspartinimas aktyvuotas šiai įkrovimo sesijai.", + "stateActive": "Įkrovimo pastiprinimas kaupiklio pagalba aktyvus", + "stateBelowLimit": "Kaupiklis išsikrovęs, jo panaudoti įkrovimo spartinimui neina", + "stateReady": "Įkrovimo spartinimas kaupikliu paruoštas" }, "batteryUsage": "Namų kaupiklis", "currents": "Įkrovimo Srovė", @@ -1088,12 +1241,15 @@ "targetSoc": "Limitas", "temp": "t.", "tempLimit": "Temp. limitas", - "unknown": "Nežinomas automobilis", + "unknown": "Svečių automobilis", "vehicleSoc": "Baterija" }, "vehicleStatus": { "awaitingAuthorization": "Laukiama autorizacijos.", "batteryBoost": "Veikia paspartinimas kaupikliu.", + "batteryBoostBelowLimit": "Kaupiklis išsikrovęs, jo panaudoti įkrovimo spartinimui neina.", + "batteryBoostDisabled": "Įkrovimo spartinimas kaupikliu išjungtas.", + "batteryBoostEnabled": "Spartinti įkrovimą iki {limit}.", "charging": "Įkraunama…", "cheapEnergyCharging": "Šiuo metu energija yra pigi.", "cheapEnergyNextStart": "Pigi energija už {duration}.", @@ -1118,6 +1274,7 @@ "targetChargeWaitForVehicle": "Įkrovimas pagal planą leidžiamas. Laukiama automobilio…", "vehicleLimit": "Automobilyje nustatytas limitas", "vehicleLimitReached": "Automobilyje nustatytas limitas pasiektas.", + "waitForAuthorization": "Prisijungtas. Laukiama autorizacijos…", "waitForVehicle": "Paruošta. Laukiama automobilio…", "welcome": "Trumpas pirminis įkrovimas jungties patikrinimui." }, @@ -1303,7 +1460,7 @@ "co2Label": "CO₂ emisijos", "co2Limit": "CO₂ limitas", "enable": "Įjungti limitą", - "loadpointDescription": "Laikinai aktyvuoja greitą įkrovimą (režime Saulė).", + "loadpointDescription": "Laikinai aktyvuoja greitą įkrovimą (režime Saulė, kai energija pigi).", "modalTitle": "Išmanaus tinklo įkrovimas", "none": "nėra", "priceLabel": "Energijos kaina", diff --git a/i18n/nl.json b/i18n/nl.json index 23c9f4def..d381ec694 100644 --- a/i18n/nl.json +++ b/i18n/nl.json @@ -105,6 +105,12 @@ "labelResidualPower": "Vermogensoverschot", "title": "Regelgedrag" }, + "currency": { + "description": "Gebruikt om energieprijzen, kosten en besparingen te tonen op basis van uw tarief.", + "example": "Uw laadprijs was {price}. U heeft {amount} bespaard.", + "label": "Valuta", + "title": "Valuta" + }, "deviceValue": { "amount": "Aantal", "broker": "Agent", @@ -128,12 +134,16 @@ "dimmed": "Gedimd", "enabled": "Ingeschakeld", "energy": "Energie", + "events": "Gebeurtenissen", "feedinPrice": "Terugleververgoeding", + "forecast": "Voorspelling", "gridPrice": "Netprijs", "heaterTempLimit": "Verwarming limiet", "hemsActiveLimit": "Actieve limiet", "hemsType": "Communicatie", "identifier": "RFID-Nummer", + "max": "max", + "messengers": "Diensten", "no": "nee", "odometer": "Kilometerstand", "org": "Organisatie", @@ -143,6 +153,7 @@ "phases1p3p": "Fase-omschakeling", "power": "Vermogen", "powerRange": "Vermogen", + "price": "Prijs", "range": "Actieradius", "singlePhase": "Monofase", "soc": "Laden", @@ -197,7 +208,7 @@ }, "ext": { "description": "Registreert energie van ongestuurde verbruikers (bijv. koelkast, wasmachine, etc) voor statistieken. Deze meters kunnen ook gebruikt worden voor Load Balancing (inclusief onderverdeling).", - "titleAdd": "Externe verbruiker toevoegen", + "titleAdd": "Toevoegen verbruikmeter", "titleEdit": "Wijzig verbruikmeter" }, "form": { @@ -214,6 +225,7 @@ "cancel": "Annuleren", "clear": "Leegmaken", "close": "Sluiten", + "confirmSave": "Er zijn niet-opgeslagen wijzigingen. Nu opslaan?", "copied": "Gekopieerd!", "copy": "Kopieer", "customHelp": "Creëer een gebruiker-gedefinieerd apparaat met evcc’s plugin systeem.", @@ -229,6 +241,7 @@ "fromYamlHint": "Opmerking: Geconfigureerd via evcc.yaml. Verwijder de betreffende regels uit het bestand om hier bewerkingen mogelijk te maken.", "hideAdvancedSettings": "Verberg geavanceerde instellingen", "invalidFileSelected": "Ongeldig bestand geselecteerd", + "legacy": "verouderde functies", "noFileSelected": "Geen bestand geselecteerd.", "off": "uit", "on": "aan", @@ -320,6 +333,7 @@ }, "defaultModeHelpKeep": "Behoud de laatst-geselecteerde modus.", "defaultModeLabel": "Standaard mode", + "defaultsHintLink": "Instellingen aanpassen", "delete": "Wissen", "electricalSubtitle": "Bij twijfel, raadpleeg een elektricien.", "electricalTitle": "Elektrisch", @@ -410,8 +424,65 @@ "welcomeBannerTitle": "Start met het configureren van uw systeem!" }, "messaging": { - "description": "Ontvang berichten over je oplaadsessies.", - "title": "Notificaties" + "addMessenger": "Diensten toevoegen", + "description": "Ontvang meldingen over je oplaadsessies.", + "event": { + "asleep": { + "messageDefault": "Laadmechanisme vrijgegeven, voertuig {vehicleName} laadt niet op.", + "title": "Tijdens het wachten op een voertuig", + "titleDefault": "Voertuig in slaapstand" + }, + "connect": { + "messageDefault": "Auto aangesloten op {pvPower}kW PV", + "title": "Wanneer een auto verbinding maakt", + "titleDefault": "Auto verbonden" + }, + "disconnect": { + "messageDefault": "Auto losgekoppeld na {connectedDuration}", + "title": "Wanneer een auto de verbinding verbreekt", + "titleDefault": "Auto losgekoppeld" + }, + "guest": { + "messageDefault": "Onbekend voertuig, gast verbonden?", + "title": "Wanneer een onbekende auto verbinding maakt", + "titleDefault": "Onbekend voertuig" + }, + "planoverrun": { + "messageDefault": "{vehicleTitle}: Planning zal uitlopen.", + "title": "Wanneer de oplaad planning uitloopt", + "titleDefault": "Planning uitgelopen" + }, + "soc": { + "messageDefault": "Batterij opgeladen tot {vehicleSoc}%", + "title": "Laadniveau update", + "titleDefault": "Laadniveau bijgewerkt" + }, + "start": { + "messageDefault": "Opladen gestart in {mode}-modus.", + "title": "Wanneer het opladen begint", + "titleDefault": "Opladen gestart" + }, + "stop": { + "messageDefault": "Het opladen van {chargedEnergy}kWh is voltooid in {chargeDuration}.", + "title": "Wanneer het opladen stopt", + "titleDefault": "Opladen voltooid" + } + }, + "eventMessage": "Bericht", + "eventTitle": "Titel", + "events": "Gebeurtenissen", + "legacyWarning": "Nieuwe meldingsconfiguratie beschikbaar! Verwijder uw configuratie hier en sla deze op om het nieuwe begeleide proces te gebruiken.", + "messengers": "Diensten", + "seePlaceholders": "zie tijdelijke aanduidingen", + "title": "Meldingen" + }, + "messenger": { + "custom": "Gebruiker gedefinieerde dienst", + "generic": "Algemene dienst", + "primary": "Specifieke dienst", + "template": "Dienst", + "titleAdd": "Dienst toevoegen", + "titleEdit": "Bewerken Dienst" }, "meter": { "cancel": "Annuleren", @@ -508,7 +579,8 @@ "labelInternalUrl": "Interne URL", "labelPort": "Poort", "labelSchema": "Schema", - "title": "Netwerk" + "title": "Netwerk", + "warningUrlPath": "De URL bevat doorgaans geen path. Weet u zeker dat dit juist is?" }, "ocpp": { "connectedChargers": "Verbonden laders", @@ -635,6 +707,75 @@ "restartingDescription": "Even geduld aub…", "restartingMessage": "Evcc wordt herstart." }, + "tariff": { + "addForecast": "Voorspelling toevoegen", + "addTariff": "Tarief toevoegen", + "co2": { + "description": "CO₂ intensiteitsvoorspelling voor netstroom. Voor CO₂-geoptimaliseerd laden en het berekenen van emissie besparingen.", + "titleAdd": "CO₂-voorspelling toevoegen", + "titleEdit": "CO₂-voorspelling aanpassen" + }, + "co2Services": "CO₂-diensten", + "customForecast": "Gebruikers eigen voorspelling", + "customTariff": "Gebruikers eigen tarieven", + "description": "Configureer uw eigen tarieven en voorspellingen. Gebruik apparaat-gebaseerde configuratie voor dynamisch beheer of gebruik YAML voor statisch beheer.", + "feedIn": { + "description": "Vergoeding voor energie die geëxporteerd wordt naar het net. Wordt gebruikt om de werkelijke kosten van het laden te berekenen.", + "titleAdd": "Net-export tarief toevoegen", + "titleEdit": "Net-export tarief aanpassen" + }, + "generic": "Generieke integraties", + "grid": { + "description": "Electriciteits-prijzen voor consumptie van het net. Gebruikt om de werkelijke laadkosten uit te rekenen, en voor het laden van de auto of thuis-accu en het inschakelen van verwarming tegen de beste prijs.", + "titleAdd": "Electriciteits tarief toevoegen voor import" + }, + "option": { + "co2": "CO₂-voorspelling toevoegen", + "feedIn": "Net-export tarief toevoegen", + "solar": "PV-voorspelling toevoegen" + }, + "planner": { + "description": "Geavanceerde instelling. Doorgaans niet nodig omdat dynamische energieprijzen en CO₂ voorspellingen gebruikt worden. Deze instelling voegt een extra methode toe die het laden planbaar maakt. Deze methode is niet voor statistieken of prijsberekeningen.", + "titleAdd": "Planner voorspelling toevoegen", + "titleEdit": "Planner voorspelling aanpassen" + }, + "services": "Diensten", + "solar": { + "description": "Voorspelling van zonne-energie van uw PV. Wordt weergegeven in de interface en gebruikt voor optimalisaties in de toekomst.", + "titleAdd": "PV voorspelling toevoegen", + "titleEdit": "PV voorspelling aanpassen" + }, + "template": "Aanbieder", + "title": "Tarieven & voorspelingen", + "titleChoice": "Wat wilt u toevoegen?", + "type": { + "co2": "CO₂ intensiteit", + "feedIn": "Terugleververgoeding", + "grid": "Grid consumptie prijs", + "planner": "Planner", + "solar": "PV" + }, + "zones": { + "add": "Zone toevoegen", + "allDays": "Alle dagen", + "allMonths": "Alle maanden", + "allTimes": "Alle tijdstippen", + "cancel": "Annuleren", + "days": "Dagen", + "edit": "Aanpassen", + "hours": "Uren", + "months": "Maanden", + "price": "Prijs", + "priceRequired": "Prijs is verplicht", + "remove": "Zone verwijderen", + "save": "Opslaan", + "selectAll": "Alle dagen", + "timeFrom": "Van", + "timeRangeError": "Starttijd moet voor de eindtijd liggen. Om middernacht te overbruggen moet u twee losse zones maken.", + "timeTo": "Tot", + "weekdays": "Weekdagen" + } + }, "tariffs": { "description": "Voer jouw energietarief in om de kosten van het laden te berekenen.", "title": "Tarieven" @@ -651,7 +792,7 @@ "validation": { "failed": "mislukt", "label": "Status", - "running": "Bezig met valideren…", + "running": "bezig met valideren…", "success": "succesvol", "unknown": "onbekend", "validate": "valideer" @@ -941,10 +1082,13 @@ "battery": "Batterij", "batteryCharge": "Batterij opladen", "batteryDischarge": "Batterij ontladen", - "batteryGridChargeActive": "net laden actief", - "batteryGridChargeLimit": "net laden wanneer", + "batteryForecastEmpty": "leeg {time}", + "batteryForecastFull": "vol {time}", + "batteryGridChargeActive": "Net laden: actief", + "batteryGridChargeLimit": "Net laden: wanneer", "batteryHold": "Batterij (aanhouden)", "batteryTooltip": "{energy} van {total} ({soc})", + "forecast": "Voorspelling: ", "forecastTooltip": "Voorspelling: resterende zonneproductie voor vandaag", "gridImport": "Netafname", "homePower": "Consumptie", @@ -971,6 +1115,7 @@ "charged": "Geladen", "co2": "⌀ CO₂", "duration": "Duur", + "emission": "Uitstoot", "fallbackName": "Laadpunt", "finished": "Eindtijd", "power": "Vermogen", @@ -983,9 +1128,14 @@ "loadpointSettings": { "batteryBoost": { "description": "Snelladen vanuit thuisbatterij tot deze is ontladen naar {limit}.", + "descriptionDisabled": "Selecteer een limiet om snel opladen vanaf de thuisbatterij mogelijk te maken.", + "disabled": "Uitgeschakeld", "label": "Batterij Boost", "mode": "Enkel beschikbaar in PV en Min+PV modus.", - "once": "Boost geactiveerd voor deze laadsessie." + "once": "Boost geactiveerd voor deze laadsessie.", + "stateActive": "Batterijboost actief", + "stateBelowLimit": "Batterij te laag voor boost", + "stateReady": "Batterijboost klaar" }, "batteryUsage": "Thuisbatterij", "currents": "Laadstroom", @@ -1091,10 +1241,13 @@ "vehicleStatus": { "awaitingAuthorization": "Wachten op autorisatie.", "batteryBoost": "Batterijboost actief.", + "batteryBoostBelowLimit": "Batterij te laag voor boost.", + "batteryBoostDisabled": "Batterijboost uitgeschakeld.", + "batteryBoostEnabled": "Boost tot de batterij tot {limit}.", "charging": "Opladen…", "cheapEnergyCharging": "Goedkope energie beschikbaar.", "cheapEnergyNextStart": "Goedkope energie in {duration}.", - "cheapEnergySet": "Prijslimit ingesteld.", + "cheapEnergySet": "Prijslimiet ingesteld.", "cleanEnergyCharging": "Schone energie beschikbaar.", "cleanEnergyNextStart": "Schone energie in {duration}.", "cleanEnergySet": "CO₂ limiet ingesteld.", @@ -1102,19 +1255,20 @@ "connected": "Verbonden.", "disconnectRequired": "Sessie beëindigd. Maak opnieuw verbinding.", "disconnected": "Niet verbonden.", - "feedinPriorityNextStart": "Hoge energieprijs start over {duration}.", - "feedinPriorityPausing": "Zonne opladen gepauzeerd om grid import te maximaliseren.", + "feedinPriorityNextStart": "Hoge terugleververgoeding start over {duration}.", + "feedinPriorityPausing": "Zonne opladen gepauzeerd om terugleveren te maximaliseren.", "finished": "Beëindigd.", "minCharge": "Minimaal opladen naar {soc}.", "pvDisable": "Onvoldoende PV overschot. Laden wordt gepauzeerd.", "pvEnable": "Voldoende PV overschot beschikbaar. Laden wordt gestart.", "scale1p": "Wordt gereduceerd naar laden op 1 fase.", "scale3p": "Wordt verhoogd naar laden op 3 fasen.", - "targetChargeActive": "Laadplan actief. Geschatte beëindiging over {duration}.", + "targetChargeActive": "Laadplan actief. Geschat einde over {duration}.", "targetChargePlanned": "Laadplan begint over {duration}.", "targetChargeWaitForVehicle": "Laadplan gereed. Wachten op voertuig…", "vehicleLimit": "Voertuiglimiet", "vehicleLimitReached": "Voertuiglimiet bereikt.", + "waitForAuthorization": "Verebonden. Wachten op autorisatie…", "waitForVehicle": "Klaar. Wachten op voertuig…", "welcome": "Verkort initiële lading om de verbinding te bevestigen." }, @@ -1124,7 +1278,7 @@ "notifications": { "dismissAll": "Alles verwijderen", "logs": "Volledige logboeken bekijken", - "modalTitle": "Notificaties" + "modalTitle": "Meldingen" }, "offline": { "configurationError": "Fout tijdens opstarten. Controleer de configuratie en start opnieuw.", @@ -1311,11 +1465,11 @@ }, "smartFeedInPriority": { "activeHoursLabel": "Gepauzeerde tijd", - "description": "Pauzeert het opladen tijdens hoge prijzen om prioriteit te geven aan terugleveren.", + "description": "Pauzeert het opladen als terugleveren aan het net veel oplevert.", "priceLabel": "Teruglevertarief", "priceLimit": "Terugleverprijs limiet", "resetWarning": "Er is geen dynamisch teruglevertarief ingesteld. Er is echter nog steeds een limiet van {limit}. Wil je de configuratie opschonen?", - "title": "Terugleveren prioritiseren" + "title": "Teruglever prioriteit" }, "startupError": { "configFile": "Gebruikt configuratiebestand:", @@ -1328,6 +1482,6 @@ "lineError": "Fout in {0}.", "lineErrorLink": "lijn {0}", "restartButton": "Herstart", - "title": "Start Error" + "title": "Opstart fout" } } diff --git a/i18n/pl.json b/i18n/pl.json index 1879ee844..d0c515b12 100644 --- a/i18n/pl.json +++ b/i18n/pl.json @@ -105,6 +105,12 @@ "labelResidualPower": "Moc resztkowa", "title": "Ustawienia postępowania" }, + "currency": { + "description": "Służy do formatowania cen energii, kosztów i oszczędności na podstawie twojej taryfy.", + "example": "Cena ładowania wyniosła {price}. Zaoszczędziłeś {amount}.", + "label": "Waluta", + "title": "Waluta" + }, "deviceValue": { "amount": "Ilość", "broker": "Broker", @@ -128,12 +134,16 @@ "dimmed": "Przyciemniony", "enabled": "Włączony", "energy": "Energia", + "events": "Wydarzenia", "feedinPrice": "Cena do sieci", + "forecast": "Prognoza", "gridPrice": "Cena z sieci", "heaterTempLimit": "Limit podgrzewacza", "hemsActiveLimit": "Limit aktywny", "hemsType": "Komunikacja", "identifier": "Identyfikator RFID", + "max": "maks", + "messengers": "Usługi", "no": "nie", "odometer": "Drogomierz", "org": "Organizacja", @@ -143,6 +153,7 @@ "phases1p3p": "Przełącznik fazowy", "power": "Moc", "powerRange": "Zasilanie", + "price": "Cena", "range": "Zasięg", "singlePhase": "Jedna faza", "soc": "Stan naładowania", @@ -211,6 +222,7 @@ "cancel": "Anuluj", "clear": "Wyraźne", "close": "Zamknij", + "confirmSave": "Są niezachowane zmiany. Zapisać teraz?", "copied": "Skopiowano!", "copy": "Kopia", "customHelp": "Utwórz urządzenie zdefiniowane przez użytkownika korzystając z systemu wtyczek evcc.", @@ -317,6 +329,8 @@ }, "defaultModeHelpKeep": "Zachowuje ostatnio wybrany tryb.", "defaultModeLabel": "Tryb domyślny", + "defaultsHint": "Tryb domyślny, zachowanie energii słonecznej i szczegóły elektryczne korzystają z rozsądnych wartości domyślnych.", + "defaultsHintLink": "Dostosuj ustawienia", "delete": "Usuń", "electricalSubtitle": "W razie wątpliwości skonsultuj się z elektrykiem.", "electricalTitle": "Elektryczny", @@ -403,13 +417,69 @@ "title": "Konfiguracja", "unconfigured": "nie skonfigurowane", "vehicles": "Moje pojazdy", - "welcomeBannerText": "Zacznij od utworzenia co najmniej jednej **ładowarki**, **grzejnika**, **sieci**, **panelu słonecznego**, **baterii** lub **dodatkowego licznika**. Jeśli chcesz tylko przetestować, wybierz **urządzenie demonstracyjne**.", + "welcomeBannerText": "Zacznij od utworzenia co najmniej jednej **ładowarki**, **podgrzewacza**, **sieci energetycznej**, **systemu pv**, **magazynu energii** lub **dodatkowego licznika**. Jeśli chcesz tylko przetestować, wybierz **urządzenie demonstracyjne**.", "welcomeBannerTitle": "Skonfigurujmy Twój system!" }, "messaging": { - "description": "Otrzymuj wiadomości o sesjach ładowania.", + "addMessenger": "Dodaj usługę", + "description": "Otrzymuj powiadomienia o swoich sesjach ładowania.", + "event": { + "asleep": { + "title": "Podczas oczekiwania na pojazd", + "titleDefault": "Pojazd uśpiony" + }, + "connect": { + "messageDefault": "Samochód podłączony do {pvPower}kW mocy PV", + "title": "Kiedy samochód się podłączy", + "titleDefault": "Samochód podłączony" + }, + "disconnect": { + "messageDefault": "Samochód odłączony po {connectedDuration}", + "title": "Kiedy samochód się odłącza", + "titleDefault": "Samochód odłączony" + }, + "guest": { + "messageDefault": "Nieznany pojazd, połączony gość?", + "title": "Kiedy podłączy się nieznany samochód", + "titleDefault": "Nieznany samochód" + }, + "planoverrun": { + "messageDefault": "{vehicleTitle}: Plan zostanie przekroczony.", + "title": "Kiedy plan ładowania zostanie przekroczony", + "titleDefault": "Przekroczenie planu" + }, + "soc": { + "messageDefault": "Akumulator trakcyjny naładowany do {vehicleSoc}%", + "title": "Aktualizacja poziomu naładowania", + "titleDefault": "Zaktualizowano poziom naładowania" + }, + "start": { + "messageDefault": "Rozpoczęto ładowanie w trybie {mode}.", + "title": "Kiedy rozpocznie się ładowanie", + "titleDefault": "Rozpoczęto ładowanie" + }, + "stop": { + "messageDefault": "Zakończono ładowanie {chargedEnergy}kWh w ciągu {chargeDuration}.", + "title": "Gdy ładowanie się zatrzyma", + "titleDefault": "Ładowanie zakończone" + } + }, + "eventMessage": "Wiadomość", + "eventTitle": "Tytuł", + "events": "Wydarzenia", + "legacyWarning": "Dostępna jest nowa konfiguracja powiadomień! Usuń i zapisz swoją konfigurację tutaj, aby skorzystać z nowego procesu.", + "messengers": "Sewisy", + "seePlaceholders": "zobacz symbole zastępcze", "title": "Powiadomienia" }, + "messenger": { + "custom": "Serwis zdefiniowany przez użytkownika", + "generic": "Ogólny serwis", + "primary": "Określony serwis", + "template": "Serwis", + "titleAdd": "Dodaj Serwis", + "titleEdit": "Edytuj Serwis" + }, "meter": { "cancel": "Anuluj", "delete": "Usuń", @@ -647,7 +717,7 @@ "validation": { "failed": "nie udało się", "label": "Status", - "running": "Weryfikacja…", + "running": "weryfikowanie…", "success": "udany", "unknown": "nieznany", "validate": "zweryfikować" @@ -930,11 +1000,14 @@ "battery": "Akumulator", "batteryCharge": "Ładowanie magazynu energii", "batteryDischarge": "Rozładowywanie magazynu energii", - "batteryGridChargeActive": "ładowanie z sieci aktywne", - "batteryGridChargeLimit": "ładowanie z sieci gdy", + "batteryForecastEmpty": "pusty {time}", + "batteryForecastFull": "pełny {time}", + "batteryGridChargeActive": "Ładowanie z sieci: aktywne", + "batteryGridChargeLimit": "Ładowanie z sieci: gdy", "batteryHold": "Magazyn energii (chroniony)", "batteryTooltip": "{energy} z {total} ({soc})", - "forecastTooltip": "prognoza: pozostała dziś produkcja energii słonecznej", + "forecast": "Prognoza: ", + "forecastTooltip": "prognoza: pozostała na dziś produkcja energii słonecznej", "gridImport": "Z sieci", "homePower": "Konsumpcja", "loadpoints": "Odbiornik energii| Odbiornik energii | {count} odbiorniki energii", @@ -971,10 +1044,15 @@ }, "loadpointSettings": { "batteryBoost": { - "description": "Szybkie ładowanie z domowego magazynu energii, aż rozładuje się do {limit}.", - "label": "Doładowanie akumulatora", + "description": "Przyspieszone ładowanie z domowego magazynu energii, aż rozładuje się do {limit}.", + "descriptionDisabled": "Wybierz limit umożliwiający przyspieszone ładowanie z domowego magazynu energii.", + "disabled": "Wyłączone", + "label": "Turbo ładowanie", "mode": "Dostępne tylko w trybie słonce i min+słońce.", - "once": "Doładowanie aktywne dla tej sesji ładowania." + "once": "Turbo ładowanie aktywne dla tej sesji.", + "stateActive": "Turbo ładowanie aktywne", + "stateBelowLimit": "Magazyn energii zbyt rozładowany dla turbo ładowania", + "stateReady": "Turbo ładowanie gotowe" }, "batteryUsage": "Domowy magazyn energii", "currents": "Prąd ładowania", @@ -1079,7 +1157,10 @@ }, "vehicleStatus": { "awaitingAuthorization": "Oczekiwanie na zezwolenie.", - "batteryBoost": "Doładowanie akumulatora aktywne.", + "batteryBoost": "Turbo ładowanie aktywne.", + "batteryBoostBelowLimit": "Magazyn energii zbyt rozładowany dla turbo ładowania.", + "batteryBoostDisabled": "Turbo ładowanie wyłączone.", + "batteryBoostEnabled": "Turbo ładowanie, aż akumulator osiągnie {limit}.", "charging": "Ładuje się…", "cheapEnergyCharging": "Dostępna tania energia.", "cheapEnergyNextStart": "Tania energia za {duration}.", diff --git a/i18n/pt.json b/i18n/pt.json index 57505b8bf..fc18636f4 100644 --- a/i18n/pt.json +++ b/i18n/pt.json @@ -44,7 +44,7 @@ }, "config": { "aux": { - "description": "Equipamento capaz de ajustar o seu consumo baseado no excedente disponível (como caldeiras AQS inteligentes). O evcc assume que esse equipamento reduz o seu consumo automaticamente, se necessário.", + "description": "Equipamento capaz de ajustar o seu consumo baseado no excedente disponível (como caldeiras inteligentes). O evcc assume que esse equipamento reduz o seu consumo automaticamente, se necessário.", "titleAdd": "Adicionar Equipamento com capacidade de auto-ajuste de consumos", "titleEdit": "Editar Equipamento com capacidade de auto-ajuste de consumos" }, @@ -105,6 +105,12 @@ "labelResidualPower": "Potência residual", "title": "Comportamento de controlo" }, + "currency": { + "description": "Utilizado para definir os preços, custos e poupanças de energia com base na sua tarifa.", + "example": "O preço cobrado foi {price}. Economizou {amount}.", + "label": "Câmbio", + "title": "Câmbio" + }, "deviceValue": { "amount": "Quantidade", "broker": "Broker", @@ -128,12 +134,16 @@ "dimmed": "Escurecido", "enabled": "Ativado", "energy": "Energia", + "events": "Eventos", "feedinPrice": "Preço de injeção na rede", + "forecast": "Previsões", "gridPrice": "Preço da Rede", "heaterTempLimit": "Limite do aquecimento", "hemsActiveLimit": "Limite ativo", "hemsType": "Comunicação", "identifier": "Identificador RFID", + "max": "max", + "messengers": "Serviços", "no": "não", "odometer": "Totalizador Km", "org": "Organização", @@ -143,6 +153,7 @@ "phases1p3p": "Comutador de fases", "power": "Energia em tempo real", "powerRange": "Potência", + "price": "Preço", "range": "Autonomia", "singlePhase": "Monofásico", "soc": "Carga", @@ -214,6 +225,7 @@ "cancel": "Cancelar", "clear": "Apagar", "close": "Fechar", + "confirmSave": "Existem modificações não guardadas. Guardar agora?", "copied": "Copiado!", "copy": "Copiar", "customHelp": "Criar um dispositivo definido pelo utilizador utilizando o sistema de plugins do evcc.", @@ -229,6 +241,7 @@ "fromYamlHint": "Nota: Configurado através do evcc.yaml. Remova a entrada do ficheiro para permitir a edição aqui.", "hideAdvancedSettings": "Ocultar definições avançadas", "invalidFileSelected": "Ficheiro selecionado inválido", + "legacy": "histórico", "noFileSelected": "Nenhum ficheiro selecionado.", "off": "desligado", "on": "ligado", @@ -295,7 +308,7 @@ "addMeter": "Adicionar contador de energia dedicado", "cancel": "Cancelar", "chargerError": { - "charging": "É necessário configurar um Carregador EV.", + "charging": "É necessário configurar um carregador.", "heating": "A configuração do aquecimento é necessária." }, "chargerLabel": { @@ -320,6 +333,8 @@ }, "defaultModeHelpKeep": "Mantém o último modo selecionado.", "defaultModeLabel": "Modo predefinido", + "defaultsHint": "O modo padrão, o comportamento solar e os detalhes elétricos utilizam valores predefinidos razoáveis.", + "defaultsHintLink": "Personalizar configurações", "delete": "Apagar", "electricalSubtitle": "Em caso de dúvida, pergunte ao seu eletricista.", "electricalTitle": "Elétrico", @@ -410,9 +425,66 @@ "welcomeBannerTitle": "Configuremos agora o seu sistema!" }, "messaging": { - "description": "Receber mensagens sobre as sessões de carregamento.", + "addMessenger": "Adicionar serviço", + "description": "Receber notificações sobre as sessões de carregamento.", + "event": { + "asleep": { + "messageDefault": "Liberação de carga, veículo {vehicleName} não está a carregar.", + "title": "Enquanto espera pelo veículo", + "titleDefault": "Veículo inativo" + }, + "connect": { + "messageDefault": "Carro conectado com {pvPower}kW PV", + "title": "Quando um carro se conecta", + "titleDefault": "Carro conectado" + }, + "disconnect": { + "messageDefault": "Carro desligado após {connectedDuration}", + "title": "Quando um carro se desliga", + "titleDefault": "Carro desligado" + }, + "guest": { + "messageDefault": "Veículo desconhecido, convidado conectado?", + "title": "Quando um carro desconhecido se conecta", + "titleDefault": "Veículo desconhecido" + }, + "planoverrun": { + "messageDefault": "{vehicleTitle}: O plano será excedido.", + "title": "Quando a carga planejada estiver prestes a exceder o limite", + "titleDefault": "Plano excedido" + }, + "soc": { + "messageDefault": "Bateria carregada a {vehicleSoc}%", + "title": "Atualização do nível de carga", + "titleDefault": "Nível de carga atualizado" + }, + "start": { + "messageDefault": "Iniciou o carregamento no modo {mode}.", + "title": "Quando o carregamento começar", + "titleDefault": "Carga iniciada" + }, + "stop": { + "messageDefault": "Concluída a carga de {chargedEnergy} kWh em {chargeDuration}.", + "title": "Quando o carregamento parar", + "titleDefault": "Carregamento concluído" + } + }, + "eventMessage": "Mensagem", + "eventTitle": "Título", + "events": "Eventos", + "legacyWarning": "Nova configuração de notificações disponível! Remova e guarde a sua configuração aqui para utilizar o novo processo guiado.", + "messengers": "Serviços", + "seePlaceholders": "ver espaços reservados", "title": "Notificações" }, + "messenger": { + "custom": "Serviço personalizado", + "generic": "Serviço genérico", + "primary": "Serviço específico", + "template": "Serviço", + "titleAdd": "Adicionar serviço", + "titleEdit": "Editar serviço" + }, "meter": { "cancel": "Cancelar", "delete": "Apagar", @@ -508,7 +580,8 @@ "labelInternalUrl": "URL interna", "labelPort": "Porta", "labelSchema": "Esquema", - "title": "Rede" + "title": "Rede", + "warningUrlPath": "A URL normalmente não precisa de um caminho. Tem a certeza de que está correto?" }, "ocpp": { "connectedChargers": "Carregadores conectados", @@ -635,6 +708,79 @@ "restartingDescription": "Espere por favor…", "restartingMessage": "evcc a reiniciar." }, + "tariff": { + "addForecast": "Adicionar previsões", + "addTariff": "Adicionar tarifa", + "co2": { + "description": "Previsão da intensidade de CO₂ para a eletricidade da rede. Para carregamento otimizado em termos de CO₂ e cálculo da redução de emissões.", + "titleAdd": "Adicionar previsão de CO₂", + "titleEdit": "Editar previsão de CO₂" + }, + "co2Services": "Serviços de CO₂", + "customForecast": "Previsão definida pelo utilizador", + "customTariff": "Tarifa definida pelo utilizador", + "description": "Configure as suas tarifas e previsões de energia. Utilize a configuração baseada em dispositivos para uma gestão dinâmica ou YAML para definições estáticas.", + "feedIn": { + "description": "Compensação pela eletricidade exportada para a rede. Utilizada para calcular os custos reais de carregamento.", + "titleAdd": "Adicionar tarifa de exportação da rede", + "titleEdit": "Editar tarifa de exportação da rede" + }, + "generic": "Integrações genéricas", + "grid": { + "description": "Preço da eletricidade para consumo da rede. Para calcular os custos reais de carregamento e otimizar o preço do carregamento de veículos, dispositivos de aquecimento ou carregamento da rede da bateria da sua casa.", + "titleAdd": "Adicionar tarifa de importação da rede", + "titleEdit": "Editar tarifa de importação da rede" + }, + "legacyWarning": "Nova configuração de tarifas disponível! Remova e guarde as suas tarifas aqui para utilizar o novo processo guiado.", + "option": { + "co2": "Adicionar previsão de CO₂", + "feedIn": "Adicionar tarifa de exportação da rede", + "grid": "Adicionar tarifa de importação da rede", + "planner": "Adicionar previsão do planeador", + "solar": "Adicionar previsão solar" + }, + "planner": { + "description": "Configuração avançada. Normalmente não é necessária, pois as tarifas dinâmicas de eletricidade ou as previsões de CO₂ são utilizadas automaticamente. Ativa uma fonte de dados adicional que é utilizada apenas para o planeamento de carregamentos, não para estatísticas e cálculos de preços.", + "titleAdd": "Adicionar previsão do planeador", + "titleEdit": "Editar previsão do planeador" + }, + "services": "Serviços", + "solar": { + "description": "Previsão de produção solar para o seu sistema fotovoltaico. Apresentada na interface e que será utilizada para algoritmos de otimização no futuro.", + "titleAdd": "Adicionar previsão solar", + "titleEdit": "Editar previsão solar" + }, + "template": "Fornecedor", + "title": "Tarifas e previsões", + "titleChoice": "O que deseja adicionar?", + "type": { + "co2": "Intensidade de CO₂", + "feedIn": "Preço de venda", + "grid": "Preço de consumo da rede", + "planner": "Planeador", + "solar": "Solar" + }, + "zones": { + "add": "Adicionar zona", + "allDays": "Todos os dias", + "allMonths": "Todos os meses", + "allTimes": "Todo o tempo", + "cancel": "Cancelar", + "days": "Dias", + "edit": "Editar", + "hours": "Horas", + "months": "Meses", + "price": "Preço", + "priceRequired": "O preço é exigido", + "remove": "Remover zona", + "save": "Guardar", + "selectAll": "Todos os dias", + "timeFrom": "De", + "timeRangeError": "A hora de início deve ser anterior à hora de fim. Para abranger a meia-noite, crie duas zonas separadas.", + "timeTo": "A", + "weekdays": "Dias da semana" + } + }, "tariffs": { "description": "Defina as suas tarifas de energia para calcular os custos das suas sessões de carregamento.", "title": "Tarifas" @@ -651,7 +797,7 @@ "validation": { "failed": "falhou", "label": "Status", - "running": "A validar…", + "running": "validação…", "success": "concluído com sucesso", "unknown": "desconhecido", "validate": "validar" @@ -940,10 +1086,13 @@ "battery": "Bateria", "batteryCharge": "Carga de Bateria", "batteryDischarge": "Descarga de Bateria", - "batteryGridChargeActive": "carga da Rede ativa", - "batteryGridChargeLimit": "carga da Rede quando", + "batteryForecastEmpty": "vazio {time}", + "batteryForecastFull": "cheio {time}", + "batteryGridChargeActive": "Carga da rede: ativa", + "batteryGridChargeLimit": "Carga da rede: quando", "batteryHold": "Bateria (suspensa)", "batteryTooltip": "{energy} de {total} ({soc})", + "forecast": "Previsões: ", "forecastTooltip": "previsão: produção solar restante de hoje", "gridImport": "Consumo da Rede", "homePower": "Consumo", @@ -970,21 +1119,27 @@ "charged": "Carregado", "co2": "CO₂", "duration": "Duração", + "emission": "Emissões", "fallbackName": "Posto de Carregamento", "finished": "Tempo de finalização", "power": "Potência", "price": "Custo", "remaining": "Tempo restante", "remoteDisabledHard": "{source}: desativado", - "remoteDisabledSoft": "{source}:carregamento PV adaptável desativado", + "remoteDisabledSoft": "{source}: carregamento PV adaptável desativado", "solar": "Solar" }, "loadpointSettings": { "batteryBoost": { "description": "Carga rápida da bateria de casa até que descarregue para {limit}.", + "descriptionDisabled": "Selecione um limite para permitir o carregamento rápido da bateria doméstica.", + "disabled": "Desativado", "label": "Boost bateria", "mode": "Disponível apenas nos modos solar e min+solar.", - "once": "Boost ativo para esta sessão de carga." + "once": "Boost ativo para esta sessão de carga.", + "stateActive": "Boost de bateria ativo", + "stateBelowLimit": "Bateria com carga insuficiente para o boost", + "stateReady": "Boost da bateria pronto" }, "batteryUsage": "Bateria de casa", "currents": "Corrente de carga", @@ -1090,6 +1245,9 @@ "vehicleStatus": { "awaitingAuthorization": "À espera de autorização.", "batteryBoost": "Boost da bateria ativo.", + "batteryBoostBelowLimit": "Bateria com carga insuficiente para boost.", + "batteryBoostDisabled": "Boost da bateria desativado.", + "batteryBoostEnabled": "Boost até a bateria atingir {limit}.", "charging": "A carregar…", "cheapEnergyCharging": "Energia barata disponível.", "cheapEnergyNextStart": "Energia barata em {duration}.", @@ -1114,6 +1272,7 @@ "targetChargeWaitForVehicle": "Plano de carregamento pronto. A aguardar veículo…", "vehicleLimit": "Limite dos veículos", "vehicleLimitReached": "Limite de veículo atingido.", + "waitForAuthorization": "Conectado. À espera de autorização…", "waitForVehicle": "Pronto. A aguardar veículo…", "welcome": "Carga inicial curta para confirmar a ligação." }, diff --git a/i18n/ro.json b/i18n/ro.json index b68d59228..a6d6ab08b 100644 --- a/i18n/ro.json +++ b/i18n/ro.json @@ -105,10 +105,16 @@ "labelResidualPower": "Putere reziduală", "title": "Comportament de control" }, + "currency": { + "description": "Folosit pentru a formata prețurile, costurile și economiile la energie în funcție de tariful dumneavoastră.", + "example": "Prețul de încărcare a fost {price}. Ați economisit {amount}.", + "label": "Valută", + "title": "Valută" + }, "deviceValue": { "amount": "Suma", "broker": "Broker", - "bucket": "Bucket", + "bucket": "Găleată", "capacity": "Capacitatea", "chargeStatus": "Stare", "chargeStatusA": "neconectat", @@ -128,12 +134,16 @@ "dimmed": "Estompat", "enabled": "Activat", "energy": "Energie", + "events": "Evenimente", "feedinPrice": "Prețul de alimentare", + "forecast": "Prognoză", "gridPrice": "Prețul rețelei", "heaterTempLimit": "Limită încălzitor", "hemsActiveLimit": "Limită activă", "hemsType": "Comunicare", "identifier": "Identificator RFID", + "max": "max", + "messengers": "Servicii", "no": "nu", "odometer": "Kilometraj", "org": "Organizație", @@ -143,6 +153,7 @@ "phases1p3p": "Comutator de fază", "power": "Putere", "powerRange": "Putere", + "price": "preț", "range": "Autonomie", "singlePhase": "Monofazat", "soc": "Stare de încărcare", @@ -172,11 +183,27 @@ "loading": "Se încarcă editorul YAML…" }, "eebus": { - "description": "Configurație care permite evcc să comunice cu alte dispozitive EEBus.", + "certificate": { + "private": "Cheie privată", + "public": "Certificat public", + "title": "Certificate" + }, + "description": "Configurație care permite evcc să comunice cu dispozitive compatibile EEBus, cum ar fi încărcătoarele sau o unitate de control a operatorului de rețea. Toate inițializările relevante și generarea de certificate se fac automat la prima pornire.", + "descriptionAdvanced": "Nu sunt necesare modificări. Efectuați modificările doar dacă știți cu adevărat ce faceți. Dacă schimbați fie SHIP-id-ul, fie certificatele, va trebui să vă asociați din nou dispozitivele.", + "interfaces": "Interfețe", + "interfacesHelp": "Limitați interfețele de rețea pe care EEBus ar trebui să le utilizeze pentru a evita problemele de comunicare. Lăsați câmpul necompletat pentru a utiliza toate interfețele. O singură înregistrare pe linie.", + "port": "Port", + "portHelp": "Portul care trebuie utilizat.", + "removeConfirm": "Toate configurațiile EEBus vor fi eliminate. Certificate și identificatori noi vor fi generate la următoarea pornire. Ești sigur?", + "shipid": "SHIP-ID", + "shipidExplain": "Identificator permanent al dispozitivului pentru identificare în rețeaua EEBus.", + "shipidHelp": "Acest SHIP-ID este legat de certificatele de mai jos.", + "ski": "SKI", + "skiExplain": "Identificator unic de securitate pentru asocierea dispozitivelor EEBus.", "title": "EEBus" }, "experimental": { - "description": "Activați interfața utilizatorului pentru funcții care sunt încă în curs de testare și care pot suferi modificări în versiunile viitoare.", + "description": "Furnizează acces timpuriu pentru funcții care sunt încă în curs de testare. Acestea pot fi instabile si pot suferi modificări sau sa fie eliminate în versiunile viitoare.", "title": "Experimental" }, "ext": { @@ -198,6 +225,7 @@ "cancel": "Anulează", "clear": "Clar", "close": "Închide", + "confirmSave": "Există modificări nesalvate. Salvați acum?", "copied": "Copiată!", "copy": "Copiere", "customHelp": "Creați un dispozitiv definit de utilizator folosind sistemul de pluginuri evcc.", @@ -207,11 +235,13 @@ "dragHandle": "Mâner de tragere", "dragItem": "Draggable: {title}", "dragList": "Listă reordonabilă", + "error": "Eroare", "experimental": "Experimental", "forceSave": "Salvați oricum", "fromYamlHint": "Notă: Configurat prin evcc.yaml. Ștergeți intrarea din fișier pentru a activa editarea aici.", "hideAdvancedSettings": "Ascunde setările avansate", "invalidFileSelected": "Fișier invalid selectat", + "legacy": "vechi", "noFileSelected": "Nu a fost selectat niciun fișier.", "off": "oprit", "on": "pe", @@ -257,7 +287,7 @@ "influx": { "description": "Scrie datele de încărcare și alte metrici în InfluxDB. Utilizați Grafana sau alte instrumente pentru a vizualiza datele.", "descriptionToken": "Consultați documentația InfluxDB pentru a afla cum se creează unul. https://docs.influxdata.com/influxdb/v2/admin/", - "labelBucket": "Bucket", + "labelBucket": "Găleată", "labelCheckInsecure": "Permiteți certificatele autosemnate", "labelDatabase": "Baza de date", "labelInsecure": "Validarea certificatului", @@ -303,6 +333,8 @@ }, "defaultModeHelpKeep": "Păstrează ultimul mod selectat.", "defaultModeLabel": "Mod implicit", + "defaultsHint": "Modul implicit, comportamentul solar și detaliile electrice utilizează valori implicite sensibile.", + "defaultsHintLink": "Modificați setările", "delete": "Șterge", "electricalSubtitle": "Dacă aveți dubii, adresați-vă electricianului dumneavoastră.", "electricalTitle": "Electric", @@ -393,9 +425,66 @@ "welcomeBannerTitle": "Să configurăm sistemul tău!" }, "messaging": { - "description": "Primiți mesaje despre sesiunile dvs. de încărcare.", + "addMessenger": "Adăugați serviciu", + "description": "Primiți notificări despre sesiunile dvs. de încărcare.", + "event": { + "asleep": { + "messageDefault": "Eliberare încărcare, vehiculul {vehicleName} nu se încarcă.", + "title": "Când așteptați vehiculul", + "titleDefault": "Vehiculul doarme" + }, + "connect": { + "messageDefault": "Mașină conectată la {pvPower}kW PV", + "title": "Când o mașină se conectează", + "titleDefault": "Mașină conectată" + }, + "disconnect": { + "messageDefault": "Mașina a fost deconectată după {connectedDuration}", + "title": "Când o mașină se deconectează", + "titleDefault": "Mașină deconectată" + }, + "guest": { + "messageDefault": "Vehicul necunoscut, oaspete conectat?", + "title": "Când o mașină necunoscută se conectează", + "titleDefault": "Vehicul necunoscut" + }, + "planoverrun": { + "messageDefault": "{vehicleTitle}: Planul va fi depășit.", + "title": "Când planul de încărcare va fi depășit", + "titleDefault": "Plan depășit" + }, + "soc": { + "messageDefault": "Bateria încărcată până la {vehicleSoc}%", + "title": "Actualizare nivel de încărcare", + "titleDefault": "Nivelul de încărcare a fost actualizat" + }, + "start": { + "messageDefault": "Încărcarea a început în modul {mode}.", + "title": "Când începe încărcarea", + "titleDefault": "Încărcarea a început" + }, + "stop": { + "messageDefault": "Încărcarea a fost finalizată cu {chargedEnergy}kWh în {chargeDuration}.", + "title": "Când se oprește încărcarea", + "titleDefault": "Încărcare finalizată" + } + }, + "eventMessage": "Mesaj", + "eventTitle": "Titlu", + "events": "Evenimente", + "legacyWarning": "Nouă configurație de notificare disponibilă! Eliminați și salvați configurația aici pentru a utiliza noul proces ghidat.", + "messengers": "Servicii", + "seePlaceholders": "vezi substituenți", "title": "Notificări" }, + "messenger": { + "custom": "Serviciu definit de utilizator", + "generic": "Serviciu generic", + "primary": "Serviciu specific", + "template": "Serviciu", + "titleAdd": "Adauga serviciu", + "titleEdit": "Editează serviciu" + }, "meter": { "cancel": "Anulare", "delete": "șterge", @@ -489,7 +578,8 @@ "labelHost": "Nume de gazdă mDNS", "labelInternalUrl": "URL intern", "labelPort": "Port", - "title": "Rețea" + "title": "Rețea", + "warningUrlPath": "De obicei, adresa URL nu necesită o cale. Ești sigur că este corectă?" }, "ocpp": { "connectedChargers": "Încărcătoare conectate", @@ -542,6 +632,7 @@ "integrations": "Integrări", "loadpoints": "Încărcare și încălzire", "meter": "Energie solară și baterii", + "services": "Servicii", "system": "Sistem", "vehicles": "Vehicule" }, @@ -615,6 +706,79 @@ "restartingDescription": "Vă rugăm să așteptați…", "restartingMessage": "Repornirea evcc." }, + "tariff": { + "addForecast": "Adauga prognoză", + "addTariff": "Adauga tafif", + "co2": { + "description": "Prognoza intensității CO₂ pentru rețeaua electrică. Pentru încărcare optimizată din punct de vedere al CO₂ și calcularea economiilor de emisii.", + "titleAdd": "Adauga prognoza CO₂", + "titleEdit": "Editează progonoza CO₂" + }, + "co2Services": "Servicii CO₂", + "customForecast": "Prognoză definită de utilizator", + "customTariff": "Tarif definit de utilizator", + "description": "Configurați-vă tarifele și previziunile energetice. Folosiți configurația bazată pe dispozitiv pentru management dinamic sau YAML pentru setări statice.", + "feedIn": { + "description": "Compensație pentru energia electrică exportată în rețea. Folosită pentru calcularea costurilor reale de încărcare.", + "titleAdd": "Adauga tariful exportului in rețea", + "titleEdit": "Editează tariful exportului in rețea" + }, + "generic": "Integrări generice", + "grid": { + "description": "Prețul energiei electrice pentru consumul din rețea. Pentru calcularea costurilor reale de încărcare și încărcarea optimizată din punct de vedere al prețului a vehiculelor, a dispozitivelor de încălzire sau încărcarea bateriei locuinței în rețea.", + "titleAdd": "Adaugă tariful importului din rețea", + "titleEdit": "Editează tariful importului din rețea" + }, + "legacyWarning": "Nouă configurație tarifară disponibilă! Eliminați și salvați tarifele aici pentru a utiliza noul proces ghidat.", + "option": { + "co2": "Adaugă prognoza CO₂", + "feedIn": "Adauga tariful exportului in rețea", + "grid": "Adaugă tariful importului din rețea", + "planner": "Adăugați o prognoză a planificatorului", + "solar": "Adăugați prognoză solară" + }, + "planner": { + "description": "Setări avansate. De obicei, nu sunt necesare, deoarece tarifele dinamice la energie electrică sau previziunile de CO₂ sunt utilizate automat. Activează o sursă de date suplimentară care este utilizată doar pentru planificarea tarifelor, nu și pentru statistici și calcule de prețuri.", + "titleAdd": "Adăugați prognoza Planificator", + "titleEdit": "Editează prognoza Planificator" + }, + "services": "Servicii", + "solar": { + "description": "Prognoza producției solare pentru sistemul dumneavoastră fotovoltaic. Se afișează în interfață și va fi utilizată pentru algoritmi de optimizare în viitor.", + "titleAdd": "Adauga prognoză solară", + "titleEdit": "Editează prognoza solară" + }, + "template": "Furnizor", + "title": "Tarife si prognoze", + "titleChoice": "Ce vrei să adaugi?", + "type": { + "co2": "Intensitate CO₂", + "feedIn": "Preț export in rețea", + "grid": "Preț consum din rețea", + "planner": "Planificator", + "solar": "Solar" + }, + "zones": { + "add": "Adauga zonă", + "allDays": "Toate zilele", + "allMonths": "Toate lunile", + "allTimes": "Toate orele", + "cancel": "Anulează", + "days": "Zile", + "edit": "Editează", + "hours": "Ore", + "months": "Luni", + "price": "Preț", + "priceRequired": "Prețul este necesar", + "remove": "Elimină zona", + "save": "Salvează", + "selectAll": "Toate zilele", + "timeFrom": "Din", + "timeRangeError": "Ora de începere trebuie să fie anterioară orei de încheiere. Pentru a se întinde peste miezul nopții, creați două zone separate.", + "timeTo": "La", + "weekdays": "Zile lucratoare din saptamână" + } + }, "tariffs": { "description": "Definiți tarifele energetice pentru a calcula costurile sesiunilor de încărcare.", "title": "Tarife" @@ -738,12 +902,14 @@ "forecast": { "co2": { "average": "Media", + "constant": "Intensitate CO₂", "lowestHour": "Ora cea mai curată", "range": "Gama" }, "modalTitle": "Prognoză", "price": { "average": "Media", + "constant": "Preț", "lowestHour": "Cea mai ieftină oră", "range": "Gama" }, @@ -787,7 +953,7 @@ "restart": { "cancel": "Anuleaza", "confirm": "Da, reporneste!", - "description": "In mod normal, restarul nu ar trebui sa fie necesar. Te rog sa trimiti un raport daca trebuie sa restartezi des EVCC.", + "description": "In mod normal, restartul nu ar trebui sa fie necesar. Te rog sa trimiti un raport daca trebuie sa restartezi des EVCC.", "disclaimer": "Nota: evcc se va inchide si se va baza pe OS sa restarteze serviciul sau.", "modalTitle": "Esti sigur ca vrei sa dai restart?" }, @@ -912,10 +1078,13 @@ "battery": "Baterie", "batteryCharge": "Incarca bateria", "batteryDischarge": "Descărcare baterie", - "batteryGridChargeActive": "încărcare rețea activă", - "batteryGridChargeLimit": "încărcare la rețea când", + "batteryForecastEmpty": "gol {time}", + "batteryForecastFull": "plin {time}", + "batteryGridChargeActive": "Încărcare din rețea: activă", + "batteryGridChargeLimit": "Încărcare din rețea: când", "batteryHold": "Baterie (inchisa)", "batteryTooltip": "{energy} din {total} ({soc})", + "forecast": "Prognoză: ", "forecastTooltip": "prognoză: producția solară rămasă astăzi", "gridImport": "Folosirea rețelei", "homePower": "Consum", @@ -942,6 +1111,7 @@ "charged": "Încărcat", "co2": "⌀ CO₂", "duration": "Durata", + "emission": "Emisie", "fallbackName": "Punct de încărcare", "finished": "Ora de finalizare", "power": "Putere", @@ -954,9 +1124,14 @@ "loadpointSettings": { "batteryBoost": { "description": "Încărcare rapidă de la bateria de acasă până când se descarcă la {limit}.", + "descriptionDisabled": "Selectați o limită pentru a permite încărcarea rapidă de la bateria de acasă.", + "disabled": "Dezactivat", "label": "Îmbunătățirea bateriei", "mode": "Disponibil numai în modul solar și min+solar.", - "once": "Boost activ pentru această sesiune de încărcare." + "once": "Boost activ pentru această sesiune de încărcare.", + "stateActive": "Boost baterie activ", + "stateBelowLimit": "Bateria este prea descărcată pentru boost", + "stateReady": "Boost baterie pregatit" }, "batteryUsage": "Baterie pentru uz casnic", "currents": "Curent încărcare", @@ -1025,7 +1200,7 @@ "setTargetTime": "nimic", "targetIsAboveLimit": "Limita configurata - {limit}, va fi ignorata in timpul incarcarii.", "targetIsAboveVehicleLimit": "Limita vehiculului este sub obiectivul de încărcare.", - "targetIsInThePast": "Alege o dată in viitor, DORELE.", + "targetIsInThePast": "Alege o dată in viitor, \"Dorele\".", "targetIsTooFarInTheFuture": "Vom ajusta planul de îndată ce vom avea mai multe informații despre viitor.", "title": "Timp dorit", "today": "astăzi", @@ -1062,6 +1237,9 @@ "vehicleStatus": { "awaitingAuthorization": "În așteptarea autorizării.", "batteryBoost": "Bateria activă.", + "batteryBoostBelowLimit": "Bateria este prea descărcată pentru boost.", + "batteryBoostDisabled": "Boost baterie dezactivat.", + "batteryBoostEnabled": "Boost până când bateria atinge {limit}.", "charging": "Încarcă…", "cheapEnergyCharging": "Energie ieftină disponibilă.", "cheapEnergyNextStart": "Energie ieftină în {duration}.", @@ -1221,7 +1399,7 @@ }, "hiddenFeatures": { "label": "Experimentală", - "value": "Afișează caracteristici experimentale." + "value": "Activează caracteristici experimentale." }, "language": { "auto": "Automat", diff --git a/i18n/sv.json b/i18n/sv.json index 27cec0237..f30e25f94 100644 --- a/i18n/sv.json +++ b/i18n/sv.json @@ -29,7 +29,7 @@ "capacity": "{energy} av {total}", "control": "Batteriinställningar", "discharge": "Förhindra urladdning i snabbt läge och vid schemalagd laddning.", - "disclaimerHint": "OBS:", + "disclaimerHint": "Obs:", "disclaimerText": "Inställningarna gäller enbart sol-läge. Laddning justeras därefter.", "gridChargeTab": "Laddning från nätet", "legendBottomName": "Prioritera laddning av hemmabatteri", @@ -105,6 +105,9 @@ "labelResidualPower": "Kvarvarande effekt", "title": "Kontrollera beteende" }, + "currency": { + "description": "Används för att samla in och formatera energipriser, kostnader och besparingar." + }, "deviceValue": { "amount": "Belopp", "broker": "Broker", @@ -128,12 +131,14 @@ "dimmed": "Nedtonad", "enabled": "Aktiverad", "energy": "Energi", + "events": "Händelser", "feedinPrice": "Inmatningspris", "gridPrice": "Nätpris", "heaterTempLimit": "Värmarbegränsning", "hemsActiveLimit": "Aktiv begränsning", "hemsType": "Kommunikation", "identifier": "RFID-identifierare", + "messengers": "Tjänster", "no": "nej", "odometer": "Mätarställning", "org": "Organisation", @@ -192,7 +197,7 @@ "title": "EEBus" }, "experimental": { - "description": "Ger tillgång till funktioner som fortfarande testas. De kan kan vara instabila och komma att ändras eller tas bort i framtida versioner.", + "description": "Ger tillgång till funktioner som fortfarande testas. De kan vara instabila och komma att ändras eller tas bort i framtida versioner.", "title": "Experimentell" }, "ext": { @@ -214,6 +219,7 @@ "cancel": "Avbryt", "clear": "Rensa", "close": "Stäng", + "confirmSave": "Det finns ej sparade ändringar. Spara nu?", "copied": "Kopierad!", "copy": "Kopiera", "customHelp": "Skapa en egen enhet med hjälp av evcc's plugin-system.", @@ -320,6 +326,8 @@ }, "defaultModeHelpKeep": "Använder senast valda läge.", "defaultModeLabel": "Standardinställning", + "defaultsHint": "Standardläget, sol-överskottsbeteendet och de elektriska detaljerna använder sig av rimliga standardvärden.", + "defaultsHintLink": "Ändra inställningar", "delete": "Radera", "electricalSubtitle": "Om du är osäker, kontakta elektriker.", "electricalTitle": "Elektrisk", @@ -410,9 +418,66 @@ "welcomeBannerTitle": "Låt oss konfigurera ditt system!" }, "messaging": { + "addMessenger": "Lägg till tjänst", "description": "Få notiser om dina laddningar.", + "event": { + "asleep": { + "messageDefault": "Sluta ladda, fordonet {vehicleName} laddar inte.", + "title": "Om fordonet inte laddar", + "titleDefault": "Fordonet sover" + }, + "connect": { + "messageDefault": "Bil ansluten {pvPower}kW", + "title": "När en bil ansluts", + "titleDefault": "Bil ansluten" + }, + "disconnect": { + "messageDefault": "Bil frånkopplad efter {connectedDuration}", + "title": "När en bil frånkopplas", + "titleDefault": "Bil frånkopplad" + }, + "guest": { + "messageDefault": "Okänt fordon, gäst inkopplad?", + "title": "När ett okänt fordon inkopplas", + "titleDefault": "Okänt fordon" + }, + "planoverrun": { + "messageDefault": "{vehicleTitle}: Laddplan kommer åsidosättas.", + "title": "När laddplan kommer att åsidosättas", + "titleDefault": "Laddplan åsidosätts" + }, + "soc": { + "messageDefault": "Batteriladdningl {vehicleSoc}%", + "title": "Uppdatera laddnivå", + "titleDefault": "Laddnivå uppdaterad" + }, + "start": { + "messageDefault": "Laddning startad i {mode}-läge.", + "title": "När laddning startar", + "titleDefault": "Laddning startad" + }, + "stop": { + "messageDefault": "Laddning klar, {chargedEnergy}kWh på {chargeDuration}.", + "title": "När laddning stoppar", + "titleDefault": "Laddning klar" + } + }, + "eventMessage": "Meddelande", + "eventTitle": "Titel", + "events": "Händelser", + "legacyWarning": "Ny notifieringskonfiguration tillgänglig! Ta bort och spara din konfiguration här för att använda den nya guidade processen.", + "messengers": "Tjänster", + "seePlaceholders": "se platshållare", "title": "Notiser" }, + "messenger": { + "custom": "Användardefinierad tjänst", + "generic": "Generisk tjänst", + "primary": "Specifik tjänst", + "template": "Tjänst", + "titleAdd": "Lägg till tjänst", + "titleEdit": "Ändra tjänst" + }, "meter": { "cancel": "Avbryt", "delete": "Radera", @@ -508,7 +573,8 @@ "labelInternalUrl": "Intern URL", "labelPort": "Port", "labelSchema": "Schema", - "title": "Nätverk" + "title": "Nätverk", + "warningUrlPath": "URL:en behöver vanligtvis ingen sökväg. Är du säker?" }, "ocpp": { "connectedChargers": "Anslutna laddare", @@ -652,7 +718,7 @@ "validation": { "failed": "misslyckades", "label": "Status", - "running": "Validerar…", + "running": "validerar…", "success": "lyckades", "unknown": "okänd", "validate": "validerar" @@ -942,10 +1008,13 @@ "battery": "Batteri", "batteryCharge": "Batteri laddas", "batteryDischarge": "Batteri laddas ur", - "batteryGridChargeActive": "nätladdning aktiv", - "batteryGridChargeLimit": "nätladdning när", + "batteryForecastEmpty": "tom {time}", + "batteryForecastFull": "full {time}", + "batteryGridChargeActive": "Nätladdning: aktiv", + "batteryGridChargeLimit": "Nätladdning: när", "batteryHold": "Batteri (låst)", "batteryTooltip": "{energy} av {total} ({soc})", + "forecast": "Prognos: ", "forecastTooltip": "Prognos: återstående solproduktion idag", "gridImport": "Import från elnät", "homePower": "Konsumtion", @@ -972,6 +1041,7 @@ "charged": "Laddat", "co2": "⌀ CO₂", "duration": "Laddtid", + "emission": "Utsläpp", "fallbackName": "Laddplats", "finished": "Sluttid", "power": "Effekt", @@ -984,9 +1054,14 @@ "loadpointSettings": { "batteryBoost": { "description": "Snabbladdning från hembatteri tills det är urladdat till {limit}.", + "descriptionDisabled": "Välj begränsning för att tillåta snabb laddning från hembatteri.", + "disabled": "Avaktiverad", "label": "Batteri Boost", "mode": "Endast tillgänglig vid 'Sol' och 'Min+Sol' läge.", - "once": "Boost är aktiverat för denna laddning." + "once": "Boost är aktiverat för denna laddning.", + "stateActive": "Batteriboost aktiv", + "stateBelowLimit": "Batterinivå för låg för boost", + "stateReady": "Beredd för batteriboost" }, "batteryUsage": "Hembatteri", "currents": "Laddström", @@ -1092,6 +1167,9 @@ "vehicleStatus": { "awaitingAuthorization": "Väntar på godkännande.", "batteryBoost": "Batteriboost är aktiv.", + "batteryBoostBelowLimit": "Batterinivå för låg för boost.", + "batteryBoostDisabled": "Batteriboost avaktiverad.", + "batteryBoostEnabled": "Boost tills batteriet är på {limit}.", "charging": "Laddar…", "cheapEnergyCharging": "Billig energi är tillgänglig.", "cheapEnergyNextStart": "Billig energi om {duration}.", diff --git a/i18n/ta.json b/i18n/ta.json index 502fbeefb..c2e32cb03 100644 --- a/i18n/ta.json +++ b/i18n/ta.json @@ -1,4 +1,24 @@ { + "authProviders": { + "authCode": "அங்கீகார குறியீடு", + "authCodeHelp": "இந்தக் குறியீட்டை நகலெடுத்து அடுத்த கட்டத்தில் பயன்படுத்தவும். {duration} வரை செல்லுபடியாகும்.", + "authorizationFailed": "ஏற்பு தோல்வியடைந்தது", + "authorizationRequired": "ஏற்பு தேவை", + "authorizationSuccessful": "ஏற்பு செய்", + "buttonConnect": "{provider} உடன் இணைக்கவும்", + "buttonDisconnect": "துண்டிக்கவும்", + "confirmLogout": "நிச்சயமாக {title} இணைப்பை துண்டிக்க விரும்புகிறீர்களா?", + "connect": "இணை", + "disconnect": "துண்டிக்கவும்", + "loggedOut": "வெற்றிகரமாக வெளியேறியது", + "logoutFailed": "வெளியேறுவதில் தோல்வி", + "modalDescriptionLogin": "{provider} உடன் இணைப்பை ஏற்படுத்த அங்கீகார செயல்முறையை முடிக்கவும்.", + "modalDescriptionLogout": "இது உங்கள் {provider} கணக்கைத் துண்டித்து அதன் தரவிற்கான அணுகலை அகற்றும்.", + "success": "{title} இப்போது இணைக்கப்பட்டு பயன்படுத்த தயாராக உள்ளது.", + "successCloseModal": "நீங்கள் இப்போது இந்த உரையாடலை மூடலாம்.", + "successCloseTab": "நீங்கள் இப்போது இந்த தாவலை மூடலாம்.", + "title": "அங்கீகார நிலை" + }, "batterySettings": { "batteryLevel": "பேட்டரி நிலை", "bufferStart": { @@ -40,8 +60,16 @@ "chargers": "ஈ.வி. சார்சர்ச்", "generic": "பொதுவான ஒருங்கிணைப்புகள்", "heatingdevices": "வெப்ப சாதனங்கள்", + "ocppConfirmContinue": "உங்கள் சார்சர் இன்னும் evcc உடன் இணைக்கப்படவில்லை. நீங்கள் நிச்சயமாக தொடர விரும்புகிறீர்களா?", + "ocppConnected": "இணைக்கப்பட்டது!", + "ocppDescription": "evcc ஆனது உள்ளமைக்கப்பட்ட OCPP சேவையகத்தைக் கொண்டுள்ளது. இந்த வழிமுறைகளைப் பின்பற்றவும்:", "ocppHelp": "இந்த URL-ஐ உங்கள் சார்ஜரின் உள்ளமைவில் நகலெடுக்கவும். விவரங்களுக்கு உற்பத்தியாளரின் கையேட்டைப் பார்க்கவும். சார்ஜர் தானாகவே அதன் தனித்துவமான அடையாளங்காட்டியை (நிலைய ID) URL-க்கு இணைக்கும். அரிதான சந்தர்ப்பங்களில், நீங்கள் அடையாளங்காட்டியை கைமுறையாக குறிப்பிட வேண்டியிருக்கலாம். எடுத்துக்காட்டு: `{url}`", "ocppLabel": "OCPP-SERVER முகவரி", + "ocppNextStep": "அடுத்த அடி", + "ocppStep1": "evcc ஐ OCPP சேவையகமாகப் பயன்படுத்த உங்கள் சார்சரை உள்ளமைக்கவும்.", + "ocppStep2": "உங்கள் சார்சர் evcc உடன் இணைக்கும் வரை காத்திருக்கவும்.", + "ocppStep3": "தொடரவும் மற்றும் உள்ளமைவை முடிக்கவும்.", + "ocppWaiting": "இணைப்புக்காக காத்திருக்கிறது", "switchsockets": "மாறக்கூடிய சாக்கெட்டுகள்", "template": "உற்பத்தியாளர்", "titleAdd": { @@ -59,17 +87,19 @@ }, "heatpump": "பயனர் வரையறுக்கப்பட்ட வெப்ப பம்ப்", "sgready": "பயனர் வரையறுக்கப்பட்ட வெப்ப பம்ப் (sg-ready via plugins)", + "sgready-boost": "பயனர் வரையறுத்த வெப்ப பம்ப் (sg-ready-boost, deprecated)", "sgready-relay": "பயனர் வரையறுக்கப்பட்ட வெப்ப பம்ப் (sg-ready via relays)", "switchsocket": "பயனர் வரையறுக்கப்பட்ட சுவிட்ச் சாக்கெட்" } }, "circuits": { "description": "ஒரு சுற்றுடன் இணைக்கப்பட்ட அனைத்து ஏற்ற புள்ளிகளின் கூட்டுத்தொகையும் கட்டமைக்கப்பட்ட ஆற்றல் மற்றும் தற்போதைய வரம்புகளை மீறாது என்பதை உறுதி செய்கிறது. ஒரு படிநிலையை உருவாக்க சுற்றுகள் கூடு கட்டப்படலாம்.", - "title": "சுமை மேலாண்மை" + "title": "சுமை மேலாண்மை", + "usableMeters": "பயன்படுத்தக்கூடிய மீட்டர் குறிப்புகள்" }, "control": { "description": "பொதுவாக இயல்புநிலை மதிப்புகள் நன்றாக இருக்கும். நீங்கள் என்ன செய்கிறீர்கள் என்று உங்களுக்குத் தெரிந்தால் மட்டுமே அவற்றை மாற்றவும்.", - "descriptionInterval": "நொடிகளில் கட்டுப்பாட்டு லூப் புதுப்பிப்பு சுழற்சியைக் கட்டுப்படுத்துங்கள். ஈ.வி.சி.சி மீட்டர் தரவைப் படிக்கிறது, சார்சிங் சக்தியை சரிசெய்கிறது மற்றும் இடைமுகம் ஐப் புதுப்பிக்கிறது. குறுகிய இடைவெளிகள் (<30 கள்) ஊசலாட்டங்களையும் தேவையற்ற நடத்தையையும் ஏற்படுத்தும்.", + "descriptionInterval": "நொடிகளில் சுழற்சியைப் புதுப்பிக்கவும். evcc மீட்டர் தரவை எவ்வளவு அடிக்கடி படிக்கிறது மற்றும் சார்சிங்கை சரிசெய்கிறது என்பதை வரையறுக்கிறது. 30 வினாடிகளின் இயல்புநிலை பாதுகாப்பான தேர்வாகும். வாகனங்கள், வால்பாக்ச்கள் மற்றும் இன்வெர்ட்டர்கள் போன்ற சாதனங்களுக்கு அவற்றின் நடத்தையை சரிசெய்ய பொதுவாக சில வினாடிகள் தேவைப்படும். உங்கள் கூறுகள் விரைவாக செயல்பட்டால், குறைந்த மதிப்புகளைப் பயன்படுத்தலாம். 10 வினாடிகளுக்கு கீழே செல்ல வேண்டாம் என்று நாங்கள் கடுமையாக பரிந்துரைக்கிறோம். ஒழுங்கற்ற கட்டுப்பாட்டு நடத்தை அல்லது சம்பிங் பவர் மதிப்புகளை நீங்கள் கவனித்தால், ஒரு பெரிய இடைவெளியைத் தேர்ந்தெடுக்கவும்.", "descriptionResidualPower": "கட்டுப்பாட்டு வளையத்தின் செயல்பாட்டு புள்ளியை மாற்றுகிறது. உங்களிடம் வீட்டு பேட்டரி இருந்தால் 100 W மதிப்பை அமைக்க பரிந்துரைக்கப்படுகிறது. இந்த வழியில் கட்டம் பயன்பாட்டை விட பேட்டரி சற்று முன்னுரிமை பெறும்.", "labelInterval": "புதுப்பிப்பு இடைவெளி", "labelResidualPower": "மீதமுள்ள ஆற்றல்", @@ -89,17 +119,23 @@ "chargedEnergy": "கட்டணம் வசூலிக்கப்பட்டது", "co2": "கிரிட் கோ ₂", "configured": "கட்டமைக்கப்பட்ட", + "connections": "இணைப்புகள்", "controllable": "கட்டுப்படுத்தக்கூடியது", "currency": "நாணயம்", "current": "நடப்பு", "currentRange": "நடப்பு", + "detected": "கண்டறியப்பட்டது", + "dimmed": "மங்கலானது", "enabled": "இயக்கப்பட்டது", "energy": "சக்தி", + "events": "நிகழ்வுகள்", "feedinPrice": "ஃபீட்-இன் விலை", "gridPrice": "கட்டம் விலை", "heaterTempLimit": "ஈட்டர் வரம்பு", - "hemsType": "கணினி", + "hemsActiveLimit": "செயலில் வரம்பு", + "hemsType": "தொடர்பு", "identifier": "RFID-IDENTIFIER", + "messengers": "சேவைகள்", "no": "இல்லை", "odometer": "ஓடோமீட்டர்", "org": "அமைப்பு", @@ -116,7 +152,7 @@ "temp": "வெப்பநிலை", "topic": "தலைப்பு", "url": "முகவரி", - "vehicleLimitSoc": "வாகன வரம்பு", + "vehicleLimitSoc": "கட்டண வரம்பு", "yes": "ஆம்" }, "deviceValueChargeStatus": { @@ -124,22 +160,47 @@ "B": "பி (இணைக்கப்பட்டுள்ளது)", "C": "சி (சார்சிங்)" }, + "deviceValueHemsType": { + "eebus": "EEBus வழியாக", + "relay": "ரிலே வழியாக" + }, "devices": { "auxMeter": "அறிவுள்ள நுகர்வோர்", "batteryStorage": "பேட்டரி சேமிப்பு", + "consumer": "நுகர்வோர்", "solarSystem": "சூரிய குடும்பம்" }, "editor": { "loading": "யாம் எடிட்டரை ஏற்றுகிறது…" }, "eebus": { - "description": "ஈ.வி.சி.சி மற்ற ஈபச் சாதனங்களுடன் தொடர்பு கொள்ள உதவும் உள்ளமைவு.", + "certificate": { + "private": "தனிப்பட்ட விசை", + "public": "பொது சான்றிதழ்", + "title": "சான்றிதழ்கள்" + }, + "description": "சார்சர்கள் அல்லது உங்கள் கிரிட் ஆபரேட்டரின் கட்டுப்பாட்டு அலகு போன்ற EEBus இணக்கமான சாதனங்களுடன் தொடர்பு கொள்ள evcc ஐ செயல்படுத்தும் கட்டமைப்பு. அனைத்து தொடர்புடைய துவக்கம் மற்றும் சான்றிதழ் உருவாக்கம் முதல் தொடக்கத்தில் தானாகவே செய்யப்படுகிறது.", + "descriptionAdvanced": "மாற்றங்கள் தேவையில்லை. நீங்கள் என்ன செய்கிறீர்கள் என்பது உங்களுக்குத் தெரிந்தால் மட்டுமே மாற்றங்களைச் செய்யுங்கள். நீங்கள் SHIP-id அல்லது சான்றிதழ்களை மாற்றினால், உங்கள் சாதனங்களை மீண்டும் இணைக்க வேண்டும்.", + "interfaces": "இடைமுகங்கள்", + "interfacesHelp": "தொடர்பு சிக்கல்களைத் தவிர்க்க EEBus பயன்படுத்த வேண்டிய பிணைய இடைமுகங்களை வரம்பிடவும். அனைத்து இடைமுகங்களையும் பயன்படுத்த புலத்தை காலியாக விடவும். ஒரு வரிக்கு ஒரு நுழைவு.", + "port": "துறைமுகம்", + "portHelp": "பயன்படுத்த வேண்டிய துறைமுகம்.", + "removeConfirm": "அனைத்து EEBus உள்ளமைவுகளும் அகற்றப்படும். அடுத்த தொடக்கத்தில் புதிய சான்றிதழ்கள் மற்றும் அடையாளங்காட்டிகள் உருவாக்கப்படும். நீங்கள் உறுதியாக இருக்கிறீர்களா?", + "shipid": "கப்பல் அடையாளம்", + "shipidExplain": "EEBus நெட்வொர்க்கில் அடையாளங்காணுவதற்கான நிரந்தர சாதன அடையாளங்காட்டி.", + "shipidHelp": "இந்த SHIP-ID கீழே உள்ள சான்றிதழ்களுடன் இணைக்கப்பட்டுள்ளது.", + "ski": "SKI", + "skiExplain": "EEBus சாதனங்களை இணைப்பதற்கான தனித்துவமான பாதுகாப்பு அடையாளங்காட்டி.", "title": "ஈபச்" }, + "experimental": { + "description": "இன்னும் சோதிக்கப்படும் அம்சங்களுக்கான ஆரம்ப அணுகலை வழங்குகிறது. இவை நிலையற்றதாக இருக்கலாம் மற்றும் எதிர்கால பதிப்புகளில் மாற்றப்படலாம் அல்லது அகற்றப்படலாம்.", + "title": "ஆய்வு" + }, "ext": { - "description": "சுமை மேலாண்மை அல்லது புள்ளிவிவர நோக்கங்களுக்காக பயன்படுத்தலாம்.", - "titleAdd": "வெளிப்புற மீட்டரைச் சேர்க்கவும்", - "titleEdit": "வெளிப்புற மீட்டரைத் திருத்தவும்" + "description": "புள்ளிவிவர நோக்கங்களுக்காக கட்டுப்பாடற்ற நுகர்வோரின் ஆற்றல் மதிப்புகளை (எ.கா. குளிர்சாதன பெட்டி, சலவை இயந்திரம் போன்றவை) பதிவு செய்கிறது. இந்த மீட்டர்கள் சுமை மேலாண்மைக்கும் பயன்படுத்தப்படலாம் (எ.கா. துணை விநியோகம்).", + "titleAdd": "நுகர்வோர் மீட்டரைச் சேர்க்கவும்", + "titleEdit": "நுகர்வோர் மீட்டரைத் திருத்தவும்" }, "form": { "danger": "இடர்", @@ -148,27 +209,47 @@ "optional": "விரும்பினால்" }, "general": { + "applyAndClose": "விண்ணப்பிக்கவும் & மூடவும்", + "authPerform": "{provider} உடன் இணைக்கவும்", + "authPerformHint": "புதிய தாவலில் திறக்கப்படும். தொடர்வதற்கு இங்கே திரும்பவும்.", + "authPrepare": "இணைப்பை ஆயத்தம் செய்யவும்", "cancel": "ரத்துசெய்", - "customHelp": "EVCC இன் சொருகி அமைப்பைப் பயன்படுத்தி பயனர் வரையறுக்கப்பட்ட சாதனத்தை உருவாக்கவும்.", + "clear": "தெளிவு", + "close": "மூடு", + "confirmSave": "சேமிக்கப்படாத மாற்றங்கள் உள்ளன. இப்போது சேமிக்கவா?", + "copied": "நகலெடுக்கப்பட்டது!", + "copy": "நகலெடு", + "customHelp": "இவிசிசியின் சொருகி அமைப்பைப் பயன்படுத்தி பயனர் வரையறுக்கப்பட்ட சாதனத்தை உருவாக்கவும்.", "customOption": "பயனர் வரையறுக்கப்பட்ட சாதனம்", "delete": "நீக்கு", "docsLink": "ஆவணங்களைக் காண்க.", + "dragHandle": "கைப்பிடியை இழுக்கவும்", + "dragItem": "இழுக்கக்கூடியது: {title}", + "dragList": "மறுவரிசைப்படுத்தக்கூடிய பட்டியல்", + "error": "பிழை", "experimental": "சோதனை", + "forceSave": "எப்படியும் சேமிக்கவும்", "fromYamlHint": "Evcc.yaml இல் கட்டமைக்கப்பட்டுள்ளது. இடைமுகம் இல் திருத்த முடியாது.", "hideAdvancedSettings": "மேம்பட்ட அமைப்புகளை மறைக்க", "invalidFileSelected": "தவறான கோப்பு தேர்ந்தெடுக்கப்பட்டது", + "legacy": "மரபு", "noFileSelected": "எந்த கோப்பும் தேர்ந்தெடுக்கப்படவில்லை.", "off": "ஆஃப்", "on": "ஆன்", "password": "கடவுச்சொல்", "readFromFile": "கோப்பிலிருந்து படியுங்கள்", "remove": "அகற்று", + "required": "தேவை", + "reset": "மீட்டமை", "save": "சேமி", + "saved": "சேமிக்கப்பட்டது.", + "saving": "சேமிக்கிறது…", "selectFile": "உலாவு", "showAdvancedSettings": "மேம்பட்ட அமைப்புகளைக் காட்டு", "telemetry": "டெலிமெட்ரி", "templateLoading": "ஏற்றுகிறது ...", "title": "தலைப்பு", + "typeDeprecated": "'{type}' வகை காலாவதியானது மற்றும் எதிர்கால பதிப்பில் அகற்றப்படும். சேஞ்ச்லாக்கைச் சரிபார்த்து, இந்தச் சாதனத்தை மீண்டும் உருவாக்கவும்.", "validateSave": "சரிபார்க்கவும் சேமிக்கவும்" }, "grid": { @@ -177,11 +258,22 @@ "titleEdit": "கட்டம் மீட்டரைத் திருத்து" }, "hems": { - "description": "ஈ.வி.சி.சியை மற்றொரு வீட்டு எரிசக்தி மேலாண்மை அமைப்புடன் இணைக்கவும்.", - "title": "எம்ச்" + "csv": { + "created": "உருவாக்கப்பட்டது", + "finished": "முடிந்தது", + "gridpower": "கிரிட் பவர் (kW)", + "limitpower": "வரம்பு (kW)", + "type": "வகை" + }, + "description": "வெளிப்புற அமைப்புகளின் ஆற்றல் வரம்பு (எ.கா. §14a EnWG, §9 EEG இடைமுகம் அல்லது உயர்-நிலை ஆற்றல் மேலாண்மை அமைப்பு). சுமை மேலாண்மை அம்சத்துடன் இணைந்து செயல்படுகிறது.", + "downloadCsv": "காபிம ஐப் பதிவிறக்கவும்", + "eventsRecorded": "பதிவுசெய்யப்பட்ட {count} கட்ட வரம்பு நிகழ்வுகள்.", + "lastEvent": "மிகச் அண்மைக் கால {timeAgo}.", + "title": "வெளிப்புற வரம்பு" }, "icon": { - "change": "மாற்றம்" + "change": "மாற்றம்", + "label": "படவுரு" }, "influx": { "description": "சார்சிங் தரவு மற்றும் பிற அளவீடுகளை இன்ஃப்ளக்ச்.டி.பி.க்கு எழுதுகிறது. தரவைக் காட்சிப்படுத்த கிராஃபானா அல்லது பிற கருவிகளைப் பயன்படுத்தவும்.", @@ -223,6 +315,7 @@ "chargerTypeLabel": "சார்சர் வகை", "chargingTitle": "நடத்தை", "circuitHelp": "ஆற்றல் மற்றும் தற்போதைய வரம்புகள் மீறப்படுவதில்லை என்பதை உறுதிப்படுத்த சுமை மேலாண்மை பணி.", + "circuitInvalid": "சர்க்யூட் இல்லை", "circuitLabel": "சுற்று", "circuitUnassigned": "ஒதுக்கப்படாதது", "defaultModeHelp": { @@ -299,6 +392,7 @@ "vehicleAutoDetection": "ஆட்டோ கண்டறிதல்", "vehicleHelpAutoDetection": "மிகவும் நம்பத்தகுந்த வாகனத்தை தானாகவே தேர்ந்தெடுக்கிறது. கையேடு மேலெழுதல் சாத்தியமாகும்.", "vehicleHelpDefault": "இந்த வண்டி இங்கே கட்டணம் வசூலிப்பதாக எப்போதும் கருதுங்கள். தானாக கண்டறிதல் முடக்கப்பட்டுள்ளது. கையேடு மேலெழுதல் சாத்தியமாகும்.", + "vehicleInvalid": "வண்டி இல்லை", "vehicleLabel": "இயல்புநிலை வண்டி", "vehiclesTitle": "வாகனங்கள்" }, @@ -315,12 +409,71 @@ "name": "பெயர்", "title": "உள்ளமைவு", "unconfigured": "கட்டமைக்கப்படவில்லை", - "vehicles": "என் வாகனங்கள்" + "vehicles": "என் வாகனங்கள்", + "welcomeBannerText": "குறைந்தது ஒரு **சார்சர்**, **ஈட்டர்**, **கிரிட்**, **சோலார்**, **பேட்டரி** அல்லது **கூடுதல் மீட்டர்** ஆகியவற்றை உருவாக்கத் தொடங்குங்கள். நீங்கள் சோதிக்க விரும்பினால், **டெமோ சாதனத்தை** தேர்ந்தெடுக்கவும்.", + "welcomeBannerTitle": "உங்கள் கணினியை உள்ளமைப்போம்!" }, "messaging": { - "description": "உங்கள் சார்சிங் அமர்வுகள் பற்றிய செய்திகளைப் பெறுங்கள்.", + "addMessenger": "சேவையைச் சேர்க்கவும்", + "description": "உங்கள் சார்சிங் அமர்வுகள் பற்றிய அறிவிப்புகளைப் பெறவும்.", + "event": { + "asleep": { + "messageDefault": "கட்டணம் வெளியீடு, வண்டி {vehicleName} சார்ச் செய்யப்படவில்லை.", + "title": "வாகனத்திற்காக காத்திருக்கும் போது", + "titleDefault": "வண்டி தூங்குகிறது" + }, + "connect": { + "messageDefault": "கார் {pvPower}kW PV இல் இணைக்கப்பட்டுள்ளது", + "title": "ஒரு கார் இணைக்கும் போது", + "titleDefault": "கார் இணைக்கப்பட்டுள்ளது" + }, + "disconnect": { + "messageDefault": "{connectedDuration}க்குப் பிறகு கார் துண்டிக்கப்பட்டது", + "title": "ஒரு கார் துண்டிக்கப்படும் போது", + "titleDefault": "கார் துண்டிக்கப்பட்டது" + }, + "guest": { + "messageDefault": "தெரியாத வண்டி, விருந்தினர் இணைக்கப்பட்டுள்ளதா?", + "title": "தெரியாத கார் இணைக்கும்போது", + "titleDefault": "தெரியாத வண்டி" + }, + "planoverrun": { + "messageDefault": "{vehicleTitle}: திட்டம் மீறப்படும்.", + "title": "திட்டம் சார்சிங் மீறப்படும் போது", + "titleDefault": "திட்டம் மீறப்பட்டது" + }, + "soc": { + "messageDefault": "பேட்டரி சார்ச் ஆனது {vehicleSoc}%", + "title": "கட்டண நிலை புதுப்பிப்பு", + "titleDefault": "கட்டண நிலை புதுப்பிக்கப்பட்டது" + }, + "start": { + "messageDefault": "{mode} பயன்முறையில் சார்ச் செய்யத் தொடங்கியது.", + "title": "சார்சிங் தொடங்கும் போது", + "titleDefault": "கட்டணம் வசூலிக்கப்பட்டது" + }, + "stop": { + "messageDefault": "{chargeDuration} இல் {chargedEnergy}kWh சார்ச் முடிந்தது.", + "title": "சார்ச் நிறுத்தப்படும் போது", + "titleDefault": "கட்டணம் முடிந்தது" + } + }, + "eventMessage": "செய்தி", + "eventTitle": "தலைப்பு", + "events": "நிகழ்வுகள்", + "legacyWarning": "புதிய அறிவிப்பு உள்ளமைவு உள்ளது! புதிய வழிகாட்டுதல் செயல்முறையைப் பயன்படுத்த, உங்கள் உள்ளமைவை அகற்றி இங்கே சேமிக்கவும்.", + "messengers": "சேவைகள்", + "seePlaceholders": "ஒதுக்கிடங்களைப் பார்க்கவும்", "title": "அறிவிப்புகள்" }, + "messenger": { + "custom": "பயனர் வரையறுக்கப்பட்ட பணி", + "generic": "பொதுவான பணி", + "primary": "குறிப்பிட்ட பணி", + "template": "பணி", + "titleAdd": "சேவையைச் சேர்க்கவும்", + "titleEdit": "திருத்த பணி" + }, "meter": { "cancel": "ரத்துசெய்", "delete": "நீக்கு", @@ -328,22 +481,31 @@ "option": { "aux": "சுய-ஒழுங்குபடுத்தும் நுகர்வோர் சேர்க்கவும்", "battery": "பேட்டரி மீட்டரைச் சேர்க்கவும்", - "ext": "வெளிப்புற மீட்டரைச் சேர்க்கவும்", + "ext": "வழக்கமான நுகர்வோரைச் சேர்க்கவும்", "pv": "சூரிய மீட்டரைச் சேர்க்கவும்" }, "save": "சேமி", "specific": "குறிப்பிட்ட ஒருங்கிணைப்புகள்", "template": "உற்பத்தியாளர்", "titleChoice": "நீங்கள் என்ன சேர்க்க விரும்புகிறீர்கள்?", + "titleLabel": "தலைப்பு", + "usage": { + "aux": "தன்வய ஒழுங்குபடுத்தும் நுகர்வோர்", + "battery": "மின்கலம்", + "charge": "நுகர்வோர் / சார்சர்", + "grid": "வலைவாய்", + "label": "பயன்பாடு", + "pv": "விளைவாக்கம்" + }, "validateSave": "சரிபார்க்கவும் சேமிக்கவும்" }, "modbus": { "baudrate": "பாட் வீதம்", - "comset": "COMSET", + "comset": "காம்செட்", "connection": "மோட்பச் இணைப்பு", - "connectionHintSerial": "சாதனம் நேரடியாக RS485 இடைமுகம் வழியாக EVCC உடன் இணைக்கப்பட்டுள்ளது.", - "connectionHintTcpip": "சாதனம் EVCC இலிருந்து LAN/WIFI வழியாக உரையாற்றக்கூடியது.", - "connectionValueSerial": "சீரியல் / யூ.எச்.பி", + "connectionHintSerial": "சாதனம் நேரடியாக RS485 (அல்லது USB-to-RS485 அடாப்டர்) வழியாக இணைக்கப்பட்டுள்ளது.", + "connectionHintTcpip": "பிணையம் (LAN/WiFi) வழியாக சாதனத்தை அணுகலாம்.", + "connectionValueSerial": "RS485", "connectionValueTcpip": "பிணையம்", "device": "சாதன பெயர்", "deviceHint": "எடுத்துக்காட்டு: /dev /ttyusb0", @@ -358,7 +520,24 @@ "protocolValueTcp": "டி.சி.பி." }, "modbusproxy": { - "description": "பல வாடிக்கையாளர்களை ஒற்றை மோட்பச் சாதனத்தை அணுக அனுமதிக்கவும்.", + "add": "பதிலாள் இணைப்பைச் சேர்க்கவும்", + "connection": "இணைப்பு #{number}", + "description": "சில மோட்பச் சாதனங்கள் ஒற்றை அல்லது மிகக் குறைவான இணைப்புகளை மட்டுமே ஆதரிக்கின்றன. evcc ஆனது ப்ராக்சியாக செயல்படும், பல கிளையண்டுகளுக்கு (ஓம் ஆட்டோமேசன், ச்கிரிப்டுகள் போன்றவை) ஒரே நேரத்தில் அணுகலை செயல்படுத்துகிறது.", + "device": "சாதனம்", + "option": { + "deny": "பிழை", + "false": "இல்லை", + "true": "அமைதி" + }, + "readonly": { + "help": { + "deny": "மோட்பச் பிழையால் எழுதும் அணுகல் தடுக்கப்பட்டது.", + "false": "எழுத்து அணுகல் அனுப்பப்பட்டது.", + "true": "பதில் இல்லாமல் எழுதும் அணுகல் தடுக்கப்பட்டது." + }, + "label": "படிக்கமட்டும்" + }, + "sourcePortHelp": "உள்வரும் கிளையன்ட் இணைப்புகளுக்கான துறைமுகம். கிடைக்க வேண்டும்.", "title": "மோட்பச் பதிலாள்" }, "mqtt": { @@ -380,14 +559,35 @@ "title": "MQTT" }, "network": { - "descriptionHost": "எம்.டி.என் -களை இயக்க .லோகல் பின்னொட்டைப் பயன்படுத்துங்கள். மொபைல் பயன்பாடு மற்றும் சில OCPP சார்சர்களைக் கண்டுபிடிப்பதற்கு பொருத்தமானது.", + "descriptionExternalUrl": "evcc உடன் இணைக்க விரும்பும் பிற சாதனங்களுக்கான முகவரி மற்றும் evcc பயன்பாட்டின் தானாகக் கண்டறிதல்.", + "descriptionHost": "உங்கள் உள்ளக நெட்வொர்க்கில் evcc ஐ அறிவிக்கப் பயன்படுகிறது.", + "descriptionInternalUrl": "evcc இன் உள்ளக பிணையம் முகவரி.", "descriptionPort": "வலை இடைமுகம் மற்றும் பநிஇ க்கான துறைமுகம். இதை மாற்றினால் உங்கள் உலாவி முகவரி ஐ புதுப்பிக்க வேண்டும்.", "descriptionSchema": "முகவரி கள் எவ்வாறு உருவாக்கப்படுகின்றன என்பதை மட்டுமே பாதிக்கிறது. HTTP களைத் தேர்ந்தெடுப்பது குறியாக்கத்தை இயக்காது.", - "labelHost": "ஓச்ட்பெயர்", + "labelExternalUrl": "வெளிப்புற முகவரி", + "labelHost": "mDNS ஓச்ட்பெயர்", + "labelInternalUrl": "உள் முகவரி", "labelPort": "துறைமுகம்", "labelSchema": "ச்கீமா", "title": "பிணையம்" }, + "ocpp": { + "connectedChargers": "இணைக்கப்பட்ட சார்சர்கள்", + "connectionStatus": "கட்டமைக்கப்பட்ட நிலைய ஐடிகள்", + "connectionStatusHelp": "கட்டமைக்கப்பட்ட சார்சர்களின் இணைப்பு நிலை.", + "detectedChargers": "கண்டறியப்பட்ட நிலைய ஐடிகள்", + "detectedHelp": "இந்த சார்சர்கள் evcc உடன் இணைக்க முயற்சித்துள்ளன. சார்சரைப் பயன்படுத்த, அதன் நிலைய ஐடியுடன் ஒரு சுமை புள்ளியை உருவாக்கவும்.", + "noChargers": "OCPP சார்சர்கள் எதுவும் கண்டறியப்படவில்லை.", + "noStations": "நிலையங்கள் இணைக்கப்படவில்லை", + "status": { + "configured": "இணைக்கப்படவில்லை", + "connected": "இணைக்கப்பட்டது", + "unknown": "தெரியவில்லை" + }, + "title": "OCPP சர்வர்", + "url": "சேவையக முகவரி", + "urlHelp": "இந்த முகவரி ஐ உங்கள் சார்சரின் உள்ளமைவில் நகலெடுக்கவும். விவரங்களுக்கு உற்பத்தியாளரின் கையேட்டைப் பார்க்கவும். சார்சர் தானாகவே அதன் தனித்துவமான அடையாளங்காட்டியை (நிலைய ஐடி) முகவரி இல் சேர்க்கும் என்று எதிர்பார்க்கப்படுகிறது. அரிதான சந்தர்ப்பங்களில், அடையாளங்காட்டியை நீங்கள் கைமுறையாகக் குறிப்பிட வேண்டியிருக்கும். எடுத்துக்காட்டு: `{url}`" + }, "options": { "boolean": { "no": "இல்லை", @@ -422,32 +622,52 @@ "integrations": "ஒருங்கிணைப்புகள்", "loadpoints": "கட்டணம் மற்றும் வெப்பமாக்கல்", "meter": "சூரிய & பேட்டரி", + "services": "சேவைகள்", "system": "கணினி", "vehicles": "வாகனங்கள்" }, + "shm": { + "cardTitle": "சன்னி ஓம் மேனேசர்", + "description": "evcc ஆனது SEMP நெறிமுறை வழியாக SMA சன்னி ஓம் மேனேசருக்கான (SHM) ஒருங்கிணைப்புடன் பொருத்தப்பட்டுள்ளது. இது ஒரே நெட்வொர்க்கில் இயங்கினால், உங்கள் சன்னி போர்ட்டல் கணக்கில் உள்நுழைந்த பிறகு, evcc இல் உள்ளமைக்கப்பட்ட அனைத்து சார்சர்களையும் புதிதாகக் கண்டுபிடிக்கப்பட்ட நுகர்வோராகச் சேர்க்க நீங்கள் தானாகவே வழங்கப்பட வேண்டும். கீழே உள்ள எந்த மாற்றங்களும் இல்லாமல், அனைத்தும் உடனடியாக பயன்படுத்த தயாராக இருக்க வேண்டும்.", + "descriptionDeviceId": "12 எழுத்துகள் HEX சரம். எல்லா சாதனங்களுக்கும் முன்னொட்டு (சார்சிங் பாயிண்ட், ..).", + "descriptionIdPattern": "அடையாளங்காட்டி முறை", + "descriptionIds": "சன்னி போர்ட்டலில் ஒவ்வொரு நுகர்வோர் சாதனத்திற்கும் ஒரு தனிப்பட்ட அடையாளங்காட்டி தேவை. evcc உங்கள் வன்பொருளின் அடிப்படையில் ஒரு தனித்துவமான அடையாளங்காட்டியை உருவாக்குகிறது. நீங்கள் evcc ஐ வேறொரு வன்பொருளுக்கு மாற்றினால், இந்த அடையாளங்காட்டிகள் மாறக்கூடும். நீங்கள் வரலாற்றைப் பராமரிக்க விரும்பினால், உருவாக்கப்பட்ட அடையாளங்காட்டிகளை இங்கே மேலெழுதலாம். உங்கள் தற்போதைய அடையாளங்காட்டிகளைச் சரிபார்க்க, SEMP முகவரி (/semp) ஐத் திறக்கவும்.", + "descriptionSempUrl": "SEMP URL", + "descriptionVendorId": "8 எழுத்துகள் ஃச் சரம். அனைத்து நிறுவனங்களின் பொது முன்னொட்டு. இயல்பாக evcc அதன் சொந்த உள் விற்பனையாளர் ஐடியைப் பயன்படுத்தும்.", + "labelDeviceId": "சாதன அடையாளம்", + "labelVendorId": "விற்பனையாளர் அடையாளம்", + "title": "SMA சன்னி ஓம் மேனேசர்" + }, "sponsor": { "addToken": "ஒப்புரவாளர் கிள்ளாக்கை உள்ளிடவும்", "changeToken": "ஒப்புரவாளர் கிள்ளாக்கை மாற்றவும்", "description": "ஒப்புரவாளர் மாதிரி திட்டத்தை பராமரிக்கவும் புதிய மற்றும் அற்புதமான அம்சங்களை நிலையானதாக உருவாக்கவும் உதவுகிறது. ஒரு ச்பான்சராக நீங்கள் அனைத்து சார்சர் செயலாக்கங்களுக்கும் அணுகலைப் பெறுவீர்கள்.", "descriptionToken": "ஸ்பான்சர்கள் {url} இலிருந்து டோக்கனைப் பெறுவீர்கள். தொடங்குவதற்கு {trialToken} வழங்குகிறோம்.", + "enterYourToken": "உங்கள் கிள்ளாக்கை உள்ளிடவும்", "error": "ஒப்புரவாளர் கிள்ளாக்கு செல்லுபடியாகாது.", + "invalid": "செல்லுபடியாகாத", "labelToken": "ஒப்புரவாளர் கிள்ளாக்கு", "title": "ச்பான்சர்சிப்", "tokenRequired": "இந்த சாதனத்தை உருவாக்குவதற்கு முன்பு நீங்கள் ஒரு ஒப்புரவாளர் கிள்ளாக்கை உள்ளமைக்க வேண்டும்.", + "tokenRequiredFeature": "இந்த அம்சத்திற்கு ஒப்புரவாளர் கிள்ளாக்கு தேவை.", "tokenRequiredLearnMore": "மேலும் அறிக.", - "trialToken": "சோதனை கிள்ளாக்கு" + "tokenRequiredShort": "ஒப்புரவாளர் கிள்ளாக்கு கட்டமைக்கப்படவில்லை.", + "trialToken": "சோதனை கிள்ளாக்கு", + "viaYaml": "evcc.yaml வழியாக", + "yourToken": "உங்கள் கிள்ளாக்கு" }, "system": { "backupRestore": { "backup": { "action": "காப்புப்பிரதியைப் பதிவிறக்குக ...", "confirmationButton": "காப்புப்பிரதியைப் பதிவிறக்கவும்", + "confirmationText": "தரவுத்தள கோப்பை பதிவிறக்கவும்.", "description": "உங்கள் தரவை ஒரு கோப்பில் காப்புப் பிரதி எடுக்கவும். கணினி தோல்வி ஏற்பட்டால் உங்கள் தரவை மீட்டெடுக்க இந்த கோப்பு பயன்படுத்தப்படலாம்.", "title": "காப்புப்பிரதி" }, "cancel": "ரத்துசெய்", "description": "உங்கள் தரவை காப்புப் பிரதி, மீட்டமை மற்றும் மீட்டமைக்கவும். உங்கள் தரவை வேறொரு கணினிக்கு நகர்த்த விரும்பினால் பயனுள்ளதாக இருக்கும்.", - "note": "குறிப்பு: மேலே உள்ள அனைத்து செயல்களும் உங்கள் தரவுத்தள தரவை மட்டுமே பாதிக்கின்றன. EVCC.YAML உள்ளமைவு கோப்பு மாறாமல் உள்ளது.", + "note": "குறிப்பு: மேலே உள்ள அனைத்து செயல்களும் உங்கள் தரவுத்தள தரவை மட்டுமே பாதிக்கின்றன. இவிசிசி.ஒய்எஎம்எல் உள்ளமைவு கோப்பு மாறாமல் உள்ளது.", "reset": { "action": "மீட்டமை ...", "confirmationButton": "மீட்டமை மற்றும் மறுதொடக்கம்", @@ -474,12 +694,16 @@ "restartRequiredDescription": "விளைவைக் காண தயவுசெய்து மறுதொடக்கம் செய்யுங்கள்.", "restartRequiredMessage": "உள்ளமைவு மாற்றப்பட்டது.", "restartingDescription": "தயவுசெய்து காத்திருங்கள்…", - "restartingMessage": "EVCC ஐ மறுதொடக்கம் செய்தல்." + "restartingMessage": "இவிசிசி ஐ மறுதொடக்கம் செய்தல்." }, "tariffs": { "description": "உங்கள் சார்சிங் அமர்வுகளின் செலவுகளைக் கணக்கிட உங்கள் ஆற்றல் கட்டணங்களை வரையறுக்கவும்.", "title": "கட்டணங்கள்" }, + "telemetry": { + "description": "evcc ஐ மேம்படுத்த உதவும் வகையில் தரவுப் பகிர்வை உள்ளமைக்கவும். உங்கள் தனியுரிமை எங்களுக்கு முக்கியமானது மற்றும் பங்கேற்பது முற்றிலும் விருப்பமானது.", + "title": "டெலிமெட்ரி" + }, "title": { "description": "முதன்மையான திரை மற்றும் உலாவி தாவலில் காட்டப்படும்.", "label": "தலைப்பு", @@ -506,6 +730,8 @@ "maximumCurrentHelp": "குறைந்தபட்ச மின்னோட்டத்தை விட அதிகமாக இருக்க வேண்டும்.", "maximumPhases": "அதிகபட்ச கட்டங்கள்", "maximumPhasesHelp": "இந்த வண்டி எத்தனை கட்டங்களை வசூலிக்க முடியும்? தேவையான குறைந்தபட்ச சூரிய உபரி மற்றும் திட்ட காலத்தைக் கணக்கிடப் பயன்படுகிறது.", + "maximumPower": "அதிகபட்ச சார்சிங் பவர்", + "maximumPowerHelp": "வண்டி பயன்படுத்தக்கூடிய அதிகபட்ச ஆற்றல்", "minimumCurrent": "குறைந்தபட்ச மின்னோட்டம்", "minimumCurrentHelp": "நீங்கள் என்ன செய்கிறீர்கள் என்று உங்களுக்குத் தெரிந்தால் மட்டுமே 6A க்கு கீழே செல்லுங்கள்.", "online": "நிகழ்நிலை பநிஇ கொண்ட வாகனங்கள்", @@ -563,7 +789,7 @@ "confettiPromise": "நீங்கள் ச்டிக்கர்கள் மற்றும் டிசிட்டல் கான்ஃபெட்டி பெறுகிறீர்கள்", "sticker": "… அல்லது ஈ.வி.சி.சி ச்டிக்கர்கள்?", "supportUs": "எங்கள் நோக்கம் சோலார் விதிமுறைகளை வசூலிப்பதே ஆகும். உங்களுக்கு தகுதியானதை செலுத்துவதன் மூலம் ஈ.வி.சி.சி.க்கு உதவுங்கள்.", - "thanks": "நன்றி, {sponsor}! உங்கள் பங்களிப்பு EVCC ஐ மேலும் உருவாக்க உதவுகிறது.", + "thanks": "நன்றி, {sponsor}! உங்கள் பங்களிப்பு இவிசிசி ஐ மேலும் உருவாக்க உதவுகிறது.", "titleNoSponsor": "எங்களுக்கு ஆதரவளிக்கவும்", "titleSponsor": "நீங்கள் ஒரு ஆதரவாளர்", "titleTrial": "சோதனை முறை", @@ -593,12 +819,14 @@ "forecast": { "co2": { "average": "சராசரி", + "constant": "CO₂ தீவிரம்", "lowestHour": "தூய்மையான மணி", "range": "வீச்சு" }, "modalTitle": "முன்னறிவிப்பு", "price": { "average": "சராசரி", + "constant": "விலை", "lowestHour": "மலிவான மணி", "range": "வீச்சு" }, @@ -616,6 +844,9 @@ "solar": "சூரிய" } }, + "general": { + "note": "குறிப்பு:" + }, "header": { "about": "பற்றி", "blog": "வலைப்பதிவு", @@ -630,7 +861,7 @@ "help": { "discussionsButton": "அறிவிலிமையம் விவாதங்கள்", "documentationButton": "ஆவணங்கள்", - "issueButton": "ஒரு பிழையைப் புகாரளிக்கவும்", + "issueButton": "சிக்கலைப் புகாரளிக்கவும்", "issueDescription": "ஒரு விசித்திரமான அல்லது தவறான நடத்தை கிடைத்ததா?", "logsButton": "பதிவுகளைக் காண்க", "logsDescription": "பிழைகளுக்கான பதிவுகளை சரிபார்க்கவும்.", @@ -647,6 +878,59 @@ "restartDescription": "அதை மீண்டும் அணைக்க முயற்சித்தீர்களா?", "secondaryActions": "Still not able பெறுநர் solve your problem? Here அரே some more heavy-handed options." }, + "issue": { + "additional": { + "description": "சிக்கலை விரைவாக மீண்டும் உருவாக்க எங்களுக்கு உதவ, உள்ளமைவு மற்றும் பதிவுகளைச் சேர்க்கவும். முடிந்தவரை பகிர்வதை ஊக்குவிக்கிறோம். மாநிலம் பொதுவாக தேவையில்லை.", + "include": "அடங்கும்", + "lines": "வரிகள்", + "logs": "பதிவுகள்", + "logsDescription": "சிக்கலைக் கண்டறிய உதவும் அண்மைக் கால பதிவு உள்ளீடுகள்.", + "showDetails": "விவரங்களைக் காட்டு", + "source": "மூலம்", + "state": "மாநிலம்", + "stateDescription": "சார்சிங் பாயிண்ட், சாதனம் மற்றும் ஆற்றல் செய்தி உட்பட முழுமையான இயக்க நேர நிலை. கோரப்பட்டால் மட்டும் சேர்க்கவும்.", + "title": "கூடுதல் தகவல்", + "uiConfig": "கட்டமைப்பு (UI)", + "uiConfigDescription": "இணைய இடைமுகம்மூலம் செய்யப்பட்ட கட்டமைப்பு அமைப்புகள்.", + "yamlConfig": "கட்டமைப்பு (YAML)", + "yamlConfigDescription": "உங்கள் முழு கட்டமைப்பு கோப்பு." + }, + "additionalContext": "கூடுதல் சூழல்", + "additionalContextPlaceholder": "உதவியாக இருக்கும் ஏதேனும் கூடுதல் தகவல்... \n- கட்டமைப்பு விவரங்கள் \n- நீங்கள் என்ன முயற்சி செய்தீர்கள் \n- சுற்றுச்சூழல் விவரங்கள்", + "createButtonDiscussion": "GitHub விவாதத்தைத் தொடங்கு...", + "createButtonIssue": "GitHub சிக்கலை உருவாக்கவும்...", + "description": "உங்கள் நிறுவல் எதிர்பார்த்தபடி செயல்படவில்லையா? உதவியைப் பெற அல்லது சிக்கல்களைப் புகாரளிக்க இந்தப் பக்கத்தைப் பயன்படுத்தவும். உங்கள் விளக்கத்தை சுருக்கமாகவும், தெளிவாகவும், பின்பற்ற எளிதாகவும் இருக்கும் போது, சிக்கலைப் புரிந்துகொள்ளவும், மீண்டும் உருவாக்கவும் எங்களுக்கு உதவ போதுமான விவரங்களை வழங்கவும்.", + "helpType": { + "discussion": "எனது அமைப்பிற்கு உதவி தேவை", + "discussionDescription": "சமூக விவாதங்கள் பதில்களை அளிக்கின்றன.", + "issue": "ஒரு பிழை கிடைத்தது", + "issueDescription": "ஏதோ உடைந்துவிட்டது, அதை சரிசெய்ய வேண்டும் என்று நான் உறுதியாக நம்புகிறேன்.", + "title": "நாம் என்ன சிக்கல் பற்றி பேசுகிறோம்?" + }, + "issueDescription": "விவரம்", + "issueTitle": "தலைப்பு", + "stepsToReproduce": "இனப்பெருக்கம் செய்வதற்கான படிகள்", + "subTitleDiscussion": "உங்கள் பிரச்சனையை விவரிக்கவும்", + "subTitleIssue": "சிக்கலை விவரிக்கவும்", + "summary": { + "confirmationButtonDiscussion": "GitHub விவாதத்தைத் தொடங்கவும்", + "confirmationButtonIssue": "GitHub சிக்கலை உருவாக்கவும்", + "copied": "நகலெடுக்கப்பட்டது!", + "copyButton": "கூடுதல் தகவலை நகலெடுக்கவும்", + "instructions": "GitHub இன் முகவரி அளவு வரம்புகள் காரணமாக, இது இரண்டு-படி செயல்முறை:", + "singleStepDescription": "உங்கள் சிக்கல் விவரங்களைக் கொண்ட முன் நிரப்பப்பட்ட படிவத்துடன் GitHub ஐத் திறக்க கீழே உள்ள பொத்தானைக் சொடுக்கு செய்யவும். முக்கியமான தரவு தானாகவே திருத்தப்பட்டது, ஆனால் பகிர்வதற்கு முன் இருமுறை சரிபார்க்கவும்.", + "step1Description": "உங்கள் தலைப்பு, விளக்கம் மற்றும் விவரங்களுடன் அடிப்படை GitHub உள்ளீட்டை உருவாக்க கீழே உள்ள பொத்தானைக் சொடுக்கு செய்யவும்.", + "step2Description": "உள்ளீட்டை உருவாக்கிய பிறகு, கீழே உள்ள கூடுதல் தகவலை நகலெடுத்து உங்கள் GitHub படிவத்தில் ஒட்டுவதற்கு இங்கே திரும்பவும். முக்கியமான தரவு திருத்தப்பட்டது, ஆனால் பகிர்வதற்கு முன் இருமுறை சரிபார்க்கவும்.", + "stepOneDiscussion": "படி 1: அடிப்படை விவாதத்தை உருவாக்கவும்", + "stepOneIssue": "படி 1: அடிப்படை சிக்கலை உருவாக்கவும்", + "stepTwo": "படி 2: கூடுதல் தகவலை நகலெடுக்கவும்", + "title": "GitHub சிக்கல் சுருக்கம்" + }, + "system": "மண்டலம்", + "timezone": "நேர மண்டலம்", + "title": "சிக்கலைப் புகாரளிக்கவும்", + "version": "பதிப்பு" + }, "log": { "areaLabel": "பரப்பளவில் வடிகட்டவும்", "areas": "அனைத்து பகுதிகளும்", @@ -682,6 +966,11 @@ "goal": "மின்சேர்வி இலக்கு", "modalTitle": "மின்சேர்வி திட்டம்", "none": "எதுவுமில்லை", + "optimization": { + "cheapest": "மலிவான", + "continuous": "தொடர்ச்சியான", + "label": "உகப்பாக்கம்" + }, "planNumber": "திட்டம் {number}", "precondition": { "description": "பேட்டரி முன்நிபந்தனைக்கு புறப்படுவதற்கு முன் {duration} சார்ஜ் செய்யவும்.", @@ -693,6 +982,8 @@ "repeating": "மறுநிகழ்தல்", "repeatingPlans": "மறுநிகழ்வு திட்டங்கள்", "selectAll": "அனைத்தையும் தேர்ந்தெடு", + "strategyDisabledDescription": "புறப்படும் நேரத்தில் முடிவதற்காக சார்சிங் முடிந்தவரை தாமதமாகத் தொடங்குகிறது. மாறும் கிரிட் விலைகள் அல்லது CO₂ கட்டணத்துடன், கூடுதல் விருப்பங்கள் இங்கே கிடைக்கின்றன.", + "strategySettings": "மூலோபாய அமைப்புகள்", "time": "நேரம்", "title": "திட்டம்", "titleMinSoc": "\"குறை. மின்னூட்டு\"", @@ -705,14 +996,18 @@ "battery": "மின்கலம்", "batteryCharge": "மின்கலம் மின்சேர்வி", "batteryDischarge": "மின்கலம் மின்நீக்கி", - "batteryGridChargeActive": "கட்டம் சார்சிங் செயலில்", - "batteryGridChargeLimit": "எப்போது கட்டம் சார்சிங்", + "batteryForecastEmpty": "காலியாக {time}", + "batteryForecastFull": "முழு {time}", + "batteryGridChargeActive": "கிரிட் சார்சிங்: செயலில் உள்ளது", + "batteryGridChargeLimit": "கிரிட் சார்சிங்: எப்போது", "batteryHold": "மின்கலம் (பூட்டபட்டது)", "batteryTooltip": "{energy} ({total}) இன் {soc}", + "forecast": "முன்னறிவிப்பு: ", "forecastTooltip": "முன்னறிவிப்பு: இன்று சூரிய விளைவாக்கம்", "gridImport": "கட்டம் பயன்பாடு", "homePower": "நுகர்வு", "loadpoints": "Charger| மின்னூட்டி | {count} chargers", + "loadpointsLimit": "{limit} வரம்பு", "noEnergy": "No அளவி data", "pv": "சூரிய குடும்பம்", "pvExport": "கட்ட ஏற்றுமதி", @@ -725,11 +1020,16 @@ "vehicleLimit": "ஈட்டர் வரம்பு", "waitForVehicle": "Ready. Waiting க்கு heater…" }, + "hemsWarning": { + "description": "{limit} ஐ விட அதிகமாகக் குறைக்கப்பட்ட சார்சிங்.", + "title": "வெளிப்புற வரம்பு:" + }, "loadpoint": { "avgPrice": "⌀ விலை", "charged": "மின்சேர்க்கப்பட்டது", "co2": "Co co₂", "duration": "காலம்", + "emission": "உமிழ்வு", "fallbackName": "மின்சேர்வி புள்ளி", "finished": "நேரம் முடிந்தது", "power": "ஆற்றல்", @@ -742,9 +1042,14 @@ "loadpointSettings": { "batteryBoost": { "description": "வீட்டு மின்கலத்திலிருந்து {limit} வரை இறங்கும் வரை விரைவு மின்னூட்டம்.", + "descriptionDisabled": "வீட்டு பேட்டரியிலிருந்து வேகமாக சார்ச் செய்ய வரம்பை தேர்ந்தெடுக்கவும்.", + "disabled": "முடக்கப்பட்டது", "label": "மின்கலம் ஊட்டம்", "mode": "ஞாயிறு மற்றும் குறை+ஞாயிறு பயன்முறையில் மட்டுமே கிடைக்கும்.", - "once": "Boost செயலில் க்கு this charging session." + "once": "Boost செயலில் க்கு this charging session.", + "stateActive": "பேட்டரி பூச்ட் செயலில் உள்ளது", + "stateBelowLimit": "ஊக்குவிப்பதற்கு பேட்டரி மிகவும் குறைவாக உள்ளது", + "stateReady": "பேட்டரி பூச்ட் ஆயத்தம்" }, "batteryUsage": "முகப்பு பேட்டரி", "currents": "மின்சேர்வி மின்னோட்டம்", @@ -840,7 +1145,7 @@ "fallbackName": "வண்டி", "moreActions": "மேலும் செயல்கள்", "none": "வண்டி இல்லை", - "notReachable": "வண்டி அடைய முடியாது. EVCC ஐ மறுதொடக்கம் செய்ய முயற்சிக்கவும்.", + "notReachable": "வண்டி அடைய முடியாது. இவிசிசி ஐ மறுதொடக்கம் செய்ய முயற்சிக்கவும்.", "targetSoc": "வரம்பு", "temp": "தற்காலிக.", "tempLimit": "தற்காலிக வரம்பு", @@ -850,6 +1155,9 @@ "vehicleStatus": { "awaitingAuthorization": "அங்கீகாரத்திற்காக காத்திருக்கிறது.", "batteryBoost": "பேட்டரி பூச்ட் செயலில்.", + "batteryBoostBelowLimit": "ஊக்குவிப்பதற்கு பேட்டரி மிகவும் குறைவாக உள்ளது.", + "batteryBoostDisabled": "பேட்டரி பூச்ட் முடக்கப்பட்டுள்ளது.", + "batteryBoostEnabled": "{limit} இல் பேட்டரி வரை அதிகரிக்கும்.", "charging": "சார்சிங்…", "cheapEnergyCharging": "மலிவான ஆற்றல் கிடைக்கிறது.", "cheapEnergyNextStart": "{duration} இல் மலிவான ஆற்றல்.", @@ -890,7 +1198,8 @@ "message": "சேவையகத்துடன் இணைக்கப்படவில்லை.", "restart": "மறுதொடக்கம்", "restartNeeded": "மாற்றங்களைப் பயன்படுத்த வேண்டும்.", - "restarting": "சேவையகம் ஒரு கணத்தில் திரும்பும்." + "restarting": "சேவையகம் ஒரு கணத்தில் திரும்பும்.", + "starting": "சேவையகத்தைத் தொடங்குகிறது..." }, "passwordModal": { "description": "உள்ளமைவு அமைப்புகளைப் பாதுகாக்க கடவுச்சொல்லை அமைக்கவும். உள்நுழைவு இல்லாமல் முதன்மையான திரையைப் பயன்படுத்துவது இன்னும் சாத்தியமாகும்.", @@ -1000,6 +1309,7 @@ "vehicle": "வண்டி" }, "settings": { + "deviceInfo": "இந்த உரையாடலில் நீங்கள் செய்யும் அமைப்புகள் இந்தச் சாதனத்தை மட்டுமே பாதிக்கும்.", "fullscreen": { "enter": "முழுத் திரையில் உள்ளிடவும்", "exit": "முழுத்திரை வெளியேறு", @@ -1007,12 +1317,18 @@ }, "hiddenFeatures": { "label": "சோதனை", - "value": "சோதனை இடைமுகம் அம்சங்களைக் காட்டு." + "value": "சோதனை அம்சங்களை இயக்கு." }, "language": { "auto": "தானியங்கி", "label": "மொழி" }, + "loadpoints": { + "help": "UIக்கான வரிசை மற்றும் தெரிவுநிலையை மாற்றவும்.", + "hide": "மறை {title}", + "label": "சார்சிங் புள்ளிகள்", + "show": "{title} காட்டு" + }, "sponsorToken": { "expires": "நீங்கள் ஒப்புரவாளர் கிள்ளாக்கு காலாவதியாகிறது {inXDays}.", "expiresUpdateUi": "{getNewToken} மற்றும் அதை இங்கே புதுப்பிக்கவும்.", @@ -1050,6 +1366,7 @@ "cleanTitle": "தூய்மையான கட்டம் சார்சிங்", "co2Label": "கோ உமிழ்வு", "co2Limit": "கோ வரம்பு", + "enable": "வரம்பை இயக்கு", "loadpointDescription": "சோலார் பயன்முறையில் தற்காலிகமாக வேகமாக சார்ச் செய்ய உதவுகிறது.", "modalTitle": "அறிவுள்ள கிரிட் சார்சிங்", "none": "எதுவுமில்லை", @@ -1060,7 +1377,7 @@ "saved": "சேமிக்கப்பட்டது." }, "smartFeedInPriority": { - "activeHoursLabel": "இடைநிறுத்தப்பட்ட மணிநேரம்", + "activeHoursLabel": "இடைநிறுத்தப்பட்ட நேரம்", "description": "இலாபகரமான கட்டம் ஊட்டத்திற்கு முன்னுரிமை அளிக்க அதிக விலையில் கட்டணம் வசூலிப்பதை இடைநிறுத்துகிறது.", "priceLabel": "தீவன வீதம்", "priceLimit": "தீவன வரம்பு", @@ -1072,6 +1389,7 @@ "configuration": "கட்டமைப்பு", "description": "தயவுசெய்து உங்கள் உள்ளமைவு கோப்பை சரிபார்க்கவும். பிழை செய்தி உதவவில்லை என்றால், {0} ஐப் பாருங்கள்.", "discussions": "அறிவிலிமையம் விவாதங்கள்", + "editConfiguration": "உள்ளமைவைத் திருத்து", "fixAndRestart": "தயவுசெய்து சிக்கலை சரிசெய்து சேவையகத்தை மறுதொடக்கம் செய்யுங்கள்.", "hint": "குறிப்பு: உங்களிடம் தவறான சாதனம் இருக்கலாம் (இன்வெர்ட்டர், மீட்டர்,…). உங்கள் பிணைய இணைப்புகளை சரிபார்க்கவும்.", "lineError": "{0} இல் பிழை.", diff --git a/i18n/tr.json b/i18n/tr.json index 2df378899..49ba19681 100644 --- a/i18n/tr.json +++ b/i18n/tr.json @@ -105,6 +105,12 @@ "labelResidualPower": "Artık güç", "title": "Kontrol davranışı" }, + "currency": { + "description": "Tarifene göre enerji fiyatlarını, maliyetleri ve tasarrufları biçimlendirmek için kullanılır.", + "example": "Doldurma ücretin {price} idi. {amount} tasarruf ettin.", + "label": "Para birimi", + "title": "Para birimi" + }, "deviceValue": { "amount": "Miktar", "broker": "Aracı", @@ -128,12 +134,16 @@ "dimmed": "Karartılmış", "enabled": "Etkin", "energy": "Enerji", + "events": "Etkinlikler", "feedinPrice": "Satış fiyatı", + "forecast": "Tahmin", "gridPrice": "Alım fiyatı", "heaterTempLimit": "Isıtıcı sınırlaması", "hemsActiveLimit": "Etkin sınır", "hemsType": "İletişim", "identifier": "RFID tanımlayıcısı", + "max": "azami", + "messengers": "Hizmetler", "no": "hayır", "odometer": "Kilometre Sayacı", "org": "Organizasyon", @@ -143,6 +153,7 @@ "phases1p3p": "Faz değiştirme", "power": "Güç", "powerRange": "“Güç”", + "price": "Fiyat", "range": "Menzil", "singlePhase": "“Tek aşamalı”", "soc": "Doluluk", @@ -172,11 +183,27 @@ "loading": "YAML düzenleyici yükleniyor…" }, "eebus": { - "description": "evcc'nin diğer EEBus cihazlarıyla iletişim kurmasını sağlayan yapılandırma.", + "certificate": { + "private": "Özel anahtar", + "public": "Açık sertifika", + "title": "Sertifikalar" + }, + "description": "EVCC'nin doldurma cihazları veya şebeke operatörünüzün kontrol ünitesi gibi EEBus uyumlu cihazlarla iletişim kurmasını sağlayan yapılandırma. İlgili tüm başlatma ve sertifika oluşturma işlemleri ilk başlatmada otomatik olarak gerçekleştirilir.", + "descriptionAdvanced": "Değişiklik yapmaya gerek yok. Yalnızca ne yaptığını gerçekten biliyorsan değişiklik yap. SHIP kimliğini veya sertifikaları değiştirirsen, cihazlarını yeniden eşleştirmen gerekir.", + "interfaces": "Arayüzler", + "interfacesHelp": "İletişim sorunlarını önlemek için EEBus'un kullanacağı ağ arayüzlerini sınırla. Tüm arayüzleri kullanmak için alanı boş bırak. Her satıra bir giriş yap.", + "port": "Port", + "portHelp": "Kullanılacak port.", + "removeConfirm": "Tüm EEBus yapılandırmaları kaldırılacak. Bir sonraki başlatmada yeni sertifikalar ve tanımlayıcılar oluşturulacak. Emin misin?", + "shipid": "SHIP-ID", + "shipidExplain": "EEBus ağında tanımlama için kalıcı cihaz tanımlayıcı.", + "shipidHelp": "Bu SHIP-ID, aşağıdaki sertifikalarla bağlantılı.", + "ski": "SKI", + "skiExplain": "EEBus cihazlarını eşleştirmek için benzersiz güvenlik tanımlayıcı.", "title": "EEBus" }, "experimental": { - "description": "Halen test aşamasında olan ve gelecek sürümlerde değişebilecek özellikler için kullanıcı arayüzünü etkinleştir.", + "description": "Hala test aşamasında olan özelliklere erken erişim sağlar. Bu özellikler istikrarsız olabilir ve gelecek sürümlerde değiştirilebilir veya kaldırılabilir.", "title": "Deneysel" }, "ext": { @@ -198,6 +225,7 @@ "cancel": "İptal", "clear": "Temizle", "close": "Kapat", + "confirmSave": "Kaydedilmemiş değişiklikler var. Şimdi kaydedilsin mi?", "copied": "Kopyalandı!", "copy": "Kopyala", "customHelp": "evcc'nin eklenti sistemini kullanarak kullanıcı tanımlı bir cihaz oluştur.", @@ -207,11 +235,13 @@ "dragHandle": "Sürükleme kolu", "dragItem": "Sürüklenebilir: {title}", "dragList": "Yeniden sıralanabilir liste", + "error": "Hata", "experimental": "Deneysel", "forceSave": "Yine de kaydet", "fromYamlHint": "Not: evcc.yaml aracılığıyla yapılandırıldı. Burada düzenlemeyi etkinleştirmek için dosyadan öğeyi kaldır.", "hideAdvancedSettings": "“Gelişmiş ayarları gizle”", "invalidFileSelected": "Geçersiz dosya seçildi", + "legacy": "eskimiş", "noFileSelected": "Seçili dosya yok.", "off": "kapalı", "on": "açık", @@ -303,6 +333,8 @@ }, "defaultModeHelpKeep": "Son seçilen modu korur.", "defaultModeLabel": "“Varsayılan şekil”", + "defaultsHint": "Varsayılan durum, GES fazlalık davranışı ve elektriksel ayrıntılar mantıklı varsayılan değerleri kullanır.", + "defaultsHintLink": "Ayarları düzenle", "delete": "“Sil”", "electricalSubtitle": "Emin değilsen elektrikçine sor.", "electricalTitle": "“Elektrikli”", @@ -393,9 +425,66 @@ "welcomeBannerTitle": "Haydi sistemini yapılandıralım!" }, "messaging": { - "description": "Dolum oturumları hakkında bildirimler al.", + "addMessenger": "Hizmet ekle", + "description": "Doldurma oturumlarıyla ilgili bildirimler al.", + "event": { + "asleep": { + "messageDefault": "Doldurmaya onay verildi, araç {vehicleName} doldurmuyor.", + "title": "Araç doldurmuyorsa", + "titleDefault": "Araç uykuda" + }, + "connect": { + "messageDefault": "Araç {pvPower}kW GES ile bağlı", + "title": "Araç bağlandığında", + "titleDefault": "Araç bağlı" + }, + "disconnect": { + "messageDefault": "{connectedDuration} sonra araç bağlantısı kesildi", + "title": "Araç bağlantısı kesildiğinde", + "titleDefault": "Araç bağlantısı kesildi" + }, + "guest": { + "messageDefault": "Bilinmeyen araç, misafir mi bağlı?", + "title": "Bilinmeyen bir araç bağlandığında", + "titleDefault": "Bilinmeyen araç" + }, + "planoverrun": { + "messageDefault": "{vehicleTitle}: Plan aşılacak.", + "title": "Planlı doldurma aşılacağı zaman", + "titleDefault": "Plan aşıldı" + }, + "soc": { + "messageDefault": "Batarya %{vehicleSoc} dolduruldu", + "title": "Doldurma seviyesi güncellemesi", + "titleDefault": "Doldurma seviyesi güncellendi" + }, + "start": { + "messageDefault": "{mode} durumunda doldurmaya başladı.", + "title": "Doldurma başladığında", + "titleDefault": "Doldurma başladı" + }, + "stop": { + "messageDefault": "Doldurma tamamlandı: {chargedEnergy}kWh {chargeDuration} içinde.", + "title": "Doldurma durduğunda", + "titleDefault": "Doldurma tamamlandı" + } + }, + "eventMessage": "Bildiri", + "eventTitle": "Başlık", + "events": "Etkinlikler", + "legacyWarning": "Yeni bildirim yapılandırması kullanıma sunuldu! Yeni kılavuzlu süreci kullanmak için yapılandırmanı buradan kaldır ve kayded.", + "messengers": "Hizmetler", + "seePlaceholders": "yer tutuculara bak", "title": "Bildirimler" }, + "messenger": { + "custom": "Kullanıcı tanımlı hizmet", + "generic": "Genel hizmet", + "primary": "Özel hizmet", + "template": "Hizmet", + "titleAdd": "Hizmet Ekle", + "titleEdit": "Hizmeti Düzenle" + }, "meter": { "cancel": "İptal", "delete": "Sil", @@ -491,7 +580,8 @@ "labelInternalUrl": "Dahili URL", "labelPort": "Port", "labelSchema": "Şema", - "title": "Ağ" + "title": "Ağ", + "warningUrlPath": "URL genellikle bir yola ihtiyaç duymaz. Bunun doğru olduğundan emin misin?" }, "ocpp": { "connectedChargers": "Bağlı doldurma cihazları", @@ -534,7 +624,7 @@ } }, "pv": { - "titleAdd": "GES Sayacı Ekle", + "titleAdd": "GES Ekle", "titleEdit": "GES Sayacını Düzenle" }, "section": { @@ -544,6 +634,7 @@ "integrations": "Bütünleştirmeler", "loadpoints": "Doldurma ve Isıtma", "meter": "GES ve Batarya", + "services": "Hizmetler", "system": "Sistem", "vehicles": "Araçlar" }, @@ -617,12 +708,85 @@ "restartingDescription": "Lütfen bekle…", "restartingMessage": "evcc yeniden başlatılıyor." }, + "tariff": { + "addForecast": "Tahmin ekle", + "addTariff": "Tarife ekle", + "co2": { + "description": "Şebeke elektriği için CO₂ yoğunluğu tahmini. CO₂ iyileştirmeli doldurma ve salım tasarruflarının hesaplanması için.", + "titleAdd": "CO₂ Tahmini Ekle", + "titleEdit": "CO₂ Tahminini Düzenle" + }, + "co2Services": "CO₂ Hizmetleri", + "customForecast": "Kullanıcı tanımlı tahmin", + "customTariff": "Kullanıcı tanımlı tarife", + "description": "Enerji tarifelerini ve tahminlerini yapılandır. Dinamik yönetim için cihaz tabanlı yapılandırmayı veya statik ayarlar için YAML kullan.", + "feedIn": { + "description": "Şebekeye ihraç edilen elektrik için alınan ücret. Gerçek doldurma maliyetlerini hesaplamak için kullanılır.", + "titleAdd": "Şebekeye İhracat Tarifesi Ekle", + "titleEdit": "Şebekeye İhracat Tarifesini düzenle" + }, + "generic": "Genel bütünleştirmeler", + "grid": { + "description": "Şebeke tüketimi için elektrik fiyatı. Gerçek doldurma maliyetlerini hesaplamak ve araçların doldurulmasını, ısıtma cihazlarının yönetimini veya ev bataryalarının şebekeden doldurmasını fiyat iyileştirmeli sağlamak için.", + "titleAdd": "Şebekeden İthalat Tarifesi Ekle", + "titleEdit": "Şebekeden İthalat Tarifesini Düzenle" + }, + "legacyWarning": "Yeni tarife yapılandırması mevcut! Yeni kılavuzlu süreci kullanmak için tarifelerini buradan kaldır ve kayded.", + "option": { + "co2": "CO₂ tahmini ekle", + "feedIn": "Şebekeye ihracat tarifesi ekle", + "grid": "Şebekeden ithalat tarifesi ekle", + "planner": "Planlayıcı tahmini ekle", + "solar": "Güneş tahmini ekle" + }, + "planner": { + "description": "Gelişmiş ayar. Dinamik elektrik tarifeleri veya CO₂ tahminleri otomatik olarak kullanıldığından genellikle gerekli değildir. İstatistik ve fiyat hesaplamaları için değil, yalnızca doldurma planlaması için kullanılan ek bir veri kaynağını etkinleştirir.", + "titleAdd": "Planlayıcı Tahmini Ekle", + "titleEdit": "Planlayıcı Tahminini Düzenle" + }, + "services": "Hizmetler", + "solar": { + "description": "GES için güneş enerjisi üretim tahmini. Arayüzde görüntülenir ve ileride iyileştirme hesaplamaları için kullanılacak.", + "titleAdd": "Güneş Tahmini Ekle", + "titleEdit": "Güneş Tahmini Düzenle" + }, + "template": "Sağlayıcı", + "title": "Tarifeler ve Tahminler", + "titleChoice": "Ne eklemek istersin?", + "type": { + "co2": "CO₂ Yoğunluğu", + "feedIn": "İhracat Fiyatı", + "grid": "Şebekeden Tüketim Fiyatı", + "planner": "Planlayıcı", + "solar": "Güneş enerjisi" + }, + "zones": { + "add": "Bölge ekle", + "allDays": "Tüm günler", + "allMonths": "Tüm aylar", + "allTimes": "Tüm zamanlar", + "cancel": "İptal", + "days": "Günler", + "edit": "Düzenle", + "hours": "Saatler", + "months": "Aylar", + "price": "Fiyat", + "priceRequired": "Fiyat zorunludur", + "remove": "Bölgeyi kaldır", + "save": "Kaydet", + "selectAll": "Tüm günler", + "timeFrom": "Başlangıç", + "timeRangeError": "Başlangıç zamanı bitiş zamanından önce olmalı. Gece yarısını kapsayacak şekilde iki ayrı bölge oluştur.", + "timeTo": "Bitiş", + "weekdays": "Hafta günleri" + } + }, "tariffs": { "description": "Doldurma oturumlarının maliyetlerini hesaplamak için elektrik tarifelerini gir.", "title": "Tarifeler" }, "telemetry": { - "description": "EVCC'yi iyileştirmek için veri paylaşımını yapılandır. Gizliliğin bizim için önemlidir ve katılım tamamen isteğe bağlıdır.", + "description": "evcc'yi iyileştirmek için veri paylaşımını yapılandır. Gizliliğin bizim için önemlidir ve katılım tamamen isteğe bağlıdır.", "title": "Uzaktan ölçüm" }, "title": { @@ -633,7 +797,7 @@ "validation": { "failed": "başarısız", "label": "Durum", - "running": "Doğrulanıyor…", + "running": "doğrulanıyor…", "success": "başarılı", "unknown": "bilinmiyor", "validate": "doğrula" @@ -681,7 +845,7 @@ "tabTitle": "Canlı topluluk" }, "savings": { - "co2Saved": "{value} CO₂ tasarruf edildi", + "co2Saved": "{value} tasarruf edildi", "co2Title": "CO₂ Emisyonu", "configurePriceCo2": "Fiyat ve CO₂ emisyonlarını yapılandır.", "footerLong": "{percent} güneş enerjisi", @@ -740,12 +904,14 @@ "forecast": { "co2": { "average": "“Ortalama”", + "constant": "CO₂ yoğunluğu", "lowestHour": "“En temiz saat”", "range": "“Aralık”" }, "modalTitle": "“Öngörü”", "price": { "average": "“Ortalama”", + "constant": "Fiyat", "lowestHour": "“En ucuz saat”", "range": "“Aralık”" }, @@ -920,10 +1086,13 @@ "battery": "Batarya", "batteryCharge": "Batarya doldurma", "batteryDischarge": "Batarya boşaltma", - "batteryGridChargeActive": "“şebekeden doldurma etkin”", - "batteryGridChargeLimit": "“şebekeden doldur şayet”", + "batteryForecastEmpty": "boş {time}", + "batteryForecastFull": "dolu {time}", + "batteryGridChargeActive": "Şebekeden doldurma: etkin", + "batteryGridChargeLimit": "Şebekeden doldurma: şayet", "batteryHold": "Batarya (kilitli)", "batteryTooltip": "{total} ({soc})'ın {energy}'ı", + "forecast": "Tahmin: ", "forecastTooltip": "“öngörü: bugün kalan güneşden üreti̇m”", "gridImport": "Şebeke kullanımı", "homePower": "Tüketim", @@ -950,6 +1119,7 @@ "charged": "Doldu", "co2": "⌀ CO₂", "duration": "Süre", + "emission": "Salım", "fallbackName": "Doldurma noktası", "finished": "“Doldurma sonu”", "power": "Güç", @@ -962,9 +1132,14 @@ "loadpointSettings": { "batteryBoost": { "description": "Ev bataryasından {limit} seviyesine düşene kadar hızlı şarj.", + "descriptionDisabled": "Ev bataryasından hızlı doldurabilmek için bir sınır seç.", + "disabled": "Devre dışı", "label": "'Batarya Takviyesi”", - "mode": "Sadece GES ve Asg.+GES modunda kullanılabilir.", - "once": "Bu doldurma oturumu için takviye etkin." + "mode": "Sadece GES ve Asg.+GES durumunda kullanılabilir.", + "once": "Bu doldurma oturumu için takviye etkin.", + "stateActive": "Batarya desteği etkin", + "stateBelowLimit": "Desdek için batarya çok düşük", + "stateReady": "Batarya desteği hazır" }, "batteryUsage": "“Ev Bataryası”", "currents": "Doldurma Akımı", @@ -1070,6 +1245,9 @@ "vehicleStatus": { "awaitingAuthorization": "İzin bekliyorum.", "batteryBoost": "Batarya takviyesi etkin.", + "batteryBoostBelowLimit": "Desdek için batarya çok düşük.", + "batteryBoostDisabled": "Batarya desteği devre dışı bırakıldı.", + "batteryBoostEnabled": "Batarya {limit} seviyesine kadar destek.", "charging": "doluyor…", "cheapEnergyCharging": "Ucuz enerji mevcut.", "cheapEnergyNextStart": "{duration} içinde ucuz enerji.", @@ -1229,7 +1407,7 @@ }, "hiddenFeatures": { "label": "Deneysel", - "value": "Deneysel özellikleri göster." + "value": "Deneysel özellikleri etkinleştir." }, "language": { "auto": "Otomatik", From 1487d37373421869cbfa25a6ece28cb3bf23fb66 Mon Sep 17 00:00:00 2001 From: Jordan Rodgers Date: Thu, 5 Mar 2026 10:53:13 -0800 Subject: [PATCH 0002/1506] OCPP: wait for BootNotification after CP connection (#27309) --- charger/ocpp.go | 5 + charger/ocpp/cp.go | 73 ++++++++++++- charger/ocpp/cp_core.go | 17 ++- charger/ocpp/cp_core_test.go | 206 +++++++++++++++++++++++++++++++++++ charger/ocpp/cp_requests.go | 5 + charger/ocpp/cp_setup.go | 14 ++- charger/ocpp/cs.go | 8 +- 7 files changed, 318 insertions(+), 10 deletions(-) create mode 100644 charger/ocpp/cp_core_test.go diff --git a/charger/ocpp.go b/charger/ocpp.go index 37b6d294e..d979efcd3 100644 --- a/charger/ocpp.go +++ b/charger/ocpp.go @@ -198,6 +198,11 @@ func NewOCPP(ctx context.Context, go conn.WatchDog(ctx, meterInterval) } + // monitor for charger reboots and re-run setup (once per CP, not per connector) + cp.MonitorReboot(ctx, func() error { + return c.cp.Setup(ctx, meterValues, meterInterval, forcePowerCtrl) + }) + return c, conn.Initialized() } diff --git a/charger/ocpp/cp.go b/charger/ocpp/cp.go index 08f878eef..52419fa04 100644 --- a/charger/ocpp/cp.go +++ b/charger/ocpp/cp.go @@ -1,8 +1,10 @@ package ocpp import ( + "context" "fmt" "sync" + "time" "github.com/evcc-io/evcc/util" "github.com/lorenzodonini/ocpp-go/ocpp1.6/core" @@ -15,11 +17,12 @@ type CP struct { mu sync.RWMutex log *util.Logger onceConnect sync.Once - onceBoot sync.Once + onceMonitor sync.Once id string connected bool + bootTimer *time.Timer // timeout for BootNotification wait after WebSocket connect connectC chan struct{} meterC chan struct{} @@ -112,6 +115,15 @@ func (cp *CP) RegisterID(id string) { cp.id = id } +// stopBootTimer cancels and clears the boot notification wait timer. +// Must be called with cp.mu held. +func (cp *CP) stopBootTimer() { + if cp.bootTimer != nil { + cp.bootTimer.Stop() + cp.bootTimer = nil + } +} + func (cp *CP) connect(connect bool) { cp.mu.Lock() defer cp.mu.Unlock() @@ -122,9 +134,37 @@ func (cp *CP) connect(connect bool) { cp.onceConnect.Do(func() { close(cp.connectC) }) + } else { + cp.stopBootTimer() } } +// onTransportConnect is called when the WebSocket connection is established. +// Instead of marking the CP as connected immediately, it waits for the +// BootNotification handshake to complete (or a timeout to expire). +func (cp *CP) onTransportConnect() { + cp.mu.Lock() + defer cp.mu.Unlock() + + cp.stopBootTimer() + cp.bootTimer = time.AfterFunc(Timeout, cp.onBootTimeout) +} + +// onBootTimeout is called when the BootNotification wait timer expires. +func (cp *CP) onBootTimeout() { + cp.mu.Lock() + if cp.bootTimer == nil { + // timer was cancelled by disconnect or BootNotification + cp.mu.Unlock() + return + } + cp.bootTimer = nil + cp.mu.Unlock() + + cp.log.DEBUG.Printf("boot notification timeout, proceeding") + cp.connect(true) +} + func (cp *CP) Connected() bool { cp.mu.RLock() defer cp.mu.RUnlock() @@ -135,3 +175,34 @@ func (cp *CP) Connected() bool { func (cp *CP) HasConnected() <-chan struct{} { return cp.connectC } + +// MonitorReboot ensures the given function runs only once per CP instance. +// Used to start the reboot monitor goroutine for multi-connector charge points. +func (cp *CP) MonitorReboot(ctx context.Context, setup func() error) { + cp.onceMonitor.Do(func() { + // drain boot notification from initial setup + select { + case <-cp.bootNotificationRequestC: + default: + } + + go cp.monitorReboot(ctx, setup) + }) +} + +func (cp *CP) monitorReboot(ctx context.Context, setup func() error) { + for { + select { + case <-ctx.Done(): + return + + case boot := <-cp.bootNotificationRequestC: + cp.log.INFO.Printf("reboot detected (model: %s, vendor: %s), re-initializing", + boot.ChargePointModel, boot.ChargePointVendor) + + if err := setup(); err != nil { + cp.log.ERROR.Printf("failed to re-initialize after reboot: %v", err) + } + } + } +} diff --git a/charger/ocpp/cp_core.go b/charger/ocpp/cp_core.go index 0fd49d128..8ff4f7236 100644 --- a/charger/ocpp/cp_core.go +++ b/charger/ocpp/cp_core.go @@ -20,9 +20,20 @@ func (cp *CP) OnBootNotification(request *core.BootNotificationRequest) (*core.B Status: core.RegistrationStatusAccepted, } - cp.onceBoot.Do(func() { - cp.bootNotificationRequestC <- request - }) + cp.mu.Lock() + cp.BootNotificationResult = request + cp.stopBootTimer() + cp.mu.Unlock() + + // mark charge point as ready for communication + cp.connect(true) + + // notify channel for Setup (initial) or monitorReboot (reconnection) + select { + case cp.bootNotificationRequestC <- request: + default: + cp.log.DEBUG.Printf("boot notification channel full, discarding") + } return res, nil } diff --git a/charger/ocpp/cp_core_test.go b/charger/ocpp/cp_core_test.go new file mode 100644 index 000000000..59553ae81 --- /dev/null +++ b/charger/ocpp/cp_core_test.go @@ -0,0 +1,206 @@ +package ocpp + +import ( + "sync/atomic" + "testing" + "time" + + "github.com/evcc-io/evcc/util" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/core" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestBootNotificationStoresResultAndConnects(t *testing.T) { + log := util.NewLogger("test") + cp := NewChargePoint(log, "test-cp") + + assert.False(t, cp.Connected(), "should not be connected initially") + assert.Nil(t, cp.BootNotificationResult, "should have no boot result initially") + + bootReq := &core.BootNotificationRequest{ + ChargePointModel: "TestModel", + ChargePointVendor: "TestVendor", + } + + res, err := cp.OnBootNotification(bootReq) + require.NoError(t, err) + assert.Equal(t, core.RegistrationStatusAccepted, res.Status) + + // should be connected after BootNotification + assert.True(t, cp.Connected(), "should be connected after BootNotification") + assert.Equal(t, bootReq, cp.BootNotificationResult, "should store boot result") + + // should have sent to channel + select { + case req := <-cp.bootNotificationRequestC: + assert.Equal(t, bootReq, req) + case <-time.After(100 * time.Millisecond): + t.Fatal("bootNotificationRequestC should have received notification") + } +} + +func TestBootNotificationStopsTimer(t *testing.T) { + log := util.NewLogger("test") + cp := NewChargePoint(log, "test-cp") + + // simulate WebSocket connect (starts timer) + cp.onTransportConnect() + + // timer should be set + cp.mu.RLock() + assert.NotNil(t, cp.bootTimer, "boot timer should be active after transport connect") + cp.mu.RUnlock() + + // BootNotification arrives - should stop timer and connect + bootReq := &core.BootNotificationRequest{ + ChargePointModel: "TestModel", + ChargePointVendor: "TestVendor", + } + _, err := cp.OnBootNotification(bootReq) + require.NoError(t, err) + + assert.True(t, cp.Connected(), "should be connected after BootNotification") + + cp.mu.RLock() + assert.Nil(t, cp.bootTimer, "boot timer should be nil after BootNotification") + cp.mu.RUnlock() + + // drain the channel + <-cp.bootNotificationRequestC +} + +func TestTransportConnectTimeoutFallback(t *testing.T) { + log := util.NewLogger("test") + cp := NewChargePoint(log, "test-cp") + + // use a short timeout for testing + origTimeout := Timeout + Timeout = 50 * time.Millisecond + defer func() { Timeout = origTimeout }() + + assert.False(t, cp.Connected(), "should not be connected initially") + + cp.onTransportConnect() + + // should not be connected yet + assert.False(t, cp.Connected(), "should not be connected immediately after transport connect") + + // should be connected after timeout (fallback) + require.Eventually(t, cp.Connected, time.Second, 10*time.Millisecond, + "should be connected after boot timeout") + + // boot timer should be cleared after firing + cp.mu.RLock() + assert.Nil(t, cp.bootTimer, "boot timer should be nil after timeout") + cp.mu.RUnlock() +} + +func TestDisconnectCancelsTimer(t *testing.T) { + log := util.NewLogger("test") + cp := NewChargePoint(log, "test-cp") + + // use a short timeout for testing + origTimeout := Timeout + Timeout = 200 * time.Millisecond + defer func() { Timeout = origTimeout }() + + cp.onTransportConnect() + + cp.mu.RLock() + assert.NotNil(t, cp.bootTimer, "boot timer should be active") + cp.mu.RUnlock() + + // disconnect should cancel timer + cp.connect(false) + + cp.mu.RLock() + assert.Nil(t, cp.bootTimer, "boot timer should be nil after disconnect") + cp.mu.RUnlock() + + // should still be disconnected (timer was cancelled) + require.Never(t, cp.Connected, 300*time.Millisecond, 10*time.Millisecond, + "should not be connected after cancelled timer") +} + +func TestBootNotificationChannelFull(t *testing.T) { + log := util.NewLogger("test") + cp := NewChargePoint(log, "test-cp") + + // fill the channel (buffer size 1) + cp.bootNotificationRequestC <- &core.BootNotificationRequest{ + ChargePointModel: "First", + } + + // second notification should be dropped (channel full, non-blocking send) + bootReq := &core.BootNotificationRequest{ + ChargePointModel: "Second", + ChargePointVendor: "TestVendor", + } + + res, err := cp.OnBootNotification(bootReq) + require.NoError(t, err) + assert.Equal(t, core.RegistrationStatusAccepted, res.Status) + + // result should still be updated even though channel was full + assert.Equal(t, bootReq, cp.BootNotificationResult) + assert.True(t, cp.Connected()) + + // channel should have the first message (second was dropped) + req := <-cp.bootNotificationRequestC + assert.Equal(t, "First", req.ChargePointModel) +} + +func TestReconnectAfterReboot(t *testing.T) { + log := util.NewLogger("test") + cp := NewChargePoint(log, "test-cp") + + // simulate initial connection + cp.connect(true) + + // simulate disconnect + cp.connect(false) + assert.False(t, cp.Connected()) + + // simulate reconnect with BootNotification (reboot) + cp.onTransportConnect() + + bootReq := &core.BootNotificationRequest{ + ChargePointModel: "TestModel", + ChargePointVendor: "TestVendor", + } + _, err := cp.OnBootNotification(bootReq) + require.NoError(t, err) + + assert.True(t, cp.Connected(), "should be connected after reboot BootNotification") + + // channel should have the notification for monitorReboot to pick up + select { + case req := <-cp.bootNotificationRequestC: + assert.Equal(t, bootReq, req) + case <-time.After(100 * time.Millisecond): + t.Fatal("bootNotificationRequestC should have received reboot notification") + } +} + +func TestMonitorRebootOnlyOnce(t *testing.T) { + log := util.NewLogger("test") + cp := NewChargePoint(log, "test-cp") + + ctx := t.Context() + var callCount atomic.Int32 + setup := func() error { callCount.Add(1); return nil } + + cp.MonitorReboot(ctx, setup) + cp.MonitorReboot(ctx, setup) + cp.MonitorReboot(ctx, setup) + + // send a boot notification to trigger the monitor + cp.bootNotificationRequestC <- &core.BootNotificationRequest{ + ChargePointModel: "TestModel", + } + + // wait for the goroutine to process it + require.Eventually(t, func() bool { return callCount.Load() == 1 }, time.Second, 10*time.Millisecond, + "setup should be called exactly once") +} diff --git a/charger/ocpp/cp_requests.go b/charger/ocpp/cp_requests.go index adc5c38fe..00850c77e 100644 --- a/charger/ocpp/cp_requests.go +++ b/charger/ocpp/cp_requests.go @@ -3,6 +3,7 @@ package ocpp import ( "errors" + "github.com/evcc-io/evcc/api" "github.com/lorenzodonini/ocpp-go/ocpp1.6/core" "github.com/lorenzodonini/ocpp-go/ocpp1.6/remotetrigger" "github.com/lorenzodonini/ocpp-go/ocpp1.6/smartcharging" @@ -72,6 +73,10 @@ func (cp *CP) SetChargingProfileRequest(connectorId int, profile *types.Charging } func (cp *CP) TriggerMessageRequest(connectorId int, requestedMessage remotetrigger.MessageTrigger) error { + if !cp.Connected() { + return api.ErrTimeout + } + rc := make(chan error, 1) err := Instance().TriggerMessage(cp.id, func(request *remotetrigger.TriggerMessageConfirmation, err error) { diff --git a/charger/ocpp/cp_setup.go b/charger/ocpp/cp_setup.go index acfe3e147..b5d4bae8c 100644 --- a/charger/ocpp/cp_setup.go +++ b/charger/ocpp/cp_setup.go @@ -105,17 +105,27 @@ func (cp *CP) Setup(ctx context.Context, meterValues string, meterInterval time. } } - // see who's there - if cp.HasRemoteTriggerFeature { + // BootNotification is normally received before Setup runs (we wait for it + // after WebSocket connect). Only trigger it as fallback for chargers that + // didn't send it (e.g. timeout-based connection without reboot). + cp.mu.RLock() + hasBootResult := cp.BootNotificationResult != nil + cp.mu.RUnlock() + + if !hasBootResult && cp.HasRemoteTriggerFeature { if err := cp.TriggerMessageRequest(0, core.BootNotificationFeatureName); err != nil { cp.log.DEBUG.Printf("failed triggering BootNotification: %v", err) } select { + case <-ctx.Done(): + return ctx.Err() case <-time.After(Timeout): cp.log.DEBUG.Printf("BootNotification timeout") case res := <-cp.bootNotificationRequestC: + cp.mu.Lock() cp.BootNotificationResult = res + cp.mu.Unlock() } } diff --git a/charger/ocpp/cs.go b/charger/ocpp/cs.go index 0babda0db..bd582892c 100644 --- a/charger/ocpp/cs.go +++ b/charger/ocpp/cs.go @@ -163,7 +163,7 @@ func (cs *CS) RegisterChargepoint(id string, newfun func() *CP, init func(*CP) e cs.mu.Unlock() if registered { - cp.connect(true) + cp.onTransportConnect() } return cp, init(cp) @@ -178,9 +178,9 @@ func (cs *CS) NewChargePoint(chargePoint ocpp16.ChargePointConnection) { if ok { cs.log.DEBUG.Printf("charge point connected: %s", chargePoint.ID()) - // trigger initial connection + // wait for BootNotification before marking as connected if cp := reg.cp; cp != nil { - cp.connect(true) + cp.onTransportConnect() } cs.mu.Unlock() @@ -199,7 +199,7 @@ func (cs *CS) NewChargePoint(chargePoint ocpp16.ChargePointConnection) { cs.regs[chargePoint.ID()] = reg delete(cs.regs, "") - cp.connect(true) + cp.onTransportConnect() cs.mu.Unlock() cs.publish() From 4d7a079eebfb53578cccfaa9e6ac421be5c27584 Mon Sep 17 00:00:00 2001 From: Marcel Goerentz <57457529+marcelGoerentz@users.noreply.github.com> Date: Fri, 6 Mar 2026 11:18:04 +0100 Subject: [PATCH 0003/1506] Warp: enable phase switching when Energy Manager present (#27962) --- charger/warp-ws.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charger/warp-ws.go b/charger/warp-ws.go index a57a9a1fb..d9288d535 100644 --- a/charger/warp-ws.go +++ b/charger/warp-ws.go @@ -103,7 +103,7 @@ func NewWarpWSFromConfig(ctx context.Context, other map[string]any) (api.Charger // only setup phase switching methods if power manager endpoint is set var phases func(int) error var getPhases func() (int, error) - if w.hasFeature(warp.FeaturePhaseSwitch) && w.pm != nil { + if (w.hasFeature(warp.FeaturePhaseSwitch) || cc.EnergyManagerURI != "") && w.pm != nil { if res, err := w.ensurePmState(); err == nil && res.ExternalControl != warp.ExternalControlDeactivated { w.pmState = &res phases = w.phases1p3p From ddc09c25ff6b743214295d8ab00d36015e692991 Mon Sep 17 00:00:00 2001 From: andig Date: Fri, 6 Mar 2026 14:04:53 +0100 Subject: [PATCH 0004/1506] chore: minor --- charger/nexblue.go | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/charger/nexblue.go b/charger/nexblue.go index 1d0658f26..71230ead2 100644 --- a/charger/nexblue.go +++ b/charger/nexblue.go @@ -219,12 +219,12 @@ func (wb *Nexblue) Enable(enable bool) error { // MaxCurrent implements the api.Charger interface func (wb *Nexblue) MaxCurrent(current int64) error { - req, _ := request.New(http.MethodPost, - fmt.Sprintf("%s/chargers/%s/cmd/set_current_limit", nexblueAPI, wb.serial), request.MarshalJSON(struct { - CurrentLimit int64 `json:"current_limit"` - }{ - current, - }), request.JSONEncoding) + uri := fmt.Sprintf("%s/chargers/%s/cmd/set_current_limit", nexblueAPI, wb.serial) + req, _ := request.New(http.MethodPost, uri, request.MarshalJSON(struct { + CurrentLimit int64 `json:"current_limit"` + }{ + current, + }), request.JSONEncoding) var res struct { Result int `json:"result"` @@ -267,12 +267,12 @@ var _ api.PhaseSwitcher = (*Nexblue)(nil) // Phases1p3p implements the api.PhaseSwitcher interface func (wb *Nexblue) Phases1p3p(phases int) error { - req, _ := request.New(http.MethodPost, - fmt.Sprintf("%s/v1/charger/%s/setting", nexblueAPI, wb.serial), request.MarshalJSON(struct { - PhaseMode int `json:"phase_mode"` - }{ - phases, - }), request.JSONEncoding) + uri := fmt.Sprintf("%s/v1/charger/%s/setting", nexblueAPI, wb.serial) + req, _ := request.New(http.MethodPost, uri, request.MarshalJSON(struct { + PhaseMode int `json:"phase_mode"` + }{ + phases, + }), request.JSONEncoding) _, err := wb.DoBody(req) return err From b6fdd752311609a5db3273f61bf8739f9274fa8c Mon Sep 17 00:00:00 2001 From: Marc Dirix Date: Sat, 7 Mar 2026 09:42:10 +0100 Subject: [PATCH 0005/1506] Add Sessy P1 meter and battery (#27956) --- templates/definition/meter/sessy-p1.yaml | 106 ++++++++++++ .../definition/meter/sessy-smart-battery.yaml | 160 ++++++++++++++++++ 2 files changed, 266 insertions(+) create mode 100644 templates/definition/meter/sessy-p1.yaml create mode 100644 templates/definition/meter/sessy-smart-battery.yaml diff --git a/templates/definition/meter/sessy-p1.yaml b/templates/definition/meter/sessy-p1.yaml new file mode 100644 index 000000000..8fc06991b --- /dev/null +++ b/templates/definition/meter/sessy-p1.yaml @@ -0,0 +1,106 @@ +template: sessy-p1 +products: + - brand: Sessy + description: + generic: Sessy P1 +params: + - name: usage + choice: ["grid"] + - name: host + - name: cache + default: 10 +render: | + type: custom + power: + source: http + uri: http://{{ .host }}/api/v2/p1/details + headers: + - content-type: application/json + jq: .power_total + cache: {{ .cache }} + timeout: 10s + energy: + source: calc + add: + - source: http + uri: http://{{ .host }}/api/v2/p1/details + headers: + - content-type: application/json + jq: .power_consumed_tariff1 + scale: 0.001 + cache: {{ .cache }} + timeout: 10s + - source: http + uri: http://{{ .host }}/api/v2/p1/details + headers: + - content-type: application/json + jq: .power_consumed_tariff2 + scale: 0.001 + cache: {{ .cache }} + timeout: 10s + - source: http + uri: http://{{ .host }}/api/v2/p1/details + headers: + - content-type: application/json + jq: .power_produced_tariff1 + scale: -0.001 + cache: {{ .cache }} + timeout: 10s + - source: http + uri: http://{{ .host }}/api/v2/p1/details + headers: + - content-type: application/json + jq: .power_produced_tariff2 + scale: -0.001 + cache: {{ .cache }} + timeout: 10s + currents: + - source: http + uri: http://{{ .host }}/api/v2/p1/details + headers: + - content-type: application/json + jq: .current_l1 + scale: 0.001 + cache: {{ .cache }} + timeout: 10s + - source: http + uri: http://{{ .host }}/api/v2/p1/details + headers: + - content-type: application/json + jq: .current_l2 + scale: 0.001 + cache: {{ .cache }} + timeout: 10s + - source: http + uri: http://{{ .host }}/api/v2/p1/details + headers: + - content-type: application/json + jq: .current_l3 + scale: 0.001 + cache: {{ .cache }} + timeout: 10s + voltages: + - source: http + uri: http://{{ .host }}/api/v2/p1/details + headers: + - content-type: application/json + jq: .voltage_l1 + scale: 0.001 + cache: {{ .cache }} + timeout: 10s + - source: http + uri: http://{{ .host }}/api/v2/p1/details + headers: + - content-type: application/json + jq: .voltage_l2 + scale: 0.001 + cache: {{ .cache }} + timeout: 10s + - source: http + uri: http://{{ .host }}/api/v2/p1/details + headers: + - content-type: application/json + jq: .voltage_l3 + scale: 0.001 + cache: {{ .cache }} + timeout: 10s diff --git a/templates/definition/meter/sessy-smart-battery.yaml b/templates/definition/meter/sessy-smart-battery.yaml new file mode 100644 index 000000000..53c6819a4 --- /dev/null +++ b/templates/definition/meter/sessy-smart-battery.yaml @@ -0,0 +1,160 @@ +template: sessy-smart-battery +covers: [sessy-smart-battery] +products: + - brand: Sessy + description: + generic: Sessy Smart Battery +capabilities: ["battery-control"] +params: + - name: usage + choice: ["battery"] + - name: host + - name: user + required: true + - name: password + required: true + - name: cache + default: 10 + - preset: battery-params + - name: capacity + default: 5 +render: | + type: custom + power: + source: http + uri: http://{{ .host }}/api/v1/power/status + auth: + type: basic + user: {{ .user }} + password: {{ .password }} + headers: + - content-type: application/json + jq: .sessy.power + cache: {{ .cache }} + timeout: 10s + energy: + source: http + uri: http://{{ .host }}/api/v1/energy/status + auth: + type: basic + user: {{ .user }} + password: {{ .password }} + headers: + - content-type: application/json + jq: .sessy_energy.import_wh + cache: {{ .cache }} + timeout: 10s + soc: + source: http + uri: http://{{ .host }}/api/v1/power/status + auth: + type: basic + user: {{ .user }} + password: {{ .password }} + headers: + - content-type: application/json + jq: .sessy.state_of_charge + scale: 100 + cache: {{ .cache }} + timeout: 10s + voltages: + - source: http + uri: http://{{ .host }}/api/v1/power/status + auth: + type: basic + user: {{ .user }} + password: {{ .password }} + headers: + - content-type: application/json + jq: .renewable_energy_phase1.voltage_rms + scale: 0.001 + cache: {{ .cache }} + timeout: 10s + - source: http + uri: http://{{ .host }}/api/v1/power/status + auth: + type: basic + user: {{ .user }} + password: {{ .password }} + headers: + - content-type: application/json + jq: .renewable_energy_phase2.voltage_rms + scale: 0.001 + cache: {{ .cache }} + timeout: 10s + - source: http + uri: http://{{ .host }}/api/v1/power/status + auth: + type: basic + user: {{ .user }} + password: {{ .password }} + headers: + - content-type: application/json + jq: .renewable_energy_phase3.voltage_rms + scale: 0.001 + cache: {{ .cache }} + timeout: 10s + batterymode: + source: switch + switch: + - case: 1 # normal + set: + source: http + uri: http://{{ .host }}/api/v1/power/active_strategy + method: POST + auth: + type: basic + user: {{ .user }} + password: {{ .password }} + headers: + - content-type: application/json + body: '{"strategy": "POWER_STRATEGY_NOM"}' + - case: 2 # hold + set: + source: sequence + set: + - source: http + uri: http://{{ .host }}/api/v1/power/active_strategy + method: POST + auth: + type: basic + user: {{ .user }} + password: {{ .password }} + headers: + - content-type: application/json + body: '{"strategy": "POWER_STRATEGY_API"}' + - source: http + uri: http://{{ .host }}/api/v1/power/setpoint + method: POST + auth: + type: basic + user: {{ .user }} + password: {{ .password }} + headers: + - content-type: application/json + body: '{"setpoint": 0}' + - case: 3 # charge + set: + source: sequence + set: + - source: http + uri: http://{{ .host }}/api/v1/power/active_strategy + method: POST + auth: + type: basic + user: {{ .user }} + password: {{ .password }} + headers: + - content-type: application/json + body: '{"strategy": "POWER_STRATEGY_API"}' + - source: http + uri: http://{{ .host }}/api/v1/power/setpoint + method: POST + auth: + type: basic + user: {{ .user }} + password: {{ .password }} + headers: + - content-type: application/json + body: '{"setpoint": -2200}' + capacity: {{ .capacity }} From 56c1d56626245bdf27abff1677037d37d33f68e1 Mon Sep 17 00:00:00 2001 From: mfuchs1984 <57141790+mfuchs1984@users.noreply.github.com> Date: Sat, 7 Mar 2026 09:46:46 +0100 Subject: [PATCH 0006/1506] Loadpoint: fix fast charging phase scaling (#27972) --- core/loadpoint.go | 10 +++-- core/loadpoint_phases_test.go | 71 +++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 4 deletions(-) diff --git a/core/loadpoint.go b/core/loadpoint.go index 8d85af6fe..45401bb02 100644 --- a/core/loadpoint.go +++ b/core/loadpoint.go @@ -1282,12 +1282,14 @@ func (lp *Loadpoint) scalePhases(phases int) error { func (lp *Loadpoint) fastCharging() error { if lp.hasPhaseSwitching() { phases := 3 - maxPower1p := Voltage * lp.effectiveMaxCurrent() // load management limit active - if circuitMaxPower := circuitMaxPower(lp.circuit); circuitMaxPower > 0 && circuitMaxPower < 1.1*maxPower1p { - phases = 1 - lp.log.DEBUG.Printf("fast charging: scaled to 1p to match %.0fW max circuit power", circuitMaxPower) + if lp.circuit != nil { + minPower3p := currentToPower(lp.effectiveMinCurrent(), 3) + if powerLimit := lp.circuit.ValidatePower(lp.chargePower, minPower3p); powerLimit < minPower3p { + phases = 1 + lp.log.DEBUG.Printf("fast charging: scaled to 1p to match %.0fW available circuit power", powerLimit) + } } if err := lp.scalePhasesIfAvailable(phases); err != nil { diff --git a/core/loadpoint_phases_test.go b/core/loadpoint_phases_test.go index eefb6f6d4..d7e720cba 100644 --- a/core/loadpoint_phases_test.go +++ b/core/loadpoint_phases_test.go @@ -479,3 +479,74 @@ func TestScalePhasesIfAvailable(t *testing.T) { ctrl.Finish() } } + +func TestFastChargingCircuitBasedPhaseScaling(t *testing.T) { + Voltage = 230 + + tc := []struct { + desc string + phases int + chargePower float64 + availableCircuitPower float64 // ValidatePower return for 3p request + expectedPhases int + noCircuit bool + }{ + {desc: "no circuit", phases: 3, chargePower: 0, expectedPhases: 3, noCircuit: true}, + {desc: "low limit, no surplus", phases: 3, chargePower: 0, availableCircuitPower: 3680, expectedPhases: 1}, + {desc: "low limit, with surplus", phases: 1, chargePower: 0, availableCircuitPower: 11040, expectedPhases: 3}, + {desc: "already charging, low limit", phases: 3, chargePower: 3680, availableCircuitPower: 3680, expectedPhases: 1}, + {desc: "already charging, high limit", phases: 1, chargePower: 3680, availableCircuitPower: 11040, expectedPhases: 3}, + {desc: "edge case: just below 3p minimum", phases: 3, chargePower: 0, availableCircuitPower: 4140 - 1, expectedPhases: 1}, + {desc: "edge case: just at 3p minimum", phases: 1, chargePower: 0, availableCircuitPower: 4140, expectedPhases: 3}, + } + + for _, tc := range tc { + t.Run(tc.desc, func(t *testing.T) { + ctrl := gomock.NewController(t) + + lp := NewLoadpoint(util.NewLogger("foo"), nil) + lp.minCurrent = 6 + lp.maxCurrent = 16 + lp.phases = tc.phases + lp.chargePower = tc.chargePower + lp.offeredCurrent = 0 // ensure MaxCurrent is called + lp.wakeUpTimer = NewTimer() + + plainCharger := api.NewMockCharger(ctrl) + phaseCharger := api.NewMockPhaseSwitcher(ctrl) + lp.charger = struct { + *api.MockCharger + *api.MockPhaseSwitcher + }{plainCharger, phaseCharger} + + if !tc.noCircuit { + circuit := api.NewMockCircuit(ctrl) + lp.circuit = circuit + + minPower3p := Voltage * 6 * 3 + + // fastCharging call to ValidatePower + circuit.EXPECT().ValidatePower(tc.chargePower, minPower3p).Return(tc.availableCircuitPower) + + // setLimit calls + circuit.EXPECT().ValidateCurrent(gomock.Any(), lp.maxCurrent).Return(lp.maxCurrent) + circuit.EXPECT().ValidatePower(tc.chargePower, float64(tc.expectedPhases)*Voltage*lp.maxCurrent).Return(float64(tc.expectedPhases) * Voltage * lp.maxCurrent) + } + + plainCharger.EXPECT().Enabled().Return(true, nil).AnyTimes() + plainCharger.EXPECT().Enable(gomock.Any()).Return(nil).AnyTimes() + + if tc.phases != tc.expectedPhases { + phaseCharger.EXPECT().Phases1p3p(tc.expectedPhases).Return(nil) + } + + plainCharger.EXPECT().MaxCurrent(int64(lp.maxCurrent)).Return(nil) + + err := lp.fastCharging() + require.NoError(t, err) + require.Equal(t, tc.expectedPhases, lp.phases, tc.desc) + + ctrl.Finish() + }) + } +} From 16d1258795a4ed0f4588b6fa084f3cb676bf81ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20He=C3=9F?= Date: Sat, 7 Mar 2026 10:04:00 +0100 Subject: [PATCH 0007/1506] Easee: warn on rogue CommandResponse not triggered by evcc (#27916) Co-authored-by: Claude Sonnet 4.6 --- charger/easee.go | 143 ++++++++++++++++++------ charger/easee_test.go | 246 +++++++++++++++++++++++++++++++++++++++--- 2 files changed, 344 insertions(+), 45 deletions(-) diff --git a/charger/easee.go b/charger/easee.go index 4929e564b..9b32e4e50 100644 --- a/charger/easee.go +++ b/charger/easee.go @@ -61,12 +61,15 @@ type Easee struct { phaseMode int currentPower, sessionEnergy, totalEnergy, currentL1, currentL2, currentL3 float64 - rfid string - lp loadpoint.API - cmdC chan easee.SignalRCommandResponse - obsC chan easee.Observation - obsTime map[easee.ObservationID]time.Time - startDone func() + rfid string + lp loadpoint.API + cmdMu sync.Mutex + pendingTicks map[int64]chan easee.SignalRCommandResponse + pendingByID map[easee.ObservationID]chan easee.SignalRCommandResponse + expectedOrphans map[easee.ObservationID]int + obsC chan easee.Observation + obsTime map[easee.ObservationID]time.Time + startDone func() } func init() { @@ -107,15 +110,17 @@ func NewEasee(ctx context.Context, user, password, charger string, timeout time. done := make(chan struct{}) c := &Easee{ - Helper: request.NewHelper(log), - charger: charger, - authorize: authorize, - log: log, - current: 6, // default current - startDone: sync.OnceFunc(func() { close(done) }), - cmdC: make(chan easee.SignalRCommandResponse), - obsC: make(chan easee.Observation), - obsTime: make(map[easee.ObservationID]time.Time), + Helper: request.NewHelper(log), + charger: charger, + authorize: authorize, + log: log, + current: 6, // default current + startDone: sync.OnceFunc(func() { close(done) }), + pendingTicks: make(map[int64]chan easee.SignalRCommandResponse), + pendingByID: make(map[easee.ObservationID]chan easee.SignalRCommandResponse), + expectedOrphans: make(map[easee.ObservationID]int), + obsC: make(chan easee.Observation), + obsTime: make(map[easee.ObservationID]time.Time), } c.Client.Timeout = timeout @@ -211,6 +216,48 @@ func (c *Easee) waitForOptionalState() { c.log.WARN.Println("did not receive full state from cloud") } +func (c *Easee) registerPendingTick(tick int64, ch chan easee.SignalRCommandResponse) { + c.cmdMu.Lock() + c.pendingTicks[tick] = ch + c.cmdMu.Unlock() +} + +func (c *Easee) unregisterPendingTick(tick int64) { + c.cmdMu.Lock() + delete(c.pendingTicks, tick) + c.cmdMu.Unlock() +} + +func (c *Easee) registerPendingByID(id easee.ObservationID, ch chan easee.SignalRCommandResponse) { + c.cmdMu.Lock() + defer c.cmdMu.Unlock() + c.pendingByID[id] = ch +} + +func (c *Easee) unregisterPendingByID(id easee.ObservationID) { + c.cmdMu.Lock() + defer c.cmdMu.Unlock() + delete(c.pendingByID, id) +} + +func (c *Easee) registerExpectedOrphan(ids ...easee.ObservationID) { + c.cmdMu.Lock() + defer c.cmdMu.Unlock() + for _, id := range ids { + c.expectedOrphans[id]++ + } +} + +func (c *Easee) consumeExpectedOrphan(id easee.ObservationID) bool { + c.cmdMu.Lock() + defer c.cmdMu.Unlock() + if c.expectedOrphans[id] > 0 { + c.expectedOrphans[id]-- + return true + } + return false +} + // check c.obsTime for presence of ALL of the following keys: easee.SESSION_ENERGY, easee.LIFETIME_ENERGY func (c *Easee) optionalStatePresent() bool { c.mux.Lock() @@ -383,12 +430,34 @@ func (c *Easee) CommandResponse(i json.RawMessage) { c.log.ERROR.Printf("invalid message: %s %v", i, err) return } + c.log.TRACE.Printf("CommandResponse %s: %+v", res.SerialNumber, res) - select { - case c.cmdC <- res: - default: + obsID := easee.ObservationID(res.ID) + + c.cmdMu.Lock() + chTick, tickOk := c.pendingTicks[res.Ticks] + chID, idOk := c.pendingByID[obsID] + c.cmdMu.Unlock() + + if tickOk { + chTick <- res + return } + + if idOk { + chID <- res + return + } + + if c.consumeExpectedOrphan(obsID) { + return + } + + c.log.WARN.Printf("rogue CommandResponse: charger %s ObservationID=%s Ticks=%d "+ + "(accepted=%v, resultCode=%d) which was not triggered by evcc — "+ + "another system may be controlling this charger", + res.SerialNumber, obsID, res.Ticks, res.WasAccepted, res.ResultCode) } func (c *Easee) chargers() ([]easee.Charger, error) { @@ -545,26 +614,28 @@ func (c *Easee) postJSONAndWait(uri string, data any) (bool, error) { return true, nil } - return false, c.waitForTickResponse(cmd.Ticks) + ch := make(chan easee.SignalRCommandResponse, 1) + c.registerPendingTick(cmd.Ticks, ch) + defer c.unregisterPendingTick(cmd.Ticks) + obsID := easee.ObservationID(cmd.CommandId) + c.registerPendingByID(obsID, ch) + defer c.unregisterPendingByID(obsID) + return false, c.waitForTickResponse(ch) } // all other response codes lead to an error return false, fmt.Errorf("invalid status: %d", resp.StatusCode) } -func (c *Easee) waitForTickResponse(expectedTick int64) error { - for { - select { - case cmdResp := <-c.cmdC: - if cmdResp.Ticks == expectedTick { - if !cmdResp.WasAccepted { - return fmt.Errorf("command rejected: %d", cmdResp.Ticks) - } - return nil - } - case <-time.After(c.Client.Timeout): - return api.ErrTimeout +func (c *Easee) waitForTickResponse(ch <-chan easee.SignalRCommandResponse) error { + select { + case cmdResp := <-ch: + if !cmdResp.WasAccepted { + return fmt.Errorf("command rejected: %d", cmdResp.Ticks) } + return nil + case <-time.After(c.Client.Timeout): + return api.ErrTimeout } } @@ -744,7 +815,15 @@ func (c *Easee) Phases1p3p(phases int) error { data.DynamicCircuitCurrentP3 = &max3 } - _, err = c.postJSONAndWait(uri, data) + // Register before POST so the SignalR CommandResponse that the Easee + // cloud sends on HTTP 200 (sync) responses is silently consumed rather + // than logged as rogue. On error we undo the registration; on 202/noop + // paths no extra CommandResponse is expected, so the counter is + // intentionally left to be consumed by the eventual SignalR message. + c.registerExpectedOrphan(easee.CIRCUIT_MAX_CURRENT_P1) + if _, err = c.postJSONAndWait(uri, data); err != nil { + c.consumeExpectedOrphan(easee.CIRCUIT_MAX_CURRENT_P1) + } } else { // charger level if phases == 3 { diff --git a/charger/easee_test.go b/charger/easee_test.go index 30c543c71..3c6b19943 100644 --- a/charger/easee_test.go +++ b/charger/easee_test.go @@ -13,6 +13,7 @@ import ( "github.com/evcc-io/evcc/util/request" "github.com/jarcoal/httpmock" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) // Helper function to create a payload @@ -30,12 +31,14 @@ func createPayload(id easee.ObservationID, timestamp time.Time, dataType easee.D func newEasee() *Easee { log := util.NewLogger("easee") e := Easee{ - Helper: request.NewHelper(log), - obsTime: make(map[easee.ObservationID]time.Time), - log: log, - startDone: func() {}, - cmdC: make(chan easee.SignalRCommandResponse), - obsC: make(chan easee.Observation), + Helper: request.NewHelper(log), + obsTime: make(map[easee.ObservationID]time.Time), + pendingTicks: make(map[int64]chan easee.SignalRCommandResponse), + pendingByID: make(map[easee.ObservationID]chan easee.SignalRCommandResponse), + expectedOrphans: make(map[easee.ObservationID]int), + log: log, + startDone: func() {}, + obsC: make(chan easee.Observation), } e.Client.Timeout = 500 * time.Millisecond //aggressive timeout to accelerate testing return &e @@ -166,15 +169,12 @@ func TestEasee_waitForTickResponse(t *testing.T) { e := newEasee() - // Set up the command channel for test and Easee to share - cmdC := make(chan easee.SignalRCommandResponse, 1) // make it buffered for ease of testing - e.cmdC = cmdC - + ch := make(chan easee.SignalRCommandResponse, 1) if tc.cmdCValue != nil { - cmdC <- *tc.cmdCValue + ch <- *tc.cmdCValue } - err := e.waitForTickResponse(tc.expectedTick) + err := e.waitForTickResponse(ch) // Assert the result if tc.expectedErr != nil { @@ -227,7 +227,18 @@ func TestEasee_postJsonAndWait(t *testing.T) { if tc.cmdResp != nil { go func() { - e.cmdC <- *tc.cmdResp + // wait for postJSONAndWait to register the per-tick channel + var ch chan easee.SignalRCommandResponse + for { + e.cmdMu.Lock() + ch = e.pendingTicks[tc.cmdResp.Ticks] + e.cmdMu.Unlock() + if ch != nil { + break + } + time.Sleep(time.Millisecond) + } + ch <- *tc.cmdResp }() } @@ -380,3 +391,212 @@ func TestEasee_MaxCurrent(t *testing.T) { //assert.Equal(t, e.current, e.dynamicChargerCurrent) } } + +func TestEasee_CommandResponse_rogue(t *testing.T) { + e := newEasee() + + rogueResp := easee.SignalRCommandResponse{ + SerialNumber: "EH123456", + Ticks: 999999999, + WasAccepted: true, + ResultCode: 0, + } + + raw, err := json.Marshal(rogueResp) + require.NoError(t, err) + + // No pending tick registered → should log WARN (not panic, not block) + assert.NotPanics(t, func() { + e.CommandResponse(raw) + }) + + // pendingTicks should still be empty + e.cmdMu.Lock() + assert.Empty(t, e.pendingTicks) + e.cmdMu.Unlock() +} + +func TestEasee_CommandResponse_legitimate(t *testing.T) { + e := newEasee() + + ticks := int64(638798974487432600) + ch := make(chan easee.SignalRCommandResponse, 1) + e.registerPendingTick(ticks, ch) + + resp := easee.SignalRCommandResponse{ + SerialNumber: "EH123456", + Ticks: ticks, + WasAccepted: true, + } + + raw, err := json.Marshal(resp) + require.NoError(t, err) + + e.CommandResponse(raw) + + // Channel should have received the response + select { + case got := <-ch: + assert.Equal(t, ticks, got.Ticks) + assert.True(t, got.WasAccepted) + case <-time.After(100 * time.Millisecond): + t.Fatal("CommandResponse did not deliver to pending channel") + } +} + +func TestEasee_CommandResponse_expectedOrphan(t *testing.T) { + e := newEasee() + + // Pre-register the expected orphan + e.registerExpectedOrphan(easee.CIRCUIT_MAX_CURRENT_P1) + + resp := easee.SignalRCommandResponse{ + SerialNumber: "EH123456", + ID: int(easee.CIRCUIT_MAX_CURRENT_P1), + Ticks: 111111111, + WasAccepted: true, + ResultCode: 0, + } + + raw, err := json.Marshal(resp) + require.NoError(t, err) + + // Should not panic and should consume the orphan counter + assert.NotPanics(t, func() { + e.CommandResponse(raw) + }) + + // Counter should now be zero — a second response would be rogue + assert.False(t, e.consumeExpectedOrphan(easee.CIRCUIT_MAX_CURRENT_P1)) +} + +func TestEasee_CommandResponse_rogueAfterOrphanConsumed(t *testing.T) { + e := newEasee() + + // Register and immediately consume via CommandResponse + e.registerExpectedOrphan(easee.CIRCUIT_MAX_CURRENT_P1) + + resp := easee.SignalRCommandResponse{ + SerialNumber: "EH123456", + ID: int(easee.CIRCUIT_MAX_CURRENT_P1), + Ticks: 111111111, + WasAccepted: true, + } + raw, err := json.Marshal(resp) + require.NoError(t, err) + e.CommandResponse(raw) // consumes the counter + + // A second identical response with counter=0 should be treated as rogue (not panic) + assert.NotPanics(t, func() { + e.CommandResponse(raw) + }) + + // pendingTicks untouched + e.cmdMu.Lock() + assert.Empty(t, e.pendingTicks) + e.cmdMu.Unlock() +} + +func TestEasee_CommandResponse_matchedByID(t *testing.T) { + e := newEasee() + + ch := make(chan easee.SignalRCommandResponse, 1) + e.registerPendingByID(easee.LOCATION, ch) + defer e.unregisterPendingByID(easee.LOCATION) + + // Ticks do NOT match any pendingTicks entry — only the ID matches + resp := easee.SignalRCommandResponse{ + SerialNumber: "EH123456", + ID: int(easee.LOCATION), + Ticks: 999999999, // not in pendingTicks + WasAccepted: true, + ResultCode: 0, + } + + raw, err := json.Marshal(resp) + require.NoError(t, err) + + assert.NotPanics(t, func() { + e.CommandResponse(raw) + }) + + select { + case got := <-ch: + assert.Equal(t, resp.Ticks, got.Ticks) + assert.True(t, got.WasAccepted) + default: + t.Fatal("expected CommandResponse to be delivered to pendingByID channel") + } + + // pendingByID consumed the response — expectedOrphans untouched + assert.False(t, e.consumeExpectedOrphan(easee.LOCATION)) +} + +func TestEasee_registerAndConsumeExpectedOrphan(t *testing.T) { + e := newEasee() + + // Not registered yet — consume returns false + assert.False(t, e.consumeExpectedOrphan(easee.CIRCUIT_MAX_CURRENT_P1)) + + // Register once + e.registerExpectedOrphan(easee.CIRCUIT_MAX_CURRENT_P1) + + // First consume succeeds + assert.True(t, e.consumeExpectedOrphan(easee.CIRCUIT_MAX_CURRENT_P1)) + + // Second consume fails (counter back to zero) + assert.False(t, e.consumeExpectedOrphan(easee.CIRCUIT_MAX_CURRENT_P1)) +} + +func TestEasee_registerExpectedOrphan_multipleRegistrations(t *testing.T) { + e := newEasee() + + // Register twice (two concurrent calls in flight) + e.registerExpectedOrphan(easee.CIRCUIT_MAX_CURRENT_P1) + e.registerExpectedOrphan(easee.CIRCUIT_MAX_CURRENT_P1) + + assert.True(t, e.consumeExpectedOrphan(easee.CIRCUIT_MAX_CURRENT_P1)) + assert.True(t, e.consumeExpectedOrphan(easee.CIRCUIT_MAX_CURRENT_P1)) + assert.False(t, e.consumeExpectedOrphan(easee.CIRCUIT_MAX_CURRENT_P1)) +} + +func TestEasee_Phases1p3p_registersExpectedOrphan(t *testing.T) { + const siteID = 12345 + const circuitID = 67890 + const chargerID = "TESTTEST" + + e := newEasee() + e.charger = chargerID + e.site = siteID + e.circuit = circuitID + + httpmock.ActivateNonDefault(e.Client) + defer httpmock.DeactivateAndReset() + + // Mock GET circuit settings + getURI := fmt.Sprintf("%s/sites/%d/circuits/%d/settings", easee.API, siteID, circuitID) + maxP1, maxP2, maxP3 := 32.0, 32.0, 32.0 + getResp := easee.CircuitSettings{ + MaxCircuitCurrentP1: &maxP1, + MaxCircuitCurrentP2: &maxP2, + MaxCircuitCurrentP3: &maxP3, + } + body, err := json.Marshal(getResp) + require.NoError(t, err) + httpmock.RegisterResponder(http.MethodGet, getURI, + httpmock.NewBytesResponder(200, body)) + + // Mock POST circuit settings — return 200 (sync) + httpmock.RegisterResponder(http.MethodPost, getURI, + httpmock.NewStringResponder(200, "")) + + err = e.Phases1p3p(1) + assert.NoError(t, err) + + // The orphan counter should have been registered before the POST. + // Since no CommandResponse arrived in this test, the counter stays at 1. + e.cmdMu.Lock() + count := e.expectedOrphans[easee.CIRCUIT_MAX_CURRENT_P1] + e.cmdMu.Unlock() + assert.Equal(t, 1, count, "expected orphan should be registered before the POST") +} From 06df09bfb1d1ebb7ae006d6c90125f7f90b155c8 Mon Sep 17 00:00:00 2001 From: chrdal <72504945+chrdal@users.noreply.github.com> Date: Sat, 7 Mar 2026 10:39:08 +0100 Subject: [PATCH 0008/1506] E.G.O. Smart Heater: update docs (#27987) --- templates/definition/charger/ego-smartheater.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/templates/definition/charger/ego-smartheater.yaml b/templates/definition/charger/ego-smartheater.yaml index 6a5dc5dcb..5dbc3ac77 100644 --- a/templates/definition/charger/ego-smartheater.yaml +++ b/templates/definition/charger/ego-smartheater.yaml @@ -9,15 +9,16 @@ requirements: de: | Der Smart Heater muss mit dem lokalen Netzwerk verbunden sein. Nachrichten müssen mindestens alle 60 Sekunden wiederholt werden, sonst schaltet sich der Heater aus Sicherheitsgründen ab. - **Wichtig:** Im Loadpoint-Setup `mincurrent: 4` und `maxcurrent: 16` setzen, sowie `phases: 1` für einphasigen Betrieb konfigurieren. + **Wichtig:** Im Loadpoint-Setup `mincurrent: 0.2` und `maxcurrent: 16` setzen, sowie `phases: 1` für einphasigen Betrieb konfigurieren. en: | The Smart Heater must be connected to the local network. Messages must be repeated at least every 60 seconds, otherwise the heater will turn off for safety reasons. - **Important:** In the loadpoint configuration, set `mincurrent: 4` and `maxcurrent: 16`, as well as `phases: 1` for single-phase operation. + **Important:** In the loadpoint configuration, set `mincurrent: 0.2` and `maxcurrent: 16`, as well as `phases: 1` for single-phase operation. evcc: ["sponsorship"] params: - name: modbus choice: ["tcpip"] + id: 247 render: | type: ego-smartheater {{- include "modbus" . }} From 7e279bc83c1ca666e8a2050e03b7a40a61e9409c Mon Sep 17 00:00:00 2001 From: andig Date: Sat, 7 Mar 2026 11:27:37 +0100 Subject: [PATCH 0009/1506] chore: better message --- core/loadpoint_vehicle.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/loadpoint_vehicle.go b/core/loadpoint_vehicle.go index 9129f6fee..870e03c4e 100644 --- a/core/loadpoint_vehicle.go +++ b/core/loadpoint_vehicle.go @@ -242,7 +242,7 @@ func (lp *Loadpoint) vehicleUnidentified() bool { // request vehicle api refresh while waiting to identify select { case <-lp.vehicleDetectTicker.C: - lp.log.DEBUG.Println("vehicle api refresh") + lp.log.DEBUG.Println("vehicle api cache reset") util.ResetCached() default: } @@ -266,7 +266,7 @@ func (lp *Loadpoint) vehicleDefaultOrDetect() { // startVehicleDetection reset connection timer and starts api refresh timer func (lp *Loadpoint) startVehicleDetection() { // flush all vehicles before detection starts - lp.log.DEBUG.Println("vehicle api refresh") + lp.log.DEBUG.Println("vehicle api cache reset") util.ResetCached() lp.vehicleDetect = lp.clock.Now() From d80d0dbc8060038bf791cbe7b5815471cd85b7cd Mon Sep 17 00:00:00 2001 From: andig Date: Sat, 7 Mar 2026 18:10:59 +0100 Subject: [PATCH 0010/1506] chore: make sharder caching configurable (#27994) --- server/socket.go | 9 +---- util/param_shard.go | 85 +++++++++++++++++++++++++--------------- util/param_shard_test.go | 34 ++++++++++++++++ 3 files changed, 89 insertions(+), 39 deletions(-) create mode 100644 util/param_shard_test.go diff --git a/server/socket.go b/server/socket.go index f096f9d38..40d042051 100644 --- a/server/socket.go +++ b/server/socket.go @@ -145,13 +145,8 @@ func (h *SocketHub) broadcast(p util.Param) { // Sharder splits data into chunks if sp, ok := (p.Val).(util.Sharder); ok { - shards := sp.Shards() - if len(shards) == 0 { - return // nothing changed, skip broadcast - } - - for _, shard := range shards { - msg[k+"."+shard.Key] = json.RawMessage(socketEncode(shard.Value)) + for key, val := range sp.ModifiedShards() { + msg[k+"."+key] = json.RawMessage(socketEncode(val)) } } else { msg[k] = json.RawMessage(socketEncode(p.Val)) diff --git a/util/param_shard.go b/util/param_shard.go index 41411682b..4ce0fe9d7 100644 --- a/util/param_shard.go +++ b/util/param_shard.go @@ -4,6 +4,7 @@ import ( "crypto/sha256" "encoding/json" "fmt" + "iter" "strings" "sync" @@ -13,7 +14,8 @@ import ( // Sharder splits data into chunks, omitting unmodified chunks type Sharder interface { - Shards() []Shard + ModifiedShards() iter.Seq2[string, any] + AllShards() iter.Seq2[string, any] } type Shard struct { @@ -36,41 +38,60 @@ func (s *sharderImpl) MarshalJSON() ([]byte, error) { return json.Marshal(s.struc) } -func (s *sharderImpl) Shards() []Shard { - ff := structs.Fields(s.struc) - res := make([]Shard, 0, len(ff)) +func (s *sharderImpl) AllShards() iter.Seq2[string, any] { + return s.shards(false) +} - shardMu.Lock() - defer shardMu.Unlock() +func (s *sharderImpl) ModifiedShards() iter.Seq2[string, any] { + return s.shards(true) +} - for _, f := range ff { - key := f.Name() - if t := f.Tag("json"); t != "" { - if n := strings.Split(t, ",")[0]; n != "" { - key = n - } - } - - // Use JSON for stable hashing (fmt.Append includes pointer addresses) - b, err := json.Marshal(f.Value()) - if err != nil { - // Fallback to fmt.Append if JSON fails - b = fmt.Append(nil, f.Value()) - } - - hash := sha256.Sum256(b) - if cached, ok := shardCache[s.prefix+key]; ok && hash == cached { - continue - } - shardCache[s.prefix+key] = hash - - res = append(res, Shard{ - Key: key, - Value: f.Value(), - }) +func (s *sharderImpl) shards(useCache bool) iter.Seq2[string, any] { + if useCache { + shardMu.Lock() + defer shardMu.Unlock() } - return res + return func(yield func(string, any) bool) { + for _, f := range structs.Fields(s.struc) { + key := jsonKey(f) + if useCache && s.skipCachedShard(key, f.Value()) { + continue + } + if !yield(key, f.Value()) { + break + } + } + } +} + +func jsonKey(f *structs.Field) string { + key := f.Name() + if t := f.Tag("json"); t != "" { + if n := strings.Split(t, ",")[0]; n != "" { + key = n + } + } + return key +} + +func (s *sharderImpl) skipCachedShard(key string, value any) bool { + // Use JSON for stable hashing (fmt.Append includes pointer addresses) + b, err := json.Marshal(value) + if err != nil { + // Fallback to fmt.Append if JSON fails + b = fmt.Append(nil, value) + } + + hash := sha256.Sum256(b) + cacheKey := s.prefix + key + + if cached, ok := shardCache[cacheKey]; ok && hash == cached { + return true + } + + shardCache[cacheKey] = hash + return false } var _ api.StructMarshaler = (*sharderImpl)(nil) diff --git a/util/param_shard_test.go b/util/param_shard_test.go new file mode 100644 index 000000000..de305306a --- /dev/null +++ b/util/param_shard_test.go @@ -0,0 +1,34 @@ +package util + +import ( + "maps" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSharder(t *testing.T) { + type S struct { + A, B string + } + + s := NewSharder("foo", S{"a", "b"}) + + assert.Equal(t, map[string]any{ + "A": "a", + "B": "b", + }, maps.Collect(s.AllShards()), "non-cached") + + assert.Equal(t, map[string]any{ + "A": "a", + "B": "b", + }, maps.Collect(s.ModifiedShards()), "cache cold") + + assert.Equal(t, map[string]any{}, maps.Collect(s.ModifiedShards()), "cache warm") + + s = NewSharder("foo", S{"a", "c"}) + + assert.Equal(t, map[string]any{ + "B": "c", + }, maps.Collect(s.ModifiedShards()), "cache modied") +} From 1a810ef2483d5b3cd871e4c729577de7987e6b85 Mon Sep 17 00:00:00 2001 From: magicWF Date: Sun, 8 Mar 2026 10:47:21 +0100 Subject: [PATCH 0011/1506] OVMS: add climater (#28011) --- vehicle/ovms.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/vehicle/ovms.go b/vehicle/ovms.go index 5e129d95f..6a741f367 100644 --- a/vehicle/ovms.go +++ b/vehicle/ovms.go @@ -219,6 +219,14 @@ func (v *Ovms) Odometer() (float64, error) { return res.Odometer / 10, err } +var _ api.VehicleClimater = (*Ovms)(nil) + +// Climater implements the api.VehicleClimater interface +func (v *Ovms) Climater() (bool, error) { + res, err := v.chargeG() + return res.Climater == 1, err +} + var _ api.VehicleFinishTimer = (*Ovms)(nil) // FinishTime implements the api.VehicleFinishTimer interface From e03233befc3388b3bb17043919320e74cc9a98a6 Mon Sep 17 00:00:00 2001 From: andig Date: Sun, 8 Mar 2026 11:29:37 +0100 Subject: [PATCH 0012/1506] Revert "OVMS: add climater (#28011)" This reverts commit 1a810ef2483d5b3cd871e4c729577de7987e6b85. --- vehicle/ovms.go | 8 -------- 1 file changed, 8 deletions(-) diff --git a/vehicle/ovms.go b/vehicle/ovms.go index 6a741f367..5e129d95f 100644 --- a/vehicle/ovms.go +++ b/vehicle/ovms.go @@ -219,14 +219,6 @@ func (v *Ovms) Odometer() (float64, error) { return res.Odometer / 10, err } -var _ api.VehicleClimater = (*Ovms)(nil) - -// Climater implements the api.VehicleClimater interface -func (v *Ovms) Climater() (bool, error) { - res, err := v.chargeG() - return res.Climater == 1, err -} - var _ api.VehicleFinishTimer = (*Ovms)(nil) // FinishTime implements the api.VehicleFinishTimer interface From 625fe4d4b9537a3820bbe70674242925d513e31d Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Sun, 8 Mar 2026 12:27:40 +0100 Subject: [PATCH 0013/1506] Translations update from Hosted Weblate (#27978) --- i18n/sv.json | 41 ++++++++++++++++++++++++++++++++++++++++- i18n/tr.json | 1 + 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/i18n/sv.json b/i18n/sv.json index f30e25f94..47e2c44bc 100644 --- a/i18n/sv.json +++ b/i18n/sv.json @@ -106,7 +106,10 @@ "title": "Kontrollera beteende" }, "currency": { - "description": "Används för att samla in och formatera energipriser, kostnader och besparingar." + "description": "Används för att samla in och formatera energipriser, kostnader och besparingar.", + "example": "Ditt laddningspris var {price}. Du sparade {amount}.", + "label": "Valuta", + "title": "Valuta" }, "deviceValue": { "amount": "Belopp", @@ -133,11 +136,13 @@ "energy": "Energi", "events": "Händelser", "feedinPrice": "Inmatningspris", + "forecast": "Prognos", "gridPrice": "Nätpris", "heaterTempLimit": "Värmarbegränsning", "hemsActiveLimit": "Aktiv begränsning", "hemsType": "Kommunikation", "identifier": "RFID-identifierare", + "max": "max", "messengers": "Tjänster", "no": "nej", "odometer": "Mätarställning", @@ -148,6 +153,7 @@ "phases1p3p": "Fasväxlare", "power": "Effekt", "powerRange": "Kraft", + "price": "Pris", "range": "Räckvidd", "singlePhase": "En-fas", "soc": "Laddnivå", @@ -235,6 +241,7 @@ "fromYamlHint": "Obs: Konfigurerad via evcc.yaml. Ta bort posten från filen för att aktivera redigering här.", "hideAdvancedSettings": "Dölj avancerade inställningar", "invalidFileSelected": "Ogiltig fil vald", + "legacy": "äldre", "noFileSelected": "Ingen fil vald.", "off": "av", "on": "på", @@ -702,6 +709,38 @@ "restartingDescription": "Vänligen vänta…", "restartingMessage": "Startar om evcc." }, + "tariff": { + "addForecast": "Lägg till prognos", + "addTariff": "Lägg till tariff", + "co2": { + "description": "CO₂-intensitetsprognos för nätel. För CO₂-optimerad laddning och beräkning av utsläppsbesparingar.", + "titleAdd": "Lägg till CO₂-prognos", + "titleEdit": "Ändra CO₂-prognos" + }, + "co2Services": "CO₂ -tjänster", + "customForecast": "Egen prognos", + "customTariff": "Egen tariff", + "description": "Konfigurera dina energitariffer och prognoser. Använd enhetsbaserad konfiguration för dynamisk styrning eller YAML för statiska inställningar.", + "feedIn": { + "description": "Ersättning för el som matas ut i nätet. För beräkning av verkliga laddningskostnader.", + "titleAdd": "Lägg till tariff för nätexport", + "titleEdit": "Ändra tariff för nätexport" + }, + "generic": "Generiska integrationer", + "grid": { + "description": "Elpris från nätet. För beräkning av verkliga laddningskostnader och prisoptimerad laddning av fordon, styrning av värme eller nätladdning av hushållsbatteriet.", + "titleAdd": "Lägg till tariff för import av el", + "titleEdit": "Ändra tariff för import av el" + }, + "legacyWarning": "Ny tariffkonfiguration tillgänglig! Ta bort konfigurationen här och spara för att använda den nya guidade processen.", + "option": { + "co2": "Lägg till CO₂ prognos", + "feedIn": "Lägg till tariff för export av el", + "grid": "Lägg till tariff för import av el", + "planner": "Lägg till planläggningsprognos", + "solar": "Lägg till solprognos" + } + }, "tariffs": { "description": "Lägg in din energitariff för att beräkna kostnaden för dina laddningar.", "title": "Tariffer" diff --git a/i18n/tr.json b/i18n/tr.json index 49ba19681..a35247b75 100644 --- a/i18n/tr.json +++ b/i18n/tr.json @@ -1272,6 +1272,7 @@ "targetChargeWaitForVehicle": "Doldurma planı hazır. Araç bekleniyor…", "vehicleLimit": "Araç sınırı", "vehicleLimitReached": "Araç sınırına ulaşıldı.", + "waitForAuthorization": "Bağlandı. Yetkilendirme bekleniyor…", "waitForVehicle": "Doldurmaya hazır. Araç bekleniyor…", "welcome": "Bağlantıyı onaylamak için kısa ilk dolum." }, From a0dccea9a301fcd2c1ffee6242fc8a459176d149 Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Sun, 8 Mar 2026 12:32:23 +0100 Subject: [PATCH 0014/1506] Websocket: split welcome message (#27967) --- server/socket.go | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/server/socket.go b/server/socket.go index 40d042051..8b5a9c128 100644 --- a/server/socket.go +++ b/server/socket.go @@ -52,10 +52,11 @@ func (h *SocketHub) ServeWebsocket(w http.ResponseWriter, r *http.Request) { InsecureSkipVerify: true, } - // https://github.com/nhooyr/websocket/issues/218 + // Safari deflate message compression is broken, enable for others + // see: https://github.com/gorilla/websocket/issues/731 ua := strings.ToLower(r.Header.Get("User-Agent")) - if strings.Contains(ua, "safari") && !strings.Contains(ua, "chrome") && !strings.Contains(ua, "android") { - acceptOptions.CompressionMode = websocket.CompressionDisabled + if strings.Contains(ua, "chrome") || strings.Contains(ua, "firefox") { + acceptOptions.CompressionMode = websocket.CompressionContextTakeover } conn, err := websocket.Accept(w, r, acceptOptions) @@ -88,6 +89,7 @@ func (h *SocketHub) subscribe(ctx context.Context, conn *websocket.Conn) error { select { case msg := <-s.send: if err := writeTimeout(ctx, socketWriteTimeout, conn, msg); err != nil { + log.TRACE.Printf("ws write error: %v, len: %.1fkB, msg: %s", err, float64(len(msg))/1024, msg[:min(len(msg), 20)]) return err } case <-ctx.Done(): @@ -112,6 +114,7 @@ func (h *SocketHub) deleteSubscriber(s *socketSubscriber) { func (h *SocketHub) welcome(subscriber *socketSubscriber, params []util.Param) { msg := make(map[string]json.RawMessage, len(params)) + sharders := make(map[string]util.Sharder) for _, p := range params { k := p.Key @@ -119,13 +122,28 @@ func (h *SocketHub) welcome(subscriber *socketSubscriber, params []util.Param) { k = "loadpoints." + p.UniqueID() } - msg[k] = json.RawMessage(socketEncode(p.Val)) + // Sharder values are split into shards and sent as a separate message + if sharder, ok := (p.Val).(util.Sharder); ok { + sharders[k] = sharder + } else { + msg[k] = json.RawMessage(socketEncode(p.Val)) + } } - b, _ := json.Marshal(msg) + // send compact state first, then potentially large sharded data + if b, err := json.Marshal(msg); err == nil { + subscriber.send <- b + } - // should not block - subscriber.send <- b + for k, sharder := range sharders { + for key, val := range sharder.AllShards() { + if b, err := json.Marshal(map[string]json.RawMessage{ + k + "." + key: json.RawMessage(socketEncode(val)), + }); err == nil { + subscriber.send <- b + } + } + } } func (h *SocketHub) broadcast(p util.Param) { From 6e12bd9d59d15a55e7cfe7cc5eed4185faaee812 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Mar 2026 12:32:33 +0100 Subject: [PATCH 0015/1506] Gpio plugin: Raspberry Pi 5 compatibility (#27815) --- charger/openwb-native_linux.go | 64 +++++++++++++++++++--------------- go.mod | 2 +- go.sum | 6 ++-- plugin/gpio_linux.go | 59 ++++++++++++++++--------------- 4 files changed, 71 insertions(+), 60 deletions(-) diff --git a/charger/openwb-native_linux.go b/charger/openwb-native_linux.go index 7ecb7bad1..9455a9caa 100644 --- a/charger/openwb-native_linux.go +++ b/charger/openwb-native_linux.go @@ -9,12 +9,18 @@ import ( "github.com/evcc-io/evcc/charger/openwb/native" "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/modbus" - "github.com/fatih/structs" - "github.com/stianeikeland/go-rpio/v4" + "github.com/warthog618/go-gpiocdev" ) const minCpWaitTime time.Duration = 5 * time.Second +// openWbGpioLines holds GPIO lines for a single charge point +type openWbGpioLines struct { + cp *gpiocdev.Line + ph1 *gpiocdev.Line + ph3 *gpiocdev.Line +} + // OpenWbNative charger implementation type OpenWbNative struct { api.Charger @@ -23,6 +29,7 @@ type OpenWbNative struct { cpWait time.Duration connector int chargeState api.ChargeStatus + gpio openWbGpioLines } // gpioAction defines a single GPIO pin operation with timing @@ -44,8 +51,10 @@ func NewOpenWbNativeFromConfig(ctx context.Context, other map[string]any) (api.C RfId string CpWait time.Duration Connector int + Chip string modbus.Settings `mapstructure:",squash"` }{ + Chip: "gpiochip0", Settings: modbus.Settings{ Baudrate: 9600, Comset: "8N1", @@ -64,11 +73,11 @@ func NewOpenWbNativeFromConfig(ctx context.Context, other map[string]any) (api.C return nil, fmt.Errorf("invalid cpwait value: %v, needs to be greater than %s", cc.CpWait, minCpWaitTime) } - return NewOpenWbNative(ctx, cc.URI, cc.Device, cc.Comset, cc.Baudrate, cc.Protocol(), cc.ID, cc.Phases1p3p, cc.RfId, cc.CpWait, cc.Connector) + return NewOpenWbNative(ctx, cc.URI, cc.Device, cc.Comset, cc.Baudrate, cc.Protocol(), cc.ID, cc.Phases1p3p, cc.RfId, cc.CpWait, cc.Connector, cc.Chip) } // NewOpenWbNative creates OpenWbNative charger -func NewOpenWbNative(ctx context.Context, uri, device, comset string, baudrate int, proto modbus.Protocol, slaveID uint8, hasPhases1p3p bool, rfIdVidPid string, cpWait time.Duration, connector int) (api.Charger, error) { +func NewOpenWbNative(ctx context.Context, uri, device, comset string, baudrate int, proto modbus.Protocol, slaveID uint8, hasPhases1p3p bool, rfIdVidPid string, cpWait time.Duration, connector int, chip string) (api.Charger, error) { log := util.NewLogger("openwb-native") log.DEBUG.Printf("Creating OpenWB native with 3 phases %t, rfid %s, cpwait %s, connector %d", hasPhases1p3p, rfIdVidPid, cpWait.String(), connector) @@ -109,14 +118,21 @@ func NewOpenWbNative(ctx context.Context, uri, device, comset string, baudrate i identify = wb.identify } - // initialize GPIO and set pins to output - if err := rpio.Open(); err != nil { - return nil, fmt.Errorf("failed to open GPIO: %w", err) - } - defer rpio.Close() - - for _, pin := range structs.Fields(native.ChargePoints[connector-1]) { - rpio.Pin(pin.Value().(int)).Output() + // initialize GPIO lines and set pins to output + pins := native.ChargePoints[connector-1] + for _, gpioConfig := range []struct { + dst **gpiocdev.Line + pin int + }{ + {&wb.gpio.cp, pins.PIN_CP}, + {&wb.gpio.ph1, pins.PIN_1P}, + {&wb.gpio.ph3, pins.PIN_3P}, + } { + line, err := gpiocdev.RequestLine(chip, gpioConfig.pin, gpiocdev.AsOutput(0)) + if err != nil { + return nil, fmt.Errorf("failed to open GPIO pin %d: %w", gpioConfig.pin, err) + } + *gpioConfig.dst = line } return decorateOpenWbNative(wb, maxCurrentMillis, phases1p3p, identify), nil @@ -147,21 +163,14 @@ var _ api.Resurrector = (*OpenWbNative)(nil) // WakeUp implements the api.Resurrector interface func (wb *OpenWbNative) WakeUp() error { - cpPin := rpio.Pin(native.ChargePoints[wb.connector-1].PIN_CP) - return wb.runGpioSequence([]gpioAction{ - {pin: cpPin.High, delay: wb.cpWait}, - {pin: cpPin.Low, delay: 0}, + {pin: func() { wb.gpio.cp.SetValue(1) }, delay: wb.cpWait}, + {pin: func() { wb.gpio.cp.SetValue(0) }, delay: 0}, }) } // runGpioSequence executes a sequence of GPIO operations func (wb *OpenWbNative) runGpioSequence(seq []gpioAction) error { - if err := rpio.Open(); err != nil { - return fmt.Errorf("failed to open GPIO: %w", err) - } - defer rpio.Close() - if err := wb.Enable(false); err != nil { return err } @@ -178,17 +187,16 @@ func (wb *OpenWbNative) runGpioSequence(seq []gpioAction) error { // gpioSwitchPhases toggles the GPIOs to switch between 1-phase and 3-phase charging func (wb *OpenWbNative) gpioSwitchPhases(phases int) error { - cpPin := rpio.Pin(native.ChargePoints[wb.connector-1].PIN_CP) - phPin := rpio.Pin(native.ChargePoints[wb.connector-1].PIN_3P) + phLine := wb.gpio.ph3 if phases == 1 { - phPin = rpio.Pin(native.ChargePoints[wb.connector-1].PIN_1P) + phLine = wb.gpio.ph1 } return wb.runGpioSequence([]gpioAction{ - {pin: cpPin.High, delay: time.Second}, // enable phases switch relay (NO), disconnect CP - {pin: phPin.High, delay: wb.cpWait / 2}, // move latching relay to desired position - {pin: phPin.Low, delay: wb.cpWait / 2}, // lock latching relay - {pin: cpPin.Low, delay: time.Second}, // disable phase switching, reconnect CP + {pin: func() { wb.gpio.cp.SetValue(1) }, delay: time.Second}, // enable phases switch relay (NO), disconnect CP + {pin: func() { phLine.SetValue(1) }, delay: wb.cpWait / 2}, // move latching relay to desired position + {pin: func() { phLine.SetValue(0) }, delay: wb.cpWait / 2}, // lock latching relay + {pin: func() { wb.gpio.cp.SetValue(0) }, delay: time.Second}, // disable phase switching, reconnect CP }) } diff --git a/go.mod b/go.mod index 06b86b14b..a163e02d2 100644 --- a/go.mod +++ b/go.mod @@ -94,12 +94,12 @@ require ( github.com/spf13/jwalterweatherman v1.1.0 github.com/spf13/pflag v1.0.10 github.com/spf13/viper v1.21.0 - github.com/stianeikeland/go-rpio/v4 v4.6.0 github.com/stretchr/testify v1.11.1 github.com/teslamotors/vehicle-command v0.4.1 github.com/tess1o/go-ecoflow v1.1.1-0.20251003083510-2ccc15a17e29 github.com/traefik/yaegi v0.16.1 github.com/volkszaehler/mbmd v0.0.0-20260131091050-86c2d25b6103 + github.com/warthog618/go-gpiocdev v0.9.1 gitlab.com/bboehmke/sunny v0.16.0 go.bug.st/serial v1.6.4 go.uber.org/mock v0.6.0 diff --git a/go.sum b/go.sum index 69e15bc1c..7212cc09a 100644 --- a/go.sum +++ b/go.sum @@ -727,8 +727,6 @@ github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3A github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= -github.com/stianeikeland/go-rpio/v4 v4.6.0 h1:eAJgtw3jTtvn/CqwbC82ntcS+dtzUTgo5qlZKe677EY= -github.com/stianeikeland/go-rpio/v4 v4.6.0/go.mod h1:A3GvHxC1Om5zaId+HqB3HKqx4K/AqeckxB7qRjxMK7o= github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= @@ -773,6 +771,10 @@ github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAh github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/volkszaehler/mbmd v0.0.0-20260131091050-86c2d25b6103 h1:I91QVys5UwYJ8G7VVQDeXN2/BPZqEeqCp7NY65BmGTU= github.com/volkszaehler/mbmd v0.0.0-20260131091050-86c2d25b6103/go.mod h1:ZkD3hHsxHxdD2KbqQrMXQd1veeFfggok+breXT+TqWQ= +github.com/warthog618/go-gpiocdev v0.9.1 h1:pwHPaqjJfhCipIQl78V+O3l9OKHivdRDdmgXYbmhuCI= +github.com/warthog618/go-gpiocdev v0.9.1/go.mod h1:dN3e3t/S2aSNC+hgigGE/dBW8jE1ONk9bDSEYfoPyl8= +github.com/warthog618/go-gpiosim v0.1.1 h1:MRAEv+T+itmw+3GeIGpQJBfanUVyg0l3JCTwHtwdre4= +github.com/warthog618/go-gpiosim v0.1.1/go.mod h1:YXsnB+I9jdCMY4YAlMSRrlts25ltjmuIsrnoUrBLdqU= github.com/woodsbury/decimal128 v1.4.0 h1:xJATj7lLu4f2oObouMt2tgGiElE5gO6mSWUjQsBgUlc= github.com/woodsbury/decimal128 v1.4.0/go.mod h1:BP46FUrVjVhdTbKT+XuQh2xfQaGki9LMIRJSFuh6THU= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= diff --git a/plugin/gpio_linux.go b/plugin/gpio_linux.go index 2d63b9e67..404665f2e 100644 --- a/plugin/gpio_linux.go +++ b/plugin/gpio_linux.go @@ -8,7 +8,7 @@ import ( "sync" "github.com/evcc-io/evcc/util" - "github.com/stianeikeland/go-rpio/v4" + "github.com/warthog618/go-gpiocdev" ) func init() { @@ -16,44 +16,44 @@ func init() { } type gpio struct { - mu sync.Mutex - typ GpioType - pin rpio.Pin + mu sync.Mutex + typ GpioType + line *gpiocdev.Line } // NewGpioPluginFromConfig creates a GPIO provider func NewGpioPluginFromConfig(ctx context.Context, other map[string]any) (Plugin, error) { - var cc struct { + cc := struct { Function GpioType Pin int + Chip string + }{ + Chip: "gpiochip0", } if err := util.DecodeOther(other, &cc); err != nil { return nil, err } - p := &gpio{ - typ: cc.Function, - pin: rpio.Pin(cc.Pin), - } - - // initialize GPIO and set pins to input - if err := rpio.Open(); err != nil { - return nil, fmt.Errorf("failed to open GPIO: %w", err) - } - defer rpio.Close() - + var opts []gpiocdev.LineReqOption switch cc.Function { case GpioTypeRead: - p.pin.Input() - p.pin.PullUp() + opts = append(opts, gpiocdev.AsInput, gpiocdev.WithPullUp) case GpioTypeWrite: - p.pin.Output() + opts = append(opts, gpiocdev.AsOutput(0)) default: return nil, fmt.Errorf("invalid type: %s", cc.Function) } - return p, nil + line, err := gpiocdev.RequestLine(cc.Chip, cc.Pin, opts...) + if err != nil { + return nil, fmt.Errorf("failed to open GPIO: %w", err) + } + + return &gpio{ + typ: cc.Function, + line: line, + }, nil } var _ BoolGetter = (*gpio)(nil) @@ -68,12 +68,12 @@ func (p *gpio) BoolGetter() (func() (bool, error), error) { p.mu.Lock() defer p.mu.Unlock() - if err := rpio.Open(); err != nil { - return false, fmt.Errorf("failed to open GPIO: %w", err) + val, err := p.line.Value() + if err != nil { + return false, fmt.Errorf("failed to read GPIO: %w", err) } - defer rpio.Close() - return p.pin.Read() != rpio.Low, nil + return val != 0, nil }, nil } @@ -89,13 +89,14 @@ func (p *gpio) BoolSetter(_ string) (func(bool) error, error) { p.mu.Lock() defer p.mu.Unlock() - if err := rpio.Open(); err != nil { - return fmt.Errorf("failed to open GPIO: %w", err) + val := 0 + if b { + val = 1 } - defer rpio.Close() - val := map[bool]rpio.State{false: rpio.Low, true: rpio.High}[b] - p.pin.Write(val) + if err := p.line.SetValue(val); err != nil { + return fmt.Errorf("failed to write GPIO: %w", err) + } return nil }, nil From cc4beb0b9981dd9ac1c304e44cd21218c48eed63 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Mar 2026 12:34:20 +0100 Subject: [PATCH 0016/1506] Debian packaging: properly handle /etc/evcc-userchoices.sh lifecycle (#27989) --- .goreleaser-nightly.yml | 1 + .goreleaser.yml | 1 + packaging/scripts/postinstall.sh | 5 +++++ packaging/scripts/postremove.sh | 6 ++++++ 4 files changed, 13 insertions(+) diff --git a/.goreleaser-nightly.yml b/.goreleaser-nightly.yml index 4e2d01636..0acc51d6d 100644 --- a/.goreleaser-nightly.yml +++ b/.goreleaser-nightly.yml @@ -85,6 +85,7 @@ nfpms: dependencies: - adduser + - ucf contents: - src: ./packaging/init/evcc.service diff --git a/.goreleaser.yml b/.goreleaser.yml index 1521acf90..f7d61b785 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -105,6 +105,7 @@ nfpms: dependencies: - adduser + - ucf contents: - src: ./packaging/init/evcc.service diff --git a/packaging/scripts/postinstall.sh b/packaging/scripts/postinstall.sh index 89d7414c1..722ba9e28 100644 --- a/packaging/scripts/postinstall.sh +++ b/packaging/scripts/postinstall.sh @@ -58,6 +58,11 @@ KEEP_ETC_SERVICE=$KEEP_ETC_SERVICE KEEP_USR_LOCAL_BIN=$KEEP_USR_LOCAL_BIN EOF + # Register file with ucfr so it is tracked as belonging to the evcc package + if [ -x /usr/bin/ucfr ]; then + ucfr evcc "$USER_CHOICE_CONFIG" + fi + # Execute the user decision if [ -f "$ETC_SERVICE" ] && [ "$KEEP_ETC_SERVICE" -eq 0 ]; then echo "Deleting old service file '$ETC_SERVICE'" diff --git a/packaging/scripts/postremove.sh b/packaging/scripts/postremove.sh index a0398ef77..610f53a68 100644 --- a/packaging/scripts/postremove.sh +++ b/packaging/scripts/postremove.sh @@ -16,6 +16,12 @@ if [ "$1" = "purge" ]; then deb-systemd-helper purge evcc.service > /dev/null || true deb-systemd-helper unmask evcc.service > /dev/null || true fi + + # Remove the user choices config file and deregister it from ucfr + if [ -x /usr/bin/ucfr ]; then + ucfr --purge evcc /etc/evcc-userchoices.sh || true + fi + rm -f /etc/evcc-userchoices.sh fi # if interactive: call `/usr/bin/evcc checkconfig` and check the return code (newer version) From 77e1c8825a4c561cd3f0c8de84f074297d68e123 Mon Sep 17 00:00:00 2001 From: andig Date: Sun, 8 Mar 2026 13:23:37 +0100 Subject: [PATCH 0017/1506] HomeAssistant: allow inverting entities by prepending minus sign (#28020) --- meter/homeassistant.go | 18 +++++++++--------- util/homeassistant/connection.go | 7 +++++++ 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/meter/homeassistant.go b/meter/homeassistant.go index de884c4a3..5ee44d0e2 100644 --- a/meter/homeassistant.go +++ b/meter/homeassistant.go @@ -68,24 +68,24 @@ func NewHomeAssistantFromConfig(other map[string]any) (api.Meter, error) { } // phase currents (optional) - if phases, err := homeassistant.ValidatePhaseEntities(cc.Currents); len(phases) > 0 { - currentsG = func() (float64, float64, float64, error) { return conn.GetPhaseFloatStates(phases) } - } else if err != nil { + if phases, err := homeassistant.ValidatePhaseEntities(cc.Currents); err != nil { return nil, fmt.Errorf("currents: %w", err) + } else if len(phases) > 0 { + currentsG = func() (float64, float64, float64, error) { return conn.GetPhaseFloatStates(phases) } } // phase voltages (optional) - if phases, err := homeassistant.ValidatePhaseEntities(cc.Voltages); len(phases) > 0 { - voltagesG = func() (float64, float64, float64, error) { return conn.GetPhaseFloatStates(phases) } - } else if err != nil { + if phases, err := homeassistant.ValidatePhaseEntities(cc.Voltages); err != nil { return nil, fmt.Errorf("voltages: %w", err) + } else if len(phases) > 0 { + voltagesG = func() (float64, float64, float64, error) { return conn.GetPhaseFloatStates(phases) } } // phase powers (optional) - if phases, err := homeassistant.ValidatePhaseEntities(cc.Powers); len(phases) > 0 { - powersG = func() (float64, float64, float64, error) { return conn.GetPhaseFloatStates(phases) } - } else if err != nil { + if phases, err := homeassistant.ValidatePhaseEntities(cc.Powers); err != nil { return nil, fmt.Errorf("powers: %w", err) + } else if len(phases) > 0 { + powersG = func() (float64, float64, float64, error) { return conn.GetPhaseFloatStates(phases) } } if cc.Soc != "" { diff --git a/util/homeassistant/connection.go b/util/homeassistant/connection.go index 58a823317..484e7cf3b 100644 --- a/util/homeassistant/connection.go +++ b/util/homeassistant/connection.go @@ -103,6 +103,9 @@ func (c *Connection) GetIntState(entity string) (int64, error) { // GetFloatState retrieves the state of an entity as float64 func (c *Connection) GetFloatState(entity string) (float64, error) { + // leading minus sign? + entity, invert := strings.CutPrefix(entity, "-") + state, err := c.GetState(entity) if err != nil { return 0, err @@ -118,6 +121,10 @@ func (c *Connection) GetFloatState(entity string) (float64, error) { return 0, fmt.Errorf("%w for entity %s", err, entity) } + if invert { + value = -value + } + return scale * value, nil } From a8ad65d3617d6d11039e9f3ae40040813e38ca8d Mon Sep 17 00:00:00 2001 From: Bryce Lampe Date: Mon, 9 Mar 2026 00:56:46 -0700 Subject: [PATCH 0018/1506] Mercedes: fix auth (#28049) --- vehicle/mercedes/helper.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vehicle/mercedes/helper.go b/vehicle/mercedes/helper.go index 7181475ca..d91ea8b6a 100644 --- a/vehicle/mercedes/helper.go +++ b/vehicle/mercedes/helper.go @@ -37,9 +37,9 @@ const ( IdUri = "https://id.mercedes-benz.com" RisApplicationVersionEMEA = "1.63.0 (3044)" RisSdkVersionEMEA = "3.26.2" - RisApplicationVersionAPAC = "1.57.0" + RisApplicationVersionAPAC = "1.63.0" RisSdkVersionAPAC = "3.150.1" - RisApplicationVersionNORAM = "3.57.0" + RisApplicationVersionNORAM = "3.63.0" RisSdkVersionNORAM = "3.150.1" RisOsVersion = "26.3" RisOsName = "ios" From 86e201453259f5b4472f8e4b8de4fa51262fa465 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20He=C3=9F?= Date: Mon, 9 Mar 2026 12:09:52 +0100 Subject: [PATCH 0019/1506] ChargeX: add heartbeat to prevent PAC_Target_Timeout fallback (#27795) (#28059) --- charger/chargex.go | 51 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/charger/chargex.go b/charger/chargex.go index da32f7809..9e5ac87a4 100644 --- a/charger/chargex.go +++ b/charger/chargex.go @@ -21,6 +21,8 @@ import ( "context" "encoding/binary" "fmt" + "sync" + "time" "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/util" @@ -35,7 +37,9 @@ type ChargeX struct { log *util.Logger conn *modbus.Connection connector uint16 + mu sync.Mutex curr float64 + enabled bool } const ( @@ -48,8 +52,9 @@ const ( chargexRegModuleState = 8 // States_CP_X offset // Holding registers (read/write) - chargexRegTargetPower = 504 // 0x01F8 PAC_Target_Power (W) - U32 - chargexRegChargingMode = 506 // 0x01FA Charging_Mode (0=Full, 1=Min, 2=NoRed) - U32 + chargexRegTargetTimeout = 500 // 0x01F4 PAC_Target_Timeout (s) - U32 + chargexRegTargetPower = 504 // 0x01F8 PAC_Target_Power (W) - U32 + chargexRegChargingMode = 506 // 0x01FA Charging_Mode (0=Full, 1=Min, 2=NoRed) - U32 ) func init() { @@ -103,9 +108,39 @@ func NewChargeX(ctx context.Context, uri string, id uint8, connector uint16) (ap return nil, fmt.Errorf("failed to initialize charging mode: %w", err) } + // Read target timeout and start heartbeat to keep PAC_Target_Power fresh. + // Without periodic updates the charger reverts to PAC_Default_Power after + // the configured timeout (default 20 min, per Aqueduct Modbus spec §3.6.1). + b, err = conn.ReadHoldingRegisters(chargexRegTargetTimeout, 2) + if err != nil { + return nil, fmt.Errorf("target timeout: %w", err) + } + if u := binary.BigEndian.Uint32(b); u > 0 { + go wb.heartbeat(ctx, time.Duration(u)*time.Second/2) + } + return wb, nil } +func (wb *ChargeX) heartbeat(ctx context.Context, timeout time.Duration) { + for tick := time.Tick(timeout); ; { + select { + case <-tick: + case <-ctx.Done(): + return + } + wb.mu.Lock() + var curr float64 + if wb.enabled { + curr = wb.curr + } + wb.mu.Unlock() + if err := wb.setCurrent(curr); err != nil { + wb.log.ERROR.Println("heartbeat:", err) + } + } +} + // moduleReg returns the register address for a module-specific register func (wb *ChargeX) moduleReg(offset uint16) uint16 { // connector is 1-indexed, convert to 0-indexed module_index @@ -195,12 +230,19 @@ func (wb *ChargeX) Enabled() (bool, error) { // Enable implements the api.Charger interface func (wb *ChargeX) Enable(enable bool) error { + wb.mu.Lock() + defer wb.mu.Unlock() + var current float64 if enable { current = wb.curr } - return wb.setCurrent(current) + err := wb.setCurrent(current) + if err == nil { + wb.enabled = enable + } + return err } // MaxCurrent implements the api.Charger interface @@ -216,6 +258,9 @@ func (wb *ChargeX) MaxCurrentMillis(current float64) error { return fmt.Errorf("invalid current %.1f", current) } + wb.mu.Lock() + defer wb.mu.Unlock() + err := wb.setCurrent(current) if err == nil { wb.curr = current From 69db34e768d6931d1f6d976e1b9fa1023ed57ab3 Mon Sep 17 00:00:00 2001 From: andig Date: Mon, 9 Mar 2026 12:11:43 +0100 Subject: [PATCH 0020/1506] HEMS: refactor handling of production/feedin limits (BC) (#27567) --- assets/js/components/Config/CircuitTags.vue | 62 ++++++++++ assets/js/components/Config/HemsModal.vue | 4 +- assets/js/components/HemsWarning.vue | 4 +- assets/js/types/evcc.ts | 23 ++-- assets/js/utils/circuits.test.ts | 51 +++++++++ assets/js/utils/circuits.ts | 28 +++++ assets/js/views/Config.vue | 62 +++------- cmd/root.go | 5 +- core/site_circuits.go | 9 +- hems/config.go | 3 + hems/eebus/eebus.go | 69 ++---------- hems/fnn/fnn-3.go | 89 ++++++++------- hems/hems/api.go | 15 --- hems/relay/relay.go | 77 ++++--------- hems/shared/helper.go | 30 ----- hems/smartgrid/circuit.go | 42 +++++++ hems/smartgrid/smartgrid.go | 28 +++++ hems/smartgrid/types.go | 2 +- i18n/de.json | 3 +- i18n/en.json | 3 +- tests/config-circuit.spec.ts | 25 ++--- tests/hems-grid.evcc.yaml | 11 ++ tests/hems-yaml.evcc.yaml | 13 ++- tests/hems.spec.ts | 118 +++++++++++--------- 24 files changed, 440 insertions(+), 336 deletions(-) create mode 100644 assets/js/components/Config/CircuitTags.vue create mode 100644 assets/js/utils/circuits.test.ts create mode 100644 assets/js/utils/circuits.ts delete mode 100644 hems/shared/helper.go create mode 100644 hems/smartgrid/circuit.go create mode 100644 tests/hems-grid.evcc.yaml diff --git a/assets/js/components/Config/CircuitTags.vue b/assets/js/components/Config/CircuitTags.vue new file mode 100644 index 000000000..92d1fc1d0 --- /dev/null +++ b/assets/js/components/Config/CircuitTags.vue @@ -0,0 +1,62 @@ + + + diff --git a/assets/js/components/Config/HemsModal.vue b/assets/js/components/Config/HemsModal.vue index b8dce4384..da3510001 100644 --- a/assets/js/components/Config/HemsModal.vue +++ b/assets/js/components/Config/HemsModal.vue @@ -80,7 +80,9 @@ export default { methods: { async loadSessions() { try { - const response = await api.get("gridsessions"); + const response = await api.get("gridsessions", { + validateStatus: (code) => [200, 404].includes(code), + }); this.sessions = response.data || []; } catch (e) { // Silently fail if no sessions available diff --git a/assets/js/components/HemsWarning.vue b/assets/js/components/HemsWarning.vue index 738947d97..1620cdffd 100644 --- a/assets/js/components/HemsWarning.vue +++ b/assets/js/components/HemsWarning.vue @@ -7,7 +7,7 @@ diff --git a/assets/js/components/Battery/BatterySettingsModal.vue b/assets/js/components/Battery/BatterySettingsModal.vue index 181110c52..e62206fa5 100644 --- a/assets/js/components/Battery/BatterySettingsModal.vue +++ b/assets/js/components/Battery/BatterySettingsModal.vue @@ -30,217 +30,14 @@ -
-

- {{ $t("batterySettings.batteryLevel") }}: - {{ fmtSoc(batterySoc) }} - - {{ line }} - -

-
-
- - - {{ fmtSoc(selectedBufferSoc) }} - - - - - - {{ fmtSoc(selectedPrioritySoc) }} - - -
-
-
- -
-
- -
-
- -
-
-
-
-
-
-
-
-
-

- - - {{ $t("batterySettings.legendTopName") }} - - - - - - {{ $t("batterySettings.legendTopAutostart") }} - - - {{ selectedBufferStartName }} - - - - -

-

- - - {{ $t("batterySettings.legendMiddleName") }} - - - - -

-

- - - {{ $t("batterySettings.legendBottomName") }} - - - - -

-
- -
- -
-
-
-
+ - - diff --git a/assets/js/components/Battery/BatteryUsageSettings.vue b/assets/js/components/Battery/BatteryUsageSettings.vue new file mode 100644 index 000000000..234aed40e --- /dev/null +++ b/assets/js/components/Battery/BatteryUsageSettings.vue @@ -0,0 +1,511 @@ + + + + + diff --git a/assets/js/components/BottomTabs/Bar.vue b/assets/js/components/BottomTabs/Bar.vue new file mode 100644 index 000000000..b7aba7cb8 --- /dev/null +++ b/assets/js/components/BottomTabs/Bar.vue @@ -0,0 +1,116 @@ + + + + + diff --git a/assets/js/components/BottomTabs/Item.vue b/assets/js/components/BottomTabs/Item.vue new file mode 100644 index 000000000..71053ab1c --- /dev/null +++ b/assets/js/components/BottomTabs/Item.vue @@ -0,0 +1,130 @@ + + + + + diff --git a/assets/js/components/BottomTabs/MoreItem.vue b/assets/js/components/BottomTabs/MoreItem.vue new file mode 100644 index 000000000..2f93dac30 --- /dev/null +++ b/assets/js/components/BottomTabs/MoreItem.vue @@ -0,0 +1,86 @@ + + + diff --git a/assets/js/components/BottomTabs/MoreMenu.vue b/assets/js/components/BottomTabs/MoreMenu.vue new file mode 100644 index 000000000..ecb26c80d --- /dev/null +++ b/assets/js/components/BottomTabs/MoreMenu.vue @@ -0,0 +1,226 @@ + + + + + diff --git a/assets/js/components/Footer/Footer.vue b/assets/js/components/Footer/Footer.vue index 4011e49c5..aecfd2f56 100644 --- a/assets/js/components/Footer/Footer.vue +++ b/assets/js/components/Footer/Footer.vue @@ -1,7 +1,7 @@ + + diff --git a/assets/js/components/Forecast/PriceChart.vue b/assets/js/components/Forecast/PriceChart.vue new file mode 100644 index 000000000..71e54fb0d --- /dev/null +++ b/assets/js/components/Forecast/PriceChart.vue @@ -0,0 +1,174 @@ + + + + + diff --git a/assets/js/components/Forecast/SolarChart.vue b/assets/js/components/Forecast/SolarChart.vue new file mode 100644 index 000000000..0248fe551 --- /dev/null +++ b/assets/js/components/Forecast/SolarChart.vue @@ -0,0 +1,139 @@ + + + + + diff --git a/assets/js/components/Forecast/chartMixin.ts b/assets/js/components/Forecast/chartMixin.ts new file mode 100644 index 000000000..4df452935 --- /dev/null +++ b/assets/js/components/Forecast/chartMixin.ts @@ -0,0 +1,67 @@ +import { defineComponent, markRaw } from "vue"; +import { echarts } from "./echarts"; + +// chartOption is provided by each consuming component's computed +type WithChartOption = { chartOption: Record }; + +export default defineComponent({ + props: { + chartWidth: { type: Number, required: true }, + endDate: { type: Date, required: true }, + scrollLeft: { type: Number, default: 0 }, + }, + emits: ["scroll"], + data(): { + chart: echarts.ECharts | null; + startDate: Date; + tooltipVisible: boolean; + } { + return { + chart: null, + tooltipVisible: false, + startDate: new Date(), + }; + }, + watch: { + chartOption: { + handler() { + this.chart?.setOption((this as unknown as WithChartOption).chartOption); + }, + deep: true, + }, + scrollLeft(val: number) { + const el = this.$refs["scrollEl"] as HTMLElement; + if (el && Math.abs(el.scrollLeft - val) > 1) { + el.scrollLeft = val; + } + }, + }, + mounted() { + this.updateStartDate(); + this.initChart(); + }, + beforeUnmount() { + this.chart?.dispose(); + }, + methods: { + updateStartDate() { + const now = new Date(); + now.setMinutes(0, 0, 0); + this.startDate = now; + }, + onScroll(e: Event) { + this.$emit("scroll", (e.target as HTMLElement).scrollLeft); + }, + initChart() { + const el = this.$refs["chartEl"] as HTMLElement; + this.chart = markRaw(echarts.init(el)); + this.chart.setOption((this as unknown as WithChartOption).chartOption); + this.chart.on("showTip", () => { + this.tooltipVisible = true; + }); + this.chart.on("hideTip", () => { + this.tooltipVisible = false; + }); + }, + }, +}); diff --git a/assets/js/components/Forecast/echarts.ts b/assets/js/components/Forecast/echarts.ts new file mode 100644 index 000000000..d8f61a122 --- /dev/null +++ b/assets/js/components/Forecast/echarts.ts @@ -0,0 +1,202 @@ +import * as echarts from "echarts/core"; +import colors from "@/colors"; +import type { ForecastSlot } from "./types"; +import { BarChart, LineChart } from "echarts/charts"; +import { + GridComponent, + TooltipComponent, + MarkPointComponent, + AxisPointerComponent, +} from "echarts/components"; +import { SVGRenderer } from "echarts/renderers"; + +echarts.use([ + BarChart, + LineChart, + GridComponent, + TooltipComponent, + AxisPointerComponent, + MarkPointComponent, + SVGRenderer, +]); + +export const FONT_FAMILY = "Montserrat, sans-serif"; + +export function markPointLabel( + color: string, + data: { coord: [string, number]; value: string; label?: { offset?: [number, number] } }[], + startDate?: Date, + endDate?: Date +) { + // threshold: points within the first 5% of the time range get shifted right + const threshold = + startDate && endDate + ? startDate.getTime() + (endDate.getTime() - startDate.getTime()) * 0.05 + : 0; + + const adjustedData = data.map((d) => { + const ts = new Date(d.coord[0]).getTime(); + if (threshold && ts < threshold) { + const baseOffset = d.label?.offset ?? [0, -2]; + return { + ...d, + label: { + ...d.label, + offset: [baseOffset[0] + 30, baseOffset[1]] as [number, number], + }, + }; + } + return d; + }); + + return { + animation: true, + clip: false, + symbol: "rect", + symbolSize: 1, + label: { + show: true, + position: "top", + fontFamily: FONT_FAMILY, + fontSize: 14, + fontWeight: "bold" as const, + color: colors.background, + backgroundColor: color, + borderRadius: 4, + padding: [5, 10], + offset: [0, -2], + formatter: (p: { data: { value: string } }) => p.data.value, + }, + data: adjustedData, + }; +} + +export function tooltipStyle( + color: string, + getChart?: () => { convertToPixel: echarts.ECharts["convertToPixel"] } | null +) { + return { + backgroundColor: color, + borderColor: color, + borderWidth: 0, + padding: [5, 10], + extraCssText: "box-shadow: none; border-radius: 4px; text-align: center;", + position( + point: [number, number], + params: { value: [string, number] }[] | { value: [string, number] }, + el: HTMLElement + ): [number, number] { + const w = el?.offsetWidth || 0; + const h = el?.offsetHeight || 0; + const margin = 8; + const arr = Array.isArray(params) ? params : [params]; + const p = arr[0]; + const chart = getChart?.(); + if (chart && p?.value) { + const pixelY = chart.convertToPixel({ seriesIndex: 0 }, p.value)?.[1]; + if (pixelY != null) { + return [point[0] - w / 2, pixelY - h - margin]; + } + } + return [point[0] - w / 2, point[1] - h - margin]; + }, + textStyle: { + fontFamily: FONT_FAMILY, + fontSize: 14, + fontWeight: "bold" as const, + color: colors.background, + }, + }; +} + +export function forecastGrid() { + return { top: 36, right: 16, bottom: 4, left: 48, borderWidth: 0 }; +} + +export function forecastXAxes(startDate: Date, endDate: Date, weekdayShort: (d: Date) => string) { + return [ + { + type: "time", + min: startDate, + max: endDate, + minInterval: 3600 * 1000, + maxInterval: 3600 * 1000, + axisLabel: { + color: colors.muted, + fontSize: 14, + lineHeight: Math.round(14 * 1.1), + margin: 4, + formatter: (value: number) => { + const date = new Date(value); + const h = date.getHours(); + if (h % 2 !== 0) return ""; + if (h === 0) return `${h}\n${weekdayShort(date)}`; + return `${h}`; + }, + }, + splitLine: { show: false }, + axisLine: { show: false }, + axisTick: { show: false }, + }, + { + type: "time", + position: "bottom", + min: startDate, + max: endDate, + minInterval: 24 * 3600 * 1000, + maxInterval: 24 * 3600 * 1000, + axisLabel: { show: false }, + axisLine: { show: false }, + axisTick: { show: false }, + splitLine: { + show: true, + showMinLine: false, + showMaxLine: false, + lineStyle: { color: colors.border || "", type: "dashed" }, + }, + }, + ]; +} + +export function forecastYAxis(overrides: Record = {}) { + const { axisLabel, ...rest } = overrides; + return { + type: "value", + min: 0, + axisLine: { show: false }, + axisTick: { show: false }, + splitLine: { + showMinLine: false, + showMaxLine: false, + lineStyle: { color: colors.border || "" }, + }, + axisLabel: { + fontSize: 14, + ...(axisLabel as Record), + }, + ...rest, + }; +} + +export function clampStart(ts: string, startDate: Date): string { + return new Date(ts) < startDate ? startDate.toISOString() : ts; +} + +export function filterForecastSlots( + slots: ForecastSlot[], + startDate: Date, + endDate: Date +): ForecastSlot[] { + if (!Array.isArray(slots)) return []; + return slots.filter((s) => new Date(s.end) >= startDate && new Date(s.start) <= endDate); +} + +export function minSlotIndex(slots: ForecastSlot[]): number { + return slots.reduce((min, s, i) => (s.value < (slots[min]?.value || Infinity) ? i : min), 0); +} + +export function maxSlotIndex(slots: ForecastSlot[]): number { + return slots.reduce((max, s, i) => (s.value > (slots[max]?.value || 0) ? i : max), 0); +} + +export { echarts }; diff --git a/assets/js/components/Helper/GenericModal.vue b/assets/js/components/Helper/GenericModal.vue index 2f55ecb32..381fc5319 100644 --- a/assets/js/components/Helper/GenericModal.vue +++ b/assets/js/components/Helper/GenericModal.vue @@ -13,9 +13,9 @@ > @@ -61,6 +67,7 @@ export default defineComponent({ value: { type: Array as PropType, default: () => [] }, options: { type: Array as PropType[]>, default: () => [] }, selectAllLabel: String, + isTopLevel: Boolean, }, emits: ["open", "update:modelValue"], data() { @@ -116,3 +123,15 @@ export default defineComponent({ }, }); + diff --git a/assets/js/views/Log.vue b/assets/js/views/Log.vue index 1c2a6556d..601be30af 100644 --- a/assets/js/views/Log.vue +++ b/assets/js/views/Log.vue @@ -60,6 +60,7 @@
Date: Tue, 7 Apr 2026 19:41:13 +0200 Subject: [PATCH 0202/1506] Revert "Goodwe-wifi: deprecate (#28898)" This reverts commit c375354f08dded981e5694e94e5dbb953a7b61b5. --- templates/definition/meter/goodwe-wifi.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/definition/meter/goodwe-wifi.yaml b/templates/definition/meter/goodwe-wifi.yaml index 73dd6b3b4..b2d4f0c66 100644 --- a/templates/definition/meter/goodwe-wifi.yaml +++ b/templates/definition/meter/goodwe-wifi.yaml @@ -1,6 +1,6 @@ template: goodwe-wifi # UDP implementation is broken -deprecated: true +# deprecated: true products: - brand: GoodWe description: From f1408ad10fa5cfb7ac0e3a3438f96bf90b2b56f1 Mon Sep 17 00:00:00 2001 From: andig Date: Wed, 8 Apr 2026 09:07:19 +0200 Subject: [PATCH 0203/1506] Fix chargeMeter capability lookup for decorated chargers (#28920) --- api/capable_test.go | 46 +++++++++++++++++++++++++++++++++++++++++++++ core/loadpoint.go | 17 ++++++++++++++++- 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/api/capable_test.go b/api/capable_test.go index a365299ea..565dd596e 100644 --- a/api/capable_test.go +++ b/api/capable_test.go @@ -66,6 +66,52 @@ func TestCap_CapableRegistryLookup(t *testing.T) { assert.False(t, ok) } +// decoratedCharger simulates a real decorated charger where Meter is NOT +// directly embedded but only available through the capability registry. +type decoratedCharger struct { + caps map[reflect.Type]any +} + +func (d *decoratedCharger) Capability(typ reflect.Type) (any, bool) { + c, ok := d.caps[typ] + return c, ok +} + +func TestCap_ExtractedCapabilityLosesRegistry(t *testing.T) { + // Reproduces https://github.com/evcc-io/evcc/issues/28915 + // When a Meter is extracted from a decorated charger via Cap[Meter], + // the extracted impl does NOT carry the Capable interface, so + // subsequent Cap[MeterEnergy] on the extracted value fails. + decorated := &decoratedCharger{ + caps: map[reflect.Type]any{ + reflect.TypeFor[Meter](): &testMeterImpl{}, + reflect.TypeFor[MeterEnergy](): &testMeterEnergyImpl{}, + }, + } + + // extract Meter from decorated source (slow path: from caps registry) + mt, ok := Cap[Meter](decorated) + require.True(t, ok) + + // Bug: extracted meter cannot find MeterEnergy because it's a standalone impl + _, ok = Cap[MeterEnergy](mt) + assert.False(t, ok, "extracted meter should NOT have MeterEnergy capability") + + // Fix: wrapping extracted meter with source's Capable preserves registry + type capableMeter struct { + Meter + Capable + } + wrapped := &capableMeter{Meter: mt, Capable: decorated} + + me, ok := Cap[MeterEnergy](wrapped) + require.True(t, ok, "wrapped meter should find MeterEnergy via Capable") + + energy, err := me.TotalEnergy() + assert.NoError(t, err) + assert.Equal(t, 99.0, energy) +} + func TestCap_NilValue(t *testing.T) { _, ok := Cap[MeterEnergy](nil) assert.False(t, ok) diff --git a/core/loadpoint.go b/core/loadpoint.go index 00a51a489..4177e6a34 100644 --- a/core/loadpoint.go +++ b/core/loadpoint.go @@ -395,6 +395,14 @@ func (lp *Loadpoint) requestUpdate() { } } +// capableMeter wraps a meter with capability lookup from its source. +// This preserves capability checks (like MeterEnergy, PhaseCurrents, PhaseVoltages) when +// the meter was extracted from a decorated charger's capability registry. +type capableMeter struct { + api.Meter + api.Capable +} + // configureChargerType ensures that chargeMeter, Rate and Timer can use charger capabilities func (lp *Loadpoint) configureChargerType(charger api.Charger) { var integrated bool @@ -404,7 +412,14 @@ func (lp *Loadpoint) configureChargerType(charger api.Charger) { integrated = true if mt, ok := api.Cap[api.Meter](charger); ok { - lp.chargeMeter = mt + // preserve charger's capability registry so that subsequent + // capability checks on chargeMeter (e.g. MeterEnergy, PhaseCurrents) + // still work for decorated chargers (https://github.com/evcc-io/evcc/issues/28915) + if c, ok := charger.(api.Capable); ok { + lp.chargeMeter = &capableMeter{Meter: mt, Capable: c} + } else { + lp.chargeMeter = mt + } } else { mt := new(wrapper.ChargeMeter) _ = lp.bus.Subscribe(evChargeCurrent, lp.evChargeCurrentWrappedMeterHandler) From e20d76534493fef2d2c907d2612bc08cc930fb37 Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Wed, 8 Apr 2026 09:08:29 +0200 Subject: [PATCH 0204/1506] Forecast UI: compacter, small fixes (#28917) --- assets/js/components/Forecast/Co2Chart.vue | 7 ------- assets/js/components/Forecast/PriceChart.vue | 11 ++--------- assets/js/components/Forecast/SolarChart.vue | 7 ------- assets/js/components/Forecast/chartMixin.ts | 7 +++++++ assets/js/components/Forecast/chartStyles.css | 7 +++++++ assets/js/components/Forecast/echarts.ts | 14 ++++++++------ assets/js/views/Forecast.vue | 9 ++++++--- 7 files changed, 30 insertions(+), 32 deletions(-) create mode 100644 assets/js/components/Forecast/chartStyles.css diff --git a/assets/js/components/Forecast/Co2Chart.vue b/assets/js/components/Forecast/Co2Chart.vue index 438098327..ea688deea 100644 --- a/assets/js/components/Forecast/Co2Chart.vue +++ b/assets/js/components/Forecast/Co2Chart.vue @@ -118,10 +118,3 @@ export default defineComponent({ }, }); - - diff --git a/assets/js/components/Forecast/PriceChart.vue b/assets/js/components/Forecast/PriceChart.vue index 14a99622e..00298fa81 100644 --- a/assets/js/components/Forecast/PriceChart.vue +++ b/assets/js/components/Forecast/PriceChart.vue @@ -131,8 +131,8 @@ export default defineComponent({ ? { opacity: 0.33 } : undefined, })), - barMaxWidth: 4, - barMinWidth: 4, + barMaxWidth: 2, + barMinWidth: 2, itemStyle: { color: priceColor, borderRadius: 2, @@ -164,10 +164,3 @@ export default defineComponent({ }, }); - - diff --git a/assets/js/components/Forecast/SolarChart.vue b/assets/js/components/Forecast/SolarChart.vue index bd13a76d9..528279bef 100644 --- a/assets/js/components/Forecast/SolarChart.vue +++ b/assets/js/components/Forecast/SolarChart.vue @@ -129,10 +129,3 @@ export default defineComponent({ }, }); - - diff --git a/assets/js/components/Forecast/chartMixin.ts b/assets/js/components/Forecast/chartMixin.ts index 4df452935..d8c9cecf3 100644 --- a/assets/js/components/Forecast/chartMixin.ts +++ b/assets/js/components/Forecast/chartMixin.ts @@ -1,5 +1,6 @@ import { defineComponent, markRaw } from "vue"; import { echarts } from "./echarts"; +import "./chartStyles.css"; // chartOption is provided by each consuming component's computed type WithChartOption = { chartOption: Record }; @@ -62,6 +63,12 @@ export default defineComponent({ this.chart.on("hideTip", () => { this.tooltipVisible = false; }); + const resetTouch = () => { + this.chart?.dispatchAction({ type: "hideTip" }); + this.tooltipVisible = false; + }; + el.addEventListener("touchend", resetTouch); + el.addEventListener("touchcancel", resetTouch); }, }, }); diff --git a/assets/js/components/Forecast/chartStyles.css b/assets/js/components/Forecast/chartStyles.css new file mode 100644 index 000000000..5b9f3e23e --- /dev/null +++ b/assets/js/components/Forecast/chartStyles.css @@ -0,0 +1,7 @@ +.forecast-chart-scroll { + overflow-x: auto; + padding-bottom: 4px; + user-select: none; + -webkit-user-select: none; + -webkit-touch-callout: none; +} diff --git a/assets/js/components/Forecast/echarts.ts b/assets/js/components/Forecast/echarts.ts index d8f61a122..d56556b54 100644 --- a/assets/js/components/Forecast/echarts.ts +++ b/assets/js/components/Forecast/echarts.ts @@ -51,7 +51,7 @@ export function markPointLabel( return { animation: true, - clip: false, + clip: true, symbol: "rect", symbolSize: 1, label: { @@ -76,11 +76,12 @@ export function tooltipStyle( getChart?: () => { convertToPixel: echarts.ECharts["convertToPixel"] } | null ) { return { + confine: true, backgroundColor: color, borderColor: color, borderWidth: 0, padding: [5, 10], - extraCssText: "box-shadow: none; border-radius: 4px; text-align: center;", + extraCssText: "box-shadow: none; border-radius: 4px; text-align: center; z-index: 1000;", position( point: [number, number], params: { value: [string, number] }[] | { value: [string, number] }, @@ -110,7 +111,7 @@ export function tooltipStyle( } export function forecastGrid() { - return { top: 36, right: 16, bottom: 4, left: 48, borderWidth: 0 }; + return { top: 36, right: 16, bottom: 4, left: 34, borderWidth: 0 }; } export function forecastXAxes(startDate: Date, endDate: Date, weekdayShort: (d: Date) => string) { @@ -129,7 +130,7 @@ export function forecastXAxes(startDate: Date, endDate: Date, weekdayShort: (d: formatter: (value: number) => { const date = new Date(value); const h = date.getHours(); - if (h % 2 !== 0) return ""; + if (h % 4 !== 0) return ""; if (h === 0) return `${h}\n${weekdayShort(date)}`; return `${h}`; }, @@ -171,7 +172,8 @@ export function forecastYAxis(overrides: Record = {}) { lineStyle: { color: colors.border || "" }, }, axisLabel: { - fontSize: 14, + fontSize: 10, + opacity: 0.5, ...(axisLabel as Record), }, ...rest, @@ -188,7 +190,7 @@ export function filterForecastSlots( endDate: Date ): ForecastSlot[] { if (!Array.isArray(slots)) return []; - return slots.filter((s) => new Date(s.end) >= startDate && new Date(s.start) <= endDate); + return slots.filter((s) => new Date(s.end) > startDate && new Date(s.start) <= endDate); } export function minSlotIndex(slots: ForecastSlot[]): number { diff --git a/assets/js/views/Forecast.vue b/assets/js/views/Forecast.vue index 65d4f6d65..2410839d4 100644 --- a/assets/js/views/Forecast.vue +++ b/assets/js/views/Forecast.vue @@ -157,7 +157,7 @@ import store from "../store"; import { adjustedSolar, ForecastType } from "@/utils/forecast"; import type { ForecastSlot } from "../components/Forecast/types"; -const MIN_HOURS = 38; +const MIN_HOURS = 76; const MAX_HOURS = 96; const SLOTS_PER_HOUR = 4; @@ -219,7 +219,7 @@ export default defineComponent({ chartWidth(): number { const ms = this.chartEndDate.getTime() - this.startDate.getTime(); const slots = Math.ceil(ms / (15 * 60 * 1000)); - return slots * 8 + 56; + return slots * 4 + 56; }, currency() { return store.state?.currency; @@ -242,7 +242,10 @@ export default defineComponent({ : this.forecast.solar; }, solarAdjustText() { - return this.$t("forecast.solarAdjustShort"); + const text = this.$t("forecast.solarAdjustShort"); + const scale = this.forecast.solar?.scale || 1; + const percentDiff = scale * 100 - 100; + return `${text} (${this.fmtPercentage(percentDiff, 0, true)})`; }, solarSubtitle(): string { const s = this.solar; From 3001cfbf41e6aa933d12cbc2e10b81dc428e0277 Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Wed, 8 Apr 2026 09:09:01 +0200 Subject: [PATCH 0205/1506] Energyflow: forecast/battery navigation (#28918) --- assets/js/components/Energyflow/Energyflow.vue | 8 ++++++++ assets/js/components/Top/Navigation.vue | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/assets/js/components/Energyflow/Energyflow.vue b/assets/js/components/Energyflow/Energyflow.vue index a6918d5cb..70335a9f1 100644 --- a/assets/js/components/Energyflow/Energyflow.vue +++ b/assets/js/components/Energyflow/Energyflow.vue @@ -652,12 +652,20 @@ export default defineComponent({ this.detailsCompleteHeight = this.$refs["detailsInner"]?.offsetHeight ?? 0; }, openBatterySettingsModal() { + if (this.experimental) { + this.$router.push("/battery"); + return; + } const modal = Modal.getOrCreateInstance( document.getElementById("batterySettingsModal") as HTMLElement ); modal.show(); }, openForecastModal() { + if (this.experimental) { + this.$router.push("/forecast"); + return; + } const modal = Modal.getOrCreateInstance( document.getElementById("forecastModal") as HTMLElement ); diff --git a/assets/js/components/Top/Navigation.vue b/assets/js/components/Top/Navigation.vue index 5ba73e434..3efc6d1d9 100644 --- a/assets/js/components/Top/Navigation.vue +++ b/assets/js/components/Top/Navigation.vue @@ -249,12 +249,20 @@ export default defineComponent({ modal.show(); }, openBatterySettingsModal() { + if (this.experimental) { + this.$router.push("/battery"); + return; + } const modal = Modal.getOrCreateInstance( document.getElementById("batterySettingsModal") as HTMLElement ); modal.show(); }, openForecastModal() { + if (this.experimental) { + this.$router.push("/forecast"); + return; + } const modal = Modal.getOrCreateInstance( document.getElementById("forecastModal") as HTMLElement ); From 411916982d4a164cd317d56c1d9a91317da3cfaf Mon Sep 17 00:00:00 2001 From: Martin Porsch Date: Wed, 8 Apr 2026 09:10:09 +0200 Subject: [PATCH 0206/1506] Fix: site soc calc was wrong if some batteries had no capacity (#28910) --- core/site.go | 47 +++++++++++++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/core/site.go b/core/site.go index 0d9ceb0eb..b4596dd8b 100644 --- a/core/site.go +++ b/core/site.go @@ -631,27 +631,19 @@ func (site *Site) updateBatteryMeters() { mm[i].Controllable = new(controllable) } - batterySocAcc := lo.SumBy(mm, func(m types.Measurement) float64 { - if m.Soc == nil { - return 0 - } - // weigh soc by capacity - if m.Capacity != nil && *m.Capacity > 0 { - return *m.Soc * *m.Capacity - } - return *m.Soc - }) - totalCapacity := lo.SumBy(mm, func(m types.Measurement) float64 { - if m.Capacity == nil { - return 0 - } - return *m.Capacity - }) + var batterySocAcc float64 + var totalCapacity float64 - // convert weighed socs to total soc - if totalCapacity == 0 { + if lo.SomeBy(mm, func(m types.Measurement) bool { return m.Capacity == nil || *m.Capacity <= 0 }) { + // any capacity is missing + batterySocAcc = sumOfSocs(mm) totalCapacity = float64(len(site.batteryMeters)) + } else { + // all capacities available - weigh soc by capacity + batterySocAcc = weightedSumOfSocs(mm) + totalCapacity = lo.SumBy(mm, func(m types.Measurement) float64 { return *m.Capacity }) } + site.battery.Soc = batterySocAcc / totalCapacity site.battery.Capacity = totalCapacity @@ -672,6 +664,25 @@ func (site *Site) updateBatteryMeters() { site.publish(keys.Battery, site.battery) } +func sumOfSocs(mm []types.Measurement) float64 { + return lo.SumBy(mm, func(m types.Measurement) float64 { + if m.Soc == nil { + return 0 + } + return *m.Soc + }) +} + +func weightedSumOfSocs(mm []types.Measurement) float64 { + return lo.SumBy(mm, func(m types.Measurement) float64 { + if m.Soc == nil { + return 0 + } + // weigh soc by capacity + return *m.Soc * *m.Capacity + }) +} + // updateAuxMeters updates aux meters func (site *Site) updateAuxMeters() { if len(site.auxMeters) == 0 { From 09d319b136566fa0e9367c75c18b54964b7428a5 Mon Sep 17 00:00:00 2001 From: Rene Nulsch <33263735+ReneNulschDE@users.noreply.github.com> Date: Wed, 8 Apr 2026 11:20:56 +0200 Subject: [PATCH 0207/1506] Mercedes: update application and SDK versions (#28938) --- vehicle/mercedes/helper.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/vehicle/mercedes/helper.go b/vehicle/mercedes/helper.go index d91ea8b6a..a53a670f9 100644 --- a/vehicle/mercedes/helper.go +++ b/vehicle/mercedes/helper.go @@ -35,12 +35,12 @@ const ( BffUriNORAM = "https://bff.amap-prod.mobilesdk.mercedes-benz.com" WidgetUriNORAM = "https://widget.amap-prod.mobilesdk.mercedes-benz.com" IdUri = "https://id.mercedes-benz.com" - RisApplicationVersionEMEA = "1.63.0 (3044)" - RisSdkVersionEMEA = "3.26.2" - RisApplicationVersionAPAC = "1.63.0" - RisSdkVersionAPAC = "3.150.1" - RisApplicationVersionNORAM = "3.63.0" - RisSdkVersionNORAM = "3.150.1" + RisApplicationVersionEMEA = "1.65.1 (3174)" + RisSdkVersionEMEA = "4.4.2" + RisApplicationVersionAPAC = "1.65.0" + RisSdkVersionAPAC = "4.4.2" + RisApplicationVersionNORAM = "3.65.0" + RisSdkVersionNORAM = "4.4.2" RisOsVersion = "26.3" RisOsName = "ios" XApplicationNameEMEA = "mycar-store-ece" From f79bddd116636cd319d095bee39d4286920a73f8 Mon Sep 17 00:00:00 2001 From: Hobby-Student <6012744+Hobby-Student@users.noreply.github.com> Date: Wed, 8 Apr 2026 11:22:11 +0200 Subject: [PATCH 0208/1506] Add Askoma ASKOHEAT+ (#28827) --- templates/definition/charger/askoheat.yaml | 84 ++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 templates/definition/charger/askoheat.yaml diff --git a/templates/definition/charger/askoheat.yaml b/templates/definition/charger/askoheat.yaml new file mode 100644 index 000000000..d5af7d6fc --- /dev/null +++ b/templates/definition/charger/askoheat.yaml @@ -0,0 +1,84 @@ +template: askoheat +products: + - brand: Askoma + description: + generic: ASKOHEAT+ +group: heating +requirements: + description: + de: | + Der ASKOHEAT+ muss über Modbus TCP erreichbar sein (Port 502). + Über das Webinterface sicherstellen, dass "Load Setpoint" im Input Setting aktiviert ist. + + **Wichtig:** Im Loadpoint `mincurrent` und `maxcurrent` passend zum Gerät setzen, + sowie `phases: 1` für einphasigen Betrieb konfigurieren. + Beispiel AHIR-BI-plus-1.75: `mincurrent: 1.1`, `maxcurrent: 7.6` (bei 230V, 1 Phase). + en: | + The ASKOHEAT+ must be reachable via Modbus TCP (port 502). + Ensure "Load Setpoint" is enabled in Input Settings via the web interface. + + **Important:** In the loadpoint configuration, set `mincurrent` and `maxcurrent` to match + your device, and configure `phases: 1` for single-phase operation. + Example AHIR-BI-plus-1.75: `mincurrent: 1.1`, `maxcurrent: 7.6` (at 230V, 1 phase). +params: + - name: host + - name: port + default: 502 + - name: id + default: 1 + - name: tempsensor + type: choice + choice: ["0", "1", "2", "3", "4"] + default: "0" + description: + de: Temperatursensor + en: Temperature sensor +render: | + type: heatpump + setmaxpower: + source: watchdog + timeout: 30s + set: + source: modbus + uri: {{ .host }}:{{ .port }} + id: {{ .id }} + register: + address: 319 + type: writesingle + encoding: int16 + getmaxpower: + source: modbus + uri: {{ .host }}:{{ .port }} + id: {{ .id }} + register: + address: 319 + type: input + encoding: int16 + power: + source: modbus + uri: {{ .host }}:{{ .port }} + id: {{ .id }} + register: + address: 317 + type: input + encoding: uint16 + temp: + source: modbus + uri: {{ .host }}:{{ .port }} + id: {{ .id }} + register: + address: {{ add 325 (mul (int .tempsensor) 2) }} + type: input + encoding: float32 + limittemp: + source: modbus + uri: {{ .host }}:{{ .port }} + id: {{ .id }} + register: + address: 640 + type: input + encoding: uint16 + icon: waterheater + features: + - heating + - integrateddevice From dec0e7d5b22530cedb038c8f647e677df72505a7 Mon Sep 17 00:00:00 2001 From: RenatusRo <126881912+RenatusRo@users.noreply.github.com> Date: Wed, 8 Apr 2026 12:35:17 +0200 Subject: [PATCH 0209/1506] Optimizer: improve error message for solar forecast (#28934) --- core/site_optimizer.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/core/site_optimizer.go b/core/site_optimizer.go index b351a9596..8ce7eb9b0 100644 --- a/core/site_optimizer.go +++ b/core/site_optimizer.go @@ -136,12 +136,16 @@ func (site *Site) optimizerUpdate(battery []types.Measurement) error { feedIn := currentRates(site.GetTariff(api.TariffUsageFeedIn)) minLen := lo.Min([]int{len(grid), len(feedIn)}) - if solarTariff != nil { - // allow empty solar forecast + // exclude empty solar forecast from minLen + if solarTariff != nil && len(solar) > 0 { minLen = min(minLen, len(solar)) } - if minLen < 8 { - return fmt.Errorf("not enough slots for optimization: %d (grid=%d, feedIn=%d, solar=%d)", minLen, len(grid), len(feedIn), len(solar)) + const expectedSlots = 8 + if minLen < expectedSlots { + if solarTariff != nil { + return fmt.Errorf("not enough forecast slots for meaningful optimization: %d < %d (grid=%d, feedIn=%d, solar=%d)", minLen, expectedSlots, len(grid), len(feedIn), len(solar)) + } + return fmt.Errorf("not enough forecast slots for meaningful optimization: %d < %d (grid=%d, feedIn=%d)", minLen, expectedSlots, len(grid), len(feedIn)) } now := time.Now() From 80900fd64846e46a2867a9b9e31cd95492fd9ccb Mon Sep 17 00:00:00 2001 From: Rishabh Vaish Date: Wed, 8 Apr 2026 03:36:38 -0700 Subject: [PATCH 0210/1506] chore: fix response body leak on 404 and hoist LoadLocation (#28612) --- tariff/pun.go | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/tariff/pun.go b/tariff/pun.go index fa86988e2..a60f0fd88 100644 --- a/tariff/pun.go +++ b/tariff/pun.go @@ -4,6 +4,7 @@ import ( "archive/zip" "bytes" "encoding/xml" + "errors" "fmt" "net/http" "net/http/cookiejar" @@ -19,6 +20,12 @@ import ( "github.com/evcc-io/evcc/util/request" ) +// ErrPunDataNotAvailable indicates that GME has not yet published prices for the requested day. +var ErrPunDataNotAvailable = errors.New("PUN data not available") + +// romeLocation is resolved once at package init to avoid repeated filesystem lookups. +var romeLocation *time.Location + type Pun struct { *embed log *util.Logger @@ -46,6 +53,7 @@ var _ api.Tariff = (*Pun)(nil) func init() { registry.Add("pun", NewPunFromConfig) + romeLocation, _ = time.LoadLocation("Europe/Rome") } func NewPunFromConfig(other map[string]any) (api.Tariff, error) { @@ -137,9 +145,14 @@ func (t *Pun) getData(day time.Time) (api.Rates, error) { } resp, err := client.Do(req) - if err != nil || resp.StatusCode == http.StatusNotFound { + if err != nil { return nil, err } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusNotFound { + return nil, fmt.Errorf("%w: %s", ErrPunDataNotAvailable, day.Format("2006-01-02")) + } body, err := request.ReadBody(resp) if err != nil { @@ -193,17 +206,12 @@ func (t *Pun) getData(day time.Time) (api.Rates, error) { date = date.AddDate(0, 0, -1) } - location, err := time.LoadLocation("Europe/Rome") - if err != nil { - return nil, fmt.Errorf("load location: %w", err) - } - price, err := strconv.ParseFloat(strings.ReplaceAll(p.PUN, ",", "."), 64) if err != nil { return nil, fmt.Errorf("parse price: %w", err) } - ts := time.Date(date.Year(), date.Month(), date.Day(), hour-1, 0, 0, 0, location) + ts := time.Date(date.Year(), date.Month(), date.Day(), hour-1, 0, 0, 0, romeLocation) ar := api.Rate{ Start: ts, End: ts.Add(time.Hour), From 462dbd0def95f86cea51041579437adf8a9f2854 Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Wed, 8 Apr 2026 17:24:08 +0200 Subject: [PATCH 0211/1506] Config UI: fix default for required choice params (#28935) --- assets/js/components/Config/DeviceModal/index.ts | 1 + assets/js/components/Config/LoadpointModal.vue | 6 +----- assets/js/components/Config/PropertyField.vue | 8 ++++---- assets/js/components/Config/VehicleModal.vue | 3 ++- tests/boost.spec.ts | 1 + tests/config-loadpoint.spec.ts | 4 ++-- 6 files changed, 11 insertions(+), 12 deletions(-) diff --git a/assets/js/components/Config/DeviceModal/index.ts b/assets/js/components/Config/DeviceModal/index.ts index f1304d289..afa1123d8 100644 --- a/assets/js/components/Config/DeviceModal/index.ts +++ b/assets/js/components/Config/DeviceModal/index.ts @@ -68,6 +68,7 @@ export type ApiData = { icon?: string; usage?: MeterTemplateUsage; title?: string; + priority?: number; identifiers?: string[]; [key: string]: any; }; diff --git a/assets/js/components/Config/LoadpointModal.vue b/assets/js/components/Config/LoadpointModal.vue index 7f5defd2f..96739b376 100644 --- a/assets/js/components/Config/LoadpointModal.vue +++ b/assets/js/components/Config/LoadpointModal.vue @@ -114,7 +114,6 @@ v-model="values.defaultMode" type="Choice" class="w-100" - required :choice="[ { key: '', name: '---' }, { key: 'off', name: $t('main.mode.off') }, @@ -302,8 +301,8 @@ type="Choice" size="w-100" class="me-2" - :choice="priorityOptions" required + :choice="priorityOptions" /> @@ -435,7 +434,6 @@ type="Choice" class="me-2" :choice="circuitOptions" - required />
@@ -465,7 +463,6 @@ type="Choice" class="me-2" :choice="allVehicleOptions" - required /> @@ -744,7 +741,6 @@ export default { name: string; }[]; result[0]!.name = "0 (default)"; - result[0]!.key = undefined; result[10]!.name = "10 (highest)"; return result; }, diff --git a/assets/js/components/Config/PropertyField.vue b/assets/js/components/Config/PropertyField.vue index 6c1ee9d48..d6d823747 100644 --- a/assets/js/components/Config/PropertyField.vue +++ b/assets/js/components/Config/PropertyField.vue @@ -51,9 +51,10 @@ v-model="value" class="form-select" :class="inputClasses" + :required="required" :disabled="disabled" > - + + + + + + @@ -473,6 +487,9 @@ import ModbusProxyIcon from "../components/MaterialIcon/ModbusProxy.vue"; import ModbusProxyModal from "../components/Config/ModbusProxyModal.vue"; import MqttIcon from "../components/MaterialIcon/Mqtt.vue"; import MqttModal from "../components/Config/MqttModal.vue"; +import RemoteAccessIcon from "../components/MaterialIcon/RemoteAccess.vue"; +import RemoteModal from "../components/Config/Remote/RemoteModal.vue"; +import { isRemoteClientActive } from "@/utils/remote"; import NetworkModal from "../components/Config/NetworkModal.vue"; import NotificationIcon from "../components/MaterialIcon/Notification.vue"; import OptimizerIcon from "../components/MaterialIcon/Optimizer.vue"; @@ -504,6 +521,7 @@ import type { SiteConfig, DeviceType, Notification, + Remote, } from "@/types/evcc"; import { CURRENCY, GRID_CONTROL } from "@/types/evcc"; import { circuitTree } from "@/utils/circuits"; @@ -555,6 +573,8 @@ export default defineComponent({ ModbusProxyModal, MqttIcon, MqttModal, + RemoteAccessIcon, + RemoteModal, NetworkModal, NotificationIcon, OptimizerIcon, @@ -763,6 +783,30 @@ export default defineComponent({ return result; }, + remote(): Remote | undefined { + return store.state?.remote; + }, + remoteTags(): DeviceTags { + const remote = this.remote; + if (!remote?.status?.url) { + return { configured: { value: false } }; + } + const tags: DeviceTags = { + enabled: { value: remote.config?.enabled }, + connected: { value: remote.status?.connected }, + }; + if (remote.status?.loginBlocked) { + tags["loginBlocked"] = { value: true, error: true }; + } + if (remote.status?.connected) { + const lastSeen = remote.status?.lastSeen; + const count = lastSeen + ? Object.keys(lastSeen).filter((u) => isRemoteClientActive(lastSeen, u)).length + : 0; + tags["activeClients"] = { value: count }; + } + return tags; + }, sponsor() { return store.state?.sponsor; }, diff --git a/cmd/root.go b/cmd/root.go index a1d10adab..be00a6c64 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -22,6 +22,7 @@ import ( "github.com/evcc-io/evcc/server/eebus" "github.com/evcc-io/evcc/server/mcp" "github.com/evcc-io/evcc/server/network" + "github.com/evcc-io/evcc/server/remote" "github.com/evcc-io/evcc/server/updater" "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/auth" @@ -237,6 +238,12 @@ func runRoot(cmd *cobra.Command, args []string) { // publish to UI go socketHub.Run(pipe.NewDropper(ignoreEmpty).Pipe(tee.Attach()), cache) + // remote access tunnel + var remoteAccess *remote.Remote + if remoteHost := os.Getenv("EVCC_REMOTE_ACCESS"); remoteHost != "" { + remoteAccess = remote.New(remoteHost, httpd.Router(), valueChan) + } + // signal ui listening valueChan <- util.Param{Key: keys.StartupCompleted, Val: false} @@ -385,6 +392,11 @@ func runRoot(cmd *cobra.Command, args []string) { YamlSource: yamlSource.tariffs, }} + // publish remote access status + if remoteAccess != nil { + valueChan <- util.Param{Key: keys.Remote, Val: remoteAccess.ConfigStatus()} + } + // publish system infos valueChan <- util.Param{Key: keys.Version, Val: util.FormattedVersion()} valueChan <- util.Param{Key: keys.Config, Val: viper.ConfigFileUsed()} @@ -429,7 +441,7 @@ func runRoot(cmd *cobra.Command, args []string) { log.INFO.Println("evcc was stopped by user. OS should restart the service. Or restart manually.") err = errors.New("restart required") // https://gokrazy.org/development/process-interface/ once.Do(func() { close(stopC) }) // signal loop to end - }, viper.ConfigFileUsed()) + }, viper.ConfigFileUsed(), remoteAccess) // show and check version, reduce api load during development if util.Version != util.DevVersion { diff --git a/core/keys/global.go b/core/keys/global.go index f8fa5eba1..354ad27bc 100644 --- a/core/keys/global.go +++ b/core/keys/global.go @@ -30,6 +30,9 @@ const ( Telemetry = "telemetry" Optimizer = "optimizer" DemoMode = "demoMode" + Remote = "remote" + RemoteClients = "remoteClients" + RemoteLastSeen = "remoteLastSeen" AuthDisabled = "authDisabled" AuthProviders = "authProviders" ) diff --git a/go.mod b/go.mod index c597feb3e..27a5a776b 100644 --- a/go.mod +++ b/go.mod @@ -54,6 +54,7 @@ require ( github.com/gregdel/pushover v1.4.0 github.com/grid-x/modbus v0.0.0-20260325140807-cf9e1b9daae0 github.com/hashicorp/go-version v1.9.0 + github.com/hashicorp/yamux v0.1.2 github.com/hasura/go-graphql-client v0.16.0 github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83 github.com/influxdata/influxdb-client-go/v2 v2.14.0 @@ -85,6 +86,7 @@ require ( github.com/robertkrimen/otto v0.5.1 github.com/samber/lo v1.53.0 github.com/sandrolain/httpcache v1.4.0 + github.com/sethvargo/go-password v0.3.1 github.com/sirupsen/logrus v1.9.4 github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 github.com/smallnest/chanx v1.2.0 diff --git a/go.sum b/go.sum index 405e56c3c..427b45073 100644 --- a/go.sum +++ b/go.sum @@ -391,6 +391,8 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= +github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= github.com/hasura/go-graphql-client v0.16.0 h1:DQLfp+djj4j5NPdJkGYym8J55hpm5etML1zqgco78Qc= github.com/hasura/go-graphql-client v0.16.0/go.mod h1:z/sO2T0zI+HnPNIevQcs+7xA6/gDOc8hgHMrNBzfL2c= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= @@ -686,6 +688,8 @@ github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc= github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg= github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfvNt0= github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0= +github.com/sethvargo/go-password v0.3.1 h1:WqrLTjo7X6AcVYfC6R7GtSyuUQR9hGyAj/f1PYQZCJU= +github.com/sethvargo/go-password v0.3.1/go.mod h1:rXofC1zT54N7R8K/h1WDUdkf9BOx5OptoxrMBcrXzvs= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= diff --git a/i18n/de.json b/i18n/de.json index 4e34cb620..3e24344f7 100644 --- a/i18n/de.json +++ b/i18n/de.json @@ -113,6 +113,7 @@ "title": "Währung" }, "deviceValue": { + "activeClients": "Aktive Clients", "amount": "Anzahl", "broker": "Broker", "bucket": "Bucket", @@ -144,6 +145,7 @@ "hemsActiveLimit": "Aktives Limit", "hemsType": "Kommunikation", "identifier": "RFID-Kennung", + "loginBlocked": "Login-Limit erreicht", "max": "max", "messengers": "Dienste", "no": "Nein", @@ -631,6 +633,38 @@ "titleAdd": "PV-Anlage hinzufügen", "titleEdit": "PV-Zähler bearbeiten" }, + "remote": { + "active": "Aktiv", + "addClient": "Client hinzufügen", + "addClientDescription": "Zugangsdaten werden ausschließlich lokal auf deiner evcc-Instanz gespeichert und überprüft.", + "addClientTitle": "Remote-Client hinzufügen", + "clientCreated": "Client erstellt", + "clients": "Clients", + "confirmDelete": "Client löschen?", + "connected": "Verbunden", + "createClient": "Client erstellen", + "description": "Externer Zugriff auf die evcc-Installation über die evcc-App. Ohne Portfreigabe oder VPN.", + "deviceName": "Gerätename", + "disconnected": "Nicht verbunden", + "done": "Fertig", + "enableLabel": "Remotezugriff aktivieren", + "expiration": "Ablauf", + "expirationNone": "Nie", + "expired": "abgelaufen", + "expiresIn": "läuft {time} ab", + "lastActive": "aktiv {time}", + "loginBlocked": "Remote-Logins sind wegen zu vieler fehlgeschlagener Anmeldeversuche für eine Minute gesperrt.", + "manualLogin": "Oder melde dich manuell unter {url} in deinem Browser mit diesen Zugangsdaten an:", + "noClients": "Noch keine Clients. Niemand kann sich verbinden.", + "password": "Passwort", + "passwordOnce": "Dieses Passwort wird nur einmal angezeigt. QR-Code scannen oder jetzt kopieren. Es wird später nicht mehr angezeigt.", + "qrInstall": "Installiere die evcc-App für {ios} oder {android}.", + "qrScan": "Scanne den Code mit der Kamera deines Smartphones, um dich zu verbinden. Klicke ihn an, wenn du bereits dein Handy nutzt.", + "removeClient": "Client entfernen", + "title": "Remotezugriff", + "url": "Öffentliche URL", + "username": "Benutzername" + }, "section": { "additionalMeter": "Zusätzliche Zähler", "general": "Allgemein", diff --git a/i18n/en.json b/i18n/en.json index f87439e35..b17667740 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -113,6 +113,7 @@ "title": "Currency" }, "deviceValue": { + "activeClients": "Active clients", "amount": "Amount", "broker": "Broker", "bucket": "Bucket", @@ -126,6 +127,7 @@ "chargedEnergy": "Charged", "co2": "Grid CO₂", "configured": "Configured", + "connected": "Connected", "connections": "Connections", "controllable": "Controllable", "currency": "Currency", @@ -144,6 +146,7 @@ "hemsActiveLimit": "Active limit", "hemsType": "Communication", "identifier": "RFID-Identifier", + "loginBlocked": "Login limit reached", "max": "max", "messengers": "Services", "no": "no", @@ -631,6 +634,38 @@ "titleAdd": "Add Solar Meter", "titleEdit": "Edit Solar Meter" }, + "remote": { + "active": "Active", + "addClient": "Add client", + "addClientDescription": "Credentials are stored and verified only locally on your evcc instance.", + "addClientTitle": "Add Remote Client", + "clientCreated": "Client created", + "clients": "Clients", + "confirmDelete": "Delete client?", + "connected": "Connected", + "createClient": "Create client", + "description": "Access your evcc installation from anywhere using the evcc mobile app. No port forwarding or VPN required.", + "deviceName": "Device name", + "disconnected": "Disconnected", + "done": "Done", + "enableLabel": "Enable remote access", + "expiration": "Expiration", + "expirationNone": "Never", + "expired": "expired", + "expiresIn": "expires {time}", + "lastActive": "active {time}", + "loginBlocked": "Remote logins are blocked for one minute due to too many failed login attempts.", + "manualLogin": "Or sign in manually at {url} in your browser using these credentials:", + "noClients": "No clients yet. No one can connect yet.", + "password": "Password", + "passwordOnce": "This password is shown only once. Scan the QR code or copy it now. You won't be able to see it again.", + "qrInstall": "Install the evcc app for {ios} or {android}.", + "qrScan": "Scan the code with your phone's camera to connect. Click it, if you're already using your phone.", + "removeClient": "Remove client", + "title": "Remote Access", + "url": "Public URL", + "username": "Username" + }, "section": { "additionalMeter": "Additional meters", "general": "General", diff --git a/package-lock.json b/package-lock.json index 829f0c83a..55d5578b7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "dayjs": "^1.11.20", "echarts": "^6.0.0", "monaco-editor": "0.52.2", + "qrcode": "^1.5.4", "smoothscroll-polyfill": "^0.4.4", "snarkdown": "^2.0.0", "vue": "^3.5.31", @@ -40,6 +41,7 @@ "@types/bootstrap": "^5.2.10", "@types/canvas-confetti": "^1.9.0", "@types/kill-port": "^2.0.3", + "@types/qrcode": "^1.5.6", "@types/smoothscroll-polyfill": "^0.3.4", "@types/wait-on": "^5.3.4", "@types/ws": "^8.18.1", @@ -3435,6 +3437,16 @@ "undici-types": "~7.18.0" } }, + "node_modules/@types/qrcode": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz", + "integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/readable-stream": { "version": "4.0.23", "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.23.tgz", @@ -4439,7 +4451,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -4449,7 +4460,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -5115,6 +5125,15 @@ "node": ">=6" } }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001780", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001780.tgz", @@ -5324,7 +5343,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -5337,7 +5355,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, "node_modules/combined-stream": { @@ -5655,6 +5672,15 @@ } } }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/deep-eql": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", @@ -5790,6 +5816,12 @@ "node": ">=8" } }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", + "license": "MIT" + }, "node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -6993,7 +7025,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" @@ -7686,7 +7717,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -9165,6 +9195,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -9215,7 +9254,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -9346,6 +9384,15 @@ "node": ">=18" } }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -9702,6 +9749,173 @@ "node": ">=6" } }, + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "license": "MIT", + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/qrcode/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/qrcode/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/qrcode/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/qrcode/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "license": "ISC" + }, + "node_modules/qrcode/node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/qs": { "version": "6.15.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", @@ -9997,6 +10211,21 @@ "regjsparser": "bin/parser" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "license": "ISC" + }, "node_modules/resolve": { "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", @@ -10298,6 +10527,12 @@ "node": ">=10" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -12470,6 +12705,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "license": "ISC" + }, "node_modules/which-typed-array": { "version": "1.1.20", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", diff --git a/package.json b/package.json index 17d725fde..ee0fe7d6f 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "dayjs": "^1.11.20", "echarts": "^6.0.0", "monaco-editor": "0.52.2", + "qrcode": "^1.5.4", "smoothscroll-polyfill": "^0.4.4", "snarkdown": "^2.0.0", "vue": "^3.5.31", @@ -62,6 +63,7 @@ "@types/bootstrap": "^5.2.10", "@types/canvas-confetti": "^1.9.0", "@types/kill-port": "^2.0.3", + "@types/qrcode": "^1.5.6", "@types/smoothscroll-polyfill": "^0.3.4", "@types/wait-on": "^5.3.4", "@types/ws": "^8.18.1", diff --git a/server/http.go b/server/http.go index a0c4cfe4f..a39d2d45a 100644 --- a/server/http.go +++ b/server/http.go @@ -15,6 +15,7 @@ import ( "github.com/evcc-io/evcc/hems/shm" "github.com/evcc-io/evcc/server/assets" "github.com/evcc-io/evcc/server/eebus" + "github.com/evcc-io/evcc/server/remote" "github.com/evcc-io/evcc/server/service" "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/auth" @@ -225,7 +226,7 @@ func (s *HTTPd) RegisterSiteHandlers(site site.API) { } // RegisterSystemHandler provides system level handlers -func (s *HTTPd) RegisterSystemHandler(site *core.Site, pub publisher, cache *util.ParamCache, auth auth.Auth, shutdown func(), configFile string) { +func (s *HTTPd) RegisterSystemHandler(site *core.Site, pub publisher, cache *util.ParamCache, auth auth.Auth, shutdown func(), configFile string, remoteAccess *remote.Remote) { router := s.Server.Handler.(*mux.Router) // api @@ -299,6 +300,13 @@ func (s *HTTPd) RegisterSystemHandler(site *core.Site, pub publisher, cache *uti "optimizer": {"POST", "/optimizer/{value:[01truefalse]+}", boolHandler(setOptimizer(pub), getOptimizer)}, } + if remoteAccess != nil { + routes["remote"] = route{"POST", "/remote/{value:[01truefalse]+}", boolHandler(remoteAccess.Enable, remoteAccess.Enabled)} + routes["remoteclients"] = route{"GET", "/remote/clients", remoteClientsHandler(remoteAccess)} + routes["createremoteclient"] = route{"POST", "/remote/clients", createRemoteClientHandler(remoteAccess)} + routes["deleteremoteclient"] = route{"DELETE", "/remote/clients", deleteRemoteClientHandler(remoteAccess)} + } + // yaml handlers for key, fun := range map[string]func() (any, any){ keys.Hems: func() (any, any) { return map[string]any{}, config.Typed{} }, diff --git a/server/http_remote_handler.go b/server/http_remote_handler.go new file mode 100644 index 000000000..5b7b417a2 --- /dev/null +++ b/server/http_remote_handler.go @@ -0,0 +1,62 @@ +package server + +import ( + "encoding/json" + "net/http" + "time" + + "github.com/evcc-io/evcc/server/remote" +) + +// remoteClientsHandler returns the list of remote tunnel clients. +func remoteClientsHandler(r *remote.Remote) http.HandlerFunc { + return func(w http.ResponseWriter, _ *http.Request) { + jsonWrite(w, r.Clients()) + } +} + +// createRemoteClientHandler creates a new tunnel client and returns the +// cleartext password (shown to the user only once). +func createRemoteClientHandler(r *remote.Remote) http.HandlerFunc { + return func(w http.ResponseWriter, req *http.Request) { + var body struct { + Username string `json:"username"` + ExpiresIn int64 `json:"expiresIn"` // seconds; 0 = never + } + if err := json.NewDecoder(req.Body).Decode(&body); err != nil { + jsonError(w, http.StatusBadRequest, err) + return + } + + client, password, err := r.CreateClient(body.Username, time.Duration(body.ExpiresIn)*time.Second) + if err != nil { + jsonError(w, http.StatusBadRequest, err) + return + } + + jsonWrite(w, struct { + Username string `json:"username"` + Password string `json:"password"` + CreatedAt time.Time `json:"createdAt"` + ExpiresAt *time.Time `json:"expiresAt,omitempty"` + }{ + Username: client.Username, + Password: password, + CreatedAt: client.CreatedAt, + ExpiresAt: client.ExpiresAt, + }) + } +} + +// deleteRemoteClientHandler removes a tunnel client by username. +// Username is passed as a query parameter to allow arbitrary characters. +func deleteRemoteClientHandler(r *remote.Remote) http.HandlerFunc { + return func(w http.ResponseWriter, req *http.Request) { + username := req.URL.Query().Get("username") + if err := r.DeleteClient(username); err != nil { + jsonError(w, http.StatusNotFound, err) + return + } + jsonWrite(w, true) + } +} diff --git a/server/remote/clients.go b/server/remote/clients.go new file mode 100644 index 000000000..f4907aec6 --- /dev/null +++ b/server/remote/clients.go @@ -0,0 +1,166 @@ +package remote + +import ( + "crypto/rand" + "errors" + "fmt" + "slices" + "strings" + "time" + + "github.com/evcc-io/evcc/core/keys" + "github.com/evcc-io/evcc/server/db/settings" + "github.com/samber/lo" + "github.com/sethvargo/go-password/password" + "golang.org/x/crypto/bcrypt" +) + +// dummyHash is a bcrypt hash of a random value, used to make the +// "unknown user" path take the same time as a real password check and +// prevent username enumeration via timing side channels. +var dummyHash []byte + +func init() { + buf := make([]byte, 16) + if _, err := rand.Read(buf); err != nil { + panic(err) + } + h, err := bcrypt.GenerateFromPassword(buf, bcrypt.DefaultCost) + if err != nil { + panic(err) + } + dummyHash = h +} + +// Client is a single tunnel basic-auth credential used by a remote client. +type Client struct { + Username string `json:"username"` + CreatedAt time.Time `json:"createdAt"` + ExpiresAt *time.Time `json:"expiresAt,omitempty"` +} + +type persistedClient struct { + Client + Hash string `json:"hash"` +} + +// loadClients reads the persisted client list. +func loadClients() []persistedClient { + var res []persistedClient + _ = settings.Json(keys.RemoteClients, &res) + return res +} + +// saveClients persists the given client list. +func saveClients(list []persistedClient) error { + return settings.SetJson(keys.RemoteClients, list) +} + +// generatePassword returns a crypto-random alphanumeric password +// with 20 characters including 4 digits (~96 bits of entropy). +func generatePassword() (string, error) { + return password.Generate(20, 4, 0, false, false) +} + +// Clients returns the list of configured clients (without password hashes). +func (r *Remote) Clients() []Client { + return lo.Map(loadClients(), func(c persistedClient, _ int) Client { + return c.Client + }) +} + +// CreateClient creates a new client with an auto-generated password. +// expiresIn <= 0 means the client never expires. +// Returns the cleartext password (shown to the user only once). +func (r *Remote) CreateClient(username string, expiresIn time.Duration) (Client, string, error) { + username = strings.TrimSpace(username) + if username == "" { + return Client{}, "", errors.New("username required") + } + // RFC 7617: ":" is the basic-auth separator; reject control chars too. + for _, r := range username { + if r == ':' || r < 0x20 || r == 0x7f { + return Client{}, "", errors.New("username contains invalid characters") + } + } + + var expires *time.Time + if expiresIn > 0 { + expires = new(time.Now().Add(expiresIn)) + } + + r.mu.Lock() + defer r.mu.Unlock() + + list := loadClients() + for _, c := range list { + if c.Username == username { + return Client{}, "", fmt.Errorf("client %q already exists", username) + } + } + + password, err := generatePassword() + if err != nil { + return Client{}, "", err + } + + hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + return Client{}, "", err + } + + c := persistedClient{ + Client: Client{ + Username: username, + CreatedAt: time.Now(), + ExpiresAt: expires, + }, + Hash: string(hash), + } + list = append(list, c) + if err := saveClients(list); err != nil { + return Client{}, "", err + } + + return c.Client, password, nil +} + +// DeleteClient removes a client by username. +func (r *Remote) DeleteClient(username string) error { + r.mu.Lock() + defer r.mu.Unlock() + + list := loadClients() + idx := slices.IndexFunc(list, func(c persistedClient) bool { + return c.Username == username + }) + + if idx == -1 { + return fmt.Errorf("client %s not found", username) + } + + return saveClients(slices.Delete(list, idx, idx+1)) +} + +// Authenticate validates basic-auth credentials. Always runs bcrypt +// (against a dummy hash on miss) to prevent username enumeration via timing. +func (r *Remote) Authenticate(username, password string) bool { + hash := dummyHash + var found *persistedClient + for _, c := range loadClients() { + if c.Username == username { + found = &c + hash = []byte(c.Hash) + break + } + } + + valid := bcrypt.CompareHashAndPassword(hash, []byte(password)) == nil + if !valid || found == nil { + return false + } + if found.ExpiresAt != nil && time.Now().After(*found.ExpiresAt) { + return false + } + return true +} diff --git a/server/remote/ratelimit.go b/server/remote/ratelimit.go new file mode 100644 index 000000000..76c43930b --- /dev/null +++ b/server/remote/ratelimit.go @@ -0,0 +1,51 @@ +package remote + +import ( + "sync" + "time" +) + +// authRateLimiter tracks failed authentication attempts in a sliding window. +// When the failure count exceeds the threshold, further attempts are blocked +// to prevent brute-force attacks. +type authRateLimiter struct { + mu sync.Mutex + failures []time.Time + window time.Duration + max int + now func() time.Time +} + +func newAuthRateLimiter() *authRateLimiter { + return &authRateLimiter{ + window: time.Minute, + max: 10, + now: time.Now, + } +} + +// allow checks whether an authentication attempt should proceed. +func (rl *authRateLimiter) allow() bool { + rl.mu.Lock() + defer rl.mu.Unlock() + + cutoff := rl.now().Add(-rl.window) + + // prune old entries + valid := rl.failures[:0] + for _, t := range rl.failures { + if t.After(cutoff) { + valid = append(valid, t) + } + } + rl.failures = valid + + return len(rl.failures) < rl.max +} + +// fail records a failed authentication attempt. +func (rl *authRateLimiter) fail() { + rl.mu.Lock() + defer rl.mu.Unlock() + rl.failures = append(rl.failures, rl.now()) +} diff --git a/server/remote/ratelimit_test.go b/server/remote/ratelimit_test.go new file mode 100644 index 000000000..f0f1a7077 --- /dev/null +++ b/server/remote/ratelimit_test.go @@ -0,0 +1,70 @@ +package remote + +import ( + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestAuthRateLimiter(t *testing.T) { + t.Run("allows requests under threshold", func(t *testing.T) { + rl := newAuthRateLimiter() + + for range rl.max { + assert.True(t, rl.allow()) + rl.fail() + } + }) + + t.Run("blocks after threshold", func(t *testing.T) { + rl := newAuthRateLimiter() + + for range rl.max { + rl.fail() + } + + assert.False(t, rl.allow()) + }) + + t.Run("recovers after window expires", func(t *testing.T) { + now := time.Now() + var mu sync.Mutex + + rl := newAuthRateLimiter() + rl.now = func() time.Time { + mu.Lock() + defer mu.Unlock() + return now + } + + for range rl.max { + rl.fail() + } + + assert.False(t, rl.allow()) + + // advance past window + mu.Lock() + now = now.Add(rl.window + time.Second) + mu.Unlock() + + assert.True(t, rl.allow()) + }) + + t.Run("successful auth does not count as failure", func(t *testing.T) { + rl := newAuthRateLimiter() + + // fill up to max-1 failures + for range rl.max - 1 { + rl.fail() + } + + // allow should still work (no fail() call = successful auth) + assert.True(t, rl.allow()) + + // still under threshold + assert.True(t, rl.allow()) + }) +} diff --git a/server/remote/remote.go b/server/remote/remote.go new file mode 100644 index 000000000..80158dc10 --- /dev/null +++ b/server/remote/remote.go @@ -0,0 +1,228 @@ +package remote + +import ( + "fmt" + "net/http" + "sync" + "time" + + "github.com/evcc-io/evcc/api/globalconfig" + "github.com/evcc-io/evcc/cmd/shutdown" + "github.com/evcc-io/evcc/core/keys" + "github.com/evcc-io/evcc/server/db/settings" + "github.com/evcc-io/evcc/util" + "github.com/evcc-io/evcc/util/request" + "github.com/evcc-io/evcc/util/sponsor" +) + +// Settings is the persisted remote access configuration. +type Settings struct { + Enabled bool `json:"enabled"` + URL string `json:"url,omitempty"` + Token string `json:"token,omitempty"` + TunnelURL string `json:"tunnelUrl,omitempty"` +} + +// Remote manages the remote access tunnel lifecycle. +type Remote struct { + mu sync.Mutex + cloudHost string + settings Settings + tunnel *Tunnel + httpHandler http.Handler + log *util.Logger + publisher chan<- util.Param + lastSeen map[string]time.Time // persisted: username → last activity + connected map[string]int // in-memory: active connection count per user +} + +// New creates a new Remote manager, loads persisted settings, and connects if enabled. +func New(cloudHost string, httpHandler http.Handler, valueChan chan<- util.Param) *Remote { + r := &Remote{ + cloudHost: cloudHost, + httpHandler: httpHandler, + log: util.NewLogger("remote"), + publisher: valueChan, + lastSeen: make(map[string]time.Time), + connected: make(map[string]int), + } + + // load saved settings + _ = settings.Json(keys.Remote, &r.settings) + _ = settings.Json(keys.RemoteLastSeen, &r.lastSeen) + + if r.settings.Enabled && r.settings.Token != "" { + go r.connect() + } + + shutdown.Register(r.disconnect) + + go func() { + for range time.Tick(time.Minute) { + r.publish() + } + }() + + return r +} + +// Enable enables or disables remote access. When enabling for the first time, +// it registers with the cloud to obtain a URL and token. +func (r *Remote) Enable(enable bool) error { + r.mu.Lock() + r.settings.Enabled = enable + r.saveSettings() + r.mu.Unlock() + + if enable { + // TODO why do we need a go routine for this? + go r.connect() + } else { + r.disconnect() + } + + r.publish() + return nil +} + +// Enabled returns whether remote access is enabled. +func (r *Remote) Enabled() bool { + r.mu.Lock() + defer r.mu.Unlock() + return r.settings.Enabled +} + +func (r *Remote) connect() { + r.mu.Lock() + token := r.settings.Token + r.mu.Unlock() + + if token == "" { + if err := r.register(); err != nil { + r.log.ERROR.Printf("registration failed: %v", err) + return + } + } + + r.log.INFO.Printf("remote access via %s", r.settings.URL) + + tunnel := NewTunnel(r.settings.TunnelURL, r.settings.Token, r.httpHandler, r.Authenticate, r.TrackActivity, r.log, r.publish) + + r.mu.Lock() + r.tunnel = tunnel + r.mu.Unlock() + + // blocks until disconnected + tunnel.run() +} + +func (r *Remote) disconnect() { + r.mu.Lock() + defer r.mu.Unlock() + + if r.tunnel != nil { + r.tunnel.Close() + r.tunnel = nil + } +} + +type registerRequest struct { + SponsorToken string `json:"sponsorToken"` +} + +type registerResponse struct { + URL string `json:"url"` + Token string `json:"token"` + TunnelURL string `json:"tunnelUrl"` +} + +// register calls the cloud registration endpoint and persists the result. +func (r *Remote) register() error { + uri := fmt.Sprintf("https://%s/api/register", r.cloudHost) + data := registerRequest{SponsorToken: sponsor.Token} + req, _ := request.New(http.MethodPost, uri, request.MarshalJSON(data), request.JSONEncoding) + + var res registerResponse + + client := request.NewHelper(r.log) + if err := client.DoJSON(req, &res); err != nil { + return err + } + + r.mu.Lock() + r.settings.URL = res.URL + r.settings.Token = res.Token + r.settings.TunnelURL = res.TunnelURL + r.saveSettings() + r.mu.Unlock() + + r.log.INFO.Printf("registered as %s", res.URL) + return nil +} + +// TrackActivity tracks remote client connections and disconnections. +func (r *Remote) TrackActivity(username string, active bool) { + r.mu.Lock() + defer r.mu.Unlock() + if active { + r.lastSeen[username] = time.Now() + r.connected[username]++ + } else if r.connected[username] > 0 { + r.connected[username]-- + } +} + +// saveSettings persists the current settings. Must be called with mu held. +func (r *Remote) saveSettings() { + if err := settings.SetJson(keys.Remote, r.settings); err != nil { + r.log.ERROR.Println(err) + } +} + +// ConfigStatus returns the current remote access config and status. +func (r *Remote) ConfigStatus() globalconfig.ConfigStatus { + r.mu.Lock() + defer r.mu.Unlock() + + connected := r.tunnel != nil && r.tunnel.IsConnected() + loginBlocked := r.tunnel != nil && r.tunnel.LoginBlocked() + + return globalconfig.ConfigStatus{ + Config: struct { + Enabled bool `json:"enabled"` + }{ + Enabled: r.settings.Enabled, + }, + Status: struct { + Connected bool `json:"connected"` + URL string `json:"url,omitempty"` + LoginBlocked bool `json:"loginBlocked"` + LastSeen map[string]time.Time `json:"lastSeen,omitempty"` + }{ + Connected: connected, + URL: r.settings.URL, + LoginBlocked: loginBlocked, + LastSeen: r.lastSeen, + }, + } +} + +// publish sends the current status to the UI via the value channel. +func (r *Remote) publish() { + if r.publisher == nil { + return + } + + // refresh lastSeen for open connections (auth only fires once) + r.mu.Lock() + now := time.Now() + for user, count := range r.connected { + if count > 0 { + r.lastSeen[user] = now + } + } + _ = settings.SetJson(keys.RemoteLastSeen, r.lastSeen) + r.mu.Unlock() + + r.publisher <- util.Param{Key: keys.Remote, Val: r.ConfigStatus()} +} diff --git a/server/remote/tunnel.go b/server/remote/tunnel.go new file mode 100644 index 000000000..303acf92b --- /dev/null +++ b/server/remote/tunnel.go @@ -0,0 +1,199 @@ +package remote + +import ( + "context" + "errors" + "fmt" + "io" + "net/http" + "sync" + "time" + + "github.com/cenkalti/backoff/v4" + "github.com/coder/websocket" + "github.com/evcc-io/evcc/util" + "github.com/evcc-io/evcc/util/sponsor" + "github.com/hashicorp/yamux" +) + +// Tunnel manages a WebSocket+yamux tunnel to the cloud proxy. +type Tunnel struct { + tunnelURL string + token string + httpHandler http.Handler + authenticate func(user, pass string) bool + trackActivity func(username string, active bool) + log *util.Logger + cancel func() + onStateChange func() + rateLimiter *authRateLimiter + + mu sync.Mutex + session *yamux.Session +} + +// NewTunnel creates a new tunnel client. +func NewTunnel(tunnelURL, token string, httpHandler http.Handler, authenticate func(user, pass string) bool, trackActivity func(string, bool), log *util.Logger, onStateChange func()) *Tunnel { + return &Tunnel{ + tunnelURL: tunnelURL, + token: token, + httpHandler: httpHandler, + authenticate: authenticate, + trackActivity: trackActivity, + log: log, + onStateChange: onStateChange, + rateLimiter: newAuthRateLimiter(), + } +} + +// run establishes the tunnel and reconnects on failure. +func (t *Tunnel) run() { + bo := backoff.NewExponentialBackOff( + backoff.WithInitialInterval(time.Second), + backoff.WithMaxInterval(60*time.Second), + backoff.WithMaxElapsedTime(0), + ) + + ctx, cancel := context.WithCancel(context.Background()) + t.cancel = cancel + + for { + ok, err := t.connect(ctx) + if err != nil && !errors.Is(err, context.Canceled) { + t.log.ERROR.Printf("tunnel: %v", err) + } + + // reset backoff after successful connection + if ok { + bo.Reset() + } + + select { + case <-ctx.Done(): + return + case <-time.After(bo.NextBackOff()): + } + } +} + +func (t *Tunnel) connect(ctx context.Context) (bool, error) { + conn, _, err := websocket.Dial(ctx, t.tunnelURL, &websocket.DialOptions{ + HTTPHeader: http.Header{ + "Authorization": []string{"Bearer " + t.token}, + "X-Sponsor-Token": []string{sponsor.Token}, + }, + }) + if err != nil { + return false, fmt.Errorf("websocket dial: %w", err) + } + + netConn := websocket.NetConn(ctx, conn, websocket.MessageBinary) + + config := yamux.DefaultConfig() + config.LogOutput = t.log.TRACE.Writer() + + session, err := yamux.Client(netConn, config) + if err != nil { + netConn.Close() // closes the underlying socket connection + return false, fmt.Errorf("yamux client: %w", err) + } + + t.changeState(session, nil) + + // accept streams from the proxy + srv := &http.Server{ + Handler: t.basicAuthMiddleware(t.httpHandler), + } + + if err := srv.Serve(session); err != nil { + t.changeState(nil, err) + } + + return true, nil +} + +func (t *Tunnel) changeState(session *yamux.Session, err error) { + t.mu.Lock() + t.session = session + t.mu.Unlock() + + if t.onStateChange != nil { + t.onStateChange() + } + + if session != nil { + t.log.INFO.Println("tunnel connected") + } else { + if errors.Is(err, context.Canceled) || errors.Is(err, io.EOF) { + t.log.INFO.Println("tunnel disconnected") + } else { + t.log.INFO.Println("tunnel disconnected:", err) + } + } +} + +// IsConnected returns whether the tunnel is currently connected. +func (t *Tunnel) IsConnected() bool { + t.mu.Lock() + defer t.mu.Unlock() + return t.session != nil +} + +// LoginBlocked returns whether login attempts are currently blocked by the rate limiter. +func (t *Tunnel) LoginBlocked() bool { + return !t.rateLimiter.allow() +} + +// Close tears down the tunnel. +func (t *Tunnel) Close() { + t.mu.Lock() + defer t.mu.Unlock() + + // close websocket; produces io.EOF in yamux which it handles silently + if t.session != nil { + t.session.Close() // closes the underlying socket connection + } + + if t.cancel != nil { + t.cancel() + t.cancel = nil + } +} + +// basicAuthMiddleware wraps a handler with HTTP basic auth, validating +// credentials against the given authenticate function per request. +// It rate-limits failed attempts to prevent brute-force attacks. +func (t *Tunnel) basicAuthMiddleware(next http.Handler) http.Handler { + rejectAuth := func(w http.ResponseWriter) { + w.Header().Set("WWW-Authenticate", `Basic realm="evcc"`) + http.Error(w, "Unauthorized", http.StatusUnauthorized) + } + + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + user, pass, ok := r.BasicAuth() + if !ok || t.authenticate == nil { + rejectAuth(w) + return + } + + if !t.rateLimiter.allow() { + t.log.INFO.Printf("login blocked for %q (rate limited)", user) + http.Error(w, "Too many failed login attempts. Try again in 1 minute.", http.StatusTooManyRequests) + return + } + + if !t.authenticate(user, pass) { + t.rateLimiter.fail() + t.log.INFO.Printf("failed login attempt for %q", user) + rejectAuth(w) + return + } + + if t.trackActivity != nil { + t.trackActivity(user, true) + defer t.trackActivity(user, false) // long-running requests (ws) + } + + next.ServeHTTP(w, r) + }) +} From 4c44ebc5d5e1fe07d9a35282e22b1f4aa9b372da Mon Sep 17 00:00:00 2001 From: andig Date: Tue, 14 Apr 2026 10:51:30 +0200 Subject: [PATCH 0251/1506] chore: fix connected --- charger/smart-evse.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/charger/smart-evse.go b/charger/smart-evse.go index 901d26985..ac01f3755 100644 --- a/charger/smart-evse.go +++ b/charger/smart-evse.go @@ -46,7 +46,7 @@ type smartEvseRestSettings struct { ModeID int `json:"mode_id"` CarConnected bool `json:"car_connected"` Evse struct { - Connected int `json:"connected"` + Connected bool `json:"connected"` Access int `json:"access"` Mode int `json:"mode"` ChargeTimer int `json:"charge_timer"` @@ -413,7 +413,7 @@ func (wb *SmartEVSE3) Diagnose() { fmt.Printf("\tMode: %s (%d)\n", res.Mode, res.ModeID) fmt.Printf("\tCar connected: %t\n", res.CarConnected) - fmt.Printf("\tEVSE connected: %d\n", res.Evse.Connected) + fmt.Printf("\tEVSE connected: %t\n", res.Evse.Connected) fmt.Printf("\tEVSE access: %d\n", res.Evse.Access) fmt.Printf("\tEVSE mode: %d\n", res.Evse.Mode) fmt.Printf("\tEVSE charge timer: %d\n", res.Evse.ChargeTimer) From b45edd218784482179e10fd685b37a0f31b42f9d Mon Sep 17 00:00:00 2001 From: andig Date: Tue, 14 Apr 2026 10:58:23 +0200 Subject: [PATCH 0252/1506] Mqtt: reset full/empty times (#29050) --- core/site_optimizer.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/core/site_optimizer.go b/core/site_optimizer.go index 8ce7eb9b0..ab6da5d6d 100644 --- a/core/site_optimizer.go +++ b/core/site_optimizer.go @@ -70,8 +70,8 @@ type batteryDetail struct { type batteryResult struct { batteryDetail - Full *time.Time `json:"full"` - Empty *time.Time `json:"empty"` + Full time.Time `json:"full,omitzero"` + Empty time.Time `json:"empty,omitzero"` } // func (br batteryResult) MarshalJSON() ([]byte, error) { @@ -471,15 +471,15 @@ func (site *Site) batteryRequest(dev config.Device[api.Meter], b types.Measureme return bat, detail } -func matchSoc(ts []float32, fun func(float32) bool) *time.Time { +func matchSoc(ts []float32, fun func(float32) bool) time.Time { for i, soc := range ts { if fun(soc) { // TODO first slot - return new(time.Now().Add(time.Duration(i+1) * tariff.SlotDuration)) + return time.Now().Add(time.Duration(i+1) * tariff.SlotDuration) } } - return nil + return time.Time{} } // continuousDemand creates a slice of power demands depending on loadpoint mode From 4b528cd355ee7a4d32503382d8ce429d421ab2ac Mon Sep 17 00:00:00 2001 From: andig Date: Tue, 14 Apr 2026 11:39:43 +0200 Subject: [PATCH 0253/1506] EEBus: handle negative production limits (#29104) --- hems/eebus/eebus.go | 29 ++++++++++++++++++----------- hems/eebus/events.go | 2 +- meter/eebus.go | 4 ++-- 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/hems/eebus/eebus.go b/hems/eebus/eebus.go index a9ef56405..ccbe35fbf 100644 --- a/hems/eebus/eebus.go +++ b/hems/eebus/eebus.go @@ -38,7 +38,7 @@ type EEBus struct { smartgridProductionId uint productionLimit ucapi.LoadLimit productionLimitActivated time.Time - failsafeProductionLimit float64 + failsafeProductionLimit *float64 heartbeat *util.Value[struct{}] interval time.Duration @@ -49,7 +49,7 @@ type Limits struct { FailsafeConsumptionActivePowerLimit float64 ProductionNominalMax float64 - FailsafeProductionActivePowerLimit float64 + FailsafeProductionActivePowerLimit *float64 FailsafeDurationMinimum time.Duration } @@ -67,7 +67,7 @@ func NewFromConfig(ctx context.Context, other map[string]any, site site.API) (*E FailsafeConsumptionActivePowerLimit: 4200, ProductionNominalMax: 0, - FailsafeProductionActivePowerLimit: 0, + FailsafeProductionActivePowerLimit: nil, // 0 is a valid limit FailsafeDurationMinimum: 2 * time.Hour, }, @@ -144,8 +144,8 @@ func NewEEBus(ctx context.Context, ski string, limits Limits, passthrough func(b if err := c.cs.CsLPPInterface.SetProductionNominalMax(limits.ProductionNominalMax); err != nil { c.log.ERROR.Println("CS LPP SetProductionNominalMax:", err) } - if c.failsafeProductionLimit > 0 { - if err := c.cs.CsLPPInterface.SetFailsafeProductionActivePowerLimit(c.failsafeProductionLimit, true); err != nil { + if c.failsafeProductionLimit != nil && *c.failsafeProductionLimit >= 0 { + if err := c.cs.CsLPPInterface.SetFailsafeProductionActivePowerLimit(*c.failsafeProductionLimit, true); err != nil { c.log.ERROR.Println("CS LPP SetFailsafeProductionActivePowerLimit:", err) } } @@ -181,7 +181,14 @@ func (c *EEBus) run() error { if heartbeatErr != nil && c.status != StatusFailsafe { // LPC-914/2 c.log.WARN.Println("missing heartbeat- entering failsafe mode") - c.setStatusAndLimit(StatusFailsafe, c.failsafeConsumptionLimit, c.failsafeProductionLimit) + c.setStatus(StatusFailsafe) + + c.setConsumptionLimit(c.failsafeConsumptionLimit) + + if c.failsafeProductionLimit != nil { + // production limit is negative, failsafe limits are always positive + c.setProductionLimit(-*c.failsafeProductionLimit, true) + } return nil } @@ -193,7 +200,10 @@ func (c *EEBus) run() error { } c.log.DEBUG.Println("heartbeat returned or failsafe duration exceeded- leaving failsafe mode") - c.setStatusAndLimit(StatusNormal, 0, 0) + c.setStatus(StatusNormal) + + c.setConsumptionLimit(0) + c.setProductionLimit(0, false) } // LPC-914/1 @@ -227,12 +237,9 @@ func (c *EEBus) run() error { return nil } -func (c *EEBus) setStatusAndLimit(status status, consumption, production float64) { +func (c *EEBus) setStatus(status status) { c.status = status c.statusUpdated = time.Now() - - c.setConsumptionLimit(consumption) - c.setProductionLimit(production, true) } func (c *EEBus) setConsumptionLimit(limit float64) { diff --git a/hems/eebus/events.go b/hems/eebus/events.go index 3dbd6bfda..beca70ba8 100644 --- a/hems/eebus/events.go +++ b/hems/eebus/events.go @@ -183,7 +183,7 @@ func (c *EEBus) updateFailsafeProductionActivePowerLimit() { c.mux.Lock() defer c.mux.Unlock() - c.failsafeProductionLimit = limit + c.failsafeProductionLimit = new(limit) } func (c *EEBus) updateFailsafeConsumptionDurationMinimum() { diff --git a/meter/eebus.go b/meter/eebus.go index 6b2cfaf3c..f2e4310c3 100644 --- a/meter/eebus.go +++ b/meter/eebus.go @@ -249,8 +249,8 @@ func (c *EEBus) Curtailed() (bool, error) { return false, err } - // Check if limit is active and has a valid power value - return limit.IsActive && limit.Value > 0, nil + // Check if limit is active and has a valid power value (valid is zero or negative) + return limit.IsActive && limit.Value <= 0, nil } // Curtail implements the api.Curtailer interface From 794a26d9166ebb23fe25deaa96a7305e3325fb05 Mon Sep 17 00:00:00 2001 From: andig Date: Tue, 14 Apr 2026 13:10:42 +0200 Subject: [PATCH 0254/1506] chore: fix naming (#29106) --- i18n/de.json | 2 +- i18n/en.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/i18n/de.json b/i18n/de.json index 3e24344f7..bbf0a02a9 100644 --- a/i18n/de.json +++ b/i18n/de.json @@ -341,7 +341,7 @@ "defaultsHintLink": "Einstellungen anpassen", "delete": "Löschen", "electricalSubtitle": "Im Zweifelsfall Elektriker fragen.", - "electricalTitle": "Elektrisch", + "electricalTitle": "Elektrik", "energyMeterHelp": "Zusätzlicher Zähler, wenn die Wallbox keinen integrierten hat.", "energyMeterLabel": "Energiezähler", "estimateLabel": "Ladestand zwischen API-Updates interpolieren", diff --git a/i18n/en.json b/i18n/en.json index b17667740..e4e669582 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -342,7 +342,7 @@ "defaultsHintLink": "Adjust settings", "delete": "Delete", "electricalSubtitle": "When in doubt, ask your electrician.", - "electricalTitle": "Electrical", + "electricalTitle": "Electrics", "energyMeterHelp": "Additional meter if the charger doesn't have an integrated one.", "energyMeterLabel": "Energy meter", "estimateLabel": "Interpolate charge level between API updates", From 812ffa47b8d9a36a6d46aedf5edbaffab8bfd86a Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Tue, 14 Apr 2026 16:46:59 +0200 Subject: [PATCH 0255/1506] Forecast UI: add feed-in, edge-to-edge layout, design improvements (#29090) --- assets/css/app.css | 9 +- assets/js/colors.ts | 3 + assets/js/components/Forecast/Co2Chart.vue | 2 +- assets/js/components/Forecast/Co2Details.vue | 57 ++++++ assets/js/components/Forecast/GridDetails.vue | 107 ++++++++++ assets/js/components/Forecast/PriceChart.vue | 141 +++++++------ assets/js/components/Forecast/SolarChart.vue | 2 +- .../js/components/Forecast/SolarDetails.vue | 79 ++++++++ assets/js/components/Forecast/chartStyles.css | 1 + assets/js/components/Forecast/echarts.ts | 10 +- assets/js/mixins/formatter.ts | 5 + assets/js/settings.ts | 4 + assets/js/utils/forecast.test.ts | 30 ++- assets/js/utils/forecast.ts | 8 +- assets/js/views/Forecast.vue | 191 ++++++++---------- i18n/en.json | 11 +- tests/config-grid.spec.ts | 2 +- 17 files changed, 484 insertions(+), 178 deletions(-) create mode 100644 assets/js/components/Forecast/Co2Details.vue create mode 100644 assets/js/components/Forecast/GridDetails.vue create mode 100644 assets/js/components/Forecast/SolarDetails.vue diff --git a/assets/css/app.css b/assets/css/app.css index cc748436c..15171e613 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -21,7 +21,7 @@ --evcc-darkest-green: #076f20ff; --evcc-darkest-green-rgb: 7, 111, 32; --evcc-yellow: #faf000; - --evcc-dark-yellow: #bbb400; + --evcc-dark-yellow: #f6bb0f; --evcc-orange: #ff9000; --evcc-orange-rgb: 255, 144, 0; --evcc-red: #fc440f; @@ -39,6 +39,7 @@ --evcc-pv: var(--evcc-dark-green); --evcc-battery: var(--evcc-darker-green); --evcc-export: var(--evcc-yellow); + --evcc-export-contrast: var(--evcc-dark-yellow); --evcc-price: #ff912fff; --evcc-co2: #00916eff; --evcc-background: var(--bs-gray-bright); @@ -100,6 +101,8 @@ :root.dark { --evcc-grid: var(--bs-gray-medium); + --evcc-export: var(--evcc-yellow); + --evcc-export-contrast: var(--evcc-yellow); --evcc-background: var(--bs-gray-deep); --evcc-box: var(--bs-gray-dark); --evcc-box-border: var(--bs-gray-darker); @@ -186,6 +189,10 @@ h5 { color: var(--evcc-co2) !important; } +.text-export { + color: var(--evcc-export-contrast) !important; +} + a { color: var(--bs-primary); } diff --git a/assets/js/colors.ts b/assets/js/colors.ts index f47242ec5..a98765010 100644 --- a/assets/js/colors.ts +++ b/assets/js/colors.ts @@ -17,6 +17,7 @@ const colors: { pricePerKWh: string | null; price: string | null; co2: string | null; + export: string | null; background: string | null; light: string | null; selfPalette: string[]; @@ -31,6 +32,7 @@ const colors: { pricePerKWh: null, price: null, co2: null, + export: null, background: null, light: null, selfPalette: ["#0FDE41FF", "#FFBD2FFF", "#FD6158FF", "#03C1EFFF", "#0F662DFF", "#FF922EFF"], @@ -75,6 +77,7 @@ export function updateCssColors() { colors.grid = style.getPropertyValue("--evcc-grid"); colors.price = style.getPropertyValue("--evcc-price"); colors.co2 = style.getPropertyValue("--evcc-co2"); + colors.export = style.getPropertyValue("--evcc-export-contrast"); colors.background = style.getPropertyValue("--evcc-background"); colors.pricePerKWh = style.getPropertyValue("--bs-gray-medium"); colors.co2PerKWh = style.getPropertyValue("--bs-gray-medium"); diff --git a/assets/js/components/Forecast/Co2Chart.vue b/assets/js/components/Forecast/Co2Chart.vue index ea688deea..5a3d15d3d 100644 --- a/assets/js/components/Forecast/Co2Chart.vue +++ b/assets/js/components/Forecast/Co2Chart.vue @@ -95,7 +95,7 @@ export default defineComponent({ { type: "line", data: this.slots.map((s) => [s.start, s.value]), - smooth: 0.05, + smooth: true, symbol: "circle", symbolSize: 6, showSymbol: false, diff --git a/assets/js/components/Forecast/Co2Details.vue b/assets/js/components/Forecast/Co2Details.vue new file mode 100644 index 000000000..3358f0b5d --- /dev/null +++ b/assets/js/components/Forecast/Co2Details.vue @@ -0,0 +1,57 @@ + + + diff --git a/assets/js/components/Forecast/GridDetails.vue b/assets/js/components/Forecast/GridDetails.vue new file mode 100644 index 000000000..70d5cecb0 --- /dev/null +++ b/assets/js/components/Forecast/GridDetails.vue @@ -0,0 +1,107 @@ + + + diff --git a/assets/js/components/Forecast/PriceChart.vue b/assets/js/components/Forecast/PriceChart.vue index 00298fa81..6a57b9019 100644 --- a/assets/js/components/Forecast/PriceChart.vue +++ b/assets/js/components/Forecast/PriceChart.vue @@ -7,6 +7,7 @@ diff --git a/assets/js/components/Forecast/SolarChart.vue b/assets/js/components/Forecast/SolarChart.vue index 528279bef..ba97ee967 100644 --- a/assets/js/components/Forecast/SolarChart.vue +++ b/assets/js/components/Forecast/SolarChart.vue @@ -103,7 +103,7 @@ export default defineComponent({ { type: "line", data, - smooth: 0.05, + smooth: true, symbol: "circle", symbolSize: 6, showSymbol: false, diff --git a/assets/js/components/Forecast/SolarDetails.vue b/assets/js/components/Forecast/SolarDetails.vue new file mode 100644 index 000000000..32497c190 --- /dev/null +++ b/assets/js/components/Forecast/SolarDetails.vue @@ -0,0 +1,79 @@ + + + diff --git a/assets/js/components/Forecast/chartStyles.css b/assets/js/components/Forecast/chartStyles.css index 5b9f3e23e..10f4a53a4 100644 --- a/assets/js/components/Forecast/chartStyles.css +++ b/assets/js/components/Forecast/chartStyles.css @@ -1,6 +1,7 @@ .forecast-chart-scroll { overflow-x: auto; padding-bottom: 4px; + margin-bottom: 0.5rem; user-select: none; -webkit-user-select: none; -webkit-touch-callout: none; diff --git a/assets/js/components/Forecast/echarts.ts b/assets/js/components/Forecast/echarts.ts index d56556b54..9fde86ba1 100644 --- a/assets/js/components/Forecast/echarts.ts +++ b/assets/js/components/Forecast/echarts.ts @@ -53,7 +53,7 @@ export function markPointLabel( animation: true, clip: true, symbol: "rect", - symbolSize: 1, + symbolSize: 0, label: { show: true, position: "top", @@ -63,7 +63,7 @@ export function markPointLabel( color: colors.background, backgroundColor: color, borderRadius: 4, - padding: [5, 10], + padding: [5, 15], offset: [0, -2], formatter: (p: { data: { value: string } }) => p.data.value, }, @@ -111,7 +111,7 @@ export function tooltipStyle( } export function forecastGrid() { - return { top: 36, right: 16, bottom: 4, left: 34, borderWidth: 0 }; + return { top: 36, right: 16, bottom: 16, left: 24, borderWidth: 0 }; } export function forecastXAxes(startDate: Date, endDate: Date, weekdayShort: (d: Date) => string) { @@ -167,13 +167,13 @@ export function forecastYAxis(overrides: Record = {}) { axisLine: { show: false }, axisTick: { show: false }, splitLine: { - showMinLine: false, + showMinLine: true, showMaxLine: false, lineStyle: { color: colors.border || "" }, }, axisLabel: { fontSize: 10, - opacity: 0.5, + opacity: 0.75, ...(axisLabel as Record), }, ...rest, diff --git a/assets/js/mixins/formatter.ts b/assets/js/mixins/formatter.ts index 8562f5972..9489dd492 100644 --- a/assets/js/mixins/formatter.ts +++ b/assets/js/mixins/formatter.ts @@ -221,6 +221,11 @@ export default defineComponent({ weekday: "short", }).format(date); }, + weekdayLong(date: Date) { + return new Intl.DateTimeFormat(this.$i18n?.locale, { + weekday: "long", + }).format(date); + }, fmtAbsoluteDate(date: Date) { const weekday = this.weekdayPrefix(date); const hour = new Intl.DateTimeFormat(this.$i18n?.locale, { diff --git a/assets/js/settings.ts b/assets/js/settings.ts index fcd200779..9ea7fcff8 100644 --- a/assets/js/settings.ts +++ b/assets/js/settings.ts @@ -21,6 +21,7 @@ const SESSIONS_GROUP = "sessions_group"; const SESSIONS_TYPE = "sessions_type"; const SETTINGS_SOLAR_ADJUSTED = "settings_solar_adjusted"; const SETTINGS_PRICE_ZOOM = "settings_price_zoom"; +const SETTINGS_HIDE_FEEDIN = "settings_hide_feedin"; const LAST_BATTERY_SMART_COST_LIMIT = "last_battery_smart_cost_limit"; const LAST_TARGET_TIME = "last_target_time"; const LAST_SOC_GOAL = "last_soc_goal"; @@ -124,6 +125,7 @@ export interface Settings { sessionsType: string; solarAdjusted: boolean; priceZoom: boolean; + hideFeedin: boolean; loadpoints: Record; lastBatterySmartCostLimit: number | undefined; lastTargetTime: string | null; @@ -151,6 +153,7 @@ const settings: Settings = reactive({ sessionsType: read(SESSIONS_TYPE), solarAdjusted: readBool(SETTINGS_SOLAR_ADJUSTED), priceZoom: readBool(SETTINGS_PRICE_ZOOM), + hideFeedin: readBool(SETTINGS_HIDE_FEEDIN), loadpoints: readJSON(LOADPOINTS), lastBatterySmartCostLimit: readNumber(LAST_BATTERY_SMART_COST_LIMIT), lastTargetTime: read(LAST_TARGET_TIME), @@ -177,6 +180,7 @@ watch(() => settings.sessionsGroup, save(SESSIONS_GROUP)); watch(() => settings.sessionsType, save(SESSIONS_TYPE)); watch(() => settings.solarAdjusted, saveBool(SETTINGS_SOLAR_ADJUSTED)); watch(() => settings.priceZoom, saveBool(SETTINGS_PRICE_ZOOM)); +watch(() => settings.hideFeedin, saveBool(SETTINGS_HIDE_FEEDIN)); watch(() => settings.loadpoints, saveJSON(LOADPOINTS), { deep: true }); watch(() => settings.lastBatterySmartCostLimit, saveNumber(LAST_BATTERY_SMART_COST_LIMIT)); watch(() => settings.lastTargetTime, save(LAST_TARGET_TIME)); diff --git a/assets/js/utils/forecast.test.ts b/assets/js/utils/forecast.test.ts index 7a1ba0265..f6f1b6b74 100644 --- a/assets/js/utils/forecast.test.ts +++ b/assets/js/utils/forecast.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "vitest"; -import { findLowestSumSlotIndex } from "./forecast"; +import { findLowestSumSlotIndex, isStaticTariff } from "./forecast"; describe("findLowestSumSlotIndex", () => { test("finds lowest sum with span of 4", () => { @@ -55,3 +55,31 @@ describe("findLowestSumSlotIndex", () => { expect(findLowestSumSlotIndex(slots, 2)).toBe(0); }); }); + +describe("isStaticTariff", () => { + test("returns false for undefined", () => { + expect(isStaticTariff(undefined)).toBe(false); + }); + + test("returns false for empty array", () => { + expect(isStaticTariff([])).toBe(false); + }); + + test("returns true when all values are identical", () => { + const slots = [ + { start: "2025-01-01T00:00:00Z", end: "2025-01-01T00:15:00Z", value: 0.25 }, + { start: "2025-01-01T00:15:00Z", end: "2025-01-01T00:30:00Z", value: 0.25 }, + { start: "2025-01-01T00:30:00Z", end: "2025-01-01T00:45:00Z", value: 0.25 }, + ]; + expect(isStaticTariff(slots)).toBe(true); + }); + + test("returns false when values differ", () => { + const slots = [ + { start: "2025-01-01T00:00:00Z", end: "2025-01-01T00:15:00Z", value: 0.25 }, + { start: "2025-01-01T00:15:00Z", end: "2025-01-01T00:30:00Z", value: 0.3 }, + { start: "2025-01-01T00:30:00Z", end: "2025-01-01T00:45:00Z", value: 0.25 }, + ]; + expect(isStaticTariff(slots)).toBe(false); + }); +}); diff --git a/assets/js/utils/forecast.ts b/assets/js/utils/forecast.ts index 66314158e..e18719493 100644 --- a/assets/js/utils/forecast.ts +++ b/assets/js/utils/forecast.ts @@ -1,4 +1,4 @@ -import type { TimeseriesEntry, SolarDetails } from "../components/Forecast/types"; +import type { ForecastSlot, TimeseriesEntry, SolarDetails } from "../components/Forecast/types"; import deepCopy from "./deepClone"; export enum ForecastType { @@ -65,6 +65,12 @@ export function adjustedSolar(solar?: SolarDetails): SolarDetails | undefined { return result; } +export function isStaticTariff(slots?: ForecastSlot[]): boolean { + if (!Array.isArray(slots) || slots.length === 0) return false; + const firstValue = slots[0].value; + return slots.every((slot) => slot.value === firstValue); +} + export interface SlotWithValue { start: string | Date; value: number; diff --git a/assets/js/views/Forecast.vue b/assets/js/views/Forecast.vue index 2410839d4..c1cd0b682 100644 --- a/assets/js/views/Forecast.vue +++ b/assets/js/views/Forecast.vue @@ -41,19 +41,10 @@
-
+
-
-

- {{ - $t("forecast.type.solar") - }} - {{ - solarSubtitle - }} -

+
+

{{ $t("forecast.type.solar") }}

- +
+ +
+
-
-
-

- {{ - $t("forecast.type.price") - }} - {{ - priceSubtitle - }} -

+
+
+

{{ $t("forecast.type.price") }}

- + +
+
-
-

- {{ - $t("forecast.type.co2") - }} - {{ - co2Subtitle - }} -

- +
+

{{ $t("forecast.type.co2") }}

+
+ +
+
@@ -149,17 +143,18 @@ import { defineComponent } from "vue"; import Header from "../components/Top/Header.vue"; import DynamicPriceIcon from "../components/MaterialIcon/DynamicPrice.vue"; import SolarChart from "../components/Forecast/SolarChart.vue"; +import SolarDetails from "../components/Forecast/SolarDetails.vue"; import PriceChart from "../components/Forecast/PriceChart.vue"; +import GridDetails from "../components/Forecast/GridDetails.vue"; import Co2Chart from "../components/Forecast/Co2Chart.vue"; -import formatter, { POWER_UNIT } from "@/mixins/formatter"; +import Co2Details from "../components/Forecast/Co2Details.vue"; +import formatter from "@/mixins/formatter"; import settings from "@/settings"; import store from "../store"; -import { adjustedSolar, ForecastType } from "@/utils/forecast"; -import type { ForecastSlot } from "../components/Forecast/types"; +import { adjustedSolar, ForecastType, isStaticTariff } from "@/utils/forecast"; const MIN_HOURS = 76; const MAX_HOURS = 96; -const SLOTS_PER_HOUR = 4; export default defineComponent({ name: "Forecast", @@ -167,8 +162,11 @@ export default defineComponent({ TopHeader: Header, DynamicPriceIcon, SolarChart, + SolarDetails, PriceChart, + GridDetails, Co2Chart, + Co2Details, }, mixins: [formatter], data() { @@ -233,6 +231,17 @@ export default defineComponent({ priceZoom() { return settings.priceZoom; }, + showFeedin() { + return !settings.hideFeedin; + }, + isGridStatic(): boolean { + if (!this.forecast.grid) return false; + if (!isStaticTariff(this.forecast.grid)) return false; + if (this.forecast.feedin?.length) { + return isStaticTariff(this.forecast.feedin); + } + return true; + }, showSolarAdjust() { return !!this.forecast.solar && this.experimental; }, @@ -241,48 +250,16 @@ export default defineComponent({ ? adjustedSolar(this.forecast.solar) : this.forecast.solar; }, - solarAdjustText() { - const text = this.$t("forecast.solarAdjustShort"); + solarAdjustPercent(): string { const scale = this.forecast.solar?.scale || 1; const percentDiff = scale * 100 - 100; - return `${text} (${this.fmtPercentage(percentDiff, 0, true)})`; + return this.fmtPercentage(percentDiff, 0, true); }, - solarSubtitle(): string { - const s = this.solar; - if (!s) return ""; - const today = s.today?.energy ? this.fmtWh(s.today.energy, POWER_UNIT.AUTO) : ""; - const tomorrow = s.tomorrow?.energy - ? this.fmtWh(s.tomorrow.energy, POWER_UNIT.AUTO) - : ""; - if (!today && !tomorrow) return ""; - const parts = []; - if (today) parts.push(`${today} ${this.$t("forecast.solar.remaining")}`); - if (tomorrow) parts.push(`${tomorrow} ${this.$t("forecast.solar.tomorrow")}`); - return parts.join(" ・ "); + solarAdjustTextShort(): string { + return `${this.$t("forecast.solarAdjustShort")} (${this.solarAdjustPercent})`; }, - priceSubtitle(): string { - const slots = this.upcomingSlots(this.forecast.grid); - if (slots.length === 0) return ""; - const values = slots.map((s) => s.value); - const min = Math.min(...values); - const max = Math.max(...values); - const avg = values.reduce((a, b) => a + b, 0) / values.length; - const fmtMin = this.fmtPricePerKWh(min, this.currency, false, false); - const fmtMax = this.fmtPricePerKWh(max, this.currency, false, true); - const fmtAvg = this.fmtPricePerKWh(avg, this.currency, false, true); - return `⌀ ${fmtAvg} ・ ${fmtMin} – ${fmtMax}`; - }, - co2Subtitle(): string { - const slots = this.upcomingSlots(this.forecast.co2); - if (slots.length === 0) return ""; - const values = slots.map((s) => s.value); - const min = Math.min(...values); - const max = Math.max(...values); - const avg = values.reduce((a, b) => a + b, 0) / values.length; - const fmtMin = this.fmtNumber(min, 0); - const fmtMax = this.fmtCo2Medium(max); - const fmtAvg = this.fmtCo2Medium(avg); - return `⌀ ${fmtAvg} ・ ${fmtMin} – ${fmtMax}`; + solarAdjustTextMedium(): string { + return `${this.$t("forecast.solarAdjustMedium")} (${this.solarAdjustPercent})`; }, }, methods: { @@ -292,6 +269,9 @@ export default defineComponent({ togglePriceZoom() { settings.priceZoom = !settings.priceZoom; }, + toggleFeedin() { + settings.hideFeedin = !settings.hideFeedin; + }, onChartScroll(val: number) { if (this.isScrolling) return; this.isScrolling = true; @@ -300,13 +280,6 @@ export default defineComponent({ this.isScrolling = false; }); }, - upcomingSlots(slots?: ForecastSlot[]): ForecastSlot[] { - if (!Array.isArray(slots)) return []; - const now = new Date(); - return slots - .filter((slot) => new Date(slot.end) > now) - .slice(0, MAX_HOURS * SLOTS_PER_HOUR); - }, }, }); @@ -321,4 +294,10 @@ export default defineComponent({ border-radius: 2rem; max-width: 480px; } +@media (max-width: 575.98px) { + .chart-edge { + margin-left: -1.5rem; + margin-right: -1.5rem; + } +} diff --git a/i18n/en.json b/i18n/en.json index e4e669582..deed0f3f9 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -796,8 +796,8 @@ "titleChoice": "What Do You Want To Add?", "type": { "co2": "CO₂ Intensity", - "feedIn": "Feed-In Price", - "grid": "Grid Consumption Price", + "feedIn": "Grid Export Price", + "grid": "Grid Import Price", "planner": "Planner", "solar": "Solar" }, @@ -972,6 +972,7 @@ "setup": "Set up tariffs and forecasts", "solar": "See expected solar production for today and the coming days. Will also be used for automatic charging optimization in the future." }, + "hideLine": "hide line", "modalTitle": "Forecast", "price": { "average": "Average", @@ -980,6 +981,7 @@ "range": "Range" }, "priceZoom": "zoom in", + "showLine": "show line", "solar": { "dayAfterTomorrow": "Day after tomorrow", "partly": "partly", @@ -988,7 +990,8 @@ "tomorrow": "Tomorrow" }, "solarAdjust": "Adjust solar forecast based on real production data{percent}.", - "solarAdjustShort": "real data adjust", + "solarAdjustMedium": "real data adjust", + "solarAdjustShort": "adjust", "type": { "co2": "CO₂ Emissions", "price": "Grid Price", @@ -1153,7 +1156,7 @@ "batteryTooltip": "{energy} of {total} ({soc})", "forecast": "Forecast: ", "forecastTooltip": "forecast: remaining solar production today", - "gridImport": "Grid use", + "gridImport": "Grid import", "homePower": "Consumption", "loadpoints": "Charger| Charger | {count} chargers", "loadpointsLimit": "{limit} limit", diff --git a/tests/config-grid.spec.ts b/tests/config-grid.spec.ts index 924cbcc73..6ce902306 100644 --- a/tests/config-grid.spec.ts +++ b/tests/config-grid.spec.ts @@ -62,7 +62,7 @@ test.describe("grid meter", async () => { // check in main ui await page.goto("/"); await page.getByTestId("visualization").click(); - await expect(page.getByTestId("energyflow")).toContainText(["Grid use", "5.0 kW"].join("")); + await expect(page.getByTestId("energyflow")).toContainText(["Grid import", "5.0 kW"].join("")); // delete #1 await page.goto("/#/config"); From d50e6be4680028397b3fee9f48449a765089e565 Mon Sep 17 00:00:00 2001 From: andig Date: Tue, 14 Apr 2026 17:06:51 +0200 Subject: [PATCH 0256/1506] chore: remove duplicate serial_number (#29107) --- templates/definition/charger/volttime.yaml | 9 ++++----- util/templates/defaults.yaml | 5 ----- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/templates/definition/charger/volttime.yaml b/templates/definition/charger/volttime.yaml index 03789fe6f..5dad5d255 100644 --- a/templates/definition/charger/volttime.yaml +++ b/templates/definition/charger/volttime.yaml @@ -20,13 +20,12 @@ params: help: de: API Token ([developer.volttime.com](https://developer.volttime.com/api-reference/authentication#personal-access-tokens)) en: API Token ([developer.volttime.com](https://developer.volttime.com/api-reference/authentication#personal-access-tokens)) - - name: serial_number + - name: serial required: true - help: - de: Seriennummer (z. B. 1008621) - en: Serial number (e.g. 1008621) + - name: serial_number + deprecated: true render: | type: plugchoice token: {{ .token }} - identity: {{ if .serial_number }}VT_{{ .serial_number }}{{ end }} + identity: {{ if (or .serial .serial_number) }}VT_{{ or .serial .serial_number }}{{ end }} connector: 1 diff --git a/util/templates/defaults.yaml b/util/templates/defaults.yaml index 698cd85e5..c51caa320 100644 --- a/util/templates/defaults.yaml +++ b/util/templates/defaults.yaml @@ -407,11 +407,6 @@ params: mask: true description: generic: PIN - - name: serial_number - private: true - description: - en: Serial Number - de: Seriennummer - name: watchdog description: generic: Watchdog From 0f65fd960f937711353465e4a7bfadd871bb5c9c Mon Sep 17 00:00:00 2001 From: andig Date: Tue, 14 Apr 2026 19:14:03 +0200 Subject: [PATCH 0257/1506] EEBus: fix deadlock on save after validate (#29119) --- server/eebus/eebus.go | 18 ++++++++----- server/eebus/eebus_test.go | 52 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 7 deletions(-) diff --git a/server/eebus/eebus.go b/server/eebus/eebus.go index 8cf5696a2..38a66dfc0 100644 --- a/server/eebus/eebus.go +++ b/server/eebus/eebus.go @@ -237,17 +237,21 @@ func (c *EEBus) UnregisterDevice(ski string, device Device) { c.log.TRACE.Printf("unregistering ski: %s", ski) c.mux.Lock() - defer c.mux.Unlock() - if idx := slices.Index(c.clients[ski], device); idx != -1 { c.clients[ski] = slices.Delete(c.clients[ski], idx, idx+1) - } - // only tear down SHIP connection when no more clients need it - if len(c.clients[ski]) == 0 { - delete(c.clients, ski) - c.service.UnregisterRemoteSKI(ski) + if len(c.clients[ski]) == 0 { + delete(c.clients, ski) + + // Tear down the SHIP session after releasing the mutex: ship-go's CloseConnection + // on a non-Complete state synchronously invokes HandleConnectionClosed, + // which calls back into evcc's connect(ski, false) — and that needs to + // acquire c.mux. Holding c.mux across this cross-layer call would + // deadlock the same goroutine on its own non-reentrant mutex. See #28942. + defer c.service.UnregisterRemoteSKI(ski) + } } + c.mux.Unlock() } func (c *EEBus) CustomerEnergyManagement() *CustomerEnergyManagement { diff --git a/server/eebus/eebus_test.go b/server/eebus/eebus_test.go index 12ae581e1..c9d3a3957 100644 --- a/server/eebus/eebus_test.go +++ b/server/eebus/eebus_test.go @@ -2,7 +2,12 @@ package eebus import ( "testing" + "time" + eebusapi "github.com/enbility/eebus-go/api" + eebusmocks "github.com/enbility/eebus-go/mocks" + spineapi "github.com/enbility/spine-go/api" + "github.com/evcc-io/evcc/util" "github.com/stretchr/testify/require" "go.yaml.in/yaml/v4" ) @@ -23,3 +28,50 @@ certificate: var res Config require.NoError(t, yaml.Unmarshal([]byte(conf), &res)) } + +// mockDevice implements Device for testing +type mockDevice struct{} + +func (d *mockDevice) Connect(connected bool) {} +func (d *mockDevice) UseCaseEvent(_ spineapi.DeviceRemoteInterface, entity spineapi.EntityRemoteInterface, event eebusapi.EventType) { +} + +var _ Device = (*mockDevice)(nil) + +// TestUnregisterDevice_MutexNotHeldDuringShipCall is the regression guard +// for issue #28942. It asserts that c.mux is NOT held at the point +// UnregisterRemoteSKI is called. The pre-fix code held c.mux across that +// cross-layer call, and ship-go's synchronous HandleConnectionClosed +// callback chain re-entered connect(ski, false) on the same goroutine, +// which then deadlocked on c.mux.Lock() (Go mutexes are non-reentrant). +// +// The assertion uses a goroutine that tries to briefly acquire c.mux from +// inside the mock's UnregisterRemoteSKI implementation; if the lock is +// held, the acquisition times out and the test fails. +func TestUnregisterDevice_MutexNotHeldDuringShipCall(t *testing.T) { + dev := &mockDevice{} + c := &EEBus{ + log: util.NewLogger("test"), + clients: map[string][]Device{"aabbcc": {dev}}, + } + + service := eebusmocks.NewServiceInterface(t) + service.EXPECT().UnregisterRemoteSKI("aabbcc").Run(func(string) { + acquired := make(chan struct{}) + go func() { + c.mux.Lock() + defer c.mux.Unlock() + close(acquired) + }() + select { + case <-acquired: + // good — mutex was free + case <-time.After(100 * time.Millisecond): + t.Errorf("c.mux was held while UnregisterRemoteSKI was called — " + + "regression to the cross-layer lock hold that caused #28942") + } + }).Once() + c.service = service + + c.UnregisterDevice("aabbcc", dev) +} From 0b198ff0dab8a473190065ef0a5eb23e2635ac2e Mon Sep 17 00:00:00 2001 From: Matthias Fechner Date: Wed, 15 Apr 2026 10:55:05 +0200 Subject: [PATCH 0258/1506] chore: allow the TAG_NAME and SHA Makefile variable to be overridden (#29123) --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index fc1168092..ee3298c54 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ # build vars -TAG_NAME := $(shell test -d .git && git describe --abbrev=0 --tags) -SHA := $(shell test -d .git && git rev-parse --short HEAD) +TAG_NAME ?= $(shell test -d .git && git describe --abbrev=0 --tags) +SHA ?= $(shell test -d .git && git rev-parse --short HEAD) COMMIT := $(SHA) # hide commit for releases ifeq ($(RELEASE),1) From b6fda3212838830c3923c900fc4601a2e2516725 Mon Sep 17 00:00:00 2001 From: Cornelis Stuurman <56003577+corstuur@users.noreply.github.com> Date: Wed, 15 Apr 2026 12:05:07 +0200 Subject: [PATCH 0259/1506] Enever: fix time parsing (#29129) --- templates/definition/tariff/enever.yaml | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/templates/definition/tariff/enever.yaml b/templates/definition/tariff/enever.yaml index 880900eb8..2b3829df5 100644 --- a/templates/definition/tariff/enever.yaml +++ b/templates/definition/tariff/enever.yaml @@ -37,6 +37,14 @@ params: "ZP", ] required: true + - name: resolution + description: + en: Price resolution + de: Preisauflösung + type: choice + choice: ["hourly", "quarterly"] + default: quarterly + required: true - preset: tariff-base - preset: tariff-features - name: interval @@ -56,7 +64,7 @@ render: | type: string config: source: http - uri: https://enever.nl/apiv3/stroomprijs_vandaag.php?token={{ .token }} + uri: https://enever.nl/apiv3/stroomprijs_vandaag.php?token={{ .token }}{{ if eq .resolution "quarterly" }}&resolution=15{{ end }} jq: | [ .data.[] | { @@ -64,7 +72,7 @@ render: | "end": ( (.datum[0:19]) as $dt | (.datum[19:]) as $tz | - ($dt | strptime("%FT%T") | mktime + 900 | strflocaltime("%FT%T")) + $tz + ($dt | strptime("%FT%T") | mktime + {{ if eq .resolution "quarterly" }}900{{ else }}3600{{ end }} | strftime("%FT%T")) + $tz ), "value": .prijs{{ .provider }} | tonumber } @@ -73,7 +81,7 @@ render: | type: string config: source: http - uri: https://enever.nl/apiv3/stroomprijs_morgen.php?token={{ .token }} + uri: https://enever.nl/apiv3/stroomprijs_morgen.php?token={{ .token }}{{ if eq .resolution "quarterly" }}&resolution=15{{ end }} jq: | [ .data.[] | { @@ -81,9 +89,9 @@ render: | "end": ( (.datum[0:19]) as $dt | (.datum[19:]) as $tz | - ($dt | strptime("%FT%T") | mktime + 900 | strflocaltime("%FT%T")) + $tz + ($dt | strptime("%FT%T") | mktime + {{ if eq .resolution "quarterly" }}900{{ else }}3600{{ end }} | strftime("%FT%T")) + $tz ), "value": .prijs{{ .provider }} | tonumber - } + } ] | tostring interval: {{ .interval }} From 81b78db887e67aab77ebbc5fece156c1657193bd Mon Sep 17 00:00:00 2001 From: andig Date: Wed, 15 Apr 2026 13:34:28 +0200 Subject: [PATCH 0260/1506] Collect 15min energy metrics (#23185) --- assets/js/components/BottomTabs/MoreMenu.vue | 5 +- assets/js/components/History/Chart.vue | 177 ++++++++++++++ assets/js/components/History/EnergyChart.vue | 170 ++++++++++++++ assets/js/components/Top/Navigation.vue | 5 + assets/js/router.ts | 5 + assets/js/views/History.vue | 103 ++++++++ core/meterenergy.go | 60 ----- core/metrics/accumulator.go | 126 ++++++++++ .../accumulator_test.go} | 24 +- core/metrics/collector.go | 126 ++++++++++ core/metrics/collector_test.go | 132 +++++++++++ core/metrics/db.go | 220 ++++++++++++++++-- core/metrics/db_test.go | 102 ++++++++ core/site.go | 81 +++---- core/site_optimizer.go | 3 +- core/site_optimizer_test.go | 85 ------- core/site_tariffs.go | 16 +- core/site_test.go | 35 --- server/http.go | 1 + server/http_history_handler.go | 51 ++++ tests/energy-history.spec.ts | 72 ++++++ tests/energy-history.sql | 42 ++++ 22 files changed, 1375 insertions(+), 266 deletions(-) create mode 100644 assets/js/components/History/Chart.vue create mode 100644 assets/js/components/History/EnergyChart.vue create mode 100644 assets/js/views/History.vue delete mode 100644 core/meterenergy.go create mode 100644 core/metrics/accumulator.go rename core/{meterenergy_test.go => metrics/accumulator_test.go} (53%) create mode 100644 core/metrics/collector.go create mode 100644 core/metrics/collector_test.go create mode 100644 core/metrics/db_test.go create mode 100644 server/http_history_handler.go create mode 100644 tests/energy-history.spec.ts create mode 100644 tests/energy-history.sql diff --git a/assets/js/components/BottomTabs/MoreMenu.vue b/assets/js/components/BottomTabs/MoreMenu.vue index 53b374709..f27d2704d 100644 --- a/assets/js/components/BottomTabs/MoreMenu.vue +++ b/assets/js/components/BottomTabs/MoreMenu.vue @@ -55,7 +55,10 @@ to="/optimize" active-class="active" > - Optimize + Optimize 🧪 + + + History 🧪 diff --git a/assets/js/components/History/Chart.vue b/assets/js/components/History/Chart.vue new file mode 100644 index 000000000..874388cf4 --- /dev/null +++ b/assets/js/components/History/Chart.vue @@ -0,0 +1,177 @@ + + + diff --git a/assets/js/components/History/EnergyChart.vue b/assets/js/components/History/EnergyChart.vue new file mode 100644 index 000000000..59705dc45 --- /dev/null +++ b/assets/js/components/History/EnergyChart.vue @@ -0,0 +1,170 @@ + + + diff --git a/assets/js/components/Top/Navigation.vue b/assets/js/components/Top/Navigation.vue index 3db3845c3..11ddb133d 100644 --- a/assets/js/components/Top/Navigation.vue +++ b/assets/js/components/Top/Navigation.vue @@ -89,6 +89,11 @@ Optimize 🧪 +
  • + + History 🧪 + +
  • diff --git a/assets/js/views/Config.vue b/assets/js/views/Config.vue index 68fe825e5..e6fba841d 100644 --- a/assets/js/views/Config.vue +++ b/assets/js/views/Config.vue @@ -427,8 +427,8 @@ - - + + diff --git a/i18n/de.json b/i18n/de.json index 7af4256b2..bbf0a02a9 100644 --- a/i18n/de.json +++ b/i18n/de.json @@ -602,9 +602,8 @@ "urlHelp": "Kopiere diese URL in die Konfiguration deiner Wallbox. Details findest du im Handbuch des Herstellers. Die Wallbox sollte automatisch ihre eindeutige Kennung (Station-ID) an die URL anhängen. In seltenen Fällen musst du die Kennung manuell angeben. Beispiel: `{url}`" }, "optimizer": { - "description": "Analysiert Solarprognose, Strompreise und deinen typischen Verbrauch, um Batterie- und Ladestrategie zu optimieren. Daten werden zur Berechnung an den evcc Optimierungsdienst übertragen. Berechnet und visualisiert aktuell nur. Steuert noch keine Geräte.", + "description": "Analysiert Solarprognose, Strompreise und deinen typischen Verbrauch, um Batterie- und Ladestrategie zu optimieren. Daten werden zur Berechnung an den evcc Optimierungsdienst übertragen.", "enable": "Optimizer aktivieren", - "info": "Es kann einige Minuten dauern, bis der Optimizer-Menüeintrag sichtbar wird. Bei neuen Installationen kann es bis zu 24 Stunden dauern, bis evcc genügend Daten gesammelt hat.", "title": "Optimizer" }, "options": { diff --git a/i18n/en.json b/i18n/en.json index 8d852e51c..deed0f3f9 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -603,9 +603,8 @@ "urlHelp": "Copy this URL into your charger's configuration. Check the manufacturer's manual for details. The charger is expected to automatically append its unique identifier (station ID) to the url. In rare cases, you may need to manually specify the identifier. Example: `{url}`" }, "optimizer": { - "description": "Analyzes solar forecast, electricity prices, and your consumption patterns to optimize battery and charging strategy. Data is sent to the evcc optimization service for processing. Currently only calculates and visualizes. Does not control devices yet.", + "description": "Analyzes solar forecast, electricity prices, and your consumption patterns to optimize battery and charging strategy. Data is sent to the evcc optimization service for processing.", "enable": "Enable Optimizer", - "info": "It may take a few minutes for the optimizer menu entry to become visible. For new installations, it may take up to 24 hours until evcc has collected enough data.", "title": "Optimizer" }, "options": { From 643f1dcb2eac361774995c667c81651580df5bb6 Mon Sep 17 00:00:00 2001 From: andig Date: Fri, 17 Apr 2026 14:22:27 +0200 Subject: [PATCH 0272/1506] chore: open source --- charger/smart-evse.go | 5 ----- templates/definition/charger/smart-evse.yaml | 1 - 2 files changed, 6 deletions(-) diff --git a/charger/smart-evse.go b/charger/smart-evse.go index ac01f3755..1f93831b5 100644 --- a/charger/smart-evse.go +++ b/charger/smart-evse.go @@ -26,7 +26,6 @@ import ( "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" ) // SmartEVSE-3.5 REST API charger implementation @@ -138,10 +137,6 @@ func NewSmartEVSE3(uri string, cache time.Duration) (api.Charger, error) { curr: 60, // 6 A in 1/10 A } - if !sponsor.IsAuthorized() { - return nil, api.ErrSponsorRequired - } - wb.apiG = util.ResettableCached(func() (smartEvseRestSettings, error) { var res smartEvseRestSettings err := wb.GetJSON(wb.uri+"/settings", &res) diff --git a/templates/definition/charger/smart-evse.yaml b/templates/definition/charger/smart-evse.yaml index c15db7708..60b52e2a2 100644 --- a/templates/definition/charger/smart-evse.yaml +++ b/templates/definition/charger/smart-evse.yaml @@ -5,7 +5,6 @@ products: generic: Smart Evse v3 / v3.5 capabilities: ["mA", "rfid", "1p3p"] requirements: - evcc: ["sponsorship"] description: de: | Anbindung über die REST API der Smart Evse Firmware (FW >= 3.6.0). From a0989c18b4001220abdd528c7d68ed2a9bf16129 Mon Sep 17 00:00:00 2001 From: andig Date: Fri, 17 Apr 2026 14:23:39 +0200 Subject: [PATCH 0273/1506] Reapply "Config UI: better explain optimizer (#29141)" This reverts commit f474245bd9b8b0c6a9cbc12b419ab365a28b4fd5. --- assets/js/components/Config/OptimizerModal.vue | 16 ++++++++++++++-- assets/js/components/Config/TelemetryModal.vue | 12 +++--------- assets/js/views/Config.vue | 4 ++-- i18n/de.json | 3 ++- i18n/en.json | 3 ++- 5 files changed, 23 insertions(+), 15 deletions(-) diff --git a/assets/js/components/Config/OptimizerModal.vue b/assets/js/components/Config/OptimizerModal.vue index a5a84aa56..23b95bb6d 100644 --- a/assets/js/components/Config/OptimizerModal.vue +++ b/assets/js/components/Config/OptimizerModal.vue @@ -1,7 +1,7 @@ @@ -32,6 +37,7 @@ import { defineComponent } from "vue"; import GenericModal from "../Helper/GenericModal.vue"; import ErrorMessage from "../Helper/ErrorMessage.vue"; +import SponsorTokenRequired from "./DeviceModal/SponsorTokenRequired.vue"; import api from "@/api"; import store from "@/store"; import { docsPrefix } from "@/i18n"; @@ -39,7 +45,10 @@ import type { AxiosError } from "axios"; export default defineComponent({ name: "OptimizerModal", - components: { GenericModal, ErrorMessage }, + components: { GenericModal, ErrorMessage, SponsorTokenRequired }, + props: { + isSponsor: Boolean, + }, data() { return { error: null as string | null, @@ -49,6 +58,9 @@ export default defineComponent({ enabled(): boolean { return !!store.state?.optimizer; }, + hasEvopt(): boolean { + return !!store.state?.evopt; + }, docsLink(): string { return `${docsPrefix()}/docs/features/optimizer`; }, diff --git a/assets/js/components/Config/TelemetryModal.vue b/assets/js/components/Config/TelemetryModal.vue index c318bea75..0c1e20fa5 100644 --- a/assets/js/components/Config/TelemetryModal.vue +++ b/assets/js/components/Config/TelemetryModal.vue @@ -7,27 +7,21 @@ data-testid="telemetry-modal" >

    {{ $t("config.telemetry.description") }}

    - + diff --git a/assets/js/views/Config.vue b/assets/js/views/Config.vue index e6fba841d..68fe825e5 100644 --- a/assets/js/views/Config.vue +++ b/assets/js/views/Config.vue @@ -427,8 +427,8 @@ - - + + diff --git a/i18n/de.json b/i18n/de.json index bbf0a02a9..7af4256b2 100644 --- a/i18n/de.json +++ b/i18n/de.json @@ -602,8 +602,9 @@ "urlHelp": "Kopiere diese URL in die Konfiguration deiner Wallbox. Details findest du im Handbuch des Herstellers. Die Wallbox sollte automatisch ihre eindeutige Kennung (Station-ID) an die URL anhängen. In seltenen Fällen musst du die Kennung manuell angeben. Beispiel: `{url}`" }, "optimizer": { - "description": "Analysiert Solarprognose, Strompreise und deinen typischen Verbrauch, um Batterie- und Ladestrategie zu optimieren. Daten werden zur Berechnung an den evcc Optimierungsdienst übertragen.", + "description": "Analysiert Solarprognose, Strompreise und deinen typischen Verbrauch, um Batterie- und Ladestrategie zu optimieren. Daten werden zur Berechnung an den evcc Optimierungsdienst übertragen. Berechnet und visualisiert aktuell nur. Steuert noch keine Geräte.", "enable": "Optimizer aktivieren", + "info": "Es kann einige Minuten dauern, bis der Optimizer-Menüeintrag sichtbar wird. Bei neuen Installationen kann es bis zu 24 Stunden dauern, bis evcc genügend Daten gesammelt hat.", "title": "Optimizer" }, "options": { diff --git a/i18n/en.json b/i18n/en.json index deed0f3f9..8d852e51c 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -603,8 +603,9 @@ "urlHelp": "Copy this URL into your charger's configuration. Check the manufacturer's manual for details. The charger is expected to automatically append its unique identifier (station ID) to the url. In rare cases, you may need to manually specify the identifier. Example: `{url}`" }, "optimizer": { - "description": "Analyzes solar forecast, electricity prices, and your consumption patterns to optimize battery and charging strategy. Data is sent to the evcc optimization service for processing.", + "description": "Analyzes solar forecast, electricity prices, and your consumption patterns to optimize battery and charging strategy. Data is sent to the evcc optimization service for processing. Currently only calculates and visualizes. Does not control devices yet.", "enable": "Enable Optimizer", + "info": "It may take a few minutes for the optimizer menu entry to become visible. For new installations, it may take up to 24 hours until evcc has collected enough data.", "title": "Optimizer" }, "options": { From bb93ebf98bcff1b197ff3cb467746a3ac9c85483 Mon Sep 17 00:00:00 2001 From: RenatusRo <126881912+RenatusRo@users.noreply.github.com> Date: Fri, 17 Apr 2026 17:06:28 +0200 Subject: [PATCH 0274/1506] Optimizer: wording on debug view (#29179) --- assets/js/views/Optimize.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/js/views/Optimize.vue b/assets/js/views/Optimize.vue index ab82e483e..37990f5a8 100644 --- a/assets/js/views/Optimize.vue +++ b/assets/js/views/Optimize.vue @@ -34,7 +34,7 @@ true ) }} - saved + net cost Date: Sat, 18 Apr 2026 09:27:18 +0200 Subject: [PATCH 0275/1506] Optimizer: don't send empty requests --- core/site_optimizer.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/core/site_optimizer.go b/core/site_optimizer.go index 03d3c697d..f175fff9e 100644 --- a/core/site_optimizer.go +++ b/core/site_optimizer.go @@ -221,6 +221,11 @@ func (site *Site) optimizerUpdate(battery []types.Measurement) error { add(site.batteryRequest(dev, b, grid, minLen, firstSlotDuration)) } + // empty request- all loadpoints disabled + if len(req.Batteries) == 0 { + return nil + } + httpClient := request.NewClient(site.log) httpClient.Timeout = 30 * time.Second From 11ec83716d803824307642eaf6831643021ecf35 Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Sat, 18 Apr 2026 09:37:29 +0200 Subject: [PATCH 0276/1506] Translations update from Hosted Weblate (#28648) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Dominik Trzebiński Co-authored-by: Jonas Gate Co-authored-by: Frits van Proosdij Co-authored-by: noxaos Co-authored-by: amtssp Co-authored-by: RTTTC <94727758+RTTTC@users.noreply.github.com> Co-authored-by: uqlel Co-authored-by: Joni Lehtola Co-authored-by: MobilettoSoft Co-authored-by: Filipe Lima Co-authored-by: aerucu Co-authored-by: bittin1ddc447d824349b2 Co-authored-by: Jaap Broekhuizen Co-authored-by: Jan --- i18n/da.json | 100 ++++++++++++++++++++++++++++++++++++++++++++++----- i18n/de.json | 15 +++++--- i18n/fi.json | 49 +++++++++++++++++++++++-- i18n/fr.json | 62 ++++++++++++++++++++++++++++---- i18n/it.json | 45 ++++++++++++++++++++++- i18n/lb.json | 64 ++++++++++++++++++++++++++++----- i18n/lt.json | 54 +++++++++++++++++++++++++--- i18n/nl.json | 75 ++++++++++++++++++++++++++++++++++---- i18n/no.json | 2 +- i18n/pl.json | 4 ++- i18n/pt.json | 62 ++++++++++++++++++++++++++++---- i18n/sv.json | 79 +++++++++++++++++++++++++++++++++++----- i18n/tr.json | 57 +++++++++++++++++++++++++---- 13 files changed, 602 insertions(+), 66 deletions(-) diff --git a/i18n/da.json b/i18n/da.json index 5adbf533f..df6b04bc6 100644 --- a/i18n/da.json +++ b/i18n/da.json @@ -40,6 +40,7 @@ "legendTopName": "Batteri-understøttet opladning af køretøj", "legendTopSubline": "når husbatteriet er over {soc}.", "modalTitle": "Hus batteri", + "noBattery": "Ingen batterier er konfigurerede.", "usageTab": "Batteri anvendelse" }, "config": { @@ -112,6 +113,7 @@ "title": "Møntfod" }, "deviceValue": { + "activeClients": "Aktive klienter", "amount": "Mængde", "broker": "Broker", "bucket": "Bucket", @@ -125,6 +127,7 @@ "chargedEnergy": "Opladet", "co2": "Elnettet CO₂", "configured": "Konfigureret", + "connected": "Forbundet", "connections": "Tilslutninger", "controllable": "Kan styres", "currency": "Møntenhed", @@ -143,6 +146,7 @@ "hemsActiveLimit": "Aktiv begrænsning", "hemsType": "Kommunikation", "identifier": "RFID-identifikator", + "loginBlocked": "Grænsen for loginforsøg er nået", "max": "max", "messengers": "Tjenester", "no": "nej", @@ -601,6 +605,12 @@ "url": "Server URL", "urlHelp": "Kopier denne URL til din laders opsætning. Tjek producentens vejledning for detaljer. Laderen tilføjer normalt automatisk sit unikke ID (stations-id) til URL’en. I sjældne tilfælde skal ID’et angives manuelt. Eksempel: {url}" }, + "optimizer": { + "description": "Analyserer sol-prognose, el-priser, og forbrugsmønstre for at optimere batteri og opladning. Data sendes til evcc optimerings-service for at blive beregnet. Aktuelt beregnes og vises data kun, der er ingen styring af enheder endnu.", + "enable": "Aktiver Optimering", + "info": "Det kan tage et par minutter for optimerings-menuen er synlig. For nye installationer kan det tage op til 24 timer før der er opsamlet tilstrækkelig med data.", + "title": "Optimering" + }, "options": { "boolean": { "no": "nej", @@ -628,6 +638,38 @@ "titleAdd": "Tilføj solmåler", "titleEdit": "Rediger solmåler" }, + "remote": { + "active": "Aktiv", + "addClient": "Tilføj klient", + "addClientDescription": "Oplysningerne gemmes og verificeres kun lokalt på din evcc-enhed.", + "addClientTitle": "Tilføj fjernklient", + "clientCreated": "Klient oprettet", + "clients": "Klienter", + "confirmDelete": "Slet klient?", + "connected": "Forbundet", + "createClient": "Opret klient", + "description": "Få adgang til din evcc-installation hvor som helst via evcc-mobilappen. Ingen viderestilling af porte eller VPN er krævet.", + "deviceName": "Navn på enhed", + "disconnected": "Afbrudt", + "done": "Færdig", + "enableLabel": "Tillad fjernadgang", + "expiration": "Udløbet", + "expirationNone": "Aldrig", + "expired": "Udløbet", + "expiresIn": "udløber {time}", + "lastActive": "aktiv {time}", + "loginBlocked": "Fjernadgang blokeret i ét minut på grund af for mange loginforsøg.", + "manualLogin": "Eller login manuelt ved {url} i din browser med disse oplysninger:", + "noClients": "Ingen klient endnu. Ingen kan forbindes endnu.", + "password": "Kode", + "passwordOnce": "Denne kode vises kun én gang. Skan QR koden eller kopier den nu. Den kan ikke vises igen.", + "qrInstall": "Installer evcc appen for {ios} eller {android}.", + "qrScan": "Skan koden med telefonens kamera for at forbinde. Klik på det hvis du allerede bruger telefonen.", + "removeClient": "Fjern klient", + "title": "Fjernadgang", + "url": "Offentlig URL", + "username": "Brugernavn" + }, "section": { "additionalMeter": "Tilføj ekstra målere", "general": "Generelle", @@ -760,7 +802,7 @@ "type": { "co2": "CO₂ Intensitet", "feedIn": "Indfødningspris", - "grid": "Forbrugspris fra elnettet", + "grid": "Købspris fra elnettet", "planner": "Planlægning", "solar": "Sol" }, @@ -854,6 +896,15 @@ "configurePriceCo2": "Lær hvordan man indstiller pris og CO₂ data.", "footerLong": "{percent} Solenergi", "footerShort": "{percent} Solenergi", + "indicator": { + "co2": "CO₂ emission", + "co2saved": "CO₂ sparet", + "none": "ingen", + "price": "energipris", + "savings": "sparet", + "solar": "solenergi" + }, + "indicatorLabel": "Overskriftsinformation", "modalTitle": "Oversigt over ladeenergi", "moneySaved": "{value} sparet", "percentGrid": "{grid} kWh elnettet", @@ -865,10 +916,11 @@ "thisYear": "dette år", "total": "hele tiden" }, - "periodLabel": "Periode:", + "periodLabel": "Periode", "priceTitle": "Energipris", "referenceGrid": "elnet", - "referenceLabel": "Referencedata:", + "referenceLabel": "Referencedata", + "sessionInfo": "Baseret på gennemførte opladninger.", "tabTitle": "Mine data" }, "sponsor": { @@ -894,15 +946,26 @@ }, "version": { "availableLong": "ny version tilgængelig", + "community": "evcc-fællesskabet", + "labelRelease": "Udgivet", + "labelVersion": "Version", + "labelWebsite": "Hjemmeside", + "latestVersion": "nyeste version", + "madeByCommunity": "Lavet af {0}.", "modalCancel": "Annuller", "modalDownload": "Hent", "modalInstalledVersion": "Installeret version", + "modalLatest": "Du bruger den nyeste version.", + "modalNextRelease": "Hvad er i næste udgave", "modalNoReleaseNotes": "Der er ingen tilgængelige release notes. Mere info om den nye version:", "modalTitle": "Ny version tilgængelig", "modalUpdate": "Installere", "modalUpdateNow": "Installer nu", "modalUpdateStarted": "Starter den nye version af evcc…", - "modalUpdateStatusStart": "Installation startede:" + "modalUpdateStatusStart": "Installation startede:", + "modalViewOnGitHub": "Se på GitHub", + "openSource": "open source", + "poweredByOpenSource": "Drevet af {0}." } }, "forecast": { @@ -912,6 +975,13 @@ "lowestHour": "Reneste time", "range": "spændvidde" }, + "empty": { + "co2": "Se når strøm i din region er grøn. Opladningsplaner bliver optimeret til lav-emission og sparet CO₂ beregnes.", + "price": "Konfigurér din dynamiske eltarif for automatisk at optimere ladeplaner og beregne besparelser.", + "setup": "Indstil tariffer og prognoser", + "solar": "Vis forventet solenergi produceret i dag og de nærmeste dage. Bliver også brugt til automatisk optimering af opladninger i fremtiden." + }, + "hideLine": "skjul linjen", "modalTitle": "Prognose", "price": { "average": "Gennemsnit", @@ -919,6 +989,8 @@ "lowestHour": "Billigste time", "range": "Spændvidde" }, + "priceZoom": "zoom ind", + "showLine": "vis linjen", "solar": { "dayAfterTomorrow": "I overmorgen", "partly": "delvis", @@ -927,10 +999,12 @@ "tomorrow": "I morgen" }, "solarAdjust": "Juster solprognosen på baggrund af produktions data{percent}.", + "solarAdjustMedium": "Justering baseret på faktiske data", + "solarAdjustShort": "justering", "type": { - "co2": "CO₂", - "price": "Pris", - "solar": "Sol" + "co2": "CO₂ Emissioner", + "price": "Strømpris", + "solar": "Solenergiproduktion" } }, "general": { @@ -1099,7 +1173,7 @@ "batteryTooltip": "{energy} af {total} ({soc})", "forecast": "Prognose: ", "forecastTooltip": "prognose: resterende solenergi produktion i dag", - "gridImport": "Forbrug fra elnet", + "gridImport": "Import fra elnet", "homePower": "Forbrug", "loadpoints": "Oplader| Oplader | {count} opladere", "loadpointsLimit": "{limit} begrænsning", @@ -1144,6 +1218,7 @@ "once": "Boost er aktiveret for denne opladning.", "stateActive": "Batteriboost er aktiv", "stateBelowLimit": "Batteriniveau er for lav til boost", + "stateHold": "Batteriet er låst", "stateReady": "Batteriboost er klar" }, "batteryUsage": "Husbatteri", @@ -1253,6 +1328,7 @@ "batteryBoostBelowLimit": "Batteriniveau er for lav til boost.", "batteryBoostDisabled": "Batteriboost er deaktiveret.", "batteryBoostEnabled": "Boost indtil batteri er på {limit}.", + "batteryBoostHold": "Batteriet låst. Boost er ikke muligt.", "charging": "Oplader…", "cheapEnergyCharging": "Billig energi er tilgængelig.", "cheapEnergyNextStart": "Billig energi i {duration}.", @@ -1492,5 +1568,13 @@ "lineErrorLink": "linje {0}", "restartButton": "Genstart", "title": "Fejl ved opstart" + }, + "tabBar": { + "battery": "Batteri", + "charge": "Oplader", + "comingSoon": "Siden under opbygning.", + "forecast": "Prognose", + "more": "Mere", + "sessions": "Sessioner" } } diff --git a/i18n/de.json b/i18n/de.json index 7af4256b2..aec63eea6 100644 --- a/i18n/de.json +++ b/i18n/de.json @@ -127,6 +127,7 @@ "chargedEnergy": "Geladen", "co2": "Netz-CO₂", "configured": "Konfiguriert", + "connected": "Verbunden", "connections": "Verbindungen", "controllable": "Steuerbar", "currency": "Währung", @@ -946,6 +947,7 @@ "modalCancel": "Abbrechen", "modalDownload": "Download", "modalInstalledVersion": "Installierte Version", + "modalLatest": "Du verwendest bereits die aktuellste Version.", "modalNextRelease": "Was ist neu im nächsten Release", "modalNoReleaseNotes": "Keine Versionshinweise verfügbar. Mehr Informationen zur neuen Version:", "modalTitle": "Neue Version verfügbar", @@ -955,7 +957,7 @@ "modalUpdateStatusStart": "Installation gestartet:", "modalViewOnGitHub": "Auf GitHub ansehen", "openSource": "Open Source", - "poweredByOpenSource": "Powered by {0}." + "poweredByOpenSource": "Unterstützt von {0}." } }, "forecast": { @@ -971,6 +973,7 @@ "setup": "Tarife und Vorhersagen einrichten", "solar": "Erwartete Solarproduktion für heute und die nächsten Tage. Wird in Zukunft auch für die automatische Ladeoptimierung verwendet." }, + "hideLine": "Linie ausblenden", "modalTitle": "Vorhersage", "price": { "average": "Durchschnitt", @@ -978,6 +981,8 @@ "lowestHour": "Günstigste Stunde", "range": "Bereich" }, + "priceZoom": "vergrößern", + "showLine": "Linie anzeigen", "solar": { "dayAfterTomorrow": "Übermorgen", "partly": "teilweise", @@ -986,10 +991,12 @@ "tomorrow": "Morgen" }, "solarAdjust": "Solarprognose anhand der realen Produktionsdaten anpassen{percent}.", + "solarAdjustMedium": "an reale Produktionsdaten anpassen", + "solarAdjustShort": "anpassen", "type": { - "co2": "CO₂", - "price": "Preis", - "solar": "Solar" + "co2": "CO₂-Emissionen", + "price": "Netzpreis", + "solar": "Solarproduktion" } }, "general": { diff --git a/i18n/fi.json b/i18n/fi.json index dc602412f..9d76ab6c1 100644 --- a/i18n/fi.json +++ b/i18n/fi.json @@ -113,6 +113,7 @@ "title": "Valuutta" }, "deviceValue": { + "activeClients": "Aktiiviset asiakasyhteydet", "amount": "Määrä", "broker": "Välittäjä (Broker)", "bucket": "Tietosäiliö (Bucket)", @@ -126,6 +127,7 @@ "chargedEnergy": "Ladattu", "co2": "Verkon CO₂", "configured": "Määritetty", + "connected": "Yhdistetty", "connections": "Yhteydet", "controllable": "Hallittava", "currency": "Valuutta", @@ -144,6 +146,7 @@ "hemsActiveLimit": "Aktiivinen raja", "hemsType": "Kommunikaatiomenetelmä", "identifier": "RFID-tunniste", + "loginBlocked": "Sisäänkirjoitumisyritysten raja saavutettu", "max": "maksimi", "messengers": "Palvelut", "no": "ei", @@ -602,6 +605,11 @@ "url": "Palvelimen URL", "urlHelp": "Kopioi tämä URL laturisi asetuksiin. Tarkista laturivalmistajan ohjeista yksityiskohdat. Laturin odotetaan lisäävän URL-osoitteeseen yksilöllinen tunnus (aseman ID). Joissain harvoissa tapauksissa sinun täytyy käsin lisätä tämä tunnus. Esim: `{url}`" }, + "optimizer": { + "description": "Analysoi aurinkoennustetta, sähkönhintaa ja kulutustottumuksiasi optimoidakseen akun ja latauspisteiden käyttöä. Tiedot lähetetään käsiteltäväksi evcc:n optimointipalveluun.", + "enable": "Käytä optimointia", + "title": "Optimointi" + }, "options": { "boolean": { "no": "ei", @@ -629,6 +637,38 @@ "titleAdd": "Lisää aurinkovoimalan mittari", "titleEdit": "Muokkaa aurinkovoimalan mittaria" }, + "remote": { + "active": "Aktiivinen", + "addClient": "Lisää etäkäyttäjä", + "addClientDescription": "Käyttäjätunnustiedot tallennetaan ja varmennetaan vain paikallisesti sinun evcc-asennuksessasi.", + "addClientTitle": "Lisää etäkäyttäjäasiakas", + "clientCreated": "Etäkäyttäjä luotu", + "clients": "Etäkäyttäjät", + "confirmDelete": "Poista etäkäyttäjä?", + "connected": "Yhdistetty", + "createClient": "Luo etäkäyttäjäasiakas", + "description": "Pääse käsiksi evcc-asennukseesi mistä vain käyttäen evcc-kännykkäsovellusta. Ei vaadi VPN:ää tai reititinohjausasetuksia.", + "deviceName": "Laitteen nimi", + "disconnected": "Ei yhteydessä", + "done": "Valmis", + "enableLabel": "Salli etäkäyttö", + "expiration": "Umpeutuu", + "expirationNone": "Ei koskaan", + "expired": "umpeutunut", + "expiresIn": "umpeutuu {time}", + "lastActive": "aktiivinen {time}", + "loginBlocked": "Etäkirjautumiset on estetty minuutiksi, koska liian monta kirjautumisyritystä on epäonnistunut.", + "manualLogin": "tai kirjaudu {url} selaimessa käyttäen seuraavia tunnuksia:", + "noClients": "Ei vielä etäyhteysasiakkaita. Kukaan ei vielä voi kirjautua etänä.", + "password": "Salasana", + "passwordOnce": "Tämä salasana näytetään vain kerran. Skannaa QR-koodi tai kopioi se, sillä et voi nähdä sitä enää uudelleen.", + "qrInstall": "Asenna evcc-sovellus {ios}:sta tai {android}:sta.", + "qrScan": "Skannaa koodi puhelimesi kameralla yhdistääksesi. Klikkaa linkkiä, jos jo käytät puhelimesi kameraa.", + "removeClient": "Poista etäyhteysasiakas", + "title": "Etäyhteys", + "url": "Julkinen URL-osoite", + "username": "Käyttäjätunnus" + }, "section": { "additionalMeter": "Ylimääräiset mittarit", "general": "Yleinen", @@ -759,8 +799,8 @@ "titleChoice": "Mitä haluat lisätä?", "type": { "co2": "CO₂-intensiteetti", - "feedIn": "Syöttöhinta", - "grid": "Verkkosähkön hinta", + "feedIn": "Sähkön myyntihinta verkkoon", + "grid": "Verkkosähkön ostohinta", "planner": "Suunnitelma", "solar": "Aurinko" }, @@ -939,6 +979,7 @@ "setup": "Määritä hinnat ja ennusteet", "solar": "Katso aurinkotuotannon ennuste täksi ja tuleviksi päiviksi. Käytetään tulevaisuudessa myös latauksien optimoitiin." }, + "hideLine": "piilota viiva", "modalTitle": "Ennuste", "price": { "average": "Keskimääräinen", @@ -947,6 +988,7 @@ "range": "Toimintamatka" }, "priceZoom": "lähennä", + "showLine": "näytä viiva", "solar": { "dayAfterTomorrow": "Ylihuomenna", "partly": "osittain", @@ -955,7 +997,8 @@ "tomorrow": "Huomenna" }, "solarAdjust": "Mukauta aurinkosääennustettu todellisten tuotantotietojen perusteella{percent}.", - "solarAdjustShort": "mitatun datan korjaus", + "solarAdjustMedium": "mitatun datan korjaus", + "solarAdjustShort": "korjaus", "type": { "co2": "CO₂-päästöt", "price": "Sähkön hinta", diff --git a/i18n/fr.json b/i18n/fr.json index 0011cf86f..40649f246 100644 --- a/i18n/fr.json +++ b/i18n/fr.json @@ -40,6 +40,7 @@ "legendTopName": "Charge véhicule par la batterie", "legendTopSubline": "lorsque la batterie domestique est au-dessus de {soc}.", "modalTitle": "Batterie domestique", + "noBattery": "Aucune batterie n'est configurée.", "usageTab": "Utilisation batterie" }, "config": { @@ -130,8 +131,9 @@ "currency": "Devise", "current": "Courant", "currentRange": "Courant", + "curtailed": "Limitation de l'injection", "detected": "Détecté", - "dimmed": "Atténué", + "dimmed": "Consommation limitée", "enabled": "Activé", "energy": "Énergie", "events": "Evénements", @@ -600,6 +602,11 @@ "url": "URL du Serveur", "urlHelp": "Copiez cette URL dans la configuration de votre chargeur. Consultez le manuel du fabricant pour plus de détails. Le chargeur devrait ajouter automatiquement son identifiant unique (ID de station) à l'URL. Dans de rares cas, vous devrez peut-être spécifier manuellement l'identifiant. Exemple : `{url}`" }, + "optimizer": { + "description": "Analyse les prévisions d'ensoleillement, les prix de l'électricité et vos habitudes de consommation afin d'optimiser la stratégie de gestion de la batterie et de recharge. Les données sont transmises au service d'optimisation evcc pour y être traitées.", + "enable": "Activer l'optimiseur", + "title": "Optimiseur" + }, "options": { "boolean": { "no": "non", @@ -642,11 +649,13 @@ "cardTitle": "Sunny Home Manager", "description": "evcc est équipé d'une intégration pour le SMA Sunny Home Manager (SHM) via le protocole SEMP. S'il fonctionne sur le même réseau, après vous être connecté à votre compte Sunny Portal, vous devriez automatiquement avoir la possibilité d'ajouter tous les chargeurs configurés dans evcc en tant que nouveaux consommateurs détectés. Tout devrait être prêt à l'emploi immédiatement, sans qu'aucun réglage ci-dessous ne soit nécessaire.", "descriptionDeviceId": "Chaîne HEX de 12 caractères. Préfixe pour tous les appareils (point de charge, etc.).", + "descriptionDeviceSerial": "Chaîne de 12 caractères en hexadécimal. Numéro de série de base pour tous les appareils (borne de recharge, etc.). Par défaut, evcc le déduit de l'adresse MAC de l'hôte.", "descriptionIdPattern": "Schéma d'identification", "descriptionIds": "Dans Sunny Portal, chaque appareil consommateur doit disposer d'un identifiant unique. evcc génère un identifiant unique basé sur votre matériel. Si vous migrez evcc vers un autre matériel, ces identifiants peuvent changer. Si vous souhaitez conserver l'historique, vous pouvez remplacer les identifiants générés ici. Ouvrez l'URL SEMP (/semp) pour vérifier vos identifiants actuels.", "descriptionSempUrl": "URL SEMP", "descriptionVendorId": "Chaîne HEX de 8 caractères. Préfixe général de toutes les entités. Par défaut, evcc utilisera son propre identifiant fournisseur interne.", "labelDeviceId": "Identifiant de l'appareil", + "labelDeviceSerial": "Numéro de série de l'appareil", "labelVendorId": "Identifiant du fournisseur", "title": "SMA Sunny Home Manager" }, @@ -850,6 +859,15 @@ "configurePriceCo2": "Infos sur la configuration du prix et des données de CO₂.", "footerLong": "{percent} énergie solaire", "footerShort": "{percent} solaire", + "indicator": { + "co2": "Émissions de CO₂", + "co2saved": "Économies de CO₂", + "none": "aucun", + "price": "prix de l'énergie", + "savings": "économies", + "solar": "énergie solaire" + }, + "indicatorLabel": "Informations d'en-tête", "modalTitle": "Aperçu de l'énergie de charge", "moneySaved": "{value} économisés", "percentGrid": "{grid} kWh réseau", @@ -861,10 +879,11 @@ "thisYear": "cette année", "total": "depuis le début" }, - "periodLabel": "Période :", + "periodLabel": "Période", "priceTitle": "Prix de l'énergie", "referenceGrid": "réseau électrique", - "referenceLabel": "Données de référence :", + "referenceLabel": "Données de référence", + "sessionInfo": "Sur la base des sessions de recharge terminées.", "tabTitle": "Mes données" }, "sponsor": { @@ -890,15 +909,26 @@ }, "version": { "availableLong": "mise à jour disponible", + "community": "Communauté evcc", + "labelRelease": "Lancement", + "labelVersion": "Version", + "labelWebsite": "Site web", + "latestVersion": "dernière version", + "madeByCommunity": "Réalisé par {0}.", "modalCancel": "Annuler", "modalDownload": "Télécharger", "modalInstalledVersion": "Version installée", + "modalLatest": "Vous utilisez la dernière version.", + "modalNextRelease": "Nouveautés de la prochaine version", "modalNoReleaseNotes": "Aucune note de version disponible. Plus d'informations sur la nouvelle version disponibles ici :", "modalTitle": "Mise à jour disponible", "modalUpdate": "Installer", "modalUpdateNow": "Installer maintenant", "modalUpdateStarted": "Démarrage de la nouvelle version d’evcc…", - "modalUpdateStatusStart": "L'installation a commencé :" + "modalUpdateStatusStart": "L'installation a commencé :", + "modalViewOnGitHub": "Voir sur GitHub", + "openSource": "open source", + "poweredByOpenSource": "Propulsé par {0}." } }, "forecast": { @@ -908,6 +938,12 @@ "lowestHour": "Heure la plus propre", "range": "Amplitude" }, + "empty": { + "co2": "Découvrez quand l'électricité du réseau dans votre région est verte. Les plans de recharge seront optimisés pour réduire les émissions et calculeront les économies de CO₂ réalisées.", + "price": "Configurez votre tarif d'électricité dynamique pour optimiser automatiquement vos plans de recharge et calculer vos économies.", + "setup": "Définir les tarifs et les prévisions", + "solar": "Consultez les prévisions de production solaire pour aujourd'hui et les prochains jours. Ces données serviront également, à l'avenir, à optimiser automatiquement la recharge." + }, "modalTitle": "Prévisions", "price": { "average": "Moyenne", @@ -915,6 +951,7 @@ "lowestHour": "Heure la moins chère", "range": "Amplitude" }, + "priceZoom": "zoomer", "solar": { "dayAfterTomorrow": "Après-demain", "partly": "partiellement", @@ -923,10 +960,11 @@ "tomorrow": "Demain" }, "solarAdjust": "Ajuster la prévision solaire se basant sur la production réelle{percent}.", + "solarAdjustShort": "ajuster les données réelles", "type": { - "co2": "CO₂", - "price": "Prix", - "solar": "Solaire" + "co2": "Émissions de CO₂", + "price": "Prix du réseau", + "solar": "Production solaire" } }, "general": { @@ -1140,6 +1178,7 @@ "once": "Boost actif pour cette session de charge.", "stateActive": "Boost batterie activé", "stateBelowLimit": "Niveau de batterie trop bas pour le boost", + "stateHold": "Batterie verrouillée", "stateReady": "Boost de batterie prêt" }, "batteryUsage": "Batterie domestique", @@ -1249,6 +1288,7 @@ "batteryBoostBelowLimit": "Niveau de batterie trop bas pour boost.", "batteryBoostDisabled": "Boost batterie désactivé.", "batteryBoostEnabled": "Boost batterie jusqu'à {limit}.", + "batteryBoostHold": "Batterie verrouillée. Mode Boost indisponible.", "charging": "En charge…", "cheapEnergyCharging": "Énergie bon marché disponible.", "cheapEnergyNextStart": "Énergie bon marché dans {duration}.", @@ -1488,5 +1528,13 @@ "lineErrorLink": "Ligne {0}", "restartButton": "Redémarrer", "title": "Erreur au démarrage" + }, + "tabBar": { + "battery": "Batterie", + "charge": "Charger", + "comingSoon": "Cette page est en cours de construction.", + "forecast": "Prévisions", + "more": "Plus", + "sessions": "Sessions" } } diff --git a/i18n/it.json b/i18n/it.json index da6463a12..940edfa9a 100644 --- a/i18n/it.json +++ b/i18n/it.json @@ -40,6 +40,7 @@ "legendTopName": "Ricarica del veicolo assistita dalla batteria", "legendTopSubline": "quando la batteria supera il {soc}.", "modalTitle": "Batteria di casa", + "noBattery": "Nessuna batteria configurata.", "usageTab": "Uso della batteria" }, "config": { @@ -601,6 +602,11 @@ "url": "URL del server", "urlHelp": "Copia questo URL nella configurazione del caricabatterie. Per ulteriori dettagli, consulta il manuale del produttore. Il caricabatterie dovrebbe aggiungere automaticamente il proprio identificativo univoco (ID stazione) all'URL. In rari casi, potrebbe essere necessario specificare manualmente l'identificativo. Esempio: `{url}`" }, + "optimizer": { + "description": "Analizza le previsioni solari, i prezzi dell'elettricità e i tuoi pattern di consumo per ottimizzare l'uso della batteria e la strategia di carica. I dati sono inviati al servizio di ottimizzazione di evcc per l'elaborazione.", + "enable": "Abilita Ottimizzatore", + "title": "Ottimizzatore" + }, "options": { "boolean": { "no": "no", @@ -853,6 +859,14 @@ "configurePriceCo2": "Scopri come configurare i dati su prezzi e CO₂.", "footerLong": "{percent} di energia solare", "footerShort": "{percent} solare", + "indicator": { + "co2": "Emissioni CO₂", + "co2saved": "CO₂ risparmiata", + "none": "nessuno", + "price": "prezzo elettricità", + "savings": "risparmi", + "solar": "energia solare" + }, "modalTitle": "Riepilogo energia caricata", "moneySaved": "{value} di risparmio", "percentGrid": "{grid} kWh rete", @@ -868,6 +882,7 @@ "priceTitle": "Prezzo energia", "referenceGrid": "rete", "referenceLabel": "Dati di riferimento:", + "sessionInfo": "In base alle sessioni di ricarica completate.", "tabTitle": "I miei dati" }, "sponsor": { @@ -893,15 +908,25 @@ }, "version": { "availableLong": "aggiornamento disponibile", + "community": "community di evcc", + "labelRelease": "Release", + "labelVersion": "Versione", + "labelWebsite": "Sito web", + "latestVersion": "ultima versione", + "madeByCommunity": "Creato da {0}.", "modalCancel": "Annulla", "modalDownload": "Download", "modalInstalledVersion": "Versione correntemente installata", + "modalLatest": "Stai utilizzando la versione più recente.", + "modalNextRelease": "Cosa c'è nella prossima release", "modalNoReleaseNotes": "Non ci sono note di rilascio disponibili. Altre informazioni circa la nuova versione si trovano qui:", "modalTitle": "Aggiornamento disponibile", "modalUpdate": "Installare", "modalUpdateNow": "Installa ora", "modalUpdateStarted": "Evcc ripartirà dopo l'aggiornamento…", - "modalUpdateStatusStart": "Installazione avviata:" + "modalUpdateStatusStart": "Installazione avviata:", + "modalViewOnGitHub": "Vedi su GitHub", + "openSource": "open source" } }, "forecast": { @@ -911,6 +936,12 @@ "lowestHour": "Ora più ecologica", "range": "Intervallo" }, + "empty": { + "co2": "Controlla quando l'energia della rete nella tua regione è prodotta da fonti pulite. I piani di ricarica saranno ottimizzati per ridurre le emissioni e terranno conto del risparmio di CO₂.", + "price": "Configura la tua tariffa dinamica dell'elettricità in modo da ottimizzare i piani di ricarica e calcolare il risparmio economico.", + "setup": "Imposta le tariffe e le previsioni", + "solar": "Visualizza la produzione solare attesa per oggi e per i giorni seguenti. Sarà utilizzato anche per l'ottimizzazione automatica della ricarica futura." + }, "modalTitle": "Previsioni", "price": { "average": "Media", @@ -918,6 +949,7 @@ "lowestHour": "Ora più economica", "range": "Intervallo" }, + "priceZoom": "ingrandisci", "solar": { "dayAfterTomorrow": "Dopodomani", "partly": "parzialmente", @@ -926,6 +958,7 @@ "tomorrow": "Domani" }, "solarAdjust": "Modifica la previsione sulla base dei dati reali{percent}.", + "solarAdjustShort": "correzione con dati reali", "type": { "co2": "CO₂", "price": "Prezzo", @@ -1137,6 +1170,7 @@ "once": "Boost attivo per questa sessione di ricarica.", "stateActive": "Boost da batteria attivo", "stateBelowLimit": "Batteria troppo scarica per il boost", + "stateHold": "Batteria bloccata", "stateReady": "Battery boost pronto" }, "batteryUsage": "Batteria di casa", @@ -1246,6 +1280,7 @@ "batteryBoostBelowLimit": "Batteria troppo scarica per il boost", "batteryBoostDisabled": "Battery boost disabilitato.", "batteryBoostEnabled": "Boost fino al {limit}.", + "batteryBoostHold": "Batteria bloccata. Boost non disponibile.", "charging": "In carica…", "cheapEnergyCharging": "Corrente economica disponibile.", "cheapEnergyNextStart": "Corrente economica in {duration}.", @@ -1485,5 +1520,13 @@ "lineErrorLink": "linea {0}", "restartButton": "Ricomincia", "title": "Errore di avvio" + }, + "tabBar": { + "battery": "Batteria", + "charge": "Carica", + "comingSoon": "Questa pagina è in costruzione.", + "forecast": "Previsione", + "more": "Di più", + "sessions": "Sessioni" } } diff --git a/i18n/lb.json b/i18n/lb.json index 4f5acc722..27f3c437b 100644 --- a/i18n/lb.json +++ b/i18n/lb.json @@ -40,6 +40,7 @@ "legendTopName": "Batterie-ënnerstëtzt Opluede vum Gefier", "legendTopSubline": "wann d'Hausbatterie iwwer {soc} ass.", "modalTitle": "Hausbatterie", + "noBattery": "Keng Batterien konfiguréiert.", "usageTab": "Batterieverbrauch" }, "config": { @@ -130,8 +131,9 @@ "currency": "Wärung", "current": "Stroum", "currentRange": "Stroum", + "curtailed": "Einspeisung limitéiert", "detected": "Erkannt", - "dimmed": "Gedimmt", + "dimmed": "Verbrauch limitéiert", "enabled": "Ageschalt", "energy": "Energie", "events": "Evenementer", @@ -600,6 +602,11 @@ "url": "Server-URL", "urlHelp": "Kopéier dës URL fir d'Konfiguratioun vun denger Wallbox. Kuck am Handbuch vum Hiersteller no Detailer. Et gëtt erwaart, datt d'Wallbox automatesch säin eenzegaartegen Identifikator (Statiouns-ID) un d'URL bäifügt. A rare Fäll musst Dir den Identifikator eventuell manuell spezifizéieren. Beispill: `{url}`" }, + "optimizer": { + "description": "Analyséiert d'Solarprognos, d'Stroumpräisser an Är Verbrauchssmuster, fir d'Batterie- a Luedstrategie ze optiméieren. D'Donnéeë ginn un den evcc-Optimiséierungsservice fir d'Berechnen geschéckt.", + "enable": "Optimizer aktivéieren", + "title": "Optimizer" + }, "options": { "boolean": { "no": "nee", @@ -642,11 +649,13 @@ "cardTitle": "Sunny Home Manager", "description": "Den evcc ass mat enger Integratioun fir den SMA Sunny Home Manager (SHM) iwwert den SEMP-Protokoll ausgestatt. Wann den SHM am selwechte Netzwierk leeft, sollt Dir no der Aschreiwung an Ärem Sunny Portal-Kont automatesch d'Méiglechkeet kréien, all an evcc konfiguréiert Luedpunkten als nei detektéiert Laaschten derbäizesetzen. Alles sollt direkt gebrauchsfäeg sinn, ouni weider Upassungen.", "descriptionDeviceId": "12-stellegen HEX-String. Präfix fir all Geräter (Luedpunkt, ..).", + "descriptionDeviceSerial": "HEX-String mat 12-Zeechen. Basis-Seriennummer fir all Apparater (Luedpunkt, ...). Standardméisseg kritt den evcc dës aus der MAC-Adress vum Host.", "descriptionIdPattern": "Identifikatiounsmuster", "descriptionIds": "Am Sunny Portal brauch all Ladung (Luedpunkt, etc.) eng eenzegaarteg Identifikatioun. evcc generéiert eng eenzegaarteg Identifikatioun, déi op Ärer Hardware baséiert. Wann Dir evcc op aner Hardware migréiert, kann dësen Identifikator sech änneren. Fir den Historique ze späicheren, kënnt Dir déi generéiert Identifikatoren hei iwwerschreiwen. Maacht d'SEMP URL op (/semp) fir Är aktuell Identifikatoren ze kontrolléieren.", "descriptionSempUrl": "SEMP-URL", "descriptionVendorId": "8-stellegen HEX-String. Allgemenge Präfix fir all Entiéiten. Standardméisseg verwennt evcc seng eegen intern Hiersteller-ID.", "labelDeviceId": "Geräter-ID", + "labelDeviceSerial": "Apparat-Seriennummer", "labelVendorId": "Hiersteller-ID", "title": "SMA Sunny Home Manager" }, @@ -833,7 +842,7 @@ }, "footer": { "community": { - "greenEnergy": "Solarenergie", + "greenEnergy": "Solar", "greenEnergySub1": "opgelueden mat evcc", "greenEnergySub2": "zënter Oktober 2022", "greenShare": "Sonnenundeel", @@ -850,6 +859,15 @@ "configurePriceCo2": "Léiert wéi een de Präis an d'CO₂-Daten konfiguréiert.", "footerLong": "{percent} Solarenergie", "footerShort": "{percent} Solar", + "indicator": { + "co2": "CO₂-Emissiounen", + "co2saved": "CO₂ agespuert", + "none": "keng", + "price": "Energiepräis", + "savings": "Erspuernesser", + "solar": "Solarenergie" + }, + "indicatorLabel": "Header-Info", "modalTitle": "Iwwersiicht vum opgeluedene Stroum", "moneySaved": "{value} gespuert", "percentGrid": "{grid} kWh Stroumnetz", @@ -861,10 +879,11 @@ "thisYear": "dëst Joer", "total": "gesamt" }, - "periodLabel": "Zäitraum:", + "periodLabel": "Zäitraum", "priceTitle": "Energiepräis", "referenceGrid": "Netz", - "referenceLabel": "Referenzdaten:", + "referenceLabel": "Referenzdaten", + "sessionInfo": "Baséiert op ofgeschlossen Opluedsessiounen.", "tabTitle": "Meng Daten" }, "sponsor": { @@ -890,15 +909,26 @@ }, "version": { "availableLong": "nei Versioun verfügbar", + "community": "evcc Communautéit", + "labelRelease": "Verëffentlechung", + "labelVersion": "Versioun", + "labelWebsite": "Websäit", + "latestVersion": "lescht Versioun", + "madeByCommunity": "Entwéckelt vun der {0}.", "modalCancel": "Ofbriechen", "modalDownload": "Download", "modalInstalledVersion": "Aktuell installéiert Versioun", + "modalLatest": "Dir benotzt déi lescht Versioun.", + "modalNextRelease": "Wat gëtt et an der nächster Versioun", "modalNoReleaseNotes": "Keng Versiounshiweiser verfügbar. Méi Informatioun iwwert déi nei Versioun:", "modalTitle": "Aktualiséierung verfügbar", "modalUpdate": "Installéieren", "modalUpdateNow": "Elo installéieren", "modalUpdateStarted": "Déi nei Versioun vun evcc starten…", - "modalUpdateStatusStart": "Installatioun ugefaang:" + "modalUpdateStatusStart": "Installatioun ugefaang:", + "modalViewOnGitHub": "Op GitHub kucken", + "openSource": "Open Source", + "poweredByOpenSource": "Bedriwwen vun {0}." } }, "forecast": { @@ -908,6 +938,12 @@ "lowestHour": "Déi properst Stonn", "range": "Beräich" }, + "empty": { + "co2": "Iwwerpréiwt, wéini d'Netzenergie an Ärer Regioun propper ass. Luedpläng ginn optimiséiert fir d'Emissiounen ze reduzéieren an CO₂ anzespueren.", + "price": "Konfiguréiert Ären dynameschen Stromtarif, fir d'Luedpläng automatesch ze optiméieren an d'Erspuernësser ze berechnen.", + "setup": "Tariffer a Prognosen opstellen", + "solar": "Kuckt déi erwaart Solarproduktioun fir haut an déi nächst Deeg. Et gëtt och an der Zukunft fir d'automatiséiert Luedoptimiséierung benotzt." + }, "modalTitle": "Prognose", "price": { "average": "Duerchschnitt", @@ -915,6 +951,7 @@ "lowestHour": "Bëllegst Stonn", "range": "Beräich" }, + "priceZoom": "erazoomen", "solar": { "dayAfterTomorrow": "Iwwermuer", "partly": "deelweis", @@ -923,10 +960,11 @@ "tomorrow": "Muer" }, "solarAdjust": "Solarprognose unhand vun de reale Produktionsdaten upassen{percent}.", + "solarAdjustShort": "u reell Daten upassen", "type": { - "co2": "CO₂", - "price": "Präis", - "solar": "Solar" + "co2": "CO₂-Emissiounen", + "price": "Netzpräis", + "solar": "Solar-Productioun" } }, "general": { @@ -1139,6 +1177,7 @@ "once": "Boost ass dir dës Luedsessioun aktivéiert.", "stateActive": "Batterieboost aktiv", "stateBelowLimit": "Batterie ze niddereg fir de Boost", + "stateHold": "Batterie gespaart", "stateReady": "Batterieboost bereet" }, "batteryUsage": "Hausbatterie", @@ -1248,6 +1287,7 @@ "batteryBoostBelowLimit": "Batterie ze niddereg fir de Boost.", "batteryBoostDisabled": "Batterieboost deaktivéiert.", "batteryBoostEnabled": "Boost bis d'Batterie bei {limit} ass.", + "batteryBoostHold": "Batterie gespaart. Boost net verfügbar.", "charging": "Luet…", "cheapEnergyCharging": "Gënschteg Energie verfügbar.", "cheapEnergyNextStart": "Gënschteg Energie an {duration}.", @@ -1487,5 +1527,13 @@ "lineErrorLink": "Linn {0}", "restartButton": "Neistart", "title": "Startup Feeler" + }, + "tabBar": { + "battery": "Batterie", + "charge": "Lueden", + "comingSoon": "Dës Säit ass amgaang gebaut ze ginn.", + "forecast": "Viraussoen", + "more": "Méi", + "sessions": "Sessiounen" } } diff --git a/i18n/lt.json b/i18n/lt.json index 674207c0f..09adec291 100644 --- a/i18n/lt.json +++ b/i18n/lt.json @@ -113,6 +113,7 @@ "title": "Valiuta" }, "deviceValue": { + "activeClients": "Aktyvūs vartotojai", "amount": "Kiekis", "broker": "Broker sistema", "bucket": "Bucket", @@ -126,6 +127,7 @@ "chargedEnergy": "Įkrauta", "co2": "Tinklo CO₂", "configured": "Sukonfigūruota", + "connected": "Prisijungta", "connections": "Jungtys", "controllable": "Valdomas", "currency": "Valiuta", @@ -144,6 +146,7 @@ "hemsActiveLimit": "Aktyvus limitas", "hemsType": "Komunikacija", "identifier": "RFID-Identifikatorius", + "loginBlocked": "Pasiektas prisijungimų limitas", "max": "max", "messengers": "Paslaugos", "no": "ne", @@ -206,7 +209,7 @@ }, "experimental": { "description": "Suteikia išankstinę prieigą prie funkcijų, kurios vis dar testuojamos. Jos gali būti nestabilios bei gali pasikeisti arba būti pašalintos būsimose versijose.", - "title": "Eksperimentinis" + "title": "Eksperimentinės" }, "ext": { "description": "Įrašo nekontroliuojamų įrenginių (pvz., šaldytuvo, skalbimo mašinos ir kt.) energijos vertes statistikos tikslais. Šie skaitikliai taip pat gali būti naudojami apkrovos valdymui (pvz., paskirstymui).", @@ -238,7 +241,7 @@ "dragItem": "Galima vilkti: {title}", "dragList": "Galima sukeisti vietomis", "error": "Klaida", - "experimental": "Eksperimentinis", + "experimental": "Eksperimentinės", "forceSave": "Vistiek įrašyti", "fromYamlHint": "Pastaba: Sukonfigūruota naudojant evcc.yaml. Pašalinkite įrašą iš failo, kad būtų galima redaguoti čia.", "hideAdvancedSettings": "Slėpti išplėstinius nustatymus", @@ -602,6 +605,12 @@ "url": "Serverio URL", "urlHelp": "Nukopijuokite šį URL į savo įkroviklio konfigūraciją. Išsamesnės informacijos ieškokite gamintojo vadove. Įkroviklis turėtų automatiškai pridėti savo unikalų identifikatorių (stotelės ID) prie URL. Retais atvejais gali tekti rankiniu būdu nurodyti identifikatorių. Pavyzdys: `{url}`" }, + "optimizer": { + "description": "Analizuoja saulės energijos prognozę, elektros energijos kainas ir jūsų vartojimo įpročius, kad optimizuotų kaupiklį ir įkrovimo strategiją. Duomenys siunčiami apdorojimui į evcc optimizavimo paslaugą. Šiuo metu tik apskaičiuoja ir vizualizuoja, įrenginių kol kas nevaldo.", + "enable": "Įjungti Optimizavimą", + "info": "Gali užtrukti kelias minutes, kol meniu pasirodys optimizatoriaus eilutė. Naujoms evcc instaliacijoms gali užtrukti iki 24 valandų, kol evcc turės pakankamai duomenų veikimui.", + "title": "Optimizavimas" + }, "options": { "boolean": { "no": "ne", @@ -629,6 +638,38 @@ "titleAdd": "Pridėti saulės skaitiklį", "titleEdit": "Redaguoti saulės skaitiklį" }, + "remote": { + "active": "Aktyvus", + "addClient": "Pridėti vartotoją", + "addClientDescription": "Įgaliojimai saugomi ir tikrinami tik lokaliai, jūsų evcc egzemplioriuje.", + "addClientTitle": "Pridėti nutolusį vartotoją", + "clientCreated": "Vartotojas sukurtas", + "clients": "Vartotojai", + "confirmDelete": "Ištrinti vartotoją?", + "connected": "Prijungta", + "createClient": "Sukurti vartotoją", + "description": "Pasiekite savo evcc sistemą iš bet kurios vietos, naudodami evcc mobiliąją programėlę. Nereikia jokio prievado peradresavimo ar VPN.", + "deviceName": "Įrenginio pavadinimas", + "disconnected": "Atjungta", + "done": "Atlikta", + "enableLabel": "Įgalinti nuotolinę prieigą", + "expiration": "Galiojimo laikas", + "expirationNone": "Niekada", + "expired": "pasibaigęs", + "expiresIn": "pasibaigs {time}", + "lastActive": "aktyvus {time}", + "loginBlocked": "Nuotoliniai prisijungimai blokuojami vienai minutei dėl per daug nepavykusių prisijungimo bandymų.", + "manualLogin": "Arba prisijunkite rankiniu būdu adresu {url} naršyklėje naudodami šiuos prisijungimo duomenis:", + "noClients": "Kol kas nėra vartotojų. Niekas dar negali prisijungti.", + "password": "Slaptažodis", + "passwordOnce": "Šis slaptažodis rodomas tik vieną kartą. Nuskaitykite QR kodą arba nukopijuokite jį dabar. Daugiau jo nebegalėsite pamatyti.", + "qrInstall": "Įdiekite evcc programėlę, skirtą {ios} arba {android}.", + "qrScan": "Nuskaitykite kodą telefono kamera, kad prisijungtumėte. Bakstelėkite ant jo, jei jau naudojate telefoną.", + "removeClient": "Pašalinti vartotoją", + "title": "Nuotolinė Prieiga", + "url": "Viešas URL", + "username": "Vartotojo vardas" + }, "section": { "additionalMeter": "Papildomi skaitikliai", "general": "Pagrindiniai", @@ -904,7 +945,7 @@ "optInSponsorship": "Gali tik rėmėjai." }, "version": { - "availableLong": "yra naujesnė versija", + "availableLong": "(yra naujesnė versija)", "community": "evcc bendruomenė", "labelRelease": "Leidimas", "labelVersion": "Versija", @@ -940,6 +981,7 @@ "setup": "Sukonfigūruoti tarifus ir prognozes", "solar": "Prognozuojama saulės energijos gamyba šiandienai ir ateinančiomis dienomis. Ateityje taip pat bus naudojama automatiniam įkrovimo optimizavimui." }, + "hideLine": "paslėpti liniją", "modalTitle": "Prognozė", "price": { "average": "Vidurkis", @@ -948,6 +990,7 @@ "range": "Diapazonas" }, "priceZoom": "priartinti", + "showLine": "rodyti liniją", "solar": { "dayAfterTomorrow": "Poryt", "partly": "dalinai", @@ -956,7 +999,8 @@ "tomorrow": "Rytoj" }, "solarAdjust": "Pakoreguoti saulės prognozę remiantis faktiškais gamybos duomenimis{percent}.", - "solarAdjustShort": "koreguoti pagal faktinius duomenis", + "solarAdjustMedium": "koreguoti pagal faktinius duomenis", + "solarAdjustShort": "koreguoti", "type": { "co2": "CO₂ Išmetimai", "price": "Pirkimo Kaina", @@ -1001,7 +1045,7 @@ }, "restartButton": "Restartuoti", "restartDescription": "Ar pabandėte išjungti ir vėl įjungti?", - "secondaryActions": "Vis dar nepavyksta išspręsti problemos? Čia rasite drastiškesnes opcijas." + "secondaryActions": "Vis dar nepavyksta išspręsti problemos? Čia rasite sudėtingesnes opcijas." }, "issue": { "additional": { diff --git a/i18n/nl.json b/i18n/nl.json index d6a7cbd57..4b9d5bd1c 100644 --- a/i18n/nl.json +++ b/i18n/nl.json @@ -126,6 +126,7 @@ "chargedEnergy": "Geladen", "co2": "Stroomnet-CO₂", "configured": "Geconfigureerd", + "connected": "Verbonden", "connections": "Verbindingen", "controllable": "Stuurbaar", "currency": "Valuta", @@ -144,6 +145,7 @@ "hemsActiveLimit": "Actieve limiet", "hemsType": "Communicatie", "identifier": "RFID-Nummer", + "loginBlocked": "Inloglimiet bereikt", "max": "max", "messengers": "Diensten", "no": "nee", @@ -602,6 +604,11 @@ "url": "Server URL", "urlHelp": "Kopieer deze URL naar de configuratie van uw lader. Raadpleeg de handleiding van de fabrikant voor meer informatie. De lader voegt naar verwachting automatisch zijn unieke identificatiecode (station-ID) toe aan de URL. In zeldzame gevallen moet u de identificatiecode mogelijk handmatig ingeven. Voorbeeld: `{url}`" }, + "optimizer": { + "description": "Analyseert zonne-energieprognoses, elektriciteitsprijzen en je verbruikspatronen om de batterij- en laadstrategie te optimaliseren. Gegevens worden naar de evcc-optimalisatiedienst gestuurd voor verwerking.", + "enable": "Activeer optimizer", + "title": "Optimizer" + }, "options": { "boolean": { "no": "nee", @@ -629,6 +636,29 @@ "titleAdd": "Meter Zonnepanelen Toevoegen", "titleEdit": "Meter Zonnepanelen Bewerken" }, + "remote": { + "active": "Actief", + "addClient": "Client toevoegen", + "addClientTitle": "Remote client toevoegen", + "clientCreated": "Client toegevoegd", + "clients": "Clients", + "confirmDelete": "Client verwijderen?", + "connected": "Verbonden", + "createClient": "Client aanmaken", + "description": "Toegang tot uw evcc installatie via de evcc mobile app. Zonder port-forwarding en zonder VPN.", + "deviceName": "Apparaat naam", + "disconnected": "Niet verbonden", + "done": "Klaar", + "enableLabel": "Toegang op afstand inschakelen", + "expirationNone": "Nooit", + "expired": "verlopen", + "expiresIn": "verloopt {time}", + "lastActive": "actief {time}", + "loginBlocked": "Inloggen op afstand is voor een minuut geblokkeerd, omdat er te veel mislukte inlogpogingen waren.", + "manualLogin": "Of log handmatig in via uw browser op {url}, met de volgende credentials:", + "password": "Wachtwoord", + "username": "Gebruikersnaam" + }, "section": { "additionalMeter": "Extra meters", "general": "Algemeen", @@ -862,6 +892,7 @@ "savings": "Besparing", "solar": "Zonne-Energie" }, + "indicatorLabel": "Header info", "modalTitle": "Oplaadenergie-overzicht", "moneySaved": "{value} bespaard", "percentGrid": "{grid} kWh net", @@ -873,10 +904,11 @@ "thisYear": "dit jaar", "total": "aller tijden" }, - "periodLabel": "Periode:", + "periodLabel": "Periode", "priceTitle": "Energieprijs", "referenceGrid": "net", - "referenceLabel": "Referentiedata:", + "referenceLabel": "Referentiedata", + "sessionInfo": "Op basis van voltooide laadsessies.", "tabTitle": "Mijn data" }, "sponsor": { @@ -902,15 +934,26 @@ }, "version": { "availableLong": "nieuwe versie beschikbaar", + "community": "evcc coomunity", + "labelRelease": "Release", + "labelVersion": "Versie", + "labelWebsite": "Website", + "latestVersion": "Nieuwste versie", + "madeByCommunity": "Gemaakt door de {0}.", "modalCancel": "Annuleer", "modalDownload": "Download", "modalInstalledVersion": "Geïnstalleerde versie", + "modalLatest": "U heeft de nieuwste versie.", + "modalNextRelease": "Wat zit er in de volgende release", "modalNoReleaseNotes": "Geen release notes beschikbaar. Meer info over de nieuwe versie:", "modalTitle": "Nieuwe versie beschikbaar", "modalUpdate": "Installeren", "modalUpdateNow": "Nu installeren", "modalUpdateStarted": "Starten van de nieuwe versie van evcc…", - "modalUpdateStatusStart": "Installatie gestart:" + "modalUpdateStatusStart": "Installatie gestart:", + "modalViewOnGitHub": "Op GitHub bekijken", + "openSource": "open source", + "poweredByOpenSource": "Powered by {0}." } }, "forecast": { @@ -920,6 +963,13 @@ "lowestHour": "Schoonste uur", "range": "Bereik" }, + "empty": { + "co2": "Kijk wanneer de netstroom in uw regio schoon is. Laadplanning wordt geoptimaliseerd voor minimale emissie en CO₂ besparing wordt berekend.", + "price": "Stel uw dynamische stroomprijzen in om laadplannen te optimaliseren en om uw besparingen mee te berekenen.", + "setup": "Tarieven en voorspellingen instellen", + "solar": "Bekijk de verwachtte zonne opbrengt voor vandaag en de komende dagen. Wordt ook gebruikt voor automatische laadplan optimalisatie in de toekomst." + }, + "hideLine": "regel verbergen", "modalTitle": "Voorspelling", "price": { "average": "Gemiddeld", @@ -927,6 +977,8 @@ "lowestHour": "Goedkoopste uur", "range": "Bereik" }, + "priceZoom": "zoom in", + "showLine": "regel tonen", "solar": { "dayAfterTomorrow": "Overmorgen", "partly": "gedeeltelijk", @@ -935,10 +987,11 @@ "tomorrow": "Morgen" }, "solarAdjust": "Pas de zonne-energie voorspelling aan op basis van echte productiegegevens{percent}.", + "solarAdjustShort": "aanpassen", "type": { - "co2": "CO₂", - "price": "Prijs", - "solar": "Zonne-energie" + "co2": "CO₂ Uitstoot", + "price": "Net-Prijs", + "solar": "Zonne-energie opbrengst" } }, "general": { @@ -1152,6 +1205,7 @@ "once": "Boost geactiveerd voor deze laadsessie.", "stateActive": "Batterijboost actief", "stateBelowLimit": "Batterij te laag voor boost", + "stateHold": "Batterij vergrendeld", "stateReady": "Batterijboost klaar" }, "batteryUsage": "Thuisbatterij", @@ -1261,6 +1315,7 @@ "batteryBoostBelowLimit": "Batterij te laag voor boost.", "batteryBoostDisabled": "Batterijboost uitgeschakeld.", "batteryBoostEnabled": "Boost de batterij tot {limit}.", + "batteryBoostHold": "Batterij vergrendeld. Boost niet beschikbaar.", "charging": "Opladen…", "cheapEnergyCharging": "Goedkope energie beschikbaar.", "cheapEnergyNextStart": "Goedkope energie in {duration}.", @@ -1500,5 +1555,13 @@ "lineErrorLink": "lijn {0}", "restartButton": "Herstart", "title": "Opstart fout" + }, + "tabBar": { + "battery": "Batterij", + "charge": "Laden", + "comingSoon": "Deze pagina is in ontwikkeling.", + "forecast": "Voorspelling", + "more": "Meer", + "sessions": "Sessies" } } diff --git a/i18n/no.json b/i18n/no.json index 41f73b942..92658c4e5 100644 --- a/i18n/no.json +++ b/i18n/no.json @@ -28,7 +28,7 @@ }, "capacity": "{energy} av {total}", "control": "Batterikontroll", - "discharge": "Forhindre utladning i hurtigmodus og planlagt lading", + "discharge": "Forhindre utladning i hurtigmodus og planlagt lading.", "disclaimerHint": "Merk:", "disclaimerText": "Kun relevant i PV-modus. Ladeadferden justeres deretter.", "gridChargeTab": "Nettlading", diff --git a/i18n/pl.json b/i18n/pl.json index ad3e1e7f9..fc1db3a70 100644 --- a/i18n/pl.json +++ b/i18n/pl.json @@ -40,6 +40,7 @@ "legendTopName": "Ładowanie pojazdu z wykorzystaniem domowego magazynu energii", "legendTopSubline": "gdy poziom naładowania domowego magazynu energii jest wyższy niż {soc}.", "modalTitle": "Domowy magazyn energii", + "noBattery": "Brak skonfigurowanego magazynu energii.", "usageTab": "Użycie magazynu" }, "config": { @@ -446,7 +447,7 @@ "guest": { "messageDefault": "Nieznany pojazd, połączony gość?", "title": "Kiedy podłączy się nieznany samochód", - "titleDefault": "Nieznany samochód" + "titleDefault": "Nieznany pojazd" }, "planoverrun": { "messageDefault": "{vehicleTitle}: Plan zostanie przekroczony.", @@ -861,6 +862,7 @@ "priceTitle": "Cena energii", "referenceGrid": "sieć", "referenceLabel": "Dane referencyjne:", + "sessionInfo": "Oparte na zakończonych sesjach ładowania.", "tabTitle": "Moje dane" }, "sponsor": { diff --git a/i18n/pt.json b/i18n/pt.json index fc18636f4..7f654842a 100644 --- a/i18n/pt.json +++ b/i18n/pt.json @@ -40,6 +40,7 @@ "legendTopName": "Carregamento de veículos com a bateria", "legendTopSubline": "quando a bateria da casa está acima de {soc}.", "modalTitle": "Bateria da Casa", + "noBattery": "Não há baterias configuradas.", "usageTab": "Gestão da bateria" }, "config": { @@ -130,8 +131,9 @@ "currency": "Moeda", "current": "Corrente", "currentRange": "Corrente", + "curtailed": "Limitação da injeção na rede", "detected": "Detectado", - "dimmed": "Escurecido", + "dimmed": "Consumo limitado", "enabled": "Ativado", "energy": "Energia", "events": "Eventos", @@ -600,6 +602,11 @@ "url": "URL do Servidor", "urlHelp": "Copie este URL para a configuração do seu carregador. Consulte o manual do fabricante para obter mais detalhes. O carregador deve anexar automaticamente o seu identificador único (ID da estação) ao URL. Em casos raros, pode ser necessário especificar manualmente o identificador. Exemplo: `{url}`" }, + "optimizer": { + "description": "Analisa as previsões de energia solar, os preços da eletricidade e os seus padrões de consumo para otimizar a estratégia de utilização da bateria e de carregamento. Os dados são enviados para o serviço de otimização evcc para serem processados.", + "enable": "Ativar o Otimizador", + "title": "Otimizador" + }, "options": { "boolean": { "no": "não", @@ -642,11 +649,13 @@ "cardTitle": "Sunny Home Manager", "description": "evcc tem integração com SMA Sunny Manager (SHM) via protocolo SEMP. Se estiver a ser executado na mesma rede, depois de aceder à sua conta no Sunny Portal, deverá aparecer automaticamente a opção de adicionar todos os Carregadores configurados no evcc como novos clientes. Tudo deverá ficar pronto a usar imediatamente sem necessidade de quaisquer ajustes.", "descriptionDeviceId": "String 12 caracteres HEX. Prefixo para todos os dispositivos (ponto de carregamento, ..).", + "descriptionDeviceSerial": "Cadeia HEX de 12 caracteres. Número de série padrão para todos os dispositivos (ponto de carregamento, etc.). Por predefinição, o evcc obtém este valor a partir do endereço MAC do host.", "descriptionIdPattern": "Padrão de identidade", "descriptionIds": "No Sunny Portal cada dispositivo necessita duma identidade única. O evcc gera uma identidade única com base no seu hardware. Se migrar o evcc para outro hardware estas identidades podem mudar. Se quiser manter o histórico pode substituir as identidades geradas aqui. Abra o SEMP URL(/semp) para ver as identidade atuais.", "descriptionSempUrl": "SEMP URL", "descriptionVendorId": "string 8 caratecteres HEX. Prefixo geral de todas as entidades. O evcc usa o seu próprio vendor ID interno Por defeito.", "labelDeviceId": "Device ID", + "labelDeviceSerial": "Número de série do equipamento", "labelVendorId": "Vendor ID", "title": "SMA Sunny Home Manager" }, @@ -850,6 +859,15 @@ "configurePriceCo2": "Saiba como configurar os dados de preço e CO₂.", "footerLong": "{percent} energia solar", "footerShort": "{percent} Sol", + "indicator": { + "co2": "Emissões de CO₂", + "co2saved": "CO₂ poupado", + "none": "nenhum", + "price": "preço da energia", + "savings": "poupança", + "solar": "energia solar" + }, + "indicatorLabel": "Informações do cabeçalho", "modalTitle": "Dados da energia carregada", "moneySaved": "{value} poupado", "percentGrid": "{grid} kWh Rede", @@ -861,10 +879,11 @@ "thisYear": "este ano", "total": "total" }, - "periodLabel": "Período:", + "periodLabel": "Período", "priceTitle": "Preço da energia", "referenceGrid": "Rede", - "referenceLabel": "Dados de referência:", + "referenceLabel": "Dados de referência", + "sessionInfo": "Com base nas sessões de carregamento concluídas.", "tabTitle": "Os meus dados" }, "sponsor": { @@ -890,15 +909,26 @@ }, "version": { "availableLong": "nova versão disponível", + "community": "comunidade evcc", + "labelRelease": "Lançamento", + "labelVersion": "Versão", + "labelWebsite": "Website", + "latestVersion": "versão mais recente", + "madeByCommunity": "Realizado por {0}.", "modalCancel": "Cancelar", "modalDownload": "Download", "modalInstalledVersion": "Versão instalada", + "modalLatest": "Está a utilizar a versão mais recente.", + "modalNextRelease": "O que está previsto para a próxima versão", "modalNoReleaseNotes": "Sem notas da versão disponível. Mais informações sobre a nova versão:", "modalTitle": "Nova versão disponível", "modalUpdate": "Instalar", "modalUpdateNow": "Instalar agora", "modalUpdateStarted": "Iniciando a nova versão do evcc…", - "modalUpdateStatusStart": "Instalação iniciada:" + "modalUpdateStatusStart": "Instalação iniciada:", + "modalViewOnGitHub": "Ver no GitHub", + "openSource": "código aberto", + "poweredByOpenSource": "Desenvolvido por {0}." } }, "forecast": { @@ -908,6 +938,12 @@ "lowestHour": "Horário mais \"Verde\"", "range": "Intervalo" }, + "empty": { + "co2": "Veja quando a energia da rede na sua região é verde. Os planos de carregamento serão otimizados para reduzir as emissões e calcular a redução de CO₂.", + "price": "Configure a sua tarifa de eletricidade dinâmica para otimizar automaticamente os planos de carregamento e calcular as poupanças.", + "setup": "Definir tarifas e previsões", + "solar": "Veja a produção solar prevista para hoje e para os próximos dias. Esta informação será também utilizada para a otimização automática do carregamento no futuro." + }, "modalTitle": "Previsões", "price": { "average": "Média", @@ -915,6 +951,7 @@ "lowestHour": "Horário mais barato", "range": "Intervalo" }, + "priceZoom": "ampliar", "solar": { "dayAfterTomorrow": "Depois de amanhã", "partly": "parcialmente", @@ -923,10 +960,11 @@ "tomorrow": "Amanhã" }, "solarAdjust": "Ajustar a previsão solar com base em dados reais de produção{percent}.", + "solarAdjustShort": "ajuste com dados reais", "type": { - "co2": "CO₂", - "price": "Preço", - "solar": "Solar" + "co2": "Emissões de CO₂", + "price": "Preço da rede", + "solar": "Produção solar" } }, "general": { @@ -1139,6 +1177,7 @@ "once": "Boost ativo para esta sessão de carga.", "stateActive": "Boost de bateria ativo", "stateBelowLimit": "Bateria com carga insuficiente para o boost", + "stateHold": "Bateria bloqueada", "stateReady": "Boost da bateria pronto" }, "batteryUsage": "Bateria de casa", @@ -1248,6 +1287,7 @@ "batteryBoostBelowLimit": "Bateria com carga insuficiente para boost.", "batteryBoostDisabled": "Boost da bateria desativado.", "batteryBoostEnabled": "Boost até a bateria atingir {limit}.", + "batteryBoostHold": "Bateria bloqueada. Função Boost não disponível.", "charging": "A carregar…", "cheapEnergyCharging": "Energia barata disponível.", "cheapEnergyNextStart": "Energia barata em {duration}.", @@ -1487,5 +1527,13 @@ "lineErrorLink": "linha {0}", "restartButton": "Reiniciar", "title": "Erro ao iniciar" + }, + "tabBar": { + "battery": "Bateria", + "charge": "Carga", + "comingSoon": "Esta página está em construção.", + "forecast": "Previsão", + "more": "Mais", + "sessions": "Sessões" } } diff --git a/i18n/sv.json b/i18n/sv.json index 628d98a07..3ed0c7dc9 100644 --- a/i18n/sv.json +++ b/i18n/sv.json @@ -113,6 +113,7 @@ "title": "Valuta" }, "deviceValue": { + "activeClients": "Aktiva klienter", "amount": "Belopp", "broker": "Broker", "bucket": "Bucket", @@ -126,6 +127,7 @@ "chargedEnergy": "Laddad", "co2": "Elnät CO₂", "configured": "Konfigurerad", + "connected": "Ansluten", "connections": "Anslutningar", "controllable": "Kontrollerbar", "currency": "Valuta", @@ -144,6 +146,7 @@ "hemsActiveLimit": "Aktiv begränsning", "hemsType": "Kommunikation", "identifier": "RFID-identifierare", + "loginBlocked": "Inloggningsgräns nådd", "max": "max", "messengers": "Tjänster", "no": "nej", @@ -339,7 +342,7 @@ "defaultsHintLink": "Ändra inställningar", "delete": "Radera", "electricalSubtitle": "Om du är osäker, kontakta elektriker.", - "electricalTitle": "Elektrisk", + "electricalTitle": "Elektricitet", "energyMeterHelp": "Extern mätare (Om laddaren inte har en integrerad mätare).", "energyMeterLabel": "Energimätare", "estimateLabel": "Interpolera laddningsnivå mellan API uppdateringar", @@ -602,6 +605,12 @@ "url": "Server URL", "urlHelp": "Kopiera denna URL till laddarens konfiguration. Se tillverkarens manual för mer information. Laddaren ska automatiskt lägga till sin unika identifierare (stations-ID) till URL:en. I sällsynta fall kan du behöva ange identifieraren manuellt. Exempel: `{url}`Kopiera denna URL till laddarens konfiguration. Se tillverkarens manual för mer information. Laddaren ska automatiskt lägga till sin unika identifierare (stations-ID) till URL:en. I sällsynta fall kan du behöva ange identifieraren manuellt. Exempel: `{url}`" }, + "optimizer": { + "description": "Analyserar solprognoser, elpriser och din typiska förbrukning för att optimera batteri- och laddningsstrategi. Data överförs till evcc optimeringstjänst för beräkning.", + "enable": "Aktivera optimerare", + "info": "Det kan ta några minuter innan optimeringsmenyn blir synlig. För nya installationer kan det ta upp till 24 timmar innan evcc har samlat in tillräckligt med data.", + "title": "Optimerare" + }, "options": { "boolean": { "no": "nej", @@ -629,6 +638,38 @@ "titleAdd": "Lägg till solcellsmätare", "titleEdit": "Ändra solcellsmätare" }, + "remote": { + "active": "Aktiv", + "addClient": "Lägg till klient", + "addClientDescription": "Inloggningsuppgifter lagras och verifieras endast lokalt på din evcc-instans.", + "addClientTitle": "Lägg till fjärklient", + "clientCreated": "Klient skapad", + "clients": "Klienter", + "confirmDelete": "Ta bort klient?", + "connected": "Ansluten", + "createClient": "Skapa klient", + "description": "Nå din evcc-installation från var som helst med evcc-mobilappen. Du behöver varken portvidarebefordran eller VPN.", + "deviceName": "Enhetsnamn", + "disconnected": "Frånkopplad", + "done": "Klar", + "enableLabel": "Aktivera fjärråtkomst", + "expiration": "Upphör", + "expirationNone": "Aldrig", + "expired": "utgången", + "expiresIn": "upphör {time}", + "lastActive": "activ {time}", + "loginBlocked": "Fjärrinloggningar blockeras i en minut efter för många misslyckade inloggningsförsök.", + "manualLogin": "Eller logga in manuellt på {url} i din webbläsare med dessa inloggningsuppgifter:", + "noClients": "Det finns inga klienter än. Ingen kan ansluta just nu.", + "password": "Lösenord", + "passwordOnce": "Det här lösenordet visas bara en gång. Skanna QR-koden eller kopiera den nu. Du kommer inte att kunna se den igen.", + "qrInstall": "Installera evcc-appen för {ios} eller {android}.", + "qrScan": "Skanna koden med din telefons kamera för att ansluta. Klicka på den om du redan använder din telefon.", + "removeClient": "Ta bort klient", + "title": "Fjärråtkomst", + "url": "Publik URL", + "username": "Användarnamn" + }, "section": { "additionalMeter": "Ytterligare mätare", "general": "Allmänna inställningar", @@ -875,10 +916,10 @@ "thisYear": "detta år", "total": "totalt" }, - "periodLabel": "Period:", + "periodLabel": "Period", "priceTitle": "Energipris", "referenceGrid": "nät", - "referenceLabel": "Referensdata:", + "referenceLabel": "Referensdata", "sessionInfo": "Baserat på avslutade laddsessioner.", "tabTitle": "Mina uppgifter" }, @@ -922,7 +963,9 @@ "modalUpdateNow": "Installera nu", "modalUpdateStarted": "Starta den nya versionen av evcc…", "modalUpdateStatusStart": "Installationen har börjat:", - "openSource": "open source" + "modalViewOnGitHub": "Se på GitHub", + "openSource": "open source", + "poweredByOpenSource": "Powered by {0}." } }, "forecast": { @@ -934,8 +977,12 @@ }, "empty": { "co2": "Se när elen i ditt område är ren. Laddningsscheman optimeras för låga utsläpp och CO₂-besparingar beräknas.", - "price": "Konfigurera rörligt elpris för att automatiskt optimera laddningsscheman och beräkna besparingar." + "price": "Konfigurera rörligt elpris för att automatiskt optimera laddningsscheman och beräkna besparingar.", + "setup": "Lägg till tariffer och prognoser", + "solar": "Förväntat solproduktion idag och imorgon. Kommer i framtiden även att användas för automatisk laddningsoptimering.", + "title": "Tariffer & prognoser" }, + "hideLine": "göm linje", "modalTitle": "Prognos", "price": { "average": "Genomsnitt", @@ -943,18 +990,22 @@ "lowestHour": "Billigaste timmen", "range": "Intervall" }, + "priceZoom": "zooma in", + "showLine": "visa linje", "solar": { "dayAfterTomorrow": "I övermorgon", "partly": "delvis", "remaining": "återstående", "today": "Idag", - "tomorrow": "I morgon" + "tomorrow": "i morgon" }, "solarAdjust": "Justera solprognosen baserat på verkliga produktionsvärden{percent}.", + "solarAdjustMedium": "Justera med verkliga data", + "solarAdjustShort": "justera", "type": { - "co2": "CO₂", - "price": "Pris", - "solar": "Sol" + "co2": "CO₂-utsläpp", + "price": "Nätpris", + "solar": "Solenergi produktion" } }, "general": { @@ -1168,6 +1219,7 @@ "once": "Boost är aktiverat för denna laddning.", "stateActive": "Batteriboost aktiv", "stateBelowLimit": "Batterinivå för låg för boost", + "stateHold": "Batteri låst", "stateReady": "Beredd för batteriboost" }, "batteryUsage": "Hembatteri", @@ -1277,6 +1329,7 @@ "batteryBoostBelowLimit": "Batterinivå för låg för boost.", "batteryBoostDisabled": "Batteriboost avaktiverad.", "batteryBoostEnabled": "Boost tills batteriet är på {limit}.", + "batteryBoostHold": "Batteri låst. Boost ej tillgänglig.", "charging": "Laddar…", "cheapEnergyCharging": "Billig energi är tillgänglig.", "cheapEnergyNextStart": "Billig energi om {duration}.", @@ -1516,5 +1569,13 @@ "lineErrorLink": "linje {0}", "restartButton": "Starta om", "title": "Startfel" + }, + "tabBar": { + "battery": "Batteri", + "charge": "Ladda", + "comingSoon": "Sida under uppbyggnad.", + "forecast": "Prognos", + "more": "Mer", + "sessions": "Laddningar" } } diff --git a/i18n/tr.json b/i18n/tr.json index a9bbc9d00..52f20a9c8 100644 --- a/i18n/tr.json +++ b/i18n/tr.json @@ -40,6 +40,7 @@ "legendTopName": "Batarya destekli araç doldurma", "legendTopSubline": "ev bataryası {soc} seviyesinin üzerindeyse.", "modalTitle": "Ev Bataryası", + "noBattery": "Batarya yapılandırılmamış.", "usageTab": "Batarya kullanımı" }, "config": { @@ -601,6 +602,11 @@ "url": "Sunucu URL'si", "urlHelp": "Bu URL'yi doldrma cihazının yapılandırmasına kopyala. Ayrıntılar için üreticinin kılavuzuna bak. Doldurma cihazının, URL'ye benzersiz tanımlayıcısını (istasyon kimliği) otomatik olarak eklemesi beklenir. Nadir durumlarda, tanımlayıcıyı elle belirtmen gerekebilir. Örnek: `{url}`" }, + "optimizer": { + "description": "Güneş enerjisi tahminlerini, elektrik fiyatlarını ve tüketim alışkanlıklarınızı analiz ederek batarya ve doldurma stratejisini iyileştirir. Veriler, işlenmek üzere evcc iyileştirme hizmetine gönderilir.", + "enable": "İyileştiriciyi Etkinleştir", + "title": "İyileştirici" + }, "options": { "boolean": { "no": "“hayır”", @@ -853,6 +859,15 @@ "configurePriceCo2": "Fiyat ve CO₂ emisyonlarını yapılandır.", "footerLong": "{percent} güneş enerjisi", "footerShort": "{percent} güneş", + "indicator": { + "co2": "CO₂ emisyonları", + "co2saved": "Azaltılan CO₂ miktarı", + "none": "yok", + "price": "enerji fiyatı", + "savings": "tasarruf", + "solar": "güneş enerjisi" + }, + "indicatorLabel": "Başlık bilgisi", "modalTitle": "Doldurma Enerjisi Genel Bakışı", "moneySaved": "{value} tasarruf edildi", "percentGrid": "{grid} kWh şebekeden", @@ -864,10 +879,11 @@ "thisYear": "“bu yıl”", "total": "tüm zaman" }, - "periodLabel": "Zaman aralığı:", + "periodLabel": "Zaman aralığı", "priceTitle": "Enerji fiyatı", "referenceGrid": "şebeke", - "referenceLabel": "Referans verileri:", + "referenceLabel": "Kaynak verileri", + "sessionInfo": "Tamamlanmış doldurma işlemlerine göre.", "tabTitle": "Verilerim" }, "sponsor": { @@ -893,15 +909,26 @@ }, "version": { "availableLong": "yeni sürüm mevcut", + "community": "evcc topluluğu", + "labelRelease": "Yayın", + "labelVersion": "Sürüm", + "labelWebsite": "Web sitesi", + "latestVersion": "en son sürüm", + "madeByCommunity": "{0} tarafından hazırlandı.", "modalCancel": "İptal", "modalDownload": "İndir", "modalInstalledVersion": "Kurulu sürüm", + "modalLatest": "En son sürümü kullanıyorsun.", + "modalNextRelease": "Bir sonraki yayında neler var", "modalNoReleaseNotes": "Sürüm bilgileri mevcut değil. Yeni sürüm hakkında bilgiler:", "modalTitle": "Yeni sürüm mevcut", "modalUpdate": "Kur", "modalUpdateNow": "Şimdi kur", "modalUpdateStarted": "evcc'nin yeni sürümü başlatılıyor…", - "modalUpdateStatusStart": "Kurulum başladı:" + "modalUpdateStatusStart": "Kurulum başladı:", + "modalViewOnGitHub": "GitHub'da görüntüle", + "openSource": "açık kaynak", + "poweredByOpenSource": "{0} tarafından desteklenmekte." } }, "forecast": { @@ -911,6 +938,12 @@ "lowestHour": "“En temiz saat”", "range": "“Aralık”" }, + "empty": { + "co2": "Bölgendeki şebeke enerjisinin ne zaman temiz olduğunu gör. Doldurma planları, düşük emisyonlara göre iyileştirilecek ve CO₂ tasarrufu hesaplanacak.", + "price": "Dinamik elektrik tarifeni yapılandırarak doldurma planlarını otomatik olarak iyileştir ve tasarrufları hesapla.", + "setup": "Fiyatlar ve tahminler oluştur", + "solar": "Bugün ve önümüzdeki günler için beklenen güneş enerjisi üretimini gör. Bu veriler gelecekte otomatik doldurma iyileştirmesi için de kullanılacak." + }, "modalTitle": "“Öngörü”", "price": { "average": "“Ortalama”", @@ -918,6 +951,7 @@ "lowestHour": "“En ucuz saat”", "range": "“Aralık”" }, + "priceZoom": "büyüt", "solar": { "dayAfterTomorrow": "“Yarından sonra”", "partly": "“kısmen”", @@ -926,10 +960,11 @@ "tomorrow": "“Yarın”" }, "solarAdjust": "Güneş enerjisi tahminlerini gerçek üretim verilerine göre ayarla{percent}.", + "solarAdjustShort": "gerçek verilere göre ayarlama", "type": { - "co2": "CO₂", - "price": "“Fiyat”", - "solar": "“Güneş”" + "co2": "CO₂ salımları", + "price": "Şebeke Fiyatı", + "solar": "Güneş Enerjisi Üretimi" } }, "general": { @@ -1142,6 +1177,7 @@ "once": "Bu doldurma oturumu için takviye etkin.", "stateActive": "Batarya desteği etkin", "stateBelowLimit": "Desdek için batarya çok düşük", + "stateHold": "Batarya kilitli", "stateReady": "Batarya desteği hazır" }, "batteryUsage": "“Ev Bataryası”", @@ -1251,6 +1287,7 @@ "batteryBoostBelowLimit": "Desdek için batarya çok düşük.", "batteryBoostDisabled": "Batarya desteği devre dışı bırakıldı.", "batteryBoostEnabled": "Batarya {limit} seviyesine kadar destek.", + "batteryBoostHold": "Batarya kilitli. Hızlandırma kullanılamıyor.", "charging": "doluyor…", "cheapEnergyCharging": "Ucuz enerji mevcut.", "cheapEnergyNextStart": "{duration} içinde ucuz enerji.", @@ -1490,5 +1527,13 @@ "lineErrorLink": "{0}. satır", "restartButton": "Yeniden Başlat", "title": "Başlama Hatası" + }, + "tabBar": { + "battery": "Batarya", + "charge": "Doldurma", + "comingSoon": "Bu sayfa yapım aşamasında.", + "forecast": "Tahmin", + "more": "Daha fazla", + "sessions": "Oturumlar" } } From 2f106579e78e38c2f88e3c031c50dc942255bfaa Mon Sep 17 00:00:00 2001 From: andig Date: Sun, 19 Apr 2026 13:55:54 +0200 Subject: [PATCH 0277/1506] Add Fritz smarthome REST API support (FritzOS 8.2+) (#29013) --- charger/fritzdect.go | 65 ++--- .../fritzdect.go => fritz/aha/aha.go} | 108 +++----- meter/fritz/aha/types.go | 15 + meter/fritz/api.go | 16 ++ meter/fritz/smarthome/smarthome.go | 258 ++++++++++++++++++ meter/fritz/smarthome/types.go | 58 ++++ meter/fritz/types.go | 71 +++++ meter/fritzdect.go | 14 +- templates/definition/charger/fritzdect.yaml | 10 + templates/definition/meter/fritzdect.yaml | 10 + templates/definition/meter/fritzgrid.yaml | 1 + 11 files changed, 516 insertions(+), 110 deletions(-) rename meter/{fritzdect/fritzdect.go => fritz/aha/aha.go} (54%) create mode 100644 meter/fritz/aha/types.go create mode 100644 meter/fritz/api.go create mode 100644 meter/fritz/smarthome/smarthome.go create mode 100644 meter/fritz/smarthome/types.go create mode 100644 meter/fritz/types.go diff --git a/charger/fritzdect.go b/charger/fritzdect.go index 1b68bbc2c..7bef93c02 100644 --- a/charger/fritzdect.go +++ b/charger/fritzdect.go @@ -1,20 +1,20 @@ package charger import ( - "errors" - "strconv" - "github.com/evcc-io/evcc/api" - "github.com/evcc-io/evcc/meter/fritzdect" + "github.com/evcc-io/evcc/meter/fritz" + "github.com/evcc-io/evcc/meter/fritz/aha" + "github.com/evcc-io/evcc/meter/fritz/smarthome" "github.com/evcc-io/evcc/util" ) // FRITZ! FritzBox AHA interface specifications: // https://fritz.com/fileadmin/user_upload/Global/Service/Schnittstellen/AHA-HTTP-Interface.pdf +// https://fritz.support/resources/SmarthomeRestApiFRITZOS82.html (REST API for FritzOS 8.2+) // FritzDECT charger implementation type FritzDECT struct { - conn *fritzdect.Connection + conn fritz.Switch *switchSocket } @@ -25,9 +25,9 @@ func init() { // NewFritzDECTFromConfig creates a fritzdect charger from generic config func NewFritzDECTFromConfig(other map[string]any) (api.Charger, error) { var cc struct { - embed `mapstructure:",squash"` - fritzdect.Settings `mapstructure:",squash"` - StandbyPower float64 + embed `mapstructure:",squash"` + fritz.Settings `mapstructure:",squash"` + StandbyPower float64 } if err := util.DecodeOther(other, &cc); err != nil { @@ -38,12 +38,20 @@ func NewFritzDECTFromConfig(other map[string]any) (api.Charger, error) { return nil, api.ErrMissingCredentials } - return NewFritzDECT(cc.embed, cc.URI, cc.AIN, cc.User, cc.Password, cc.StandbyPower) + return NewFritzDECT(cc.embed, cc.URI, cc.AIN, cc.User, cc.Password, cc.StandbyPower, cc.Firmware82) } // NewFritzDECT creates a new connection with standbypower for charger -func NewFritzDECT(embed embed, uri, ain, user, password string, standbypower float64) (*FritzDECT, error) { - conn, err := fritzdect.NewConnection(uri, ain, user, password) +func NewFritzDECT(embed embed, uri, ain, user, password string, standbypower float64, firmware82 bool) (*FritzDECT, error) { + var conn fritz.Switch + var err error + + // Use new REST API if firmware82 is set, otherwise use legacy LUA API + if firmware82 { + conn, err = smarthome.NewConnection(uri, ain, user, password) + } else { + conn, err = aha.NewConnection(uri, ain, user, password) + } if err != nil { return nil, err } @@ -59,18 +67,13 @@ func NewFritzDECT(embed embed, uri, ain, user, password string, standbypower flo // Status implements the api.Charger interface func (c *FritzDECT) Status() (api.ChargeStatus, error) { - resp, err := c.conn.ExecCmd("getswitchpresent") + present, err := c.conn.SwitchPresent() if err != nil { return api.StatusNone, err } - present, err := strconv.ParseBool(resp) - if err == nil && !present { - err = api.ErrNotAvailable - } - - if err != nil { - return api.StatusNone, err + if !present { + return api.StatusNone, api.ErrNotAvailable } return c.switchSocket.Status() @@ -78,33 +81,15 @@ func (c *FritzDECT) Status() (api.ChargeStatus, error) { // Enabled implements the api.Charger interface func (c *FritzDECT) Enabled() (bool, error) { - resp, err := c.conn.ExecCmd("getswitchstate") - if err != nil { - return false, err - } - - return strconv.ParseBool(resp) + return c.conn.SwitchState() } // Enable implements the api.Charger interface func (c *FritzDECT) Enable(enable bool) error { - cmd := "setswitchoff" if enable { - cmd = "setswitchon" + return c.conn.SwitchOn() } - - // on 0/1 - DECT Switch state off/on (empty if unknown or error) - resp, err := c.conn.ExecCmd(cmd) - if err != nil { - return err - } - - on, err := strconv.ParseBool(resp) - if err == nil && enable != on { - err = errors.New("switch failed") - } - - return err + return c.conn.SwitchOff() } var _ api.MeterEnergy = (*FritzDECT)(nil) diff --git a/meter/fritzdect/fritzdect.go b/meter/fritz/aha/aha.go similarity index 54% rename from meter/fritzdect/fritzdect.go rename to meter/fritz/aha/aha.go index ef28e850d..e98542f47 100644 --- a/meter/fritzdect/fritzdect.go +++ b/meter/fritz/aha/aha.go @@ -1,9 +1,6 @@ -package fritzdect +package aha import ( - "crypto/md5" - "encoding/hex" - "encoding/xml" "errors" "fmt" "net/url" @@ -12,44 +9,24 @@ import ( "time" "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/meter/fritz" "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/request" "github.com/evcc-io/evcc/util/transport" - "golang.org/x/text/encoding/unicode" ) // FRITZ! FritzBox AHA interface and authentication specifications: // https://fritz.com/fileadmin/user_upload/Global/Service/Schnittstellen/AHA-HTTP-Interface.pdf // https://fritz.com/fileadmin/user_upload/Global/Service/Schnittstellen/AVM_Technical_Note_-_Session_ID.pdf -// FritzDECT settings -type Settings struct { - URI, AIN, User, Password string -} - // FritzDECT connection type Connection struct { *request.Helper - *Settings + *fritz.Settings SID string updated time.Time } -// https://fritz.com/fileadmin/user_upload/Global/Service/Schnittstellen/AVM_Technical_Note_-_Session_ID_english_2021-05-03.pdf -const sessionTimeout = 15 * time.Minute - -// Devicestats structures getbasicdevicesstats command response (AHA-HTTP-Interface) -type Devicestats struct { - XMLName xml.Name `xml:"devicestats"` - Energy Energy `xml:"energy"` -} - -// Energy structures getbasicdevicesstats command energy response (AHA-HTTP-Interface) -type Energy struct { - XMLName xml.Name `xml:"energy"` - Values []string `xml:"stats"` -} - // NewConnection creates FritzDECT connection func NewConnection(uri, ain, user, password string) (*Connection, error) { if uri == "" { @@ -60,7 +37,7 @@ func NewConnection(uri, ain, user, password string) (*Connection, error) { return nil, errors.New("missing ain") } - settings := &Settings{ + settings := &fritz.Settings{ URI: strings.TrimRight(uri, "/"), AIN: ain, User: user, @@ -82,11 +59,13 @@ func NewConnection(uri, ain, user, password string) (*Connection, error) { // ExecCmd execautes an FritzDECT AHA-HTTP-Interface command func (c *Connection) ExecCmd(function string) (string, error) { // refresh Fritzbox session id - if time.Since(c.updated) >= sessionTimeout { - if err := c.getSessionID(); err != nil { + if time.Since(c.updated) >= fritz.SessionTimeout { + sid, err := c.GetSessionID(c.Helper) + if err != nil { return "", err } // update session timestamp + c.SID = sid c.updated = time.Now() } @@ -123,7 +102,7 @@ func (c *Connection) CurrentPower() (float64, error) { var _ api.MeterEnergy = (*Connection)(nil) -// CurrentPower implements the api.MeterEnergy interface +// TotalEnergy implements the api.MeterEnergy interface func (c *Connection) TotalEnergy() (float64, error) { // Energy value in Wh (total switch energy, refresh approximately every 2 minutes) resp, err := c.ExecCmd("getswitchenergy") @@ -136,53 +115,48 @@ func (c *Connection) TotalEnergy() (float64, error) { return energy / 1000, err // Wh ==> KWh } -// Fritzbox helpers (credits to https://github.com/rsdk/ahago) +// SwitchPresent checks if the device is connected +func (c *Connection) SwitchPresent() (bool, error) { + resp, err := c.ExecCmd("getswitchpresent") + if err != nil { + return false, err + } + return strconv.ParseBool(resp) +} -// getSessionID fetches a session-id based on the username and password in the connection struct -func (c *Connection) getSessionID() error { - uri := fmt.Sprintf("%s/login_sid.lua", c.URI) - body, err := c.GetBody(uri) +// SwitchState returns the current switch state +func (c *Connection) SwitchState() (bool, error) { + resp, err := c.ExecCmd("getswitchstate") + if err != nil { + return false, err + } + return strconv.ParseBool(resp) +} + +// SwitchOn turns the switch on +func (c *Connection) SwitchOn() error { + resp, err := c.ExecCmd("setswitchon") if err != nil { return err } - var v struct { - SID string - Challenge string - BlockTime string + on, err := strconv.ParseBool(resp) + if err == nil && !on { + err = errors.New("switch on failed") } - - if err = xml.Unmarshal(body, &v); err == nil && v.SID == "0000000000000000" { - var challresp string - if challresp, err = createChallengeResponse(v.Challenge, c.Password); err == nil { - params := url.Values{ - "username": {c.User}, - "response": {challresp}, - } - - if body, err = c.GetBody(uri + "?" + params.Encode()); err == nil { - err = xml.Unmarshal(body, &v) - if v.SID == "0000000000000000" { - return errors.New("invalid user or password") - } - c.SID = v.SID - } - } - } - return err } -// createChallengeResponse creates the Fritzbox challenge response string -func createChallengeResponse(challenge, pass string) (string, error) { - encoder := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewEncoder() - utf16le, err := encoder.String(challenge + "-" + pass) +// SwitchOff turns the switch off +func (c *Connection) SwitchOff() error { + resp, err := c.ExecCmd("setswitchoff") if err != nil { - return "", err + return err } - hash := md5.Sum([]byte(utf16le)) - md5hash := hex.EncodeToString(hash[:]) - - return challenge + "-" + md5hash, nil + off, err := strconv.ParseBool(resp) + if err == nil && off { + err = errors.New("switch off failed") + } + return err } diff --git a/meter/fritz/aha/types.go b/meter/fritz/aha/types.go new file mode 100644 index 000000000..0af22c3d6 --- /dev/null +++ b/meter/fritz/aha/types.go @@ -0,0 +1,15 @@ +package aha + +import "encoding/xml" + +// Devicestats structures getbasicdevicesstats command response (AHA-HTTP-Interface) +type Devicestats struct { + XMLName xml.Name `xml:"devicestats"` + Energy Energy `xml:"energy"` +} + +// Energy structures getbasicdevicesstats command energy response (AHA-HTTP-Interface) +type Energy struct { + XMLName xml.Name `xml:"energy"` + Values []string `xml:"stats"` +} diff --git a/meter/fritz/api.go b/meter/fritz/api.go new file mode 100644 index 000000000..371f241ac --- /dev/null +++ b/meter/fritz/api.go @@ -0,0 +1,16 @@ +package fritz + +// Meter defines the interface for Fritz connections (both legacy LUA and REST) +type Meter interface { + CurrentPower() (float64, error) + TotalEnergy() (float64, error) +} + +// Switch extends Meter with switch control capabilities +type Switch interface { + Meter + SwitchPresent() (bool, error) + SwitchState() (bool, error) + SwitchOn() error + SwitchOff() error +} diff --git a/meter/fritz/smarthome/smarthome.go b/meter/fritz/smarthome/smarthome.go new file mode 100644 index 000000000..624af3daf --- /dev/null +++ b/meter/fritz/smarthome/smarthome.go @@ -0,0 +1,258 @@ +package smarthome + +import ( + "errors" + "fmt" + "net/url" + "strings" + "time" + + "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/meter/fritz" + "github.com/evcc-io/evcc/util" + "github.com/evcc-io/evcc/util/request" + "github.com/evcc-io/evcc/util/transport" +) + +// FRITZ! Smarthome REST API (FritzOS 8.2+) +// https://fritz.support/resources/SmarthomeRestApiFRITZOS82.html + +// Connection implements the new REST API for Fritz smarthome devices +type Connection struct { + *request.Helper + *fritz.Settings + SID string + UID string // device UID (AIN with space) + updated time.Time + unitG util.Cacheable[Unit] +} + +// NewConnection creates a new REST API connection +func NewConnection(uri, ain, user, password string) (*Connection, error) { + if uri == "" { + uri = "https://fritz.box" + } + + if ain == "" { + return nil, errors.New("missing ain") + } + + settings := &fritz.Settings{ + URI: strings.TrimRight(uri, "/"), + AIN: ain, + User: user, + Password: password, + } + + log := util.NewLogger("fritzsmarthome").Redact(password) + + conn := &Connection{ + Helper: request.NewHelper(log), + Settings: settings, + UID: ainToUID(ain), + } + + conn.Client.Transport = request.NewTripper(log, transport.Insecure()) + + // cache unit data for 2 seconds to avoid excessive API calls + conn.unitG = util.ResettableCached(func() (Unit, error) { + return conn.getUnit() + }, 2*time.Second) + + return conn, nil +} + +// ainToUID converts AIN format to UID format by adding space +// AIN: "116300015376" -> UID: "11630 0015376" +func ainToUID(ain string) string { + // Remove any existing spaces first + ain = strings.ReplaceAll(ain, " ", "") + if len(ain) >= 5 { + return ain[:5] + " " + ain[5:] + } + return ain +} + +// getUnit fetches unit data from REST API +func (c *Connection) getUnit() (Unit, error) { + if err := c.refreshSession(); err != nil { + return Unit{}, err + } + + // Try to get the specific unit first + uri := fmt.Sprintf("%s/api/v0/smarthome/overview/units/%s", c.URI, url.PathEscape(c.UID)) + + req, _ := request.New("GET", uri, nil, map[string]string{ + "Authorization": "AVM-SID " + c.SID, + }, request.AcceptJSON) + + var unit Unit + if err := c.DoJSON(req, &unit); err != nil { + // Fall back to getting all units and finding ours + return c.findUnit() + } + + if !unit.IsConnected { + return unit, api.ErrNotAvailable + } + + return unit, nil +} + +// findUnit searches for our unit in the list of all units +func (c *Connection) findUnit() (Unit, error) { + uri := fmt.Sprintf("%s/api/v0/smarthome/overview/units", c.URI) + + req, err := request.New("GET", uri, nil, map[string]string{ + "Authorization": "AVM-SID " + c.SID, + }, request.AcceptJSON) + if err != nil { + return Unit{}, err + } + + var units []Unit + if err := c.DoJSON(req, &units); err != nil { + return Unit{}, err + } + + // Search for matching unit by UID or AIN + for _, unit := range units { + unitAIN := strings.ReplaceAll(unit.UID, " ", "") + if unit.UID == c.UID || unitAIN == c.AIN { + if !unit.IsConnected { + return unit, api.ErrNotAvailable + } + return unit, nil + } + } + + return Unit{}, fmt.Errorf("unit not found: %s", c.AIN) +} + +// CurrentPower implements the api.Meter interface +func (c *Connection) CurrentPower() (float64, error) { + unit, err := c.unitG.Get() + if err != nil { + return 0, err + } + + if unit.Statistics != nil && len(unit.Statistics.Powers) > 0 { + if stats := unit.Statistics.Powers[0].Values; len(stats) > 0 { + return (float64)(stats[0]) / 1000, nil + } + } + + return 0, api.ErrNotAvailable +} + +var _ api.MeterEnergy = (*Connection)(nil) + +// TotalEnergy implements the api.MeterEnergy interface +func (c *Connection) TotalEnergy() (float64, error) { + unit, err := c.unitG.Get() + if err != nil { + return 0, err + } + + if unit.Statistics != nil && len(unit.Statistics.Energies) > 0 { + if stats := unit.Statistics.Energies[len(unit.Statistics.Energies)-1].Values; len(stats) > 0 { + return (float64)(stats[0]) / 1000, nil + } + } + + return 0, api.ErrNotAvailable +} + +// SwitchPresent checks if the device is connected +func (c *Connection) SwitchPresent() (bool, error) { + unit, err := c.unitG.Get() + if err != nil { + if errors.Is(err, api.ErrNotAvailable) { + return false, nil + } + return false, err + } + return unit.IsConnected, nil +} + +// SwitchState returns the current switch state +func (c *Connection) SwitchState() (bool, error) { + unit, err := c.unitG.Get() + if err != nil { + return false, err + } + + if unit.Interfaces.OnOffInterface == nil { + return false, errors.New("device has no switch") + } + + return unit.Interfaces.OnOffInterface.State == "on", nil +} + +// SwitchOn turns the switch on +func (c *Connection) SwitchOn() error { + return c.setSwitch(true) +} + +// SwitchOff turns the switch off +func (c *Connection) SwitchOff() error { + return c.setSwitch(false) +} + +// setSwitch sets the switch state via REST API +func (c *Connection) setSwitch(on bool) error { + if err := c.refreshSession(); err != nil { + return err + } + + state := "off" + if on { + state = "on" + } + + uri := fmt.Sprintf("%s/api/v0/smarthome/overview/units/%s", c.URI, url.PathEscape(c.UID)) + + data := map[string]any{ + "onOffInterface": map[string]string{ + "state": state, + }, + } + + req, _ := request.New("PUT", uri, request.MarshalJSON(data), map[string]string{ + "Authorization": "AVM-SID " + c.SID, + }, request.JSONEncoding) + + var unit Unit + if err := c.DoJSON(req, &unit); err != nil { + return err + } + + // Reset cache after state change + c.unitG.Reset() + + // Verify state was changed + if unit.Interfaces.OnOffInterface != nil { + actualState := unit.Interfaces.OnOffInterface.State == "on" + if actualState != on { + return errors.New("switch state change failed") + } + } + + return nil +} + +// refreshSession ensures we have a valid session ID +func (c *Connection) refreshSession() error { + // refresh Fritzbox session id + if time.Since(c.updated) >= fritz.SessionTimeout { + sid, err := c.GetSessionID(c.Helper) + if err != nil { + return err + } + // update session timestamp + c.SID = sid + c.updated = time.Now() + } + + return nil +} diff --git a/meter/fritz/smarthome/types.go b/meter/fritz/smarthome/types.go new file mode 100644 index 000000000..9b74a48cc --- /dev/null +++ b/meter/fritz/smarthome/types.go @@ -0,0 +1,58 @@ +package smarthome + +// Unit represents a smarthome unit with its interfaces +type Unit struct { + GroupUID string `json:"groupUid,omitempty"` + UID string `json:"UID,omitempty"` + DeviceUID string `json:"deviceUid"` + UnitType string `json:"unitType"` + IsConnected bool `json:"isConnected"` + Statistics *Statistics `json:"statistics,omitempty"` + Interfaces *Interfaces `json:"interfaces,omitempty"` +} + +type Interfaces struct { + MultimeterInterface *MultimeterInterface `json:"multimeterInterface,omitempty"` + OnOffInterface *OnOffInterface `json:"onOffInterface,omitempty"` + TemperatureInterface *TemperatureInterface `json:"temperatureInterface,omitempty"` +} + +type Statistics struct { + Temperatures []ElementFloat `json:"temperatures,omitempty"` + Powers []Element `json:"powers,omitempty"` + Voltages []Element `json:"voltages,omitempty"` + Energies []Element `json:"energies,omitempty"` +} + +type ElementFloat struct { + Interval int64 `json:"interval"` + StasticsState string `json:"statisticsState"` + Period string `json:"period"` + Values []float64 `json:"values,omitempty"` +} + +type Element struct { + Interval int64 `json:"interval"` + StasticsState string `json:"statisticsState"` + Period string `json:"period"` + Values []int64 `json:"values,omitempty"` +} + +// MultimeterInterface contains power/energy measurements +type MultimeterInterface struct { + State string `json:"state"` + Power float64 `json:"power"` // W + Voltage float64 `json:"voltage"` // V + Current float64 `json:"current"` // A + Energy float64 `json:"energy"` // Wh +} + +// OnOffInterface contains switch state +type OnOffInterface struct { + State string `json:"state"` // "on" or "off" +} + +type TemperatureInterface struct { + State string `json:"state"` // "on" or "off" + Celsius float64 `json:"celsius"` +} diff --git a/meter/fritz/types.go b/meter/fritz/types.go new file mode 100644 index 000000000..7b5599517 --- /dev/null +++ b/meter/fritz/types.go @@ -0,0 +1,71 @@ +package fritz + +import ( + "crypto/md5" + "encoding/hex" + "encoding/xml" + "errors" + "fmt" + "net/url" + "time" + + "github.com/evcc-io/evcc/util/request" + "golang.org/x/text/encoding/unicode" +) + +// https://fritz.com/fileadmin/user_upload/Global/Service/Schnittstellen/AVM_Technical_Note_-_Session_ID_english_2021-05-03.pdf +const SessionTimeout = 15 * time.Minute + +// FritzDECT settings +type Settings struct { + URI, AIN, User, Password string + Firmware82 bool // use new REST API (FritzOS 8.2+) +} + +// Fritzbox helpers (credits to https://github.com/rsdk/ahago) + +// getSessionID fetches a session-id based on the username and password in the connection struct +func (s Settings) GetSessionID(c *request.Helper) (string, error) { + uri := fmt.Sprintf("%s/login_sid.lua", s.URI) + body, err := c.GetBody(uri) + if err != nil { + return "", err + } + + var v struct { + SID string + Challenge string + } + + if err = xml.Unmarshal(body, &v); err == nil && v.SID == "0000000000000000" { + var challresp string + if challresp, err = s.createChallengeResponse(v.Challenge); err == nil { + params := url.Values{ + "username": {s.User}, + "response": {challresp}, + } + + if body, err = c.GetBody(uri + "?" + params.Encode()); err == nil { + if err = xml.Unmarshal(body, &v); err == nil && v.SID == "0000000000000000" { + return "", errors.New("invalid user or password") + } + } + } + } + + return v.SID, err +} + +// createChallengeResponse creates the Fritzbox challenge response string +func (s Settings) createChallengeResponse(challenge string) (string, error) { + encoder := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewEncoder() + utf16le, err := encoder.String(challenge + "-" + s.Password) + if err != nil { + return "", err + } + + hash := md5.Sum([]byte(utf16le)) + md5hash := hex.EncodeToString(hash[:]) + + return challenge + "-" + md5hash, nil +} diff --git a/meter/fritzdect.go b/meter/fritzdect.go index 1a3f97d6c..f6e5a2627 100644 --- a/meter/fritzdect.go +++ b/meter/fritzdect.go @@ -2,12 +2,15 @@ package meter import ( "github.com/evcc-io/evcc/api" - "github.com/evcc-io/evcc/meter/fritzdect" + "github.com/evcc-io/evcc/meter/fritz" + "github.com/evcc-io/evcc/meter/fritz/aha" + "github.com/evcc-io/evcc/meter/fritz/smarthome" "github.com/evcc-io/evcc/util" ) // AVM FritzBox AHA interface specifications: // https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/AHA-HTTP-Interface.pdf +// https://fritz.support/resources/SmarthomeRestApiFRITZOS82.html (REST API for FritzOS 8.2+) func init() { registry.Add("fritzdect", NewFritzDECTFromConfig) @@ -15,7 +18,7 @@ func init() { // NewFritzDECTFromConfig creates a fritzdect meter from generic config func NewFritzDECTFromConfig(other map[string]any) (api.Meter, error) { - var cc fritzdect.Settings + var cc fritz.Settings if err := util.DecodeOther(other, &cc); err != nil { return nil, err } @@ -24,5 +27,10 @@ func NewFritzDECTFromConfig(other map[string]any) (api.Meter, error) { return nil, api.ErrMissingCredentials } - return fritzdect.NewConnection(cc.URI, cc.AIN, cc.User, cc.Password) + // Use new REST API if firmware82 is set, otherwise use legacy LUA API + if cc.Firmware82 { + return smarthome.NewConnection(cc.URI, cc.AIN, cc.User, cc.Password) + } + + return aha.NewConnection(cc.URI, cc.AIN, cc.User, cc.Password) } diff --git a/templates/definition/charger/fritzdect.yaml b/templates/definition/charger/fritzdect.yaml index 8099513bf..922f4d127 100644 --- a/templates/definition/charger/fritzdect.yaml +++ b/templates/definition/charger/fritzdect.yaml @@ -25,6 +25,15 @@ params: required: true - name: ain required: true + - name: firmware82 + advanced: true + type: bool + description: + de: Neue REST-API verwenden (FritzOS 8.2+) + en: Use new REST API (FritzOS 8.2+) + help: + de: Verwende die neue REST-API für FritzOS ab Version 8.2 + en: Use the new REST API for FritzOS version 8.2 and later - preset: switchsocket render: | type: fritzdect @@ -32,4 +41,5 @@ render: | user: {{ .user }} password: {{ .password }} ain: {{ .ain }} # switch actor identification number without blanks (see AIN number on switch sticker) + firmware82: {{ .firmware82 }} {{ include "switchsocket" . }} diff --git a/templates/definition/meter/fritzdect.yaml b/templates/definition/meter/fritzdect.yaml index 98dbb6934..caab3a1b8 100644 --- a/templates/definition/meter/fritzdect.yaml +++ b/templates/definition/meter/fritzdect.yaml @@ -27,9 +27,19 @@ params: required: true - name: ain required: true + - name: firmware82 + advanced: true + type: bool + description: + de: Neue REST-API verwenden (FritzOS 8.2+) + en: Use new REST API (FritzOS 8.2+) + help: + de: Verwende die neue REST-API für FritzOS ab Version 8.2 + en: Use the new REST API for FritzOS version 8.2 and later render: | type: fritzdect uri: {{ .uri }} user: {{ .user }} password: {{ .password }} ain: {{ .ain }} # switch actor identification number without blanks (see AIN number on switch sticker) + firmware82: {{ .firmware82 }} diff --git a/templates/definition/meter/fritzgrid.yaml b/templates/definition/meter/fritzgrid.yaml index 0d11df4d1..f67125749 100644 --- a/templates/definition/meter/fritzgrid.yaml +++ b/templates/definition/meter/fritzgrid.yaml @@ -20,3 +20,4 @@ render: | user: {{ .user }} password: {{ .password }} ain: {{ .ain }} # switch actor identification number without blanks (see AIN number on switch sticker) + firmware82: true From cd176a43998e619861143014c9908e6c634e4b7b Mon Sep 17 00:00:00 2001 From: andig Date: Sun, 19 Apr 2026 14:18:21 +0200 Subject: [PATCH 0278/1506] chore: migrate metrics columns either or, not both --- core/metrics/db.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/core/metrics/db.go b/core/metrics/db.go index e1ef96150..06809f30f 100644 --- a/core/metrics/db.go +++ b/core/metrics/db.go @@ -187,17 +187,17 @@ func SetupSchema() error { if err := m.RenameColumn(new(meter), old, "import"); err != nil { return err } - } - - // meter: split energy direction #2 - if old := "pos"; m.HasColumn(new(meter), old) { - if err := m.RenameColumn(new(meter), old, "import"); err != nil { - return err + } else { + // meter: split energy direction #2 + if old := "pos"; m.HasColumn(new(meter), old) { + if err := m.RenameColumn(new(meter), old, "import"); err != nil { + return err + } } - } - if old := "neg"; m.HasColumn(new(meter), old) { - if err := m.RenameColumn(new(meter), old, "export"); err != nil { - return err + if old := "neg"; m.HasColumn(new(meter), old) { + if err := m.RenameColumn(new(meter), old, "export"); err != nil { + return err + } } } From c968d68947e71f6b2368ca75afbe4972e97ade09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20He=C3=9F?= Date: Sun, 19 Apr 2026 14:26:07 +0200 Subject: [PATCH 0279/1506] Add domain knowledge docs for AI agents (#29228) --- AGENTS.md | 24 +++ docs/agents/core-domain.md | 147 +++++++++++++++ docs/agents/easee-architecture.md | 269 +++++++++++++++++++++++++++ docs/agents/hardware-integrations.md | 148 +++++++++++++++ docs/agents/plugin-system.md | 61 ++++++ docs/agents/web-ui-api.md | 87 +++++++++ 6 files changed, 736 insertions(+) create mode 100644 docs/agents/core-domain.md create mode 100644 docs/agents/easee-architecture.md create mode 100644 docs/agents/hardware-integrations.md create mode 100644 docs/agents/plugin-system.md create mode 100644 docs/agents/web-ui-api.md diff --git a/AGENTS.md b/AGENTS.md index 75476d4a0..f7a467b3a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -24,6 +24,30 @@ This file provides guidance to AI coding agents when working with code in this r - `evcc --template-type [type] --template [file]` - test device templates - `make docs` - generate template documentation +## Domain Knowledge + +Deep documentation on specific subsystems is available in `docs/agents/`. Load what you need based on the task: + +| File | When to load | +|------|-------------| +| [Core Domain](docs/agents/core-domain.md) | Control loop, loadpoint logic, PV surplus, charge modes, tariffs, interfaces | +| [Hardware Integrations](docs/agents/hardware-integrations.md) | Charger/meter/vehicle implementations, adding new devices | +| [Easee Architecture](docs/agents/easee-architecture.md) | Easee charger (REST+SignalR, async correlation, concurrency) | +| [Plugin System](docs/agents/plugin-system.md) | Plugin layer (HTTP, MQTT, Modbus, SunSpec, JS) | +| [Web UI & API](docs/agents/web-ui-api.md) | REST API, WebSocket, Vue frontend, authentication | + +### Loading guide by task type + +- **Charger implementation** — hardware-integrations + core-domain +- **Easee charger work** — easee-architecture + core-domain +- **Meter implementation** — hardware-integrations + plugin-system +- **Vehicle implementation** — hardware-integrations +- **UI/frontend work** — web-ui-api +- **API endpoint work** — web-ui-api + core-domain +- **Config/template work** — plugin-system +- **Control loop / charging logic** — core-domain +- **Bug in any area** — core-domain + relevant topic file(s) + ## Architecture Guidelines ### Core Components diff --git a/docs/agents/core-domain.md b/docs/agents/core-domain.md new file mode 100644 index 000000000..42ba9ac3f --- /dev/null +++ b/docs/agents/core-domain.md @@ -0,0 +1,147 @@ +# Core Domain: Site, Loadpoint, and the Control Loop + +## Object Hierarchy + +``` +Site (orchestrator — core/site.go) +├── Meters: Grid, PV[], Battery[], Auxiliary[], External[] +├── Tariffs: Grid, FeedIn, CO2, Solar +├── Coordinator (vehicle <-> loadpoint assignment) +├── Prioritizer (power allocation fairness) +└── Loadpoints[] (core/loadpoint.go) + ├── Charger (api.Charger — hardware controller) + ├── Vehicle (api.Vehicle — EV battery state via cloud API) + ├── ChargeMeter (api.Meter — AC power at charger) + └── Circuit (optional — electrical domain limits) +``` + +## Key Interfaces (api/api.go) + +### Meter +- `Meter` — `CurrentPower() (float64, error)` — watts +- `MeterEnergy` — `TotalEnergy() (float64, error)` — kWh +- `PhaseCurrents` / `PhaseVoltages` / `PhasePowers` — per-phase readings + +### Battery +- `Battery` — `Soc() (float64, error)` — 0-100% +- `BatteryCapacity` — kWh +- `BatteryController` — set charge/discharge/hold mode + +### Charger +- `Charger` — `Status()`, `Enabled()`, `Enable(bool)`, `MaxCurrent(int64)` +- `ChargerEx` — milliamp-precision current via `MaxCurrentMillis(float64)` +- `PhaseSwitcher` — `Phases1p3p(int) error` +- `ChargeRater` — `ChargedEnergy() (float64, error)` +- `ChargeTimer` — `ChargeDuration() (time.Duration, error)` + +### Vehicle +- `Vehicle` — `Soc()`, `Capacity()`, `Identifiers()`, `Phases()`, `OnIdentified()` +- `VehicleRange`, `VehicleOdometer`, `VehicleClimater`, `VehicleFinishTimer`, `VehiclePosition` +- `ChargeController` — remote start/stop on vehicle +- `CurrentLimiter` — `GetMinMaxCurrent()` for vehicle-side current limits +- `CurrentController` — some vehicles (Tesla, Fiat) also implement `MaxCurrent()` to set charge current from the vehicle side + +## Charge Modes + +| Mode | Behavior | +|------|----------| +| `OFF` | Disabled (unless welcome charge) | +| `NOW` | Max current immediately | +| `MINPV` | Min current when PV surplus; fast if cheap tariff | +| `PV` | Ramp current proportional to available solar | + +## Charge States (IEC 61851) + +- `A` — not connected +- `B` — connected, not charging +- `C` — connected, charging + +## The Control Loop (Site.update — runs every N seconds) + +``` +1. Update all meters (grid, PV, battery, aux) +2. For each loadpoint: UpdateChargePowerAndCurrents() +3. Calculate site power balance: + sitePower = gridPower + batteryPower + excessDCPower + + residualPower - auxPower - flexiblePower +4. Apply battery priority rules (prioritySoc, bufferSoc) +5. Get tariff rates +6. For EACH loadpoint: Update(sitePower, ...) + ├── Read charger status + ├── Detect/identify vehicle + ├── Check plan requirements (minSOC, target time) + ├── Check limits (limitSOC, limitEnergy) + ├── MODE switch -> calculate target current + ├── Cap at maxCurrent, respect circuit limits + ├── Send MaxCurrent() to charger + └── Record metrics +7. Push updates to WebSocket + metrics +``` + +The loop is stateless per cycle: always re-reads actual state, calculates +optimal current, sends single command. Resilient to restarts and missed updates. + +## PV Surplus Charging (pvMaxCurrent in core/loadpoint.go) + +``` +1. Read effective min/max current limits +2. Reduce sitePower by battery boost power +3. Consider phase switching (1p <-> 3p) if supported +4. deltaCurrent = powerToCurrent(-sitePower, activePhases) + targetCurrent = effectiveCurrent + deltaCurrent +5. Below minCurrent -> start disable timer (default 3 min) +6. Surplus returns -> start enable timer (default 1 min) +7. Cap at maxCurrent +``` + +## Battery Priority Rules + +| Setting | Effect | +|---------|--------| +| `prioritySoc` | Below this: battery charges first, EV gets 0 | +| `bufferSoc` | Above this: EV can draw from battery reserves | +| `bufferStartSoc` | Above this: EV charging can begin even if importing | + +## Effective Price Calculation + +``` +greenShare = (max(pvPower,0) + max(batteryPower,0)) / totalChargePower +effectivePrice = gridPrice * (1 - greenShare) + feedInPrice * greenShare +``` + +## Concurrency Model + +- **Site** owns `RWMutex` for its state (meters, battery, tariffs) +- **Loadpoint** owns `RWMutex` for its state (charger, vehicle, current) +- **Coordinator** owns `RWMutex` for vehicle <-> loadpoint tracking +- No global locks — ordering prevents deadlocks + +### Channels + +| Channel | Scope | Buffer | Purpose | +|---------|-------|--------|---------| +| `valueChan` | Site | Unbounded (`chanx.NewUnboundedChan`) | State changes -> DB + UI (ordering) | +| `lpUpdateChan` | Site | 1 | Early loadpoint update requests | +| `pushChan` | Loadpoint | Buffered | User notifications | + +## Tariff Integration + +Types: `TariffUsageGrid`, `TariffUsageFeedIn`, `TariffUsageCo2`, `TariffUsagePlanner`, `TariffUsageSolar` + +### Smart Features +- **Cheap-tariff override** — rate below threshold -> fast charge +- **Smart feed-in** — feed-in rate above threshold -> prioritize export +- **Planner** (`core/planner/planner.go`) — finds cheapest time slots for target SOC/energy by deadline + - `optimalPlan()` — cheapest non-contiguous slots + - `continuousPlan()` — cheapest continuous window (fallback) + +## Key File Locations + +- `api/api.go` — all core interfaces +- `core/site.go` — Site orchestrator + control loop +- `core/loadpoint.go` — Loadpoint state machine (pvMaxCurrent, mode switch) +- `core/site_battery.go` — battery priority logic +- `core/site_tariffs.go` — tariff integration +- `core/planner/planner.go` — charge time optimization +- `core/prioritizer/prioritizer.go` — power allocation across loadpoints +- `core/circuit/circuit.go` — electrical domain limits diff --git a/docs/agents/easee-architecture.md b/docs/agents/easee-architecture.md new file mode 100644 index 000000000..1890e5e24 --- /dev/null +++ b/docs/agents/easee-architecture.md @@ -0,0 +1,269 @@ +# Easee Charger Architecture + +The Easee integration communicates with the Easee cloud over two distinct channels: + +1. **REST API** (`https://api.easee.com/api`) — synchronous control commands and configuration. +2. **SignalR WebSocket** (`https://streams.easee.com/hubs/chargers`) — asynchronous real-time state updates and command confirmations. + +Commands are sent via REST; their acknowledgement and state changes are delivered asynchronously via SignalR. + +## Authentication + +### Flow + +Authentication uses username/password credentials against: +``` +POST https://api.easee.com/api/accounts/login +``` + +Returns a `Token` struct with `accessToken` (short-lived JWT), `refreshToken`, `expiresIn`, and `tokenType`. + +### Token Lifecycle + +Wrapped in `oauth2.TokenSource` via `oauth.RefreshTokenSource`. Near expiry, automatically calls: +``` +POST https://api.easee.com/api/accounts/refresh_token +``` +Falls back to full re-login if refresh fails. + +### Token Caching + +`TokenSource` is shared per user via `cache.New[oauth2.TokenSource]()`. Multiple `Easee` instances with the same user email share a single token source, preventing redundant re-authentication. + +## Initialization Sequence + +`NewEasee` performs these steps in order: + +1. **Charger Discovery** — If no serial provided, queries `GET /api/chargers` and expects exactly one charger. +2. **Site and Circuit Discovery** — `GET /api/chargers/{chargerID}/site`. Searches for a single-charger circuit for circuit-level phase control. +3. **SignalR Connection** — Creates client with `WithMaxElapsedTime(0)` to retry forever (default 15-min cap would silently stop updates). +4. **Subscription** — On every `ClientConnected`, sends `SubscribeWithCurrentState(chargerID, true)` to replay full current state before switching to push-on-change. +5. **Startup Gate** — Blocks until `CHARGER_OP_MODE` is received (one-shot `sync.OnceFunc`). +6. **Optional State Wait** — Waits up to 3s for `SESSION_ENERGY`, `LIFETIME_ENERGY`, `TOTAL_POWER`. WARN if missing but initialization succeeds. + +## SignalR Back-Channel + +### Why SignalR is Required + +1. **Commands are fire-and-forget at HTTP level.** HTTP response only confirms cloud received the request. Success/failure arrives via SignalR `CommandResponse`. +2. **State is event-driven, not pollable.** No REST endpoint streams charger state. +3. **Ticks correlation only works with a live connection.** If SignalR drops mid-command, the waiter times out. + +### Server -> Client Methods + +#### `ProductUpdate(json.RawMessage)` +Primary state channel. Carries a single `Observation` with `ID` (ObservationID), `Value`, `DataType`, and `Timestamp`. + +- **Timestamp deduplication**: older timestamps for the same ID are silently dropped. +- **Non-blocking fan-out**: observation sent on `obsC` via non-blocking select. + +#### `CommandResponse(json.RawMessage)` +Async acknowledgement for REST commands. Contains `Ticks` (correlation key), `WasAccepted`, `ResultCode`, and `ID` (ObservationID). + +Routes through three maps in order: +1. `pendingTicks[res.Ticks]` — primary correlation for async (HTTP 202) commands +2. `pendingByID[ObservationID(res.ID)]` — fallback when Ticks mismatch +3. `expectedOrphans[ObservationID(res.ID)]` — counter for sync (HTTP 200) endpoints that still produce a CommandResponse + +Unmatched responses are logged as WARN (rogue response from external system). + +#### `ChargerUpdate` / `SubscribeToMyProduct` +Logged at TRACE, not processed further. + +## Command Flow and Async Correlation + +### REST Command Endpoints + +``` +POST /api/chargers/{chargerID}/commands/{action} (start/stop/pause/resume) +POST /api/chargers/{chargerID}/settings (enable, DCC, PhaseMode, SmartCharging) +POST /api/sites/{siteID}/circuits/{circuitID}/settings (dynamic circuit currents) +``` + +### Response Handling + +| HTTP Status | Meaning | Behavior | +|-------------|---------|----------| +| `200` | Synchronous / already applied | Returns immediately | +| `202` | Asynchronous, Ticks provided | Waits for matching CommandResponse | +| other | Error | Returns error | + +### Ticks Correlation + +On 202, the body contains `RestCommandResponse` with a `Ticks` field (.NET DateTime.Ticks). If `Ticks == 0`, the command was a no-op. + +Each in-flight command creates a **buffered channel** (capacity 1), registered in both `pendingTicks` and `pendingByID`, cleaned up via `defer`. + +### The Sync/Async Mismatch + +Some endpoints return HTTP `200` but still fire a `CommandResponse` via SignalR. The observed case is circuit settings (`POST /api/sites/{siteID}/circuits/{circuitID}/settings`) which returns `200` but generates `CommandResponse` with `ID=22` (`CIRCUIT_MAX_CURRENT_P1`). + +Handled via the **expected-orphan counter**: +```go +expectedOrphans map[easee.ObservationID]int // protected by cmdMu +``` + +Before a POST to a known 200-returning endpoint, increment the counter. When `CommandResponse` arrives with no pending match, decrement and silently consume. Counter at 0 means genuinely rogue. + +## State Management + +### Internal State Fields (all protected by `sync.RWMutex`) + +| Field | Observation | Notes | +|-------|------------|-------| +| `opMode` | `CHARGER_OP_MODE` (109) | Central state machine | +| `chargerEnabled` | `IS_ENABLED` (31) | Hardware enable state | +| `smartCharging` | `SMART_CHARGING` (102) | LED color mode | +| `currentPower` | `TOTAL_POWER` (120) | Watts (API sends kW, multiplied by 1000) | +| `sessionEnergy` | `SESSION_ENERGY` (121) | kWh, special zero-handling | +| `totalEnergy` | `LIFETIME_ENERGY` (124) | kWh, updated ~hourly | +| `currentL1/L2/L3` | `IN_CURRENT_T3/T4/T5` (183/184/185) | Phase currents in A | +| `phaseMode` | `PHASE_MODE` (38) | 1=single, 2=auto, 3=locked 3-phase | +| `dynamicCircuitCurrent[3]` | `DYNAMIC_CIRCUIT_CURRENT_P1/P2/P3` (111/112/113) | Per-phase circuit limit | +| `maxChargerCurrent` | `MAX_CHARGER_CURRENT` (47) | Hardware max (non-volatile) | +| `dynamicChargerCurrent` | `DYNAMIC_CHARGER_CURRENT` (48) | Volatile current limit | +| `reasonForNoCurrent` | `REASON_FOR_NO_CURRENT` (96) | Debug enum | +| `pilotMode` | `PILOT_MODE` (100) | CP signal state A-F | +| `rfid` | `USER_IDTOKEN` (128) | Last scanned RFID token | + +### Session Energy Zero-value Protection + +`sessionEnergy` is never set to `0` from a `ProductUpdate` — the API sends spurious zeros erratically. Session reset is driven by op-mode transition: when `CHARGER_OP_MODE` transitions from disconnected to awaiting-start, `sessionEnergy` resets to `0` with a fresh timestamp. + +## Charger Operation Modes + +``` +0 = Offline — no cloud connection +1 = Disconnected — no car plugged in +2 = AwaitingStart — car plugged, waiting for authorization/start +3 = Charging — actively charging +4 = Completed — car full or finished, cable still plugged +5 = Error — fault condition +6 = ReadyToCharge — ready, current available +7 = AwaitingAuthentication — RFID auth required +8 = Deauthenticating — finishing authentication teardown +``` + +### Mapping to evcc Status + +| opMode | evcc Status | +|--------|------------| +| 1 (Disconnected) | A | +| 2, 4, 6, 7, 8 | B | +| 3 (Charging) | C | +| 0, 5 and others | error | + +## Enable/Disable Flow + +### Enable = true + +1. If `chargerEnabled == false`: POST settings `{ enabled: true }` and wait. +2. If `opMode == Disconnected`: return (no cable). +3. If `opMode == AwaitingAuthentication && authorize`: action = `start_charging`. +4. Otherwise: action = `resume_charging`. +5. POST `/commands/{action}` and wait. +6. Wait for `opMode` to reach enabled state. +7. Wait for `dynamicChargerCurrent` to reach `32` (Easee sets this on resume). +8. Call `MaxCurrent(c.current)` to restore previous setpoint. + +### Enable = false + +1. If disconnected or (awaiting auth && !authorize): return. +2. POST `/commands/pause_charging` and wait. +3. Wait for `opMode` to reach disabled state. +4. Wait for `dynamicChargerCurrent` to reach `0`. + +### State Waiting Pattern + +Both `waitForChargerEnabledState` and `waitForDynamicChargerCurrent` use: +1. Short-circuit check: if already in target state, return immediately. +2. Open a timer. +3. Loop on `obsC` channel. +4. On timer expiry: **one final check** before returning `api.ErrTimeout`. + +The final check handles the race where the state update arrived between the last channel read and the timer fire. + +## Phase Control + +### Circuit-Level (preferred, when circuit is known) + +Phase switching by zeroing dynamic circuit current on unused phases: +``` +POST /api/sites/{siteID}/circuits/{circuitID}/settings +``` + +For 1-phase: set P2=0, P3=0. For 3-phase: restore all three. + +This POST returns HTTP `200` but still fires a `CommandResponse` with `ID=22` (expected orphan). + +### Charger-Level (fallback) + +Uses `PhaseMode` setting: `1` for single-phase, `2` (auto) for 3-phase. +After changing PhaseMode, `Enable(false)` is called — the loadpoint then re-enables, because PhaseMode changes only take effect after a charging cycle restart. + +## Authorization Mode (`authorize`) + +When `authorize: true`, evcc sends `start_charging` to authorize sessions when the charger enters `ModeAwaitingAuthentication`. This enables fully unattended operation but is incompatible with RFID-based vehicle identification. + +When `authorize: false`, evcc does nothing in mode 7 — the charger waits for external authorization (RFID card or app). + +Setting `authorize: true` also prevents the charger from auto-starting at 32A on plug-in, giving evcc full control from the first amp. + +## Concurrency Model + +### Mutexes + +| Mutex | Type | Protects | +|-------|------|----------| +| `c.mux` | `sync.RWMutex` | All charger state fields | +| `dispatcher.mu` | `sync.Mutex` | `pendingTicks`, `pendingByID`, `expectedOrphans` maps (inside `CommandDispatcher`) | + +Command dispatch was extracted into `charger/easee/dispatcher.go` (`CommandDispatcher` struct). The two mutexes are intentionally separate to prevent the SignalR receive loop from blocking on command dispatch operations. + +### Observation Channel + +`obsC chan Observation` is unbuffered. `ProductUpdate` sends via non-blocking select — if no waiter is listening, the notification is dropped. The authoritative state is always in the struct fields; the channel is only a notification mechanism. + +**Design constraint**: any waiter on `obsC` must include a final state check after timer expiry before returning `api.ErrTimeout`. + +## Known Design Concerns + +1. **SESSION_ENERGY zero-value protection** — defensive measure based on field observations; root cause unverified. +2. **LIFETIME_ENERGY** — inaccurate by design, API pushes updates ~hourly. +3. **current vs dynamicChargerCurrent drift** — evcc's desired setpoint and charger's confirmed value can drift around pause/resume cycles. Resynced via `MaxCurrent(c.current)` after resume. +4. **Multi-charger circuits** — only circuit-level phase control when charger is alone on its circuit. Multi-charger circuits fall back to less precise charger-level control. +5. **Stale CommandResponses after reconnect** — if SignalR drops mid-command, the response may arrive after reconnect with no pending entry, triggering a false-positive rogue WARN. Acceptable trade-off. + +## API Endpoints Summary + +| Method | Endpoint | Used For | +|--------|----------|----------| +| `POST` | `/accounts/login` | Initial authentication | +| `POST` | `/accounts/refresh_token` | Token refresh | +| `GET` | `/chargers` | Auto-discover charger ID | +| `GET` | `/chargers/{id}/site` | Discover site and circuit | +| `POST` | `/chargers/{id}/settings` | Enable/disable, DCC, PhaseMode, SmartCharging | +| `POST` | `/chargers/{id}/commands/{action}` | start/stop/pause/resume charging | +| `GET` | `/sites/{siteId}/circuits/{circuitId}/settings` | Read max circuit currents | +| `POST` | `/sites/{siteId}/circuits/{circuitId}/settings` | Set dynamic circuit currents (phase switching) | + +### SignalR Hub + +| Endpoint | `https://streams.easee.com/hubs/chargers` | +|----------|------------------------------------------| +| Client -> Server | `SubscribeWithCurrentState(chargerID, true)` | +| Server -> Client | `ProductUpdate`, `ChargerUpdate`, `SubscribeToMyProduct`, `CommandResponse` | + +## Configuration + +| Parameter | Required | Default | Notes | +|-----------|----------|---------|-------| +| `user` | yes | | Easee account email | +| `password` | yes | | Easee account password | +| `charger` | no | | Charger serial; auto-detected if exactly one on account | +| `timeout` | no | `20s` | HTTP timeout for all API calls and command waits | +| `authorize` | no | `false` | If true, evcc sends `start_charging` to authorize sessions | + +Supported products: Easee Home, Easee Charge, Easee Charge Lite, Easee Charge Core. +Declared capabilities: `1p3p` (phase switching), `rfid` (RFID identification). +Requires evcc sponsorship. diff --git a/docs/agents/hardware-integrations.md b/docs/agents/hardware-integrations.md new file mode 100644 index 000000000..2fddf3fd5 --- /dev/null +++ b/docs/agents/hardware-integrations.md @@ -0,0 +1,148 @@ +# Hardware Integrations: Chargers, Meters, Vehicles + +## Integration Pattern + +All device types use a registry-based factory pattern: + +```go +// Self-registration in init() +func init() { + registry.AddCtx("typename", NewFromConfig) +} + +func NewFromConfig(ctx context.Context, other map[string]interface{}) (api.Charger, error) { + // Parse config, create client, return implementation +} +``` + +Optional interfaces are added via the decorator pattern: +```go +//go:generate decorate -f decorateXxx -b *Xxx -t "api.PhaseSwitcher,Phases1p3p,func(int) error" +``` + +## Charger Implementations + +### By Protocol + +| Protocol | Examples | +|----------|---------| +| HTTP/REST | Easee (REST+SignalR), Wallbox, go-e, OpenWB, Shelly | +| Modbus RTU/TCP | KEBA, Wallbe, CFOS, Bender, Delta, Mennekes | +| OCPP 1.6 | Generic charge point server | +| EEBus/ISO 15118 | EEBus SPINE protocol | +| UDP/Custom | KEBA UDP, OpenEVSE, Wattpilot, NRGKick | +| MQTT | OpenWB, Tasmota, Shelly | +| Smart Socket | Shelly, Tapo, TP-Link, FritzDECT | + +### Required Charger Interface + +```go +type Charger interface { + ChargeState + Enabled() (bool, error) + Enable(enable bool) error + CurrentController +} +``` + +Where `ChargeState` provides `Status() (ChargeStatus, error)` (A/B/C) and +`CurrentController` provides `MaxCurrent(current int64) error`. + +### Optional Charger Interfaces + +- `ChargerEx` — `MaxCurrentMillis(float64)` for milliamp precision +- `PhaseSwitcher` — `Phases1p3p(int)` to switch 1p/3p +- `Meter` / `MeterEnergy` — built-in power/energy measurement +- `PhaseCurrents` / `PhaseVoltages` — per-phase readings +- `ChargeRater` — `ChargedEnergy()` for session energy +- `ChargeTimer` — `ChargeDuration()` for session time +- `Identifier` — `Identify()` for RFID/vehicle identification + +### Key Implementations + +- **Easee** — REST + async SignalR; see `docs/agents/easee-architecture.md` for full detail +- **OCPP** (`charger/ocpp.go`) — Full 1.6 with charge point management +- **go-e** — Dual API (v1 local HTTP, v2 cloud); phase switching on v2 +- **EEBus** — Complex SPINE protocol with USE cases (CEM, EV, EVCC) +- **Generic configurable** (`charger/charger.go`) — plugin-driven via YAML template + +### Adding a New Charger + +1. Create `charger/xxx.go` (or YAML template in `templates/definition/charger/`) +2. Implement `NewXxxFromConfig()` returning `api.Charger` +3. Required: `Status()`, `Enabled()`, `Enable()`, `MaxCurrent()` +4. Register: `registry.AddCtx("xxx", factory)` in `init()` +5. Optional: add decorator for PhaseSwitcher, Meter, etc. +6. Add template: `templates/definition/charger/xxx.yaml` for UI metadata + +## Meter Implementations + +### By Category + +| Category | Examples | +|----------|---------| +| Modbus/SunSpec | SDM630, SMA, Fronius, Victron | +| HTTP/REST | Homewizard, Shelly Gen3, E3DC | +| Smart Home | HomeAssistant entities, Homematic | +| Battery/Storage | Tesla Powerwall, LG ESS, Zendure | + +### Required Meter Interface + +```go +type Meter interface { + CurrentPower() (float64, error) // watts +} +``` + +### Optional: `MeterEnergy`, `PhaseCurrents/Voltages/Powers`, `Battery`, `BatteryCapacity` + +### Key Implementations +- **mbmd** (`meter/mbmd.go`) — RS485 device library with auto-detection +- **SunSpec** (`plugin/sunspec.go`) — Modbus model-based point queries +- **Generic** — plugin-driven (HTTP, Modbus, MQTT sources) + +## Vehicle Integrations + +| Manufacturer | API Type | +|-------------|----------| +| Tesla | Fleet API + vehicle-command proxy | +| VW Group | WeConnect (VW, Audi, Skoda, Seat, Cupra) | +| Hyundai/Kia | BlueLink (regional variants) | +| BMW/Mini | ConnectedDrive v2 | +| Mercedes | Official API | +| Renault/Nissan | Renault API + Carwings | +| Ford | FordConnect (US/EU) | +| Porsche | Porsche Connect | +| PSA Group | Peugeot, Citroen, DS, Opel | +| Generic | OVMS, Tronity | + +### Required Vehicle Interface + +```go +type Vehicle interface { + Battery // Soc() (float64, error) + BatteryCapacity // Capacity() float64 + IconDescriber // Icon() string + FeatureDescriber // Features() []Feature + PhaseDescriber // Phases() int + TitleDescriber // GetTitle() string + SetTitle(string) + Identifiers() []string + OnIdentified() ActionConfig +} +``` + +### Optional: `SocLimiter`, `ChargeState`, `VehicleRange`, `VehicleOdometer`, `VehicleClimater`, `VehicleFinishTimer`, `VehiclePosition`, `CurrentLimiter`, `CurrentController`, `ChargeController`, `Resurrector` + +### Polling Strategy + +Configurable: always / while charging / while connected. Interval-based caching +to avoid excessive cloud API calls. OAuth2 token handling built into each provider. + +## Auto-Detection (`cmd/detect/`) + +Task-based parallel IP scanning: +`ping` -> `tcp_http` -> `tcp_modbus` -> `sunspec` -> device-specific probes + +Detects: OpenWB, SMA, KEBA, E3DC, Sonnen, Tesla Powerwall, Wallbe, Fronius, +Tasmota, Shelly, Phoenix, and many more. diff --git a/docs/agents/plugin-system.md b/docs/agents/plugin-system.md new file mode 100644 index 000000000..c37a76922 --- /dev/null +++ b/docs/agents/plugin-system.md @@ -0,0 +1,61 @@ +# Plugin System + +The plugin system (`plugin/`) provides protocol-level abstraction for device +communication. Plugins implement typed getter/setter interfaces and are composed +into charger, meter, or vehicle implementations via configuration. + +## Plugin Types + +| Plugin | Protocol | Key Config | +|--------|----------|------------| +| `http` | HTTP/REST | `uri`, `method`, `headers`, `auth`, `cache`, `timeout` | +| `mqtt` | MQTT | `topic`, `retained`, `payload` template, `timeout` | +| `modbus` | Modbus TCP/RTU | `uri`, `register`, `scale`, `baudrate`, `rtu` | +| `sunspec` | SunSpec/Modbus | Model-based point queries via device tree | +| `js` | JavaScript/WASM | Inline script evaluation | +| `go` | Go runtime | Dynamic Go code | +| `gpio` | Linux GPIO | Digital I/O for relays | + +## Getter/Setter Interfaces + +```go +type StringGetter func() (string, error) +type FloatGetter func() (float64, error) +type IntGetter func() (int64, error) +type BoolGetter func() (bool, error) +// + corresponding Setter types +``` + +## Pipeline Transforms + +Plugins support chained transforms: `scale`, `offset`, `lookup`, `regex`. + +## Template-Based Device Configuration + +Devices can be defined entirely via YAML templates using plugins: + +```yaml +# templates/definition/charger/example.yaml +status: + source: http + uri: http://{{ .host }}/status +enable: + source: http + uri: http://{{ .host }}/enable + method: POST +maxcurrent: + source: http + uri: http://{{ .host }}/current/{{ .maxcurrent }} +``` + +The generic configurable charger (`charger/charger.go`) wires these plugin +configs into the `api.Charger` interface at runtime. + +## Key Files + +- `plugin/config.go` — plugin registry and config types +- `plugin/http.go` — HTTP plugin +- `plugin/mqtt.go` — MQTT plugin +- `plugin/modbus.go` — Modbus plugin +- `plugin/sunspec.go` — SunSpec plugin +- `charger/charger.go` — generic configurable charger using plugins diff --git a/docs/agents/web-ui-api.md b/docs/agents/web-ui-api.md new file mode 100644 index 000000000..fed6d2331 --- /dev/null +++ b/docs/agents/web-ui-api.md @@ -0,0 +1,87 @@ +# Web UI & REST API + +## Server Architecture + +- **Router:** gorilla/mux, strict slash +- **Middleware:** GZIP, CORS (`*`), ETag caching, request logging, JSON headers, JWT auth +- **Timeouts:** Read 5s, Write 10s, Idle 120s +- **Static assets:** embedded in binary (`fs.FS`) +- **Default port:** 7070 + +## REST API (base `/api/`) + +### Site-level +- `POST /buffersoc/{value}`, `/prioritysoc/{value}`, `/residualpower/{value}` etc. +- `GET /tariff/{tariff}` — tariff rates +- `GET /sessions` — charging history +- `GET /state` — complete system state (supports jq filtering) + +### Per-loadpoint (`/loadpoints/{id}/...`) +- `POST mode/{value}` — off/now/minpv/pv +- `POST limitsoc/{value}`, `limitenergy/{value}` — charge limits +- `POST mincurrent/{value}`, `maxcurrent/{value}` — current limits +- `POST phases/{value}` — phase config +- `POST priority/{value}`, `batteryboost/{value}` +- `POST plan/energy/{value}/{time}` — schedule plan +- `POST vehicle/{name}` — select vehicle +- `POST smartcostlimit/{value}` — smart cost threshold + +### Configuration (`/config/...`, auth required) +- CRUD for devices (chargers, meters, vehicles, tariffs) +- Template browsing and testing +- Site, loadpoint, circuit, HEMS, messaging config +- `GET /config/evcc.yaml` — YAML export + +### System (`/system/...`, auth required) +- Log viewing, cache clear, DB backup/restore/reset, shutdown + +### Handler Pattern +Generic `handler[T]` with type conversion, setter, getter. +Specialized: `floatHandler`, `intHandler`, `boolHandler`, `durationHandler`. + +## WebSocket (`/ws`) + +- `coder/websocket` (RFC 6455) +- Pub/sub via `SocketHub` +- Buffered channels (1024 per subscriber) +- Welcome message with full state snapshot +- Incremental updates as JSON key-value pairs with dot-notation keys: + ```json + {"loadpoints.1.mode": "solar", "site.gridPower": 1234} + ``` +- Write timeout: 10s, compression (disabled for Safari) + +## State Flow + +1. WS connects -> receives welcome with full state +2. App emits `util.Param` on changes +3. Hub broadcasts to subscribers +4. Frontend `store.update(msg)` merges via dot-notation +5. Components reactively re-render + +## Authentication + +- JWT, 90-day lifetime +- HttpOnly cookie (`auth`) with `SameSite=Strict` +- Also accepts `Authorization: Bearer ` header +- Modes: Disabled, Locked (demo), Configured (password) +- Protects `/api/config` and `/api/system` + +## MQTT Integration + +- Publishes state changes to configurable broker +- Subscribes to control topics +- Retained messages for state persistence + +## Key Files + +- `server/http.go` — router setup +- `server/http_auth.go` — authentication +- `server/http_site_handler.go` — state + request handlers +- `server/http_config_*.go` — config endpoints +- `server/http_loadpoint_handler.go` — per-loadpoint endpoints +- `server/socket.go` — WebSocket pub/sub +- `assets/js/app.ts` — Vue app entry +- `assets/js/store.ts` — reactive state store +- `assets/js/api.ts` — Axios clients +- `assets/js/router.ts` — route definitions From 6414d8add33b2f5d08e18797917ff69fdda37e95 Mon Sep 17 00:00:00 2001 From: andig Date: Sun, 19 Apr 2026 14:42:03 +0200 Subject: [PATCH 0280/1506] Forecast: temporarily remove adjustment (#29244) --- assets/js/components/Forecast/ForecastModal.vue | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/assets/js/components/Forecast/ForecastModal.vue b/assets/js/components/Forecast/ForecastModal.vue index ab5df04fc..c0769fd1b 100644 --- a/assets/js/components/Forecast/ForecastModal.vue +++ b/assets/js/components/Forecast/ForecastModal.vue @@ -95,7 +95,8 @@ export default defineComponent({ return settings.solarAdjusted; }, showSolarAdjust() { - return !!this.forecast.solar && this.experimental; + // TODO fix https://github.com/evcc-io/evcc/issues/29165 + return !!this.forecast.solar && this.experimental && false; }, solar() { if (this.showSolarAdjust && this.solarAdjusted) { From b751090b03828aa2ca346820e0b5684e68d0b7bd Mon Sep 17 00:00:00 2001 From: Mathias Walter Date: Sun, 19 Apr 2026 17:25:45 +0200 Subject: [PATCH 0281/1506] Solarprognose: deprecate forecast_days (#29249) --- .../definition/tariff/solarprognose.yaml | 45 +++++++++---------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/templates/definition/tariff/solarprognose.yaml b/templates/definition/tariff/solarprognose.yaml index 00103c9eb..85a07da51 100644 --- a/templates/definition/tariff/solarprognose.yaml +++ b/templates/definition/tariff/solarprognose.yaml @@ -39,13 +39,7 @@ params: de: Prognosealgorithmus (mosmix, own-v1 oder clearsky) advanced: true - name: forecast_days - description: - en: Number of additional days to forecast (1-6) - de: Anzahl zusätzlicher Tage für die Prognose (1-6) - help: - en: "Number of additional days to forecast (1-6), excluding today. Remember: evcc calculates energy for today, tomorrow, and the day after tomorrow, effectively handling up to three days of solar forecast data, but the UI shows up to 96 hours." - de: "Anzahl der zusätzlichen Tage für die Prognose (1-6), ohne den heutigen Tag. Bedenke: evcc berechnet die Energie für heute, morgen und übermorgen und verarbeitet dabei effektiv bis zu drei Tage Solarprognosedaten, zeigt jedoch bis zu 96 Stunden an." - default: 4 + deprecated: true advanced: true - name: interval default: 1h @@ -57,7 +51,7 @@ render: | forecast: source: http # Base URL with required params (format, type, access token), then optional item-based parameters - uri: https://www.solarprognose.de/web/solarprediction/api/v1?_format=json&type=hourly&project=https://evcc.io&access-token={{ .token }}{{ if .item }}&item={{ .item }}{{ if .id }}&id={{ .id }}{{ else }}&token={{ unquote .uniquetoken }}{{ end }}{{ end }}{{ if .algorithm }}&algorithm={{ .algorithm }}{{ end }}{{ if .forecast_days }}&end_day={{ .forecast_days }}{{ end }} + uri: https://www.solarprognose.de/web/solarprediction/api/v1?_format=json&type=hourly&project=https://evcc.io&access-token={{ .token }}{{ if .item }}&item={{ .item }}{{ if .id }}&id={{ .id }}{{ else }}&token={{ unquote .uniquetoken }}{{ end }}{{ end }}{{ if .algorithm }}&algorithm={{ .algorithm }}{{ end }} jq: | def step: 3600; # hourly interval @@ -65,7 +59,10 @@ render: | (.data // {}) | if type == "object" and length > 0 then to_entries - | map({key, value: (.value[0] * 1000)}) + | map( + select(.value | type == "array" and length > 0) + | {key, value: ((.value[0] // 0) * 1000)} + ) | from_entries else {} @@ -74,19 +71,21 @@ render: | . as $root | ($root | lookup) as $L | ($L | keys | map(tonumber) | sort) as $ts - | ($ts[0]) as $min - | ($ts[-1]) as $max - | reduce range($min; $max + step; step) as $t - ( { last: null, out: [] }; - ($L[($t|tostring)]?) as $v - | .last = (if $v == null then .last else $v end) - | .out += - [ { - start: ($t | strftime("%FT%TZ")), - end: (($t + step) | strftime("%FT%TZ")), - value: (.last // 0) - } ] - ) - | .out + | if ($ts | length) == 0 then + [] + else + reduce range($ts[0]; $ts[-1] + step; step) as $t + ( { last: null, out: [] }; + ($L[($t|tostring)]?) as $v + | .last = (if $v == null then .last else $v end) + | .out += + [ { + start: ($t | strftime("%FT%TZ")), + end: (($t + step) | strftime("%FT%TZ")), + value: (.last // 0) + } ] + ) + | .out + end | tostring interval: {{ .interval }} From 9138ef81f6c80dccba4b4ab2e952cfcbcabb2ae9 Mon Sep 17 00:00:00 2001 From: andig Date: Sun, 19 Apr 2026 17:35:45 +0200 Subject: [PATCH 0282/1506] Fix metrics table migration (#29250) --- core/metrics/db.go | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/core/metrics/db.go b/core/metrics/db.go index 06809f30f..d84fcdffa 100644 --- a/core/metrics/db.go +++ b/core/metrics/db.go @@ -182,23 +182,24 @@ func SetupSchema() error { } } + rename := func(from, to string) error { + if table := new(meter); m.HasColumn(table, from) && !m.HasColumn(table, to) { + return m.RenameColumn(table, from, to) + } + return nil + } + // meter: split energy direction - if old := "val"; m.HasColumn(new(meter), old) { - if err := m.RenameColumn(new(meter), old, "import"); err != nil { - return err - } - } else { - // meter: split energy direction #2 - if old := "pos"; m.HasColumn(new(meter), old) { - if err := m.RenameColumn(new(meter), old, "import"); err != nil { - return err - } - } - if old := "neg"; m.HasColumn(new(meter), old) { - if err := m.RenameColumn(new(meter), old, "export"); err != nil { - return err - } - } + if err := rename("val", "import"); err != nil { + return err + } + + // meter: split energy direction #2 + if err := rename("pos", "import"); err != nil { + return err + } + if err := rename("neg", "export"); err != nil { + return err } return db.Instance.AutoMigrate(new(meter)) From 1617f4ef544b767705b727f48af544f6c0b65cf0 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Apr 2026 19:09:36 +0000 Subject: [PATCH 0283/1506] Remove YAML-specific device template input hints (#29259) --- templates/definition/charger/evbox-livo.yaml | 4 ++-- templates/definition/meter/solax-hybrid-cloud.yaml | 4 ++-- templates/definition/meter/solax-inverter-cloud.yaml | 4 ++-- util/templates/defaults.yaml | 8 ++++---- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/templates/definition/charger/evbox-livo.yaml b/templates/definition/charger/evbox-livo.yaml index cb6d119e7..5c5f35cc6 100644 --- a/templates/definition/charger/evbox-livo.yaml +++ b/templates/definition/charger/evbox-livo.yaml @@ -6,8 +6,8 @@ products: requirements: evcc: ["eebus"] description: - de: Das Gerät benötigt eine feste IP Adresse. Es ist wichtig, zuerst EEBus einzurichten. Danach erkennt das Ladegerät evcc als HEMS-Gerät im Netzwerk. Verwende das Installationstool, um evcc als HEMS auszuwählen. Kopiere anschließend den angegebenen SKI aus der Installations-App und fügen ihn zu Ihrem YAML hinzu. - en: The device requires a fixed IP addres. It's important to set up EEBus first. After setting up EEBus the charger will recognize evcc as a HEMS device on the network. Please use the installer tool to select evcc as HEMS. After this has been done, copy the given SKI from the Install app and add it to your yaml. + de: Das Gerät benötigt eine feste IP Adresse. Es ist wichtig, zuerst EEBus einzurichten. Danach erkennt das Ladegerät evcc als HEMS-Gerät im Netzwerk. Verwende das Installationstool, um evcc als HEMS auszuwählen. Kopiere anschließend den angegebenen SKI aus der Installations-App und füge ihn zur Konfiguration hinzu. + en: The device requires a fixed IP address. It's important to set up EEBus first. After setting up EEBus the charger will recognize evcc as a HEMS device on the network. Please use the installer tool to select evcc as HEMS. After this has been done, copy the given SKI from the Install app and add it to the configuration. params: - preset: eebus render: | diff --git a/templates/definition/meter/solax-hybrid-cloud.yaml b/templates/definition/meter/solax-hybrid-cloud.yaml index 680d2565c..693f13833 100644 --- a/templates/definition/meter/solax-hybrid-cloud.yaml +++ b/templates/definition/meter/solax-hybrid-cloud.yaml @@ -22,8 +22,8 @@ params: description: generic: SolaxCloud TokenID help: - de: "[solaxcloud.com](https://www.solaxcloud.com/) -> Support -> Drittanbieter-Ökosystem (alte Website) oder Dienst -> API (neue Website), den Wert von `tokenID` hier in Anführungszeichen eintragen (Beispiel: '20241028488283838')" - en: "[solaxcloud.com](https://www.solaxcloud.com/) -> Support -> Third-party Ecology (old site) or Service -> API (new site), put the value of `tokenID` here in single quotes (Example: '20241028488283838')" + de: "[solaxcloud.com](https://www.solaxcloud.com/) -> Support -> Drittanbieter-Ökosystem (alte Website) oder Dienst -> API (neue Website), den Wert von `tokenID` hier eintragen (Beispiel: 20241028488283838)" + en: "[solaxcloud.com](https://www.solaxcloud.com/) -> Support -> Third-party Ecology (old site) or Service -> API (new site), enter the value of `tokenID` here (Example: 20241028488283838)" - name: serial required: true description: diff --git a/templates/definition/meter/solax-inverter-cloud.yaml b/templates/definition/meter/solax-inverter-cloud.yaml index d43952b51..86d5c8b1d 100644 --- a/templates/definition/meter/solax-inverter-cloud.yaml +++ b/templates/definition/meter/solax-inverter-cloud.yaml @@ -22,8 +22,8 @@ params: description: generic: SolaxCloud TokenID help: - de: "[solaxcloud.com](https://www.solaxcloud.com/) -> Support -> Drittanbieter-Ökosystem (alte Website) oder Dienst -> API (neue Website), den Wert von `tokenID` hier in Anführungszeichen eintragen (Beispiel: '20241028488283838')" - en: "[solaxcloud.com](https://www.solaxcloud.com/) -> Support -> Third-party Ecology (old site) or Service -> API (new site), put the value of `tokenID` here in single quotes (Example: '20241028488283838')" + de: "[solaxcloud.com](https://www.solaxcloud.com/) -> Support -> Drittanbieter-Ökosystem (alte Website) oder Dienst -> API (neue Website), den Wert von `tokenID` hier eintragen (Beispiel: 20241028488283838)" + en: "[solaxcloud.com](https://www.solaxcloud.com/) -> Support -> Third-party Ecology (old site) or Service -> API (new site), enter the value of `tokenID` here (Example: 20241028488283838)" - name: serial required: true description: diff --git a/util/templates/defaults.yaml b/util/templates/defaults.yaml index 576df08c3..3b98dec06 100644 --- a/util/templates/defaults.yaml +++ b/util/templates/defaults.yaml @@ -59,8 +59,8 @@ params: de: Passwort en: Password help: - de: Bei führenden Nullen bitte in einfache Hochkommata setzen - en: Use single quotes in case of leading zeros + de: Zugangspasswort des Dienstes + en: Service account password mask: true - name: capacity unit: kWh @@ -292,8 +292,8 @@ params: en: Actor Identification Number (AIN) de: Aktoridentifikationsnummer (AIN) help: - en: Printed on the type label on the back of the device. Embed it in double quotes in case of leading zeroes. - de: Ist auf dem Typenschild auf der Geräterückseite aufgedruckt. Bei führenden Nullen bitte in doppelte Hochkommata setzen. + en: Printed on the type label on the back of the device. + de: Ist auf dem Typenschild auf der Geräterückseite aufgedruckt. - name: coarsecurrent type: bool description: From 63459e18e99a16eac473b49625425d820cd318d8 Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Mon, 20 Apr 2026 09:02:53 +0200 Subject: [PATCH 0284/1506] Forecast: temporarily remove adjustment (2) (#29260) --- assets/js/settings.ts | 2 +- assets/js/views/Forecast.vue | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/assets/js/settings.ts b/assets/js/settings.ts index 9ea7fcff8..1657c0bc1 100644 --- a/assets/js/settings.ts +++ b/assets/js/settings.ts @@ -151,7 +151,7 @@ const settings: Settings = reactive({ savingsIndicator: read(SAVINGS_INDICATOR), sessionsGroup: read(SESSIONS_GROUP), sessionsType: read(SESSIONS_TYPE), - solarAdjusted: readBool(SETTINGS_SOLAR_ADJUSTED), + solarAdjusted: false, //readBool(SETTINGS_SOLAR_ADJUSTED), # temporarily disable, https://github.com/evcc-io/evcc/issues/29165 priceZoom: readBool(SETTINGS_PRICE_ZOOM), hideFeedin: readBool(SETTINGS_HIDE_FEEDIN), loadpoints: readJSON(LOADPOINTS), diff --git a/assets/js/views/Forecast.vue b/assets/js/views/Forecast.vue index c1cd0b682..d15863a4a 100644 --- a/assets/js/views/Forecast.vue +++ b/assets/js/views/Forecast.vue @@ -243,7 +243,8 @@ export default defineComponent({ return true; }, showSolarAdjust() { - return !!this.forecast.solar && this.experimental; + // TODO fix https://github.com/evcc-io/evcc/issues/29165 + return !!this.forecast.solar && this.experimental && false; }, solar() { return this.showSolarAdjust && this.solarAdjusted From 0918c62b44ecb7f698a5ae946f04af247ce7779a Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Apr 2026 09:16:02 +0200 Subject: [PATCH 0285/1506] Sungrow iHomeManager: fix voltage and per-phase active power decoding (#27985) --- templates/definition/meter/sungrow-ihm.yaml | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/templates/definition/meter/sungrow-ihm.yaml b/templates/definition/meter/sungrow-ihm.yaml index 06cb6d664..d58e1f131 100644 --- a/templates/definition/meter/sungrow-ihm.yaml +++ b/templates/definition/meter/sungrow-ihm.yaml @@ -36,21 +36,21 @@ render: | - source: modbus {{- include "modbus" . | indent 2 }} register: - address: 8554 # Register 8555 (A-phase voltage) + address: 4356 # Register 4357 (Phase A voltage, phase-to-neutral) type: input decode: uint16 scale: 0.1 # Factor 0.1 V - source: modbus {{- include "modbus" . | indent 2 }} register: - address: 8555 # Register 8556 (B-phase voltage) + address: 4358 # Register 4359 (Phase B voltage, phase-to-neutral) type: input decode: uint16 scale: 0.1 - source: modbus {{- include "modbus" . | indent 2 }} register: - address: 8556 # Register 8557 (C-phase voltage) + address: 4360 # Register 4361 (Phase C voltage, phase-to-neutral) type: input decode: uint16 scale: 0.1 @@ -58,24 +58,21 @@ render: | - source: modbus {{- include "modbus" . | indent 2 }} register: - address: 8558 # Register 8559 (Phase A active power, U32) + address: 8558 # Register 8559 (Phase A active power, S32, signed import/export) type: input - decode: uint32s - scale: 1 # Factor 1 W + decode: int32s - source: modbus {{- include "modbus" . | indent 2 }} register: address: 8560 # Register 8561 (Phase B active power) type: input - decode: uint32s - scale: 1 + decode: int32s - source: modbus {{- include "modbus" . | indent 2 }} register: address: 8562 # Register 8563 (Phase C active power) type: input - decode: uint32s - scale: 1 + decode: int32s {{- end }} {{- if eq .usage "pv" }} type: custom From 4bfc1ba6e9b95b6be6546bcdcf25e0c437e9d6ea Mon Sep 17 00:00:00 2001 From: andig Date: Mon, 20 Apr 2026 16:33:33 +0200 Subject: [PATCH 0286/1506] myPV AC Thor: support extra heater on relay (#29266) --- charger/mypv.go | 67 +++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 59 insertions(+), 8 deletions(-) diff --git a/charger/mypv.go b/charger/mypv.go index cf6fc386b..67982419f 100644 --- a/charger/mypv.go +++ b/charger/mypv.go @@ -46,13 +46,18 @@ type MyPv struct { } const ( - elwaRegSetPower = 1000 - elwaRegTempLimit = 1002 - elwaRegStatus = 1003 - elwaRegLoadState = 1059 - elwaRegPower = 1000 // https://github.com/evcc-io/evcc/issues/18020#issuecomment-2585300804 - elwaRegOperationState = 1077 - elwaERegOperationState = elwaRegStatus // same register for elwa-e operation state + elwaRegSetPower = 1000 + elwaRegTempLimit = 1002 + elwaRegStatus = 1003 + elwaRegLoadState = 1059 + elwaRegPower = 1000 // https://github.com/evcc-io/evcc/issues/18020#issuecomment-2585300804 + elwaRegOperationState = 1077 + elwaERegOperationState = elwaRegStatus // same register for elwa-e operation state + elwaRegRelayState = 1058 + elwaRegVoltage = 1061 + elwaRegOperationMode = 1065 // https://github.com/evcc-io/evcc/discussions/23708 + elwaRegMaxControlledPower = 1014 // max. power for linear controlled output + elwaRegMaxCombinedPower = 1071 // (max. power for linear controlled output + configured relais power) * 1.10 ) var elwaTemp = []uint16{1001, 1030, 1031} @@ -266,6 +271,7 @@ func (wb *MyPv) MaxCurrentMillis(current float64) error { phases = p } } + power := uint16(voltage * current * float64(phases)) err := wb.setPower(power) @@ -285,7 +291,52 @@ func (wb *MyPv) CurrentPower() (float64, error) { return 0, err } - return float64(binary.BigEndian.Uint16(b)), nil + res := float64(binary.BigEndian.Uint16(b)) + if wb.name != "ac-thor" { + return res, nil + } + + c, err := wb.conn.ReadHoldingRegisters(elwaRegOperationMode, 1) + if err != nil { + return 0, err + } + wb.log.TRACE.Printf("operation mode %d", binary.BigEndian.Uint16(c)) + + // AC Thor operation mode != 3 + if binary.BigEndian.Uint16(c) != 3 { + return res, nil + } + + // AC Thor operation mode == 3 "Warm water 9 + 9kW" + // with extra heater on internal relay + // see https://github.com/evcc-io/evcc/discussions/23708 + f, err := wb.conn.ReadHoldingRegisters(elwaRegRelayState, 1) + if err != nil { + return 0, err + } + + // relay inactive + if binary.BigEndian.Uint16(f) != 1 { + return res, nil + } + + // get power of heater on relay as set in web interface + // (scale factor must be used for correct setting in web interface) + d, err := wb.conn.ReadHoldingRegisters(elwaRegMaxControlledPower, 1) + if err != nil { + return 0, err + } + + e, err := wb.conn.ReadHoldingRegisters(elwaRegMaxCombinedPower, 1) + if err != nil { + return 0, err + } + wb.log.TRACE.Printf("max. power: controlled %.0f W / combined %.0f W", float64(binary.BigEndian.Uint16(d)), float64(binary.BigEndian.Uint16(e))) + + // relay power = combined power - controlled power, finally corrected with 110% factor + res += float64(int(binary.BigEndian.Uint16(e))-int(binary.BigEndian.Uint16(d))) / wb.scale / 1.1 + + return res, nil } var _ api.Battery = (*MyPv)(nil) From 7d2f507f3a15647446a0270d406b680a222cce3c Mon Sep 17 00:00:00 2001 From: Mathias Walter Date: Tue, 21 Apr 2026 05:54:45 +0200 Subject: [PATCH 0287/1506] Log UI: sort the log area multiselect naturally (#29289) --- assets/js/views/Log.vue | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/assets/js/views/Log.vue b/assets/js/views/Log.vue index 601be30af..436e8e0a7 100644 --- a/assets/js/views/Log.vue +++ b/assets/js/views/Log.vue @@ -171,7 +171,10 @@ export default defineComponent({ }); }, areaOptions() { - return this.availableAreas.map((area) => ({ name: area, value: area })); + return this.availableAreas + .slice() + .sort((a, b) => a.localeCompare(b, undefined, { numeric: true })) + .map((area) => ({ name: area, value: area })); }, areasLabel() { if (this.areas.length === 0) { From 1d3a963dc82d3f8fd540daa147b7b8f4e71d90fb Mon Sep 17 00:00:00 2001 From: VallahDieWaldfee <164005630+VallahDieWaldfee@users.noreply.github.com> Date: Tue, 21 Apr 2026 08:35:57 +0200 Subject: [PATCH 0288/1506] Nibe S-series: update docs (#29293) --- .../definition/charger/nibe-s-series.yaml | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/templates/definition/charger/nibe-s-series.yaml b/templates/definition/charger/nibe-s-series.yaml index c9cae6d39..d943e48ce 100644 --- a/templates/definition/charger/nibe-s-series.yaml +++ b/templates/definition/charger/nibe-s-series.yaml @@ -7,16 +7,21 @@ group: heating requirements: description: de: | - Modbus Verbindung zur NIBE S Wärmepumpe. Firmware 4.7.5 oder neuer benötigt. - In der Inneneinheit muss im Menü 7.5.9 Modbus aktiviert sein. - Zur Verwendung der Leistungssteuerung muss im Menü 7.4 unter dem Punkt AUX von Modbus ext. Leistungsbegrenzung ausgewählt sein. - Es muss einmalig im Holding Register 3032 der Wert `1` (uint8) geschrieben werden. + Modbus Verbindung zur NIBE S Wärmepumpe. + Firmware 4.7.5 oder neuer benötigt. + Benötigte Einstellungen in der Steuereinheit: + - In der Inneneinheit muss im Menü 7.5.9 Modbus aktiviert sein. + - Zur Verwendung der Leistungssteuerung muss im Menü 7.4 unter dem Punkt AUX von Modbus ext. Leistungsbegrenzung ausgewählt sein. In den Ladepunkteinstellungen muss eine minimale Leistung von 1,2kW (1-phasig) eingestellt werden. Sollte der Normalverbrauch höher sein, muss dieser als minimale Leistung eingestellt werden. + - Im Menü 7.4 dürfen die Optionen SG-Ready A und SG-Ready B nicht vergeben sein. + - Es muss einmalig im Holding Register 3032 der Wert `1` (uint8) geschrieben werden. en: | Modbus connection to the NIBE S heat pump. Firmware version 4.7.5 or newer is required. - In the indoor unit, Modbus must be enabled in menu 7.5.9 - To use the power control function, external power limitation must be selected under Menu 7.4, item AUX from modbus. - The value `1` (uint8) must be written once to holding register 3032. + Required settings in the control unit: + - In the indoor unit, Modbus must be enabled in menu 7.5.9 + - To use power control, “external power limitation via Modbus” must be selected under AUX in menu 7.4. In the charging point settings, a minimum power of 1.2 kW (single-phase) must be configured. If the base load is higher, it must be set as the minimum power. + - In menu 7.4, the options “SG-Ready A” and “SG-Ready B” must not be assigned. + - The value `1` (uint8) must be written once to holding register 3032. params: - name: modbus choice: ["tcpip"] From 2fb900c8eae7dc06482384a64d7c3dd30c4d2390 Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Tue, 21 Apr 2026 10:31:19 +0200 Subject: [PATCH 0289/1506] History UI: small fixes (#29138) --- assets/js/components/History/Chart.vue | 14 +++++++---- assets/js/views/History.vue | 4 ++++ core/metrics/db.go | 7 +++--- core/metrics/db_test.go | 33 ++++++++++++++++++++++++++ 4 files changed, 50 insertions(+), 8 deletions(-) diff --git a/assets/js/components/History/Chart.vue b/assets/js/components/History/Chart.vue index 874388cf4..4e71801fc 100644 --- a/assets/js/components/History/Chart.vue +++ b/assets/js/components/History/Chart.vue @@ -72,10 +72,14 @@ export default defineComponent({ const color = colors.palette[i % colors.palette.length]; const fill = dimColor(color); - const points = s.data.map((slot) => ({ - x: new Date(slot.start).getTime(), - y: slotPower(slot, "import") - slotPower(slot, "export"), - })); + const points = s.data.map((slot) => { + const start = new Date(slot.start).getTime(); + const end = new Date(slot.end).getTime(); + return { + x: (start + end) / 2, + y: slotPower(slot, "import") - slotPower(slot, "export"), + }; + }); return { label: s.name, @@ -85,7 +89,7 @@ export default defineComponent({ fill: true, pointRadius: 0, borderWidth: 1.5, - tension: 0.3, + tension: 0.05, }; }); diff --git a/assets/js/views/History.vue b/assets/js/views/History.vue index 4d818ebca..a346f2d97 100644 --- a/assets/js/views/History.vue +++ b/assets/js/views/History.vue @@ -1,6 +1,10 @@ @@ -31,6 +32,8 @@ import Item from "./Item.vue"; import MoreIcon from "../MaterialIcon/More.vue"; import MoreMenu from "./MoreMenu.vue"; import { isUserConfigError } from "@/utils/fatal"; +import { isNewVersionAvailable, isNewVersionUnacknowledged } from "@/utils/version"; +import settings from "@/settings"; import type { FatalError, Sponsor, EvOpt, AuthProviders } from "@/types/evcc"; export default defineComponent({ @@ -46,6 +49,7 @@ export default defineComponent({ evopt: { type: Object as PropType, required: false }, installed: String, commit: String, + availableVersion: String, }, data() { return { open: false }; @@ -69,14 +73,27 @@ export default defineComponent({ showConfigBadge() { return this.sponsorExpires || isUserConfigError(this.fatal); }, + newVersionAvailable() { + return isNewVersionAvailable(this.installed, this.availableVersion); + }, + showVersionBadge() { + return isNewVersionUnacknowledged( + this.installed, + this.availableVersion, + settings.lastAcknowledgedVersion + ); + }, showRootBadge() { - return this.authorizationRequired || this.showConfigBadge; + return this.authorizationRequired || this.showConfigBadge || this.showVersionBadge; }, badgeClass() { if (this.fatal.length > 0) { return "bg-danger"; } - return "bg-warning"; + if (this.authorizationRequired || this.showConfigBadge) { + return "bg-warning"; + } + return "bg-darker-green"; }, }, methods: { diff --git a/assets/js/components/BottomTabs/MoreMenu.vue b/assets/js/components/BottomTabs/MoreMenu.vue index f27d2704d..1749ae2d3 100644 --- a/assets/js/components/BottomTabs/MoreMenu.vue +++ b/assets/js/components/BottomTabs/MoreMenu.vue @@ -11,8 +11,14 @@ class="dropdown-item d-flex align-items-center" @click="openAboutModal" > + evcc {{ versionLabel }} + +
    + + +
    @@ -68,11 +94,21 @@ import "@h2d2/shopicons/es/filled/lightning"; import Loadpoint from "./Loadpoint.vue"; import { defineComponent, type PropType } from "vue"; -import type { UiLoadpoint, SMART_COST_TYPE, Timeout, Vehicle, BATTERY_MODE } from "@/types/evcc"; +import type { + UiLoadpoint, + SMART_COST_TYPE, + Timeout, + Vehicle, + BATTERY_MODE, + Forecast, + CURRENCY, +} from "@/types/evcc"; +import ChargingPlanModal from "../ChargingPlans/ChargingPlanModal.vue"; +import SettingsModal from "../Loadpoints/SettingsModal.vue"; export default defineComponent({ name: "Loadpoints", - components: { Loadpoint }, + components: { Loadpoint, ChargingPlanModal, SettingsModal }, props: { loadpoints: { type: Array as PropType, default: () => [] }, vehicles: { type: Array as PropType }, @@ -82,14 +118,14 @@ export default defineComponent({ tariffGrid: Number, tariffCo2: Number, tariffFeedIn: Number, - currency: String, + currency: String as PropType, selectedId: String, gridConfigured: Boolean, pvConfigured: Boolean, batteryConfigured: Boolean, batterySoc: Number, batteryMode: String as PropType, - forecast: Object, // as PropType, + forecast: Object as PropType, }, emits: ["id-changed"], data() { @@ -188,6 +224,25 @@ export default defineComponent({ } }, 1000); }, + openChargingPlanModal(loadpointId: string, openArrivalTab = false) { + const modal = this.$refs["chargingPlanModal"] as + | InstanceType + | undefined; + + if (openArrivalTab) { + modal?.showArrivalTab(); + } else { + modal?.showDepartureTab(); + } + + modal?.open(loadpointId); + }, + openSettingsModal(loadpointId: string) { + const modal = this.$refs["settingsModal"] as + | InstanceType + | undefined; + modal?.open(loadpointId); + }, }, }); diff --git a/assets/js/components/Loadpoints/SettingsModal.vue b/assets/js/components/Loadpoints/SettingsModal.vue index 34d254b93..e3a11a253 100644 --- a/assets/js/components/Loadpoints/SettingsModal.vue +++ b/assets/js/components/Loadpoints/SettingsModal.vue @@ -1,30 +1,31 @@ + + diff --git a/assets/js/components/Sessions/EnergyGroupedChart.vue b/assets/js/components/Sessions/EnergyGroupedChart.vue index 18c39eb21..31e69bd9c 100644 --- a/assets/js/components/Sessions/EnergyGroupedChart.vue +++ b/assets/js/components/Sessions/EnergyGroupedChart.vue @@ -1,7 +1,7 @@