From b5907192f563fcfca8b62f3e56fc37ef78e29669 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 16 Aug 2026 10:23:04 +0200 Subject: [PATCH 001/135] SunEnergyXT 500: fix battery control (#32866) --- .../definition/meter/sunenergyxt-500.yaml | 71 +++++++++++++++---- 1 file changed, 56 insertions(+), 15 deletions(-) diff --git a/templates/definition/meter/sunenergyxt-500.yaml b/templates/definition/meter/sunenergyxt-500.yaml index 65eb09e5e..c43b0c1c0 100644 --- a/templates/definition/meter/sunenergyxt-500.yaml +++ b/templates/definition/meter/sunenergyxt-500.yaml @@ -1,23 +1,25 @@ # Used API docs for SunEnergyXT 500 Series: https://github.com/SunEnergyXT/SunEnergyXT-500-Series/blob/main/API.en.md template: sunenergyxt-500 products: + # single entry: the `model` param carries the Standard/PRO distinction - brand: SunEnergyXT description: - generic: SunEnergyXT 500 - - brand: SunEnergyXT - description: - generic: SunEnergyXT 500 PRO + generic: SunEnergyXT 500 / 500 PRO capabilities: ["battery-control"] requirements: description: en: | The battery's local mode must be enabled in the manufacturer's app. Furthermore, the device's IP address must be set to a fixed, permanent IP address in the router. - If the `grid` mode is used, an external meter (e.g., Shelly) must be connected to the battery to measure the grid connection point (via the manufacturer's app). + An external meter (e.g., Shelly) measuring the grid connection point must be connected to the battery via the manufacturer's app. + It is required for the `grid` usage, and also for the `battery` usage, because the normal battery mode enables the device's + zero feed-in regulation, which reads this meter. de: | Der lokale Modus der Batterie muss in der Hersteller-App aktiviert werden. Des Weiteren muss die IP-Adresse des Geräts im Router auf eine feste, dauerhafte IP-Adresse eingestellt werden. - Wenn der `grid` Modus verwendet wird, muss mit der Batterie ein externer Zähler (z. B. Shelly) verbunden sein, der den Hausanschlusspunkt misst (dieser wird über die Hersteller-App hinzugefügt). + Mit der Batterie muss ein externer Zähler (z. B. Shelly) verbunden sein, der den Hausanschlusspunkt misst (dieser wird über die Hersteller-App hinzugefügt). + Er wird für die Verwendung `grid` benötigt und ebenso für die Verwendung `battery`, da der normale Batteriemodus die + Nulleinspeise-Regelung des Geräts aktiviert, die diesen Zähler ausliest. caveats: - description: de: | @@ -27,25 +29,57 @@ caveats: Currently, each head storage must be individually added in EVCC. Connecting multiple head storages into one system has been announced by the manufacturer but is not yet implemented on the software side. # link: + - description: + de: | + `maxacpower` und `maxdischargepower` werden aus dem gewählten `model` abgeleitet, wenn sie leer bleiben. + Ein ausdrücklich gesetzter Wert hat Vorrang und wird nicht auf das Modell-Limit begrenzt. + en: | + `maxacpower` and `maxdischargepower` are derived from the selected `model` when left empty. + An explicitly configured value takes precedence and is not clamped to the model limit. params: - name: usage type: choice choice: ["grid", "pv", "battery"] required: true + - name: model + type: choice + choice: ["standard", "pro"] + required: true + description: + de: Modell + en: Model + help: + de: | + `standard` für die SunEnergyXT 500, `pro` für die SunEnergyXT 500 PRO. + Bestimmt die maximale AC- und Entladeleistung (800 W bzw. 2400 W). + en: | + `standard` for the SunEnergyXT 500, `pro` for the SunEnergyXT 500 PRO. + Determines the maximum AC and discharge power (800 W resp. 2400 W). - name: host required: true + # maxacpower/maxdischargepower: leave empty to derive the limit from `model` - name: maxacpower - default: 800.0 - - preset: battery-params + - preset: battery-capacity + - preset: battery-minmaxsoc + # head unit alone; each extension pack adds another 5 kWh - name: capacity - default: 4.8 + default: 5 + help: + de: | + Gesamtkapazität des Speichers: Kopfspeicher 5 kWh, jede Erweiterung +5 kWh. + Also 5 (nur Kopfspeicher), 10 (+1 Erweiterung) oder 15 (+2 Erweiterungen). + Die Anzahl der erkannten Akkupakete steht im Feld `ON` unter `/read`. + en: | + Total storage capacity: head unit 5 kWh, each extension pack adds 5 kWh. + So 5 (head unit only), 10 (+1 extension) or 15 (+2 extensions). + The number of detected battery packs is reported in the `ON` field of `/read`. + # grid charging is limited to -2400..0 W on both models - name: maxchargepower - default: 2400.0 + default: 2400 - name: maxdischargepower - default: 800.0 - name: cache advanced: true - default: 1s + default: 2s - name: timeout advanced: true default: 10s @@ -84,8 +118,10 @@ render: | uri: http://{{ .host }}/write headers: - Content-Type: application/json - body: '{"state":{"MM":1}}' - - case: 2 # hold + # GS must be reset explicitly: MM (zero feed-in regulation) and GS (manual + # setpoint) are independent, so a leftover GS would keep grid charging. + body: '{"state":{"MM":1,"GS":0}}' + - case: 2 # hold set: source: http method: POST @@ -100,8 +136,12 @@ render: | uri: http://{{ .host }}/write headers: - Content-Type: application/json + # GS < 0 means grid import / grid charging body: '{"state":{"MM":0,"GS":-{{ .maxchargepower | int }}}}' - {{- include "battery-params" . }} + {{- include "battery-capacity" . }} + {{- include "battery-minmaxsoc" . }} + maxchargepower: {{ .maxchargepower | int }} # W + maxdischargepower: {{ if gt (.maxdischargepower | int) 0 }}{{ .maxdischargepower | int }}{{ else }}{{ ternary 2400 800 (eq .model "pro") }}{{ end }} # W {{- end }} {{- if eq .usage "pv" }} power: @@ -110,4 +150,5 @@ render: | cache: {{ .cache }} timeout: {{ .timeout }} jq: .state.reported.PV + maxacpower: {{ if gt (.maxacpower | int) 0 }}{{ .maxacpower | int }}{{ else }}{{ ternary 2400 800 (eq .model "pro") }}{{ end }} # W {{- end }} From e772d8413a82631b32ca7fd68157acb6155e9b04 Mon Sep 17 00:00:00 2001 From: andig Date: Sun, 16 Aug 2026 11:10:03 +0200 Subject: [PATCH 002/135] chore: publish the homebrew package as a cask (#32874) --- .goreleaser.yml | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/.goreleaser.yml b/.goreleaser.yml index 56c7630cf..6e8727589 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -126,7 +126,7 @@ nfpms: preremove: ./packaging/scripts/preremove.sh postremove: ./packaging/scripts/postremove.sh -brews: +homebrew_casks: - skip_upload: '{{ if eq (envOrDefault "MAKE_LATEST" "true") "true" }}false{{ else }}true{{ end }}' repository: owner: evcc-io @@ -134,15 +134,14 @@ brews: commit_author: name: andig email: info@evcc.io - directory: Formula homepage: "https://evcc.io" description: "Sonne tanken ☀️🚘" - license: "MIT" - test: | - system "#{bin}/evcc --version" - service: | - run [opt_bin/"evcc"] - working_dir HOMEBREW_PREFIX - keep_alive true - log_path var/"log/evcc.log" - error_log_path var/"log/evcc.log" + binaries: + - evcc + hooks: + post: + # the binaries are not notarized, so gatekeeper would refuse to run them + install: | + if OS.mac? + system_command "/usr/bin/xattr", args: ["-dr", "com.apple.quarantine", "#{staged_path}/evcc"] + end From f0e2dd39d359626ee8d970cb9c36a5b9b38bbc65 Mon Sep 17 00:00:00 2001 From: andig Date: Sun, 16 Aug 2026 11:20:28 +0200 Subject: [PATCH 003/135] chore: fix release notes changelog template (#32877) --- .github/workflows/command-backport.yml | 9 +++------ .goreleaser.yml | 2 -- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/.github/workflows/command-backport.yml b/.github/workflows/command-backport.yml index 3c6d8139f..138a8836a 100644 --- a/.github/workflows/command-backport.yml +++ b/.github/workflows/command-backport.yml @@ -157,18 +157,15 @@ jobs: TITLE: ${{ steps.pr.outputs.title }} ACTOR: ${{ github.event.comment.user.login }} run: | - # the release line the branch was cut from, 0.313 for release/0.313.1 - line=$(echo "${TARGET#release/}" | cut -d. -f1,2) - # the pull request is authored by the deploy token, so assign the # requester. the body stays empty, squashing it would carry the text # into the release branch as the commit description. - # the marker trails the title so the changelog filters and groups still - # see the original prefix, goreleaser strips it from the release notes + # the title stays untouched so the changelog filters and groups still + # see the original prefix, the backport label identifies the pull request url=$(gh pr create \ --base "$TARGET" \ --head "$BRANCH" \ - --title "$TITLE [$line backport]" \ + --title "$TITLE" \ --label backport \ --assignee "$ACTOR" \ --body "") diff --git a/.goreleaser.yml b/.goreleaser.yml index 6e8727589..ff4d59c91 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -71,8 +71,6 @@ snapshot: changelog: sort: asc - # drop the marker the backport command appends to the pull request title - format: '{{ .SHA }} {{ reReplaceAll ` \[\d+\.\d+ backport\]` .Message "" }}' filters: exclude: - "^chore" From d1b1ded0e62ff92101908f5ee36a95bcee59e679 Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Sun, 16 Aug 2026 11:24:40 +0200 Subject: [PATCH 004/135] chore: fix release workflow (#32878) --- .github/workflows/command-backport.yml | 9 +++------ .goreleaser.yml | 2 -- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/.github/workflows/command-backport.yml b/.github/workflows/command-backport.yml index 3c6d8139f..138a8836a 100644 --- a/.github/workflows/command-backport.yml +++ b/.github/workflows/command-backport.yml @@ -157,18 +157,15 @@ jobs: TITLE: ${{ steps.pr.outputs.title }} ACTOR: ${{ github.event.comment.user.login }} run: | - # the release line the branch was cut from, 0.313 for release/0.313.1 - line=$(echo "${TARGET#release/}" | cut -d. -f1,2) - # the pull request is authored by the deploy token, so assign the # requester. the body stays empty, squashing it would carry the text # into the release branch as the commit description. - # the marker trails the title so the changelog filters and groups still - # see the original prefix, goreleaser strips it from the release notes + # the title stays untouched so the changelog filters and groups still + # see the original prefix, the backport label identifies the pull request url=$(gh pr create \ --base "$TARGET" \ --head "$BRANCH" \ - --title "$TITLE [$line backport]" \ + --title "$TITLE" \ --label backport \ --assignee "$ACTOR" \ --body "") diff --git a/.goreleaser.yml b/.goreleaser.yml index 56c7630cf..b21d3bff2 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -71,8 +71,6 @@ snapshot: changelog: sort: asc - # drop the marker the backport command appends to the pull request title - format: '{{ .SHA }} {{ reReplaceAll ` \[\d+\.\d+ backport\]` .Message "" }}' filters: exclude: - "^chore" From 52ab97c939a8dce27524e901858c4992cd192d6c Mon Sep 17 00:00:00 2001 From: andig Date: Sun, 16 Aug 2026 12:15:58 +0200 Subject: [PATCH 005/135] chore: make the restored docker cache mounts writable (#32882) --- .github/actions/docker-cache/action.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/actions/docker-cache/action.yml b/.github/actions/docker-cache/action.yml index 250fecf31..e543a5469 100644 --- a/.github/actions/docker-cache/action.yml +++ b/.github/actions/docker-cache/action.yml @@ -32,6 +32,12 @@ runs: ${{ runner.os }}-docker-mounts-${{ inputs.cache-scope }}- ${{ runner.os }}-docker-mounts-main- + # the go module cache restores read-only, which fails the dance's cleanup + # of the source directory after injection + - name: Make cache mounts writable + shell: bash + run: test -d .docker-cache && chmod -R u+w .docker-cache || true + - name: Inject cache mounts uses: reproducible-containers/buildkit-cache-dance@5422eac04292c961a382e0f584ea0f03ad9da723 # v3.4.0 with: From 3c25327f7ec6d64dc24eade250fb9d43dea56f2f Mon Sep 17 00:00:00 2001 From: andig Date: Sun, 16 Aug 2026 12:16:36 +0200 Subject: [PATCH 006/135] chore: build the release and nightly images on depot (#32883) --- .github/workflows/command-nightly.yml | 1 + .github/workflows/nightly-docker.yml | 16 +++++++--------- .github/workflows/nightly.yml | 1 + .github/workflows/release.yml | 16 +++++++--------- 4 files changed, 16 insertions(+), 18 deletions(-) diff --git a/.github/workflows/command-nightly.yml b/.github/workflows/command-nightly.yml index c3f2e1dc7..d0b1aee23 100644 --- a/.github/workflows/command-nightly.yml +++ b/.github/workflows/command-nightly.yml @@ -60,6 +60,7 @@ jobs: permissions: contents: read actions: read + id-token: write # depot OIDC secrets: inherit report: diff --git a/.github/workflows/nightly-docker.yml b/.github/workflows/nightly-docker.yml index 9db513a49..4800fe0af 100644 --- a/.github/workflows/nightly-docker.yml +++ b/.github/workflows/nightly-docker.yml @@ -13,6 +13,7 @@ jobs: permissions: contents: read + id-token: write # depot OIDC steps: - uses: actions/checkout@v7 @@ -27,14 +28,10 @@ jobs: username: ${{ secrets.DOCKER_USER }} password: ${{ secrets.DOCKER_PASS }} - - name: Setup Buildx - id: buildx - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4 - - - name: Restore build cache - uses: ./.github/actions/docker-cache - with: - builder: ${{ steps.buildx.outputs.name }} + # depot builders keep the Dockerfile's cache mounts on their own volumes, + # so they need neither buildx nor the cache dance + - name: Setup Depot + uses: depot/setup-action@15c09a5f77a0840ad4bce955686522a257853461 # v1.7.1 - name: Define tags id: meta @@ -46,8 +43,9 @@ jobs: type=raw,value=nightly.{{date 'YYYYMMDD'}}-{{sha}} - name: Publish - uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7 + uses: depot/build-push-action@98e78adca7817480b8185f474a400b451d74e287 # v1.18.0 with: + project: ${{ secrets.DEPOT_PROJECT_ID }} context: . platforms: linux/amd64,linux/arm64,linux/arm/v6 push: true diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 3aaacc9f1..2bdbebabd 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -48,6 +48,7 @@ jobs: permissions: contents: read actions: read + id-token: write # depot OIDC secrets: inherit hassio: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 622d37156..09c2fd088 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -46,6 +46,7 @@ jobs: runs-on: depot-ubuntu-24.04-arm permissions: contents: read + id-token: write # depot OIDC steps: - uses: actions/checkout@v7 @@ -59,14 +60,10 @@ jobs: username: ${{ secrets.DOCKER_USER }} password: ${{ secrets.DOCKER_PASS }} - - name: Setup Buildx - id: buildx - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4 - - - name: Restore build cache - uses: ./.github/actions/docker-cache - with: - builder: ${{ steps.buildx.outputs.name }} + # depot builders keep the Dockerfile's cache mounts on their own volumes, + # so they need neither buildx nor the cache dance + - name: Setup Depot + uses: depot/setup-action@15c09a5f77a0840ad4bce955686522a257853461 # v1.7.1 - name: Meta id: meta @@ -79,8 +76,9 @@ jobs: latest=${{ needs.guard.outputs.latest }} - name: Publish - uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7 + uses: depot/build-push-action@98e78adca7817480b8185f474a400b451d74e287 # v1.18.0 with: + project: ${{ secrets.DEPOT_PROJECT_ID }} context: . platforms: linux/amd64,linux/arm64,linux/arm/v6 push: true From eec0a8f46cf964f10e6e5d0d4dd8ab5c325e7213 Mon Sep 17 00:00:00 2001 From: andig Date: Sun, 16 Aug 2026 13:00:39 +0200 Subject: [PATCH 007/135] GoodWe: list ESA series under hybrid template (#32884) --- templates/definition/meter/goodwe-hybrid.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/definition/meter/goodwe-hybrid.yaml b/templates/definition/meter/goodwe-hybrid.yaml index 9055336f4..7ea3e48ab 100644 --- a/templates/definition/meter/goodwe-hybrid.yaml +++ b/templates/definition/meter/goodwe-hybrid.yaml @@ -2,7 +2,7 @@ template: goodwe-hybrid products: - brand: GoodWe description: - generic: ET/EH/BH/BT Hybrid Inverter + generic: ET/EH/BH/BT/ESA Hybrid Inverter capabilities: ["battery-control", "curtail"] params: - name: usage From 24498ad1f6c2d480af555f561de03b664c1d6c37 Mon Sep 17 00:00:00 2001 From: andig Date: Sun, 16 Aug 2026 13:26:51 +0200 Subject: [PATCH 008/135] Modbus templates: share block reads (#32887) --- .../definition/meter/anker-solix-x1.yaml | 30 +++++++++++++++++++ templates/definition/meter/atmoce.yaml | 18 +++++++++++ .../definition/meter/fox-ess-avocado.yaml | 18 +++++++++++ .../definition/meter/fox-ess-h3-smart.yaml | 18 +++++++++++ .../definition/meter/siemens-7kt1665.yaml | 18 +++++++++++ templates/definition/meter/sma-hybrid.yaml | 18 +++++++++++ templates/definition/meter/storaxe.yaml | 18 +++++++++++ .../definition/meter/victron-energy.yaml | 18 +++++++++++ templates/definition/meter/victron-gx.yaml | 27 +++++++++++++++++ 9 files changed, 183 insertions(+) diff --git a/templates/definition/meter/anker-solix-x1.yaml b/templates/definition/meter/anker-solix-x1.yaml index 49458e0ea..479cbbbdd 100644 --- a/templates/definition/meter/anker-solix-x1.yaml +++ b/templates/definition/meter/anker-solix-x1.yaml @@ -32,6 +32,9 @@ render: | address: 10644 # Meter Total Active Power (W, positive = import, negative = export) type: input decode: int32s + block: # 10632-10645 + register: 10632 + count: 14 energy: source: modbus {{- include "modbus" . | indent 2 }} @@ -47,18 +50,27 @@ render: | address: 10638 # Meter Phase A Active Power type: input decode: int32s + block: # 10632-10645 + register: 10632 + count: 14 - source: modbus {{- include "modbus" . | indent 4 }} register: address: 10640 # Meter Phase B Active Power type: input decode: int32s + block: # 10632-10645 + register: 10632 + count: 14 - source: modbus {{- include "modbus" . | indent 4 }} register: address: 10642 # Meter Phase C Active Power type: input decode: int32s + block: # 10632-10645 + register: 10632 + count: 14 currents: - source: modbus {{- include "modbus" . | indent 4 }} @@ -66,6 +78,9 @@ render: | address: 10635 # Meter Phase A Current type: input decode: uint16 + block: # 10632-10645 + register: 10632 + count: 14 scale: 0.01 - source: modbus {{- include "modbus" . | indent 4 }} @@ -73,6 +88,9 @@ render: | address: 10636 # Meter Phase B Current type: input decode: uint16 + block: # 10632-10645 + register: 10632 + count: 14 scale: 0.01 - source: modbus {{- include "modbus" . | indent 4 }} @@ -80,6 +98,9 @@ render: | address: 10637 # Meter Phase C Current type: input decode: uint16 + block: # 10632-10645 + register: 10632 + count: 14 scale: 0.01 voltages: - source: modbus @@ -88,6 +109,9 @@ render: | address: 10632 # Meter Phase A Voltage type: input decode: uint16 + block: # 10632-10645 + register: 10632 + count: 14 scale: 0.1 - source: modbus {{- include "modbus" . | indent 4 }} @@ -95,6 +119,9 @@ render: | address: 10633 # Meter Phase B Voltage type: input decode: uint16 + block: # 10632-10645 + register: 10632 + count: 14 scale: 0.1 - source: modbus {{- include "modbus" . | indent 4 }} @@ -102,6 +129,9 @@ render: | address: 10634 # Meter Phase C Voltage type: input decode: uint16 + block: # 10632-10645 + register: 10632 + count: 14 scale: 0.1 {{- end }} {{- if eq .usage "pv" }} diff --git a/templates/definition/meter/atmoce.yaml b/templates/definition/meter/atmoce.yaml index 6a15ddfce..18cc6dd7b 100644 --- a/templates/definition/meter/atmoce.yaml +++ b/templates/definition/meter/atmoce.yaml @@ -84,6 +84,9 @@ render: | address: 60090 # Phase A Grid Current (A * 100) type: holding decode: int16 + block: # 60089-60094 + register: 60089 + count: 6 scale: 0.01 - source: modbus {{- include "modbus" . | indent 2 }} @@ -91,6 +94,9 @@ render: | address: 60092 # Phase B Grid Current (A * 100) type: holding decode: int16 + block: # 60089-60094 + register: 60089 + count: 6 scale: 0.01 - source: modbus {{- include "modbus" . | indent 2 }} @@ -98,6 +104,9 @@ render: | address: 60094 # Phase C Grid Current (A * 100) type: holding decode: int16 + block: # 60089-60094 + register: 60089 + count: 6 scale: 0.01 voltages: - source: modbus @@ -106,6 +115,9 @@ render: | address: 60089 # Phase A Grid Voltage (V * 10) type: holding decode: uint16 + block: # 60089-60094 + register: 60089 + count: 6 scale: 0.1 - source: modbus {{- include "modbus" . | indent 2 }} @@ -113,6 +125,9 @@ render: | address: 60091 # Phase B Grid Voltage (V * 10) type: holding decode: uint16 + block: # 60089-60094 + register: 60089 + count: 6 scale: 0.1 - source: modbus {{- include "modbus" . | indent 2 }} @@ -120,6 +135,9 @@ render: | address: 60093 # Phase C Grid Voltage (V * 10) type: holding decode: uint16 + block: # 60089-60094 + register: 60089 + count: 6 scale: 0.1 {{- end }} {{- if eq .usage "pv" }} diff --git a/templates/definition/meter/fox-ess-avocado.yaml b/templates/definition/meter/fox-ess-avocado.yaml index a97ef5d19..46c21aaea 100644 --- a/templates/definition/meter/fox-ess-avocado.yaml +++ b/templates/definition/meter/fox-ess-avocado.yaml @@ -60,6 +60,9 @@ render: | address: 39123 type: holding decode: int16 + block: # 39123-39131 + register: 39123 + count: 9 scale: 0.1 - source: modbus {{- include "modbus" . | indent 2 }} @@ -67,6 +70,9 @@ render: | address: 39124 type: holding decode: int16 + block: # 39123-39131 + register: 39123 + count: 9 scale: 0.1 - source: modbus {{- include "modbus" . | indent 2 }} @@ -74,6 +80,9 @@ render: | address: 39125 type: holding decode: int16 + block: # 39123-39131 + register: 39123 + count: 9 scale: 0.1 currents: - source: modbus @@ -82,6 +91,9 @@ render: | address: 39126 type: holding decode: int32 + block: # 39123-39131 + register: 39123 + count: 9 scale: 0.001 - source: modbus {{- include "modbus" . | indent 2 }} @@ -89,6 +101,9 @@ render: | address: 39128 type: holding decode: int32 + block: # 39123-39131 + register: 39123 + count: 9 scale: 0.001 - source: modbus {{- include "modbus" . | indent 2 }} @@ -96,6 +111,9 @@ render: | address: 39130 type: holding decode: int32 + block: # 39123-39131 + register: 39123 + count: 9 scale: 0.001 {{- end }} {{- if eq .usage "pv" }} diff --git a/templates/definition/meter/fox-ess-h3-smart.yaml b/templates/definition/meter/fox-ess-h3-smart.yaml index 2ca4d9123..d422a1f78 100644 --- a/templates/definition/meter/fox-ess-h3-smart.yaml +++ b/templates/definition/meter/fox-ess-h3-smart.yaml @@ -60,36 +60,54 @@ render: | address: 39279 # PV1 type: holding decode: int32 + block: # 39279-39290 + register: 39279 + count: 12 - source: modbus {{- include "modbus" . | indent 4 }} register: address: 39281 # PV2 type: holding decode: int32 + block: # 39279-39290 + register: 39279 + count: 12 - source: modbus {{- include "modbus" . | indent 4 }} register: address: 39283 # PV3 type: holding decode: int32 + block: # 39279-39290 + register: 39279 + count: 12 - source: modbus {{- include "modbus" . | indent 4 }} register: address: 39285 # PV4 type: holding decode: int32 + block: # 39279-39290 + register: 39279 + count: 12 - source: modbus {{- include "modbus" . | indent 4 }} register: address: 39287 # PV5 type: holding decode: int32 + block: # 39279-39290 + register: 39279 + count: 12 - source: modbus {{- include "modbus" . | indent 4 }} register: address: 39289 # PV6 type: holding decode: int32 + block: # 39279-39290 + register: 39279 + count: 12 maxacpower: {{ .maxacpower }} {{- end }} {{- if eq .usage "battery" }} diff --git a/templates/definition/meter/siemens-7kt1665.yaml b/templates/definition/meter/siemens-7kt1665.yaml index b876d1804..63254ef4f 100644 --- a/templates/definition/meter/siemens-7kt1665.yaml +++ b/templates/definition/meter/siemens-7kt1665.yaml @@ -36,6 +36,9 @@ render: | address: 7 type: input decode: uint32 + block: # 1-12 + register: 1 + count: 12 scale: 0.0001 - source: modbus {{- include "modbus" . | indent 2 }} @@ -43,6 +46,9 @@ render: | address: 9 type: input decode: uint32 + block: # 1-12 + register: 1 + count: 12 scale: 0.0001 - source: modbus {{- include "modbus" . | indent 2 }} @@ -50,6 +56,9 @@ render: | address: 11 type: input decode: uint32 + block: # 1-12 + register: 1 + count: 12 scale: 0.0001 voltages: - source: modbus @@ -58,6 +67,9 @@ render: | address: 1 type: input decode: uint32 + block: # 1-12 + register: 1 + count: 12 scale: 0.01 - source: modbus {{- include "modbus" . | indent 2 }} @@ -65,6 +77,9 @@ render: | address: 3 type: input decode: uint32 + block: # 1-12 + register: 1 + count: 12 scale: 0.01 - source: modbus {{- include "modbus" . | indent 2 }} @@ -72,4 +87,7 @@ render: | address: 5 type: input decode: uint32 + block: # 1-12 + register: 1 + count: 12 scale: 0.01 diff --git a/templates/definition/meter/sma-hybrid.yaml b/templates/definition/meter/sma-hybrid.yaml index 1abc11227..999ce4acc 100644 --- a/templates/definition/meter/sma-hybrid.yaml +++ b/templates/definition/meter/sma-hybrid.yaml @@ -88,6 +88,9 @@ render: | address: 31265 # SMA Modbus Profile: Metering.GridMs.WIn.phsA # Power drawn from grid phase L1, unsigned, zero if no consumption type: input decode: uint32nan + block: # 31259-31270 + register: 31259 + count: 12 scale: 1 - source: modbus {{- include "modbus" . | indent 8 }} @@ -95,6 +98,9 @@ render: | address: 31259 # SMA Modbus Profile: Metering.GridMs.W.phsA # Power grid feeding L1, unsigned, zero if no feeding type: input decode: uint32nan + block: # 31259-31270 + register: 31259 + count: 12 scale: -1 - source: calc add: @@ -104,6 +110,9 @@ render: | address: 31267 # SMA Modbus Profile: Metering.GridMs.WIn.phsB # Power drawn from grid phase L2, unsigned, zero if no consumption type: input decode: uint32nan + block: # 31259-31270 + register: 31259 + count: 12 scale: 1 - source: modbus {{- include "modbus" . | indent 8 }} @@ -111,6 +120,9 @@ render: | address: 31261 # SMA Modbus Profile: Metering.GridMs.W.phsB # Power grid feeding L2, unsigned, zero if no feeding type: input decode: uint32nan + block: # 31259-31270 + register: 31259 + count: 12 scale: -1 - source: calc add: @@ -120,6 +132,9 @@ render: | address: 31269 # SMA Modbus Profile: Metering.GridMs.WIn.phsC # Power drawn from grid phase L3, unsigned, zero if no consumption type: input decode: uint32nan + block: # 31259-31270 + register: 31259 + count: 12 scale: 1 - source: modbus {{- include "modbus" . | indent 8 }} @@ -127,6 +142,9 @@ render: | address: 31263 # SMA Modbus Profile: Metering.GridMs.W.phsC # Power grid feeding L3, unsigned, zero if no feeding type: input decode: uint32nan + block: # 31259-31270 + register: 31259 + count: 12 scale: -1 {{- end }} {{- if eq .usage "pv" }} diff --git a/templates/definition/meter/storaxe.yaml b/templates/definition/meter/storaxe.yaml index 50a84d613..9d60cf33f 100644 --- a/templates/definition/meter/storaxe.yaml +++ b/templates/definition/meter/storaxe.yaml @@ -34,6 +34,9 @@ render: | address: 6 # ACVoltageL1 type: input decode: int16 + block: # 6-11 + register: 6 + count: 6 scale: 0.1 - source: modbus {{- include "modbus" . | indent 4 }} @@ -41,6 +44,9 @@ render: | address: 7 # ACVoltageL2 type: input decode: int16 + block: # 6-11 + register: 6 + count: 6 scale: 0.1 - source: modbus {{- include "modbus" . | indent 4 }} @@ -48,6 +54,9 @@ render: | address: 8 # ACVoltageL3 type: input decode: int16 + block: # 6-11 + register: 6 + count: 6 scale: 0.1 currents: - source: modbus @@ -56,6 +65,9 @@ render: | address: 9 # ACCurrentL1 type: input decode: int16 + block: # 6-11 + register: 6 + count: 6 scale: 0.1 - source: modbus {{- include "modbus" . | indent 4 }} @@ -63,6 +75,9 @@ render: | address: 10 # ACCurrentL2 type: input decode: int16 + block: # 6-11 + register: 6 + count: 6 scale: 0.1 - source: modbus {{- include "modbus" . | indent 4 }} @@ -70,6 +85,9 @@ render: | address: 11 # ACCurrentL3 type: input decode: int16 + block: # 6-11 + register: 6 + count: 6 scale: 0.1 soc: source: modbus diff --git a/templates/definition/meter/victron-energy.yaml b/templates/definition/meter/victron-energy.yaml index 4234c1be0..976e15d16 100644 --- a/templates/definition/meter/victron-energy.yaml +++ b/templates/definition/meter/victron-energy.yaml @@ -91,6 +91,9 @@ render: | address: 808 # ACout pv power L1 type: input decode: uint16 + block: # 808-813 + register: 808 + count: 6 - source: modbus uri: {{ joinHostPort .host .port }} id: 100 # com.victronenergy.system @@ -98,6 +101,9 @@ render: | address: 809 # ACout pv power L2 type: input decode: uint16 + block: # 808-813 + register: 808 + count: 6 - source: modbus uri: {{ joinHostPort .host .port }} id: 100 # com.victronenergy.system @@ -105,6 +111,9 @@ render: | address: 810 # ACout pv power L3 type: input decode: uint16 + block: # 808-813 + register: 808 + count: 6 - source: modbus uri: {{ joinHostPort .host .port }} id: 100 # com.victronenergy.system @@ -112,6 +121,9 @@ render: | address: 811 # ACin pv power L1 type: input decode: uint16 + block: # 808-813 + register: 808 + count: 6 - source: modbus uri: {{ joinHostPort .host .port }} id: 100 # com.victronenergy.system @@ -119,6 +131,9 @@ render: | address: 812 # ACin pv power L2 type: input decode: uint16 + block: # 808-813 + register: 808 + count: 6 - source: modbus uri: {{ joinHostPort .host .port }} id: 100 # com.victronenergy.system @@ -126,6 +141,9 @@ render: | address: 813 # ACin pv power L3 type: input decode: uint16 + block: # 808-813 + register: 808 + count: 6 - source: modbus uri: {{ joinHostPort .host .port }} id: 100 # com.victronenergy.system diff --git a/templates/definition/meter/victron-gx.yaml b/templates/definition/meter/victron-gx.yaml index e104d3c58..3763cd8d2 100644 --- a/templates/definition/meter/victron-gx.yaml +++ b/templates/definition/meter/victron-gx.yaml @@ -63,6 +63,9 @@ render: | address: 3916 # L1 energy forward type: input decode: uint32 + block: # 3910-3921 + register: 3910 + count: 12 scale: 0.01 - source: modbus uri: {{ joinHostPort .host .port }} @@ -71,6 +74,9 @@ render: | address: 3918 # L2 energy forward type: input decode: uint32 + block: # 3910-3921 + register: 3910 + count: 12 scale: 0.01 - source: modbus uri: {{ joinHostPort .host .port }} @@ -79,6 +85,9 @@ render: | address: 3920 # L3 energy forward type: input decode: uint32 + block: # 3910-3921 + register: 3910 + count: 12 scale: 0.01 currents: - source: modbus @@ -88,6 +97,9 @@ render: | address: 3911 # L1 current type: input decode: int16 + block: # 3910-3921 + register: 3910 + count: 12 scale: 0.1 - source: modbus uri: {{ joinHostPort .host .port }} @@ -96,6 +108,9 @@ render: | address: 3913 # L2 current type: input decode: int16 + block: # 3910-3921 + register: 3910 + count: 12 scale: 0.1 - source: modbus uri: {{ joinHostPort .host .port }} @@ -104,6 +119,9 @@ render: | address: 3915 # L3 current type: input decode: int16 + block: # 3910-3921 + register: 3910 + count: 12 scale: 0.1 voltages: - source: modbus @@ -113,6 +131,9 @@ render: | address: 3910 # L1 voltage type: input decode: uint16 + block: # 3910-3921 + register: 3910 + count: 12 scale: 0.1 - source: modbus uri: {{ joinHostPort .host .port }} @@ -121,6 +142,9 @@ render: | address: 3912 # L2 voltage type: input decode: uint16 + block: # 3910-3921 + register: 3910 + count: 12 scale: 0.1 - source: modbus uri: {{ joinHostPort .host .port }} @@ -129,4 +153,7 @@ render: | address: 3914 # L3 voltage type: input decode: uint16 + block: # 3910-3921 + register: 3910 + count: 12 scale: 0.1 From bb6854b1edb9be1898e84479becfef40e67ff4e0 Mon Sep 17 00:00:00 2001 From: andig Date: Sun, 16 Aug 2026 17:18:34 +0200 Subject: [PATCH 009/135] Optimizer: respect session energy limit when vehicle soc is unknown (#32895) --- core/loadpoint/api.go | 2 ++ core/loadpoint/mock.go | 14 +++++++++++ core/optimizer.md | 4 ++++ core/site_optimizer.go | 27 ++++++++++++--------- core/site_optimizer_test.go | 48 +++++++++++++++++++++++++++++++++++++ 5 files changed, 84 insertions(+), 11 deletions(-) diff --git a/core/loadpoint/api.go b/core/loadpoint/api.go index ef4b5b05a..93a4dd36a 100644 --- a/core/loadpoint/api.go +++ b/core/loadpoint/api.go @@ -219,6 +219,8 @@ type API interface { GetRemainingDuration() time.Duration // GetRemainingEnergy is the remaining charge energy in kWh GetRemainingEnergy() float64 + // GetChargedEnergy returns session charge energy in Wh + GetChargedEnergy() float64 // // vehicles diff --git a/core/loadpoint/mock.go b/core/loadpoint/mock.go index c156ad5a2..b2509b049 100644 --- a/core/loadpoint/mock.go +++ b/core/loadpoint/mock.go @@ -221,6 +221,20 @@ func (mr *MockAPIMockRecorder) GetChargePowerFlexibility(rates any) *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChargePowerFlexibility", reflect.TypeOf((*MockAPI)(nil).GetChargePowerFlexibility), rates) } +// GetChargedEnergy mocks base method. +func (m *MockAPI) GetChargedEnergy() float64 { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetChargedEnergy") + ret0, _ := ret[0].(float64) + return ret0 +} + +// GetChargedEnergy indicates an expected call of GetChargedEnergy. +func (mr *MockAPIMockRecorder) GetChargedEnergy() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChargedEnergy", reflect.TypeOf((*MockAPI)(nil).GetChargedEnergy)) +} + // GetChargerRef mocks base method. func (m *MockAPI) GetChargerRef() string { m.ctrl.T.Helper() diff --git a/core/optimizer.md b/core/optimizer.md index c792b84cc..f403699a5 100644 --- a/core/optimizer.md +++ b/core/optimizer.md @@ -47,3 +47,7 @@ Use minimum of energy consumption cost. - home battery or loadpoint/vehicle... - capacity, soc and charge goals - charge/discharge power limits and efficiency + +Without vehicle capacity or soc a configured session energy limit is modelled instead: +state is the session's charged energy, goal is the limit. Loadpoints with neither are +not modelled at all- their power is added to the base load. diff --git a/core/site_optimizer.go b/core/site_optimizer.go index 0af96ef38..3952062ba 100644 --- a/core/site_optimizer.go +++ b/core/site_optimizer.go @@ -522,8 +522,9 @@ func (site *Site) optimizerRequest(battery []types.Measurement) (optimizer.Optim continue } - // unknown vehicle capacity: account for the consumption as uncontrollable load - if v := lp.GetVehicle(); v == nil || v.Capacity() == 0 { + // no vehicle capacity and no session energy limit to model against: + // account for the consumption as uncontrollable load + if v := lp.GetVehicle(); v == nil || (v.Capacity() == 0 && lp.GetLimitEnergy() == 0) { unmodelled += unmodelledPower(lp) continue } @@ -790,18 +791,22 @@ func (site *Site) loadpointRequest(lp loadpoint.API, minLen int, firstSlotDurati // vehicle v := lp.GetVehicle() - maxSoc := v.Capacity() * 1e3 // Wh - if v := lp.EffectiveLimitSoc(); v > 0 { - maxSoc *= float64(v) / 100 - } else if v := lp.GetLimitEnergy(); v > 0 { - maxSoc = v * 1e3 + capacity := v.Capacity() // kWh + soc := lp.GetSoc() // percent + + // without capacity or soc there is no battery state to model, but a session energy + // limit still bounds the charge- use charged energy as state (see remainingLimitEnergy) + if limit := lp.GetLimitEnergy(); limit > 0 && (capacity == 0 || soc == 0) { + bat.SInitial = float32(lp.GetChargedEnergy()) // Wh + bat.SMax = max(bat.SInitial, float32(limit*1e3)) // prevent infeasible if limit already exceeded + } else { + maxSoc := capacity * float64(lp.EffectiveLimitSoc()) * 10 // Wh + bat.SInitial = float32(capacity * soc * 10) // Wh + bat.SMax = max(bat.SInitial, float32(maxSoc)) // prevent infeasible if current soc above maximum } - bat.SInitial = float32(v.Capacity() * lp.GetSoc() * 10) // Wh - bat.SMax = max(bat.SInitial, float32(maxSoc)) // prevent infeasible if current soc above maximum - detail.Type = batteryTypeVehicle - detail.Capacity = v.Capacity() + detail.Capacity = capacity if vt := v.GetTitle(); vt != "" { if detail.Title != "" { diff --git a/core/site_optimizer_test.go b/core/site_optimizer_test.go index 634e90ebe..446e8889c 100644 --- a/core/site_optimizer_test.go +++ b/core/site_optimizer_test.go @@ -320,6 +320,54 @@ func TestBatteryRequestSocLimitsClamp(t *testing.T) { }) } +// charge goal for vehicles with and without known capacity/soc, see #32890 +func TestLoadpointRequestChargeGoal(t *testing.T) { + site := &Site{log: util.NewLogger("foo")} + + for _, tc := range []struct { + name string + capacity, soc float64 // kWh, percent + limitSoc int // percent + limitEnergy, charged float64 // kWh, Wh + wantInitial, wantSMax float32 // Wh + }{ + {"soc limit", 50, 20, 80, 0, 0, 10000, 40000}, + {"no capacity, energy limit", 0, 0, 100, 10, 0, 0, 10000}, + {"no capacity, energy limit partially charged", 0, 0, 100, 10, 4000, 4000, 10000}, + {"no capacity, limit exceeded", 0, 0, 100, 10, 11000, 11000, 11000}, + {"capacity but no soc, energy limit", 50, 0, 100, 10, 0, 0, 10000}, + {"capacity but no soc, no energy limit", 50, 0, 100, 0, 0, 0, 50000}, + } { + t.Run(tc.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + + v := api.NewMockVehicle(ctrl) + v.EXPECT().Capacity().Return(tc.capacity).AnyTimes() + v.EXPECT().GetTitle().Return("").AnyTimes() + + lp := loadpoint.NewMockAPI(ctrl) + lp.EXPECT().GetVehicle().Return(v).AnyTimes() + lp.EXPECT().GetSoc().Return(tc.soc).AnyTimes() + lp.EXPECT().EffectiveLimitSoc().Return(tc.limitSoc).AnyTimes() + lp.EXPECT().GetLimitEnergy().Return(tc.limitEnergy).AnyTimes() + lp.EXPECT().GetChargedEnergy().Return(tc.charged).AnyTimes() + lp.EXPECT().GetTitle().Return("lp").AnyTimes() + lp.EXPECT().EffectiveMinPower().Return(1380.0).AnyTimes() + lp.EXPECT().EffectiveMaxPower().Return(11000.0).AnyTimes() + lp.EXPECT().GetMode().Return(api.ModePV).AnyTimes() + lp.EXPECT().GetStatus().Return(api.StatusB).AnyTimes() + lp.EXPECT().GetSmartCostLimit().Return(nil).AnyTimes() + lp.EXPECT().EffectivePlanStrategy().Return(api.PlanStrategy{}).AnyTimes() + lp.EXPECT().GetPlanGoal().Return(0.0, false).AnyTimes() + + req, _ := site.loadpointRequest(lp, 8, 15*time.Minute, nil) + + assert.Equal(t, tc.wantInitial, req.SInitial) + assert.Equal(t, tc.wantSMax, req.SMax) + }) + } +} + func TestOptimizerChargingStrategy(t *testing.T) { site := &Site{log: util.NewLogger("foo")} From dfad373ab30762049185dbee52af42e3c15e6a05 Mon Sep 17 00:00:00 2001 From: Wolle141 <301343094+Wolle141@users.noreply.github.com> Date: Sun, 16 Aug 2026 18:22:31 +0200 Subject: [PATCH 010/135] Add CHINT ECH10K-TH-EU inverter (#32858) --- .../definition/meter/chint-ech-hybrid.yaml | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 templates/definition/meter/chint-ech-hybrid.yaml diff --git a/templates/definition/meter/chint-ech-hybrid.yaml b/templates/definition/meter/chint-ech-hybrid.yaml new file mode 100644 index 000000000..550bc116a --- /dev/null +++ b/templates/definition/meter/chint-ech-hybrid.yaml @@ -0,0 +1,93 @@ +template: chint-ech-hybrid + +products: + - brand: CHINT + description: + generic: ECH10K-TH-EU Hybrid Inverter + +requirements: + description: + de: Getestet mit CHINT ECH10K-TH-EU und CPS ECD500 Batteriesystem über die EMS-RS485-Schnittstelle. + en: Tested with CHINT ECH10K-TH-EU and CPS ECD500 battery system via the EMS RS485 interface. + +params: + - name: usage + choice: ["grid", "pv", "battery"] + - name: modbus + choice: ["rs485"] + baudrate: 9600 + id: 1 + +render: | + type: custom + + {{- if eq .usage "grid" }} + power: + source: modbus + {{- include "modbus" . | indent 2 }} + register: + type: input + address: 1792 # 0x0700 Total grid power, 1 W + decode: int32 + currents: + - source: modbus + {{- include "modbus" . | indent 4 }} + register: + type: input + address: 2095 # 0x082F L1 current, 0.01 A + decode: int16 + scale: 0.01 + - source: modbus + {{- include "modbus" . | indent 4 }} + register: + type: input + address: 2096 # 0x0830 L2 current, 0.01 A + decode: int16 + scale: 0.01 + - source: modbus + {{- include "modbus" . | indent 4 }} + register: + type: input + address: 2097 # 0x0831 L3 current, 0.01 A + decode: int16 + scale: 0.01 + {{- end }} + + {{- if eq .usage "pv" }} + power: + source: modbus + {{- include "modbus" . | indent 2 }} + register: + type: input + address: 1801 # 0x0709 PV power, 1 W + decode: uint16 + {{- end }} + + {{- if eq .usage "battery" }} + power: + source: calc + mul: + - source: modbus + {{- include "modbus" . | indent 4 }} + register: + type: input + address: 1536 # 0x0600 Battery voltage, 0.1 V + decode: uint16 + scale: 0.1 + + - source: modbus + {{- include "modbus" . | indent 4 }} + register: + type: input + address: 1537 # 0x0601 Battery current, 0.1 A + decode: int16 + scale: 0.1 + + soc: + source: modbus + {{- include "modbus" . | indent 2 }} + register: + type: input + address: 1539 # 0x0603 Battery SOC, % + decode: uint16 + {{- end }} From fa6f757618556cfad5abffdd16ef8519d5d42d13 Mon Sep 17 00:00:00 2001 From: andig Date: Sun, 16 Aug 2026 18:24:18 +0200 Subject: [PATCH 011/135] Revert "Optimizer: respect session energy limit when vehicle soc is unknown (#32895)" This reverts commit bb6854b1edb9be1898e84479becfef40e67ff4e0. --- core/loadpoint/api.go | 2 -- core/loadpoint/mock.go | 14 ----------- core/optimizer.md | 4 ---- core/site_optimizer.go | 27 +++++++++------------ core/site_optimizer_test.go | 48 ------------------------------------- 5 files changed, 11 insertions(+), 84 deletions(-) diff --git a/core/loadpoint/api.go b/core/loadpoint/api.go index 93a4dd36a..ef4b5b05a 100644 --- a/core/loadpoint/api.go +++ b/core/loadpoint/api.go @@ -219,8 +219,6 @@ type API interface { GetRemainingDuration() time.Duration // GetRemainingEnergy is the remaining charge energy in kWh GetRemainingEnergy() float64 - // GetChargedEnergy returns session charge energy in Wh - GetChargedEnergy() float64 // // vehicles diff --git a/core/loadpoint/mock.go b/core/loadpoint/mock.go index b2509b049..c156ad5a2 100644 --- a/core/loadpoint/mock.go +++ b/core/loadpoint/mock.go @@ -221,20 +221,6 @@ func (mr *MockAPIMockRecorder) GetChargePowerFlexibility(rates any) *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChargePowerFlexibility", reflect.TypeOf((*MockAPI)(nil).GetChargePowerFlexibility), rates) } -// GetChargedEnergy mocks base method. -func (m *MockAPI) GetChargedEnergy() float64 { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetChargedEnergy") - ret0, _ := ret[0].(float64) - return ret0 -} - -// GetChargedEnergy indicates an expected call of GetChargedEnergy. -func (mr *MockAPIMockRecorder) GetChargedEnergy() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChargedEnergy", reflect.TypeOf((*MockAPI)(nil).GetChargedEnergy)) -} - // GetChargerRef mocks base method. func (m *MockAPI) GetChargerRef() string { m.ctrl.T.Helper() diff --git a/core/optimizer.md b/core/optimizer.md index f403699a5..c792b84cc 100644 --- a/core/optimizer.md +++ b/core/optimizer.md @@ -47,7 +47,3 @@ Use minimum of energy consumption cost. - home battery or loadpoint/vehicle... - capacity, soc and charge goals - charge/discharge power limits and efficiency - -Without vehicle capacity or soc a configured session energy limit is modelled instead: -state is the session's charged energy, goal is the limit. Loadpoints with neither are -not modelled at all- their power is added to the base load. diff --git a/core/site_optimizer.go b/core/site_optimizer.go index 3952062ba..0af96ef38 100644 --- a/core/site_optimizer.go +++ b/core/site_optimizer.go @@ -522,9 +522,8 @@ func (site *Site) optimizerRequest(battery []types.Measurement) (optimizer.Optim continue } - // no vehicle capacity and no session energy limit to model against: - // account for the consumption as uncontrollable load - if v := lp.GetVehicle(); v == nil || (v.Capacity() == 0 && lp.GetLimitEnergy() == 0) { + // unknown vehicle capacity: account for the consumption as uncontrollable load + if v := lp.GetVehicle(); v == nil || v.Capacity() == 0 { unmodelled += unmodelledPower(lp) continue } @@ -791,22 +790,18 @@ func (site *Site) loadpointRequest(lp loadpoint.API, minLen int, firstSlotDurati // vehicle v := lp.GetVehicle() - capacity := v.Capacity() // kWh - soc := lp.GetSoc() // percent - - // without capacity or soc there is no battery state to model, but a session energy - // limit still bounds the charge- use charged energy as state (see remainingLimitEnergy) - if limit := lp.GetLimitEnergy(); limit > 0 && (capacity == 0 || soc == 0) { - bat.SInitial = float32(lp.GetChargedEnergy()) // Wh - bat.SMax = max(bat.SInitial, float32(limit*1e3)) // prevent infeasible if limit already exceeded - } else { - maxSoc := capacity * float64(lp.EffectiveLimitSoc()) * 10 // Wh - bat.SInitial = float32(capacity * soc * 10) // Wh - bat.SMax = max(bat.SInitial, float32(maxSoc)) // prevent infeasible if current soc above maximum + maxSoc := v.Capacity() * 1e3 // Wh + if v := lp.EffectiveLimitSoc(); v > 0 { + maxSoc *= float64(v) / 100 + } else if v := lp.GetLimitEnergy(); v > 0 { + maxSoc = v * 1e3 } + bat.SInitial = float32(v.Capacity() * lp.GetSoc() * 10) // Wh + bat.SMax = max(bat.SInitial, float32(maxSoc)) // prevent infeasible if current soc above maximum + detail.Type = batteryTypeVehicle - detail.Capacity = capacity + detail.Capacity = v.Capacity() if vt := v.GetTitle(); vt != "" { if detail.Title != "" { diff --git a/core/site_optimizer_test.go b/core/site_optimizer_test.go index 446e8889c..634e90ebe 100644 --- a/core/site_optimizer_test.go +++ b/core/site_optimizer_test.go @@ -320,54 +320,6 @@ func TestBatteryRequestSocLimitsClamp(t *testing.T) { }) } -// charge goal for vehicles with and without known capacity/soc, see #32890 -func TestLoadpointRequestChargeGoal(t *testing.T) { - site := &Site{log: util.NewLogger("foo")} - - for _, tc := range []struct { - name string - capacity, soc float64 // kWh, percent - limitSoc int // percent - limitEnergy, charged float64 // kWh, Wh - wantInitial, wantSMax float32 // Wh - }{ - {"soc limit", 50, 20, 80, 0, 0, 10000, 40000}, - {"no capacity, energy limit", 0, 0, 100, 10, 0, 0, 10000}, - {"no capacity, energy limit partially charged", 0, 0, 100, 10, 4000, 4000, 10000}, - {"no capacity, limit exceeded", 0, 0, 100, 10, 11000, 11000, 11000}, - {"capacity but no soc, energy limit", 50, 0, 100, 10, 0, 0, 10000}, - {"capacity but no soc, no energy limit", 50, 0, 100, 0, 0, 0, 50000}, - } { - t.Run(tc.name, func(t *testing.T) { - ctrl := gomock.NewController(t) - - v := api.NewMockVehicle(ctrl) - v.EXPECT().Capacity().Return(tc.capacity).AnyTimes() - v.EXPECT().GetTitle().Return("").AnyTimes() - - lp := loadpoint.NewMockAPI(ctrl) - lp.EXPECT().GetVehicle().Return(v).AnyTimes() - lp.EXPECT().GetSoc().Return(tc.soc).AnyTimes() - lp.EXPECT().EffectiveLimitSoc().Return(tc.limitSoc).AnyTimes() - lp.EXPECT().GetLimitEnergy().Return(tc.limitEnergy).AnyTimes() - lp.EXPECT().GetChargedEnergy().Return(tc.charged).AnyTimes() - lp.EXPECT().GetTitle().Return("lp").AnyTimes() - lp.EXPECT().EffectiveMinPower().Return(1380.0).AnyTimes() - lp.EXPECT().EffectiveMaxPower().Return(11000.0).AnyTimes() - lp.EXPECT().GetMode().Return(api.ModePV).AnyTimes() - lp.EXPECT().GetStatus().Return(api.StatusB).AnyTimes() - lp.EXPECT().GetSmartCostLimit().Return(nil).AnyTimes() - lp.EXPECT().EffectivePlanStrategy().Return(api.PlanStrategy{}).AnyTimes() - lp.EXPECT().GetPlanGoal().Return(0.0, false).AnyTimes() - - req, _ := site.loadpointRequest(lp, 8, 15*time.Minute, nil) - - assert.Equal(t, tc.wantInitial, req.SInitial) - assert.Equal(t, tc.wantSMax, req.SMax) - }) - } -} - func TestOptimizerChargingStrategy(t *testing.T) { site := &Site{log: util.NewLogger("foo")} From d22b67fd183930d3c4654fbd3990dd83feafac01 Mon Sep 17 00:00:00 2001 From: andig Date: Sun, 16 Aug 2026 19:32:33 +0200 Subject: [PATCH 012/135] Reapply "Optimizer: respect session energy limit when vehicle soc is unknown (#32895)" This reverts commit fa6f757618556cfad5abffdd16ef8519d5d42d13. --- core/loadpoint/api.go | 2 ++ core/loadpoint/mock.go | 14 +++++++++++ core/optimizer.md | 4 ++++ core/site_optimizer.go | 27 ++++++++++++--------- core/site_optimizer_test.go | 48 +++++++++++++++++++++++++++++++++++++ 5 files changed, 84 insertions(+), 11 deletions(-) diff --git a/core/loadpoint/api.go b/core/loadpoint/api.go index ef4b5b05a..93a4dd36a 100644 --- a/core/loadpoint/api.go +++ b/core/loadpoint/api.go @@ -219,6 +219,8 @@ type API interface { GetRemainingDuration() time.Duration // GetRemainingEnergy is the remaining charge energy in kWh GetRemainingEnergy() float64 + // GetChargedEnergy returns session charge energy in Wh + GetChargedEnergy() float64 // // vehicles diff --git a/core/loadpoint/mock.go b/core/loadpoint/mock.go index c156ad5a2..b2509b049 100644 --- a/core/loadpoint/mock.go +++ b/core/loadpoint/mock.go @@ -221,6 +221,20 @@ func (mr *MockAPIMockRecorder) GetChargePowerFlexibility(rates any) *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChargePowerFlexibility", reflect.TypeOf((*MockAPI)(nil).GetChargePowerFlexibility), rates) } +// GetChargedEnergy mocks base method. +func (m *MockAPI) GetChargedEnergy() float64 { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetChargedEnergy") + ret0, _ := ret[0].(float64) + return ret0 +} + +// GetChargedEnergy indicates an expected call of GetChargedEnergy. +func (mr *MockAPIMockRecorder) GetChargedEnergy() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChargedEnergy", reflect.TypeOf((*MockAPI)(nil).GetChargedEnergy)) +} + // GetChargerRef mocks base method. func (m *MockAPI) GetChargerRef() string { m.ctrl.T.Helper() diff --git a/core/optimizer.md b/core/optimizer.md index c792b84cc..f403699a5 100644 --- a/core/optimizer.md +++ b/core/optimizer.md @@ -47,3 +47,7 @@ Use minimum of energy consumption cost. - home battery or loadpoint/vehicle... - capacity, soc and charge goals - charge/discharge power limits and efficiency + +Without vehicle capacity or soc a configured session energy limit is modelled instead: +state is the session's charged energy, goal is the limit. Loadpoints with neither are +not modelled at all- their power is added to the base load. diff --git a/core/site_optimizer.go b/core/site_optimizer.go index 0af96ef38..3952062ba 100644 --- a/core/site_optimizer.go +++ b/core/site_optimizer.go @@ -522,8 +522,9 @@ func (site *Site) optimizerRequest(battery []types.Measurement) (optimizer.Optim continue } - // unknown vehicle capacity: account for the consumption as uncontrollable load - if v := lp.GetVehicle(); v == nil || v.Capacity() == 0 { + // no vehicle capacity and no session energy limit to model against: + // account for the consumption as uncontrollable load + if v := lp.GetVehicle(); v == nil || (v.Capacity() == 0 && lp.GetLimitEnergy() == 0) { unmodelled += unmodelledPower(lp) continue } @@ -790,18 +791,22 @@ func (site *Site) loadpointRequest(lp loadpoint.API, minLen int, firstSlotDurati // vehicle v := lp.GetVehicle() - maxSoc := v.Capacity() * 1e3 // Wh - if v := lp.EffectiveLimitSoc(); v > 0 { - maxSoc *= float64(v) / 100 - } else if v := lp.GetLimitEnergy(); v > 0 { - maxSoc = v * 1e3 + capacity := v.Capacity() // kWh + soc := lp.GetSoc() // percent + + // without capacity or soc there is no battery state to model, but a session energy + // limit still bounds the charge- use charged energy as state (see remainingLimitEnergy) + if limit := lp.GetLimitEnergy(); limit > 0 && (capacity == 0 || soc == 0) { + bat.SInitial = float32(lp.GetChargedEnergy()) // Wh + bat.SMax = max(bat.SInitial, float32(limit*1e3)) // prevent infeasible if limit already exceeded + } else { + maxSoc := capacity * float64(lp.EffectiveLimitSoc()) * 10 // Wh + bat.SInitial = float32(capacity * soc * 10) // Wh + bat.SMax = max(bat.SInitial, float32(maxSoc)) // prevent infeasible if current soc above maximum } - bat.SInitial = float32(v.Capacity() * lp.GetSoc() * 10) // Wh - bat.SMax = max(bat.SInitial, float32(maxSoc)) // prevent infeasible if current soc above maximum - detail.Type = batteryTypeVehicle - detail.Capacity = v.Capacity() + detail.Capacity = capacity if vt := v.GetTitle(); vt != "" { if detail.Title != "" { diff --git a/core/site_optimizer_test.go b/core/site_optimizer_test.go index 634e90ebe..446e8889c 100644 --- a/core/site_optimizer_test.go +++ b/core/site_optimizer_test.go @@ -320,6 +320,54 @@ func TestBatteryRequestSocLimitsClamp(t *testing.T) { }) } +// charge goal for vehicles with and without known capacity/soc, see #32890 +func TestLoadpointRequestChargeGoal(t *testing.T) { + site := &Site{log: util.NewLogger("foo")} + + for _, tc := range []struct { + name string + capacity, soc float64 // kWh, percent + limitSoc int // percent + limitEnergy, charged float64 // kWh, Wh + wantInitial, wantSMax float32 // Wh + }{ + {"soc limit", 50, 20, 80, 0, 0, 10000, 40000}, + {"no capacity, energy limit", 0, 0, 100, 10, 0, 0, 10000}, + {"no capacity, energy limit partially charged", 0, 0, 100, 10, 4000, 4000, 10000}, + {"no capacity, limit exceeded", 0, 0, 100, 10, 11000, 11000, 11000}, + {"capacity but no soc, energy limit", 50, 0, 100, 10, 0, 0, 10000}, + {"capacity but no soc, no energy limit", 50, 0, 100, 0, 0, 0, 50000}, + } { + t.Run(tc.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + + v := api.NewMockVehicle(ctrl) + v.EXPECT().Capacity().Return(tc.capacity).AnyTimes() + v.EXPECT().GetTitle().Return("").AnyTimes() + + lp := loadpoint.NewMockAPI(ctrl) + lp.EXPECT().GetVehicle().Return(v).AnyTimes() + lp.EXPECT().GetSoc().Return(tc.soc).AnyTimes() + lp.EXPECT().EffectiveLimitSoc().Return(tc.limitSoc).AnyTimes() + lp.EXPECT().GetLimitEnergy().Return(tc.limitEnergy).AnyTimes() + lp.EXPECT().GetChargedEnergy().Return(tc.charged).AnyTimes() + lp.EXPECT().GetTitle().Return("lp").AnyTimes() + lp.EXPECT().EffectiveMinPower().Return(1380.0).AnyTimes() + lp.EXPECT().EffectiveMaxPower().Return(11000.0).AnyTimes() + lp.EXPECT().GetMode().Return(api.ModePV).AnyTimes() + lp.EXPECT().GetStatus().Return(api.StatusB).AnyTimes() + lp.EXPECT().GetSmartCostLimit().Return(nil).AnyTimes() + lp.EXPECT().EffectivePlanStrategy().Return(api.PlanStrategy{}).AnyTimes() + lp.EXPECT().GetPlanGoal().Return(0.0, false).AnyTimes() + + req, _ := site.loadpointRequest(lp, 8, 15*time.Minute, nil) + + assert.Equal(t, tc.wantInitial, req.SInitial) + assert.Equal(t, tc.wantSMax, req.SMax) + }) + } +} + func TestOptimizerChargingStrategy(t *testing.T) { site := &Site{log: util.NewLogger("foo")} From ba8596be961d6495dde98cb692e19a27d5583ab3 Mon Sep 17 00:00:00 2001 From: andig Date: Sun, 16 Aug 2026 19:35:44 +0200 Subject: [PATCH 013/135] Optimizer chart: day boundaries with weekday at 00:00 (#32894) --- assets/js/components/Optimize/ChargeChart.vue | 2 +- assets/js/components/Optimize/chart.test.ts | 35 +++++++++++++++++++ assets/js/components/Optimize/chart.ts | 22 +++++++++--- 3 files changed, 54 insertions(+), 5 deletions(-) create mode 100644 assets/js/components/Optimize/chart.test.ts diff --git a/assets/js/components/Optimize/ChargeChart.vue b/assets/js/components/Optimize/ChargeChart.vue index 1b89cb910..ad03867d9 100644 --- a/assets/js/components/Optimize/ChargeChart.vue +++ b/assets/js/components/Optimize/ChargeChart.vue @@ -146,7 +146,7 @@ export default defineComponent({ ...tooltipStyle(colors.text || ""), formatter: this.tooltipFormatter, }, - xAxis: slotXAxis(this.times), + xAxis: slotXAxis(this.times, this.weekdayShort), yAxis: forecastYAxis({ min: undefined, position: "right", diff --git a/assets/js/components/Optimize/chart.test.ts b/assets/js/components/Optimize/chart.test.ts new file mode 100644 index 000000000..cca07af43 --- /dev/null +++ b/assets/js/components/Optimize/chart.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, test } from "vite-plus/test"; +import { isMidnight, slotXAxis } from "./chart"; + +// 15min slots starting at the given local hour +function times(startHour: number, count: number): number[] { + const start = new Date(2025, 0, 1, startHour).getTime(); + return Array.from({ length: count }, (_, i) => start + i * 15 * 60 * 1000); +} + +describe("slotXAxis", () => { + const weekdayShort = () => "Thu"; + // 20:00 to 08:00, midnight at index 16 + const slots = times(20, 48); + const axis = slotXAxis(slots, weekdayShort); + const label = (i: number) => axis.axisLabel.formatter(String(slots[i])); + + test("labels full hours only", () => { + expect(label(0)).toBe("20"); // 20:00 + expect(label(1)).toBe(""); // 20:15 + expect(label(4)).toBe(""); // 21:00, not a step hour + expect(label(32)).toBe("4"); // 04:00 + }); + + test("shows the weekday at midnight", () => { + expect(label(16)).toBe("0\nThu"); + }); + + test("draws a split line at day boundaries only", () => { + expect(isMidnight(slots[16])).toBe(true); + expect(axis.splitLine.interval(16)).toBe(true); + expect(axis.splitLine.interval(15)).toBe(false); + // no line at the axis start, even if the first slot is midnight + expect(slotXAxis(times(0, 4), weekdayShort).splitLine.interval(0)).toBe(false); + }); +}); diff --git a/assets/js/components/Optimize/chart.ts b/assets/js/components/Optimize/chart.ts index 9ddf6f2ff..2aa026c4b 100644 --- a/assets/js/components/Optimize/chart.ts +++ b/assets/js/components/Optimize/chart.ts @@ -1,4 +1,5 @@ import { xAxisLabelStyle } from "../Forecast/echarts"; +import colors from "@/colors"; import type { BatteryDetail } from "@/types/evcc"; // loadpoint part of a vehicle entry title: "Carport (blue e-Golf)" → "Carport" @@ -19,22 +20,35 @@ export function slotTimes(timestamp: string, dt: number[]): number[] { return res; } -// category x axis over slot times, labels at full hours every 4h (6h on mobile) -export function slotXAxis(times: number[]) { +export function isMidnight(time?: number): boolean { + if (time === undefined) return false; + const d = new Date(time); + return d.getHours() === 0 && d.getMinutes() === 0; +} + +// category x axis over slot times, labels at full hours every 4h (6h on mobile), +// day boundaries as split line with the weekday shown at 00:00 +export function slotXAxis(times: number[], weekdayShort: (d: Date) => string) { const step = window.innerWidth < 576 ? 6 : 4; return { type: "category", data: times.map(String), axisLine: { show: false }, axisTick: { show: false }, - splitLine: { show: false }, + splitLine: { + show: true, + // split lines sit at the left edge of their slot + interval: (index: number) => index > 0 && isMidnight(times[index]), + lineStyle: { color: colors.muted || "", type: "solid" }, + }, axisLabel: { ...xAxisLabelStyle(), interval: 0, formatter: (value: string) => { const d = new Date(Number(value)); if (d.getMinutes() !== 0 || d.getHours() % step !== 0) return ""; - return String(d.getHours()); + const label = String(d.getHours()); + return d.getHours() === 0 ? `${label}\n${weekdayShort(d)}` : label; }, }, }; From bc38df1ed6d80f1459da2becda963189af93f8ea Mon Sep 17 00:00:00 2001 From: andig Date: Sun, 16 Aug 2026 19:45:18 +0200 Subject: [PATCH 014/135] Add Solplanet hybrid inverter (Modbus TCP) (#32888) --- .../definition/meter/solplanet-modbus.yaml | 201 ++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 templates/definition/meter/solplanet-modbus.yaml diff --git a/templates/definition/meter/solplanet-modbus.yaml b/templates/definition/meter/solplanet-modbus.yaml new file mode 100644 index 000000000..613d17237 --- /dev/null +++ b/templates/definition/meter/solplanet-modbus.yaml @@ -0,0 +1,201 @@ +template: solplanet-modbus +products: + - brand: Solplanet + description: + generic: Hybrid inverter (Modbus TCP) +requirements: + description: + de: | + Nur für Wechselrichter mit eigenem Modbus-TCP-Ethernet-Port (z.B. ASW-TH Serie, KACO Blueplanet Hybrid NH3, Port eth 5&6), Protokoll AISWEI "MB001_ASW GEN-Modbus" V2.1.7. + Ältere Geräte ohne diesen Port benötigen stattdessen das `solplanet-ai-dongle` Template. + Die Import-/Export-Zählerstände des Netzzählers (46443/46445) werden von dieser Firmware nicht über Modbus TCP bereitgestellt. + Bei zwei Batteriepaketen (BMS1 + BMS2) je einen Batteriezähler mit Batteriespeichernummer 1 und 2 anlegen. + en: | + Only for inverters with a dedicated Modbus TCP ethernet port (e.g. ASW-TH series, KACO Blueplanet Hybrid NH3, port eth 5&6), protocol AISWEI "MB001_ASW GEN-Modbus" V2.1.7. + Older devices without that port need the `solplanet-ai-dongle` template instead. + The grid meter import/export energy counters (46443/46445) are not exposed over Modbus TCP by this firmware. + With two battery packs (BMS1 + BMS2) add one battery meter each, using battery number 1 and 2. +capabilities: ["battery-control", "curtail"] +params: + - name: usage + choice: ["grid", "pv", "battery"] + - name: modbus + choice: ["tcpip"] + port: 502 + id: 1 + - name: battery + type: choice + choice: [1, 2] + required: true + usages: ["battery"] + description: + de: Batteriespeichernummer + en: Battery number + - preset: battery-params + - name: maxchargepower + required: true +render: | + type: custom + {{- if eq .usage "grid" }} + power: + source: modbus + {{- include "modbus" . | indent 2 }} + register: + address: 6433 # 46434 total system power (smart meter) + type: holding + decode: int32 + {{- end }} + {{- if eq .usage "pv" }} + power: + source: modbus + {{- include "modbus" . | indent 2 }} + register: + address: 1600 # 31601 PV total power + type: input + decode: uint32 + energy: + source: modbus + {{- include "modbus" . | indent 2 }} + register: + address: 1604 # 31605 PV E-Total (0.1 kWh) + type: input + decode: uint32 + scale: 0.1 + curtail: # allowed feed-in power as percent of nominal + source: sequence + set: + - source: modbus + {{- include "modbus" . | indent 6 }} + register: + address: 5402 # 45403 active power set (%Pn * 100) + type: writesingle + encoding: uint16 + scale: 100 + # active power control: disabled at 100% (unlimited), enabled while curtailed + - source: switch + switch: + - case: 100 # uncurtailed + set: + source: const + value: 0 + set: + source: modbus + {{- include "modbus" . | indent 14 }} + register: + address: 4000 # 44001 active power control function + type: writesingle + encoding: uint16 + default: + source: const + value: 1 + set: + source: modbus + {{- include "modbus" . | indent 10 }} + register: + address: 4000 + type: writesingle + encoding: uint16 + curtailed: # the power limit only applies while active power control is enabled + source: go + in: + - name: ena + type: int + config: + source: modbus + {{- include "modbus" . | indent 8 }} + register: + address: 4000 + type: holding + decode: uint16 + - name: percent + type: int + config: + source: modbus + {{- include "modbus" . | indent 8 }} + register: + address: 5402 + type: holding + decode: uint16 + scale: 0.01 + script: | + res := 100 + if ena != 0 && percent > 0 && percent < 100 { + res = percent + } + res + {{- end }} + {{- if eq .usage "battery" }} + power: + source: modbus + {{- include "modbus" . | indent 2 }} + register: + address: {{ if eq .battery "2" }}1751{{ else }}1618{{ end }} # 31752/31619 BMS2/1 battery power + type: input + decode: int32 + soc: + source: modbus + {{- include "modbus" . | indent 2 }} + register: + address: {{ if eq .battery "2" }}1754{{ else }}1621{{ end }} # 31755/31622 BMS2/1 battery SoC + type: input + decode: uint16 + batterymode: + source: switch + switch: + - case: 1 # normal + set: + source: const + value: 2 # normal + set: + source: modbus + {{- include "modbus" . | indent 10 }} + register: + address: 1103 # 41104 battery working mode + type: writesingle + encoding: uint16 + - case: 2 # hold + set: + source: sequence + set: + - source: const + value: 4 # custom + set: + source: modbus + {{- include "modbus" . | indent 14 }} + register: + address: 1103 + type: writesingle + encoding: uint16 + - source: const + value: 0 + set: + source: modbus + {{- include "modbus" . | indent 14 }} + register: + address: 1152 # 41153 charge/discharge power, negative charges + type: writesingle + encoding: int16 + - case: 3 # charge + set: + source: sequence + set: + - source: const + value: 4 + set: + source: modbus + {{- include "modbus" . | indent 14 }} + register: + address: 1103 + type: writesingle + encoding: uint16 + - source: const + value: {{ mul .maxchargepower -1 }} + set: + source: modbus + {{- include "modbus" . | indent 14 }} + register: + address: 1152 + type: writesingle + encoding: int16 + {{- include "battery-params" . }} + {{- end }} From 47fd73be3393ed7410d5567ec30b29b6c9b06437 Mon Sep 17 00:00:00 2001 From: Ingo <71161062+iseeberg79@users.noreply.github.com> Date: Sun, 16 Aug 2026 21:17:29 +0200 Subject: [PATCH 015/135] Site: feed the optimizer a trailing percentile solar scale (#32849) --- core/site.go | 12 ++++ core/site_tariffs.go | 93 +++++++++++++++++++++++-------- core/site_tariffs_test.go | 46 +++++++++++++++ server/http_db_metrics_handler.go | 4 ++ 4 files changed, 133 insertions(+), 22 deletions(-) diff --git a/core/site.go b/core/site.go index 6f0191616..d0a7e36b2 100644 --- a/core/site.go +++ b/core/site.go @@ -35,6 +35,7 @@ import ( "github.com/evcc-io/evcc/util/config" "github.com/evcc-io/evcc/util/modbus" "github.com/evcc-io/evcc/util/telemetry" + "github.com/jinzhu/now" "github.com/samber/lo" "github.com/smallnest/chanx" "golang.org/x/sync/errgroup" @@ -120,6 +121,8 @@ type Site struct { optimizerMu sync.Mutex // guards optimizer runs optimizerUpdated time.Time // last optimizer run, guarded by optimizerMu + + solarScaleCached func() (float64, error) // util.Cached wrapper around querySolarScale } // MetersConfig contains the site's meter configuration @@ -354,6 +357,15 @@ func NewSite() *Site { collectors: make(map[string]*metrics.Collector), } + // the result only depends on completed days, so it cannot change within a day + site.solarScaleCached = util.Cached(func() (float64, error) { + scale, err := site.querySolarScale(now.BeginningOfDay()) + if err != nil { + site.log.ERROR.Printf("solar scale percentile: %v, falling back to unadjusted forecast", err) + } + return scale, err + }, 24*time.Hour) + return site } diff --git a/core/site_tariffs.go b/core/site_tariffs.go index b62c99bc7..e45e806d1 100644 --- a/core/site_tariffs.go +++ b/core/site_tariffs.go @@ -3,6 +3,7 @@ package core import ( "encoding/json" "math" + "slices" "time" "github.com/evcc-io/evcc/api" @@ -26,7 +27,7 @@ func (s forecastSeries) MarshalBytes() ([]byte, error) { } type solarDetails struct { - Scale float64 `json:"scale"` // scale factor yield/forecasted today, 1 if unscaled + Scale float64 `json:"scale"` // trailing percentile solar scale factor, 1 if unscaled Today dailyDetails `json:"today"` // tomorrow Tomorrow dailyDetails `json:"tomorrow"` // tomorrow DayAfterTomorrow dailyDetails `json:"dayAfterTomorrow"` // day after tomorrow @@ -239,8 +240,8 @@ func (site *Site) solarDetails(solar api.Rates) solarDetails { return res } -// effectiveSolarScale returns the solar forecast scale if forecast adjustment -// is enabled, 1 otherwise. +// effectiveSolarScale returns the solar forecast scale used to adjust the +// optimizer's solar input if forecast adjustment is enabled, 1 otherwise. func (site *Site) effectiveSolarScale() float64 { if !site.GetSolarAdjusted() { return 1 @@ -248,39 +249,87 @@ func (site *Site) effectiveSolarScale() float64 { return site.solarScale() } -// solarScale returns the ratio of produced solar energy to forecasted solar -// energy for the current day, queried from the metrics database. Used to -// adjust forecasts when PV is consistently under-/over-producing relative -// to the forecast. Returns 1.0 when not enough data is available to make -// the ratio meaningful. +const ( + solarScaleWindow = 30 // trailing window of days to consider + solarScaleMinSamples = 14 // minimum daily ratios before applying a scale + solarScalePercentile = 0.5 // percentile of the daily ratio distribution to use + solarScaleMinEnergy = 0.5 // kWh, skip days where either side is too small for a meaningful ratio +) + +// solarScale computes a scale factor for the solar forecast by sorting the daily +// produced/forecasted solar ratio over a trailing window of completed days and +// picking the value at a configured percentile (window: solarScaleWindow, +// percentile: solarScalePercentile). This captures the installation's systematic +// bias (soiling, shading, model error) instead of a single day's weather noise. +// The current (partial) day is excluded; returns 1 when there is not enough history. +// +// Depends only on completed days, so it's cached instead of recomputed per run. +// +// The result only depends on completed days, so it cannot change within a day. It is +// cached accordingly instead of being recomputed on every optimizer run. func (site *Site) solarScale() float64 { - series, err := metrics.QueryEnergy(now.BeginningOfDay(), time.Now(), "day", true) + scale, err := site.solarScaleCached() if err != nil { - site.log.ERROR.Printf("solar forecast scale: %v", err) return 1 } + return scale +} - var pv, fcst float64 +// querySolarScale does the actual metrics query and percentile calculation +// for solarScale, given the current beginning-of-day boundary. +func (site *Site) querySolarScale(bod time.Time) (float64, error) { + from := bod.AddDate(0, 0, -solarScaleWindow) + series, err := metrics.QueryEnergy(from, time.Now(), "day", true) + if err != nil { + return 0, err + } + + pv := make(map[string]float64, solarScaleWindow) + fcst := make(map[string]float64, solarScaleWindow) for _, s := range series { - if len(s.Data) == 0 { - continue - } + var m map[string]float64 switch s.Group { case metrics.PV: - pv = s.Data[0].Energy + m = pv case metrics.Forecast: - fcst = s.Data[0].Energy + m = fcst + default: + continue + } + for _, d := range s.Data { + m[d.Start.Format("2006-01-02")] = d.Energy } } - const minEnergy = 0.5 // kWh - if fcst <= 0 || pv <= minEnergy { - return 1 + today := bod.Format("2006-01-02") + ratios := make([]float64, 0, len(fcst)) + for day, f := range fcst { + // skip today (partial) and dark days where the ratio is noise. The threshold + // applies to production as well: a near-zero yield against a healthy forecast + // is a fault (snow, soiling, inverter or metering outage), not a bias that + // should be projected onto the next solarScaleWindow days. + if p := pv[day]; day != today && f > solarScaleMinEnergy && p > solarScaleMinEnergy { + ratios = append(ratios, p/f) + } } - scale := pv / fcst - site.log.DEBUG.Printf("solar forecast: produced %.3fkWh, forecasted %.3fkWh, scale %.3f", pv, fcst, scale) - return scale + scale, ok := percentileOf(ratios, solarScalePercentile, solarScaleMinSamples) + if !ok { + return 1, nil + } + site.log.DEBUG.Printf("solar scale P%.0f over %d days = %.3f", solarScalePercentile*100, len(ratios), scale) + return scale, nil +} + +// percentileOf returns the p-th percentile (0..1) of values by nearest-rank on the +// sorted series, or false when fewer than minSamples are present. +func percentileOf(values []float64, p float64, minSamples int) (float64, bool) { + if len(values) < minSamples { + return 0, false + } + s := slices.Clone(values) + slices.Sort(s) + return s[int(p*float64(len(s)-1))], true } func (site *Site) isDynamicTariff(usage api.TariffUsage) bool { diff --git a/core/site_tariffs_test.go b/core/site_tariffs_test.go index c8dd322ce..73d6d778a 100644 --- a/core/site_tariffs_test.go +++ b/core/site_tariffs_test.go @@ -92,3 +92,49 @@ func TestTimeseriesMarshal(t *testing.T) { }) } } + +func TestPercentileOf(t *testing.T) { + // n values of v + fill := func(n int, v float64) []float64 { + s := make([]float64, n) + for i := range s { + s[i] = v + } + return s + } + + t.Run("too few samples returns false", func(t *testing.T) { + _, ok := percentileOf(nil, 0.5, solarScaleMinSamples) + assert.False(t, ok) + + _, ok = percentileOf(fill(solarScaleMinSamples-1, 0.9), 0.5, solarScaleMinSamples) + assert.False(t, ok) + }) + + t.Run("stable cluster", func(t *testing.T) { + v, ok := percentileOf(fill(20, 0.9), 0.5, solarScaleMinSamples) + assert.True(t, ok) + assert.InDelta(t, 0.9, v, 0.001) + }) + + // P50 rejects outlier days for free: a broken forecast feed (recent ratio + // ~2.3) and a metering outage (ratio ~0.16) do not move the result as + // long as they stay a minority of the window. + t.Run("outlier days do not move P50", func(t *testing.T) { + ratios := fill(20, 0.9) // healthy installation bias + ratios = append(ratios, fill(4, 2.3)...) // broken forecast feed + ratios = append(ratios, fill(8, 0.16)...) // metering outage + + v, ok := percentileOf(ratios, 0.5, solarScaleMinSamples) + assert.True(t, ok) + assert.InDelta(t, 0.9, v, 0.001) + }) + + t.Run("higher percentile shifts toward the upper tail", func(t *testing.T) { + ratios := append(fill(15, 0.8), fill(15, 1.2)...) + + p50, _ := percentileOf(ratios, 0.5, solarScaleMinSamples) + p90, _ := percentileOf(ratios, 0.9, solarScaleMinSamples) + assert.Less(t, p50, p90) + }) +} diff --git a/server/http_db_metrics_handler.go b/server/http_db_metrics_handler.go index 05d1423c4..f606e549b 100644 --- a/server/http_db_metrics_handler.go +++ b/server/http_db_metrics_handler.go @@ -7,6 +7,7 @@ import ( "github.com/evcc-io/evcc/core/metrics" "github.com/evcc-io/evcc/server/db" + "github.com/evcc-io/evcc/util" ) // deleteResult reports the number of affected rows @@ -58,6 +59,9 @@ func deleteEnergyHandler(w http.ResponseWriter, r *http.Request) { return } + // invalidate cached values (e.g. the solar scale) derived from the deleted history + util.ResetCached() + jsonWrite(w, deleteResult{rows}) } From 64d48e58005f67211319db140883c254b1eb2323 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 17 Aug 2026 07:30:49 +0000 Subject: [PATCH 016/135] Loadpoint: fix config error for disabled loadpoints (#32880) Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: andig Co-authored-by: Claude Opus 5 (1M context) Co-authored-by: Michael Geers --- server/http_config_loadpoint_handler.go | 4 +++- server/http_config_loadpoint_handler_test.go | 1 + tests/config-disable.spec.ts | 18 +++++++++++++++++- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/server/http_config_loadpoint_handler.go b/server/http_config_loadpoint_handler.go index 23f875c48..c30359536 100644 --- a/server/http_config_loadpoint_handler.go +++ b/server/http_config_loadpoint_handler.go @@ -13,6 +13,7 @@ import ( "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/config" "github.com/evcc-io/evcc/util/templates" + "github.com/go-viper/mapstructure/v2" "github.com/gorilla/mux" ) @@ -119,8 +120,9 @@ func loadpointConfig(dev config.Device[loadpoint.API]) (loadpointFullConfig, err return loadpointFullConfig{}, err } + // non-strict: runtime settings (mode, ...) are persisted alongside the config var static loadpoint.StaticConfig - if err := util.DecodeOther(staticMap, &static); err != nil { + if err := mapstructure.Decode(staticMap, &static); err != nil { return loadpointFullConfig{}, err } diff --git a/server/http_config_loadpoint_handler_test.go b/server/http_config_loadpoint_handler_test.go index e9ddf4a49..a8050bd62 100644 --- a/server/http_config_loadpoint_handler_test.go +++ b/server/http_config_loadpoint_handler_test.go @@ -25,6 +25,7 @@ func TestLoadpointConfigDisabledNilInstance(t *testing.T) { "charger": "wallbox", "meter": "lp-meter", "title": "Garage", + "mode": "pv", // runtime setting persisted by the settings adapter }, } diff --git a/tests/config-disable.spec.ts b/tests/config-disable.spec.ts index 4d94087ed..744bb07e7 100644 --- a/tests/config-disable.spec.ts +++ b/tests/config-disable.spec.ts @@ -59,6 +59,21 @@ test.describe("disable / enable", async () => { await page.reload(); await createLoadpoint(page, "Garage"); + // restart to bring both loadpoints live + await restart(); + await page.reload(); + + // switch charge mode on dashboard; runtime setting is persisted next to the loadpoint config + await page.goto("/"); + const modeButton = page + .getByTestId("loadpoint") + .nth(0) + .getByTestId("mode") + .getByRole("button", { name: "Fast" }); + await modeButton.click(); + await expect(modeButton).toHaveClass(/active/); + await page.goto("/#/config"); + const target = page.getByTestId("loadpoint").nth(0); await toggleLoadpointDisable(page, 0, "Disable"); @@ -66,10 +81,11 @@ test.describe("disable / enable", async () => { // card shows disabled state await expect(disabledBadge(target)).toBeVisible(); - // restart, no fatal + // restart, no fatal, both loadpoints still listed await restart(); await page.reload(); await expectNoFatal(page); + await expect(page.getByTestId("loadpoint")).toHaveCount(2); await expect(disabledBadge(target)).toBeVisible(); // re-enable by clicking the disabled card From 4cc75b1a45c730d36aa128c8406214c50f78f318 Mon Sep 17 00:00:00 2001 From: andig Date: Mon, 17 Aug 2026 14:50:55 +0200 Subject: [PATCH 017/135] Optimizer: extend horizon to 48h plus end of day (#32893) --- core/site_optimizer.go | 24 ++++++++++++++++++++++-- core/site_optimizer_test.go | 25 +++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/core/site_optimizer.go b/core/site_optimizer.go index 3952062ba..d19de7f86 100644 --- a/core/site_optimizer.go +++ b/core/site_optimizer.go @@ -413,8 +413,7 @@ func (site *Site) optimizerRequest(battery []types.Measurement) (optimizer.Optim } if optimizerURI() == OPTIMIZER_URI { - // limit to 2 days for sake of performance - minLen = min(2*96, minLen) + minLen = slotsUntil(grid, optimizerHorizon(time.Now()), minLen) } if expectedSlots := 8; minLen < expectedSlots { @@ -1127,6 +1126,27 @@ func currentRates(tariff api.Tariff) api.Rates { }) } +// optimizerHorizon is the timeframe the hosted optimizer is limited to for sake +// of performance: 48 hours, extended to the end of that day. In the early hours +// the extension would add almost a full day, hence it only applies past 6:00. +func optimizerHorizon(t time.Time) time.Time { + horizon := t.Add(48 * time.Hour) + if t.Hour() < 6 { + return horizon + } + return now.With(horizon).EndOfDay() +} + +// slotsUntil limits maxLen to the slots starting before the given horizon +func slotsUntil(rates api.Rates, horizon time.Time, maxLen int) int { + if i := slices.IndexFunc(rates[:min(maxLen, len(rates))], func(slot api.Rate) bool { + return slot.Start.After(horizon) + }); i >= 0 { + return i + } + return maxLen +} + func timeSteps(minLen int, now time.Time) []int { res := make([]int, 0, minLen) diff --git a/core/site_optimizer_test.go b/core/site_optimizer_test.go index 446e8889c..94c99e743 100644 --- a/core/site_optimizer_test.go +++ b/core/site_optimizer_test.go @@ -7,6 +7,7 @@ import ( "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/core/loadpoint" "github.com/evcc-io/evcc/core/types" + "github.com/evcc-io/evcc/tariff" "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/config" optimizer "github.com/evcc-io/optimizer/client" @@ -29,6 +30,30 @@ func TestLoadpointProfile(t *testing.T) { require.Equal(t, []float64{250, 250, 250, 250, 250, 250, 250, 50}, loadpointProfile(lp, 8)) } +func TestOptimizerHorizon(t *testing.T) { + ts := time.Date(2025, 1, 1, 10, 30, 0, 0, time.Local) + horizon := optimizerHorizon(ts) + + // 48h plus end of day + assert.Equal(t, time.Date(2025, 1, 3, 23, 59, 59, int(time.Second-time.Nanosecond), time.Local), horizon) + + // before 6:00 the day is not extended + assert.Equal(t, time.Date(2025, 1, 3, 5, 30, 0, 0, time.Local), + optimizerHorizon(time.Date(2025, 1, 1, 5, 30, 0, 0, time.Local))) + + rates := make(api.Rates, 0, 4*96) + for slot := ts.Truncate(tariff.SlotDuration); len(rates) < cap(rates); slot = slot.Add(tariff.SlotDuration) { + rates = append(rates, api.Rate{Start: slot, End: slot.Add(tariff.SlotDuration)}) + } + + // 4 days of slots from 10:30, capped at the last slot of Jan 3rd + assert.Equal(t, 246, slotsUntil(rates, horizon, len(rates))) + assert.Equal(t, time.Date(2025, 1, 3, 23, 45, 0, 0, time.Local), rates[245].Start) + + // shorter forecast is not extended + assert.Equal(t, 8, slotsUntil(rates, horizon, 8)) +} + func TestApplyPrecondition(t *testing.T) { ctrl := gomock.NewController(t) From 9034f453555c4a0e99c2ccf7bd952581bd12e5d8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 18 Aug 2026 08:15:51 +0200 Subject: [PATCH 018/135] Tesla Powerwall Fleet: fix template not selectable in ui (#32934) --- templates/definition/meter/tesla-powerwall-fleet.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/templates/definition/meter/tesla-powerwall-fleet.yaml b/templates/definition/meter/tesla-powerwall-fleet.yaml index 92f806fe5..84c577edd 100644 --- a/templates/definition/meter/tesla-powerwall-fleet.yaml +++ b/templates/definition/meter/tesla-powerwall-fleet.yaml @@ -19,6 +19,8 @@ requirements: The [myteslamate.com](https://www.myteslamate.com/tesla-api-application-registration/) guide explains the process and generates a free Access and Refresh Token. With this token pair and your Client ID created in the Tesla Developer Account, evcc can directly communicate with the Tesla Fleet API. params: + - name: usage + choice: ["battery"] - name: host - name: password required: true From de3f8995cdf2b8a10745b7221c58031ae3d1764c Mon Sep 17 00:00:00 2001 From: andig Date: Tue, 18 Aug 2026 09:23:03 +0200 Subject: [PATCH 019/135] Templates: support class-local include files (#32871) --- .../definition/charger/features.tpl | 2 +- .../definition/charger}/heatpumpswitch.tpl | 0 .../definition/charger/homeassistant.yaml | 2 +- .../definition/charger}/ocpp.tpl | 0 .../definition/charger}/switchsocket.tpl | 0 templates/definition/embed.go | 3 ++ .../definition/meter}/battery-params.tpl | 0 templates/definition/tariff/allinpower.yaml | 2 +- templates/definition/tariff/amber.yaml | 2 +- templates/definition/tariff/awattar.yaml | 2 +- .../definition/tariff/base.tpl | 2 +- templates/definition/tariff/bkw.yaml | 2 +- templates/definition/tariff/ckw.yaml | 2 +- .../definition/tariff/demo-dynamic-grid.yaml | 2 +- templates/definition/tariff/ekz.yaml | 2 +- templates/definition/tariff/elering.yaml | 2 +- .../definition/tariff/energinet-price.yaml | 2 +- templates/definition/tariff/energinet.yaml | 2 +- .../definition/tariff/energy-charts-api.yaml | 4 +-- .../definition/tariff/energyforecast.yaml | 4 +-- .../tariff/energypriceforecast.yaml | 4 +-- templates/definition/tariff/enever.yaml | 4 +-- templates/definition/tariff/entsoe.yaml | 4 +-- .../definition/tariff/epex-predictor.yaml | 4 +-- .../definition/tariff/epexprijzen-nl.yaml | 2 +- templates/definition/tariff/esios.yaml | 2 +- .../definition/tariff/features.tpl | 2 +- templates/definition/tariff/fixed-zones.yaml | 2 +- templates/definition/tariff/fixed.yaml | 2 +- templates/definition/tariff/groupe-e.yaml | 2 +- templates/definition/tariff/nordpool.yaml | 4 +-- templates/definition/tariff/omie.yaml | 4 +-- templates/definition/tariff/pstryk.yaml | 2 +- templates/definition/tariff/pun.yaml | 2 +- templates/definition/tariff/smartenergy.yaml | 2 +- templates/definition/tariff/spottyenergy.yaml | 2 +- templates/definition/tariff/stekker.yaml | 4 +-- .../tariff/stroomprijsprognose.yaml | 4 +-- templates/definition/tariff/tibber.yaml | 2 +- templates/definition/tariff/voltcast.yaml | 4 +-- templates/definition/vehicle/aiways.yaml | 4 +-- templates/definition/vehicle/alpine.yaml | 4 +-- templates/definition/vehicle/audi.yaml | 4 +-- .../definition/vehicle/base.tpl | 4 +-- templates/definition/vehicle/bmw.yaml | 4 +-- templates/definition/vehicle/cardata.yaml | 4 +-- templates/definition/vehicle/carwings.yaml | 4 +-- templates/definition/vehicle/citroen.yaml | 4 +-- .../definition/vehicle/common.tpl | 2 +- .../definition/vehicle/connected-cars.yaml | 4 +-- templates/definition/vehicle/dacia.yaml | 4 +-- .../vehicle/drivesomethinggreater.yaml | 4 +-- templates/definition/vehicle/ds.yaml | 4 +-- templates/definition/vehicle/evnotify.yaml | 4 +-- .../definition/vehicle/features.tpl | 2 +- templates/definition/vehicle/fiat.yaml | 4 +-- templates/definition/vehicle/flobz.yaml | 4 +-- .../vehicle/ford-connect-query.yaml | 4 +-- .../definition/vehicle/ford-connect.yaml | 4 +-- templates/definition/vehicle/genesis.yaml | 6 ++-- .../definition/vehicle/homeassistant.yaml | 4 +-- templates/definition/vehicle/hyundai-us.yaml | 4 +-- templates/definition/vehicle/hyundai.yaml | 6 ++-- .../definition/vehicle/ioBroker.bmw.yaml | 4 +-- templates/definition/vehicle/iso15118.yaml | 4 +-- .../definition/vehicle/jaguar-landrover.yaml | 4 +-- templates/definition/vehicle/kia.yaml | 6 ++-- .../definition/vehicle/language.tpl | 2 +- templates/definition/vehicle/lexus.yaml | 4 +-- templates/definition/vehicle/mazda2mqtt.yaml | 4 +-- templates/definition/vehicle/mercedes.yaml | 4 +-- templates/definition/vehicle/mg.yaml | 4 +-- templates/definition/vehicle/mg2mqtt.yaml | 4 +-- templates/definition/vehicle/mini.yaml | 4 +-- templates/definition/vehicle/mz2mqtt.yaml | 4 +-- .../definition/vehicle/nissan-ariya.yaml | 4 +-- templates/definition/vehicle/nissan.yaml | 4 +-- .../definition/vehicle/niu-e-scooter.yaml | 2 +- templates/definition/vehicle/octopus-de.yaml | 4 +-- templates/definition/vehicle/octopus-it.yaml | 4 +-- templates/definition/vehicle/offline.yaml | 4 +-- templates/definition/vehicle/opel.yaml | 4 +-- .../definition/vehicle/outlanderphev.yaml | 4 +-- templates/definition/vehicle/ovms.yaml | 4 +-- templates/definition/vehicle/peugeot.yaml | 4 +-- templates/definition/vehicle/polestar.yaml | 4 +-- templates/definition/vehicle/porsche.yaml | 4 +-- templates/definition/vehicle/renault.yaml | 4 +-- templates/definition/vehicle/seat-cupra.yaml | 4 +-- templates/definition/vehicle/seat.yaml | 4 +-- templates/definition/vehicle/skoda.yaml | 4 +-- templates/definition/vehicle/smart-hello.yaml | 4 +-- templates/definition/vehicle/smart.yaml | 4 +-- templates/definition/vehicle/subaru.yaml | 4 +-- templates/definition/vehicle/tesla-ble.yaml | 4 +-- templates/definition/vehicle/tesla.yaml | 4 +-- templates/definition/vehicle/teslafi.yaml | 4 +-- templates/definition/vehicle/teslalogger.yaml | 4 +-- templates/definition/vehicle/teslamate.yaml | 4 +-- templates/definition/vehicle/tessie.yaml | 4 +-- templates/definition/vehicle/tibber.yaml | 4 +-- templates/definition/vehicle/toyota.yaml | 4 +-- templates/definition/vehicle/tronity.yaml | 4 +-- .../definition/vehicle/volvo-connected.yaml | 4 +-- templates/definition/vehicle/volvo2mqtt.yaml | 4 +-- templates/definition/vehicle/vw.yaml | 4 +-- templates/definition/vehicle/zero.yaml | 4 +-- util/templates/formatvalue_test.go | 2 +- util/templates/init.go | 30 +++++++++++++++- util/templates/init_test.go | 36 +++++++++++++++++++ util/templates/render_instance.go | 2 +- util/templates/render_testing.go | 8 ++--- util/templates/template.go | 10 ++++-- util/templates/template_modbus_test.go | 6 ++-- util/templates/template_test.go | 36 +++++++++---------- vehicle/template_test.go | 6 ++-- 116 files changed, 285 insertions(+), 212 deletions(-) rename util/templates/includes/charger-features.tpl => templates/definition/charger/features.tpl (86%) rename {util/templates/includes => templates/definition/charger}/heatpumpswitch.tpl (100%) rename {util/templates/includes => templates/definition/charger}/ocpp.tpl (100%) rename {util/templates/includes => templates/definition/charger}/switchsocket.tpl (100%) rename {util/templates/includes => templates/definition/meter}/battery-params.tpl (100%) rename util/templates/includes/tariff-base.tpl => templates/definition/tariff/base.tpl (94%) rename util/templates/includes/tariff-features.tpl => templates/definition/tariff/features.tpl (70%) rename util/templates/includes/vehicle-base.tpl => templates/definition/vehicle/base.tpl (65%) rename util/templates/includes/vehicle-common.tpl => templates/definition/vehicle/common.tpl (95%) rename util/templates/includes/vehicle-features.tpl => templates/definition/vehicle/features.tpl (95%) rename util/templates/includes/vehicle-language.tpl => templates/definition/vehicle/language.tpl (53%) create mode 100644 util/templates/init_test.go diff --git a/util/templates/includes/charger-features.tpl b/templates/definition/charger/features.tpl similarity index 86% rename from util/templates/includes/charger-features.tpl rename to templates/definition/charger/features.tpl index b4569dd9e..abf514045 100644 --- a/util/templates/includes/charger-features.tpl +++ b/templates/definition/charger/features.tpl @@ -1,4 +1,4 @@ -{{ define "charger-features" }} +{{ define "features" }} {{- if or (eq .heating "true") (eq .integrateddevice "true") }} features: {{- if eq .heating "true" }} diff --git a/util/templates/includes/heatpumpswitch.tpl b/templates/definition/charger/heatpumpswitch.tpl similarity index 100% rename from util/templates/includes/heatpumpswitch.tpl rename to templates/definition/charger/heatpumpswitch.tpl diff --git a/templates/definition/charger/homeassistant.yaml b/templates/definition/charger/homeassistant.yaml index cfb4235f6..41ca1c0fe 100644 --- a/templates/definition/charger/homeassistant.yaml +++ b/templates/definition/charger/homeassistant.yaml @@ -213,4 +213,4 @@ render: | - {{ .voltageL3 }} {{- end }} phases: {{ .phaseswitch }} - {{ include "charger-features" . }} + {{ include "features" . }} diff --git a/util/templates/includes/ocpp.tpl b/templates/definition/charger/ocpp.tpl similarity index 100% rename from util/templates/includes/ocpp.tpl rename to templates/definition/charger/ocpp.tpl diff --git a/util/templates/includes/switchsocket.tpl b/templates/definition/charger/switchsocket.tpl similarity index 100% rename from util/templates/includes/switchsocket.tpl rename to templates/definition/charger/switchsocket.tpl diff --git a/templates/definition/embed.go b/templates/definition/embed.go index 3202a14b6..1f60bfa3b 100644 --- a/templates/definition/embed.go +++ b/templates/definition/embed.go @@ -4,3 +4,6 @@ import "embed" //go:embed charger/*.yaml meter/*.yaml vehicle/*.yaml tariff/*.yaml messenger/*.yaml circuit/*.yaml hems/*.yaml var YamlTemplates embed.FS + +//go:embed */*.tpl +var TemplateIncludes embed.FS diff --git a/util/templates/includes/battery-params.tpl b/templates/definition/meter/battery-params.tpl similarity index 100% rename from util/templates/includes/battery-params.tpl rename to templates/definition/meter/battery-params.tpl diff --git a/templates/definition/tariff/allinpower.yaml b/templates/definition/tariff/allinpower.yaml index 7d560e630..c476cf593 100644 --- a/templates/definition/tariff/allinpower.yaml +++ b/templates/definition/tariff/allinpower.yaml @@ -10,7 +10,7 @@ params: - preset: tariff-base render: | type: custom - {{ include "tariff-base" . }} + {{ include "base" . }} forecast: source: http uri: https://api.allinpower.nl/troodon/api/p/spot_market/prices/?product_type=ELK diff --git a/templates/definition/tariff/amber.yaml b/templates/definition/tariff/amber.yaml index 1873ed11c..655f2d4c1 100644 --- a/templates/definition/tariff/amber.yaml +++ b/templates/definition/tariff/amber.yaml @@ -20,4 +20,4 @@ render: | token: {{ .token }} siteid: {{ .siteid }} channel: {{ .channel }} - {{ include "tariff-base" . }} + {{ include "base" . }} diff --git a/templates/definition/tariff/awattar.yaml b/templates/definition/tariff/awattar.yaml index af44c45bc..acb5086b3 100644 --- a/templates/definition/tariff/awattar.yaml +++ b/templates/definition/tariff/awattar.yaml @@ -13,4 +13,4 @@ params: render: | type: awattar region: {{ .region }} - {{ include "tariff-base" . }} + {{ include "base" . }} diff --git a/util/templates/includes/tariff-base.tpl b/templates/definition/tariff/base.tpl similarity index 94% rename from util/templates/includes/tariff-base.tpl rename to templates/definition/tariff/base.tpl index f508795ed..84f91b48a 100644 --- a/util/templates/includes/tariff-base.tpl +++ b/templates/definition/tariff/base.tpl @@ -1,4 +1,4 @@ -{{ define "tariff-base" }} +{{ define "base" }} {{- if .charges }} charges: {{ .charges }} {{- end }} diff --git a/templates/definition/tariff/bkw.yaml b/templates/definition/tariff/bkw.yaml index 1dd8c7af5..dde7b1726 100644 --- a/templates/definition/tariff/bkw.yaml +++ b/templates/definition/tariff/bkw.yaml @@ -14,7 +14,7 @@ params: default: 0.081 render: | type: custom - {{ include "tariff-base" . }} + {{ include "base" . }} forecast: source: http uri: https://api.bkw.ch/api/dyntariffs/v1/tariffs diff --git a/templates/definition/tariff/ckw.yaml b/templates/definition/tariff/ckw.yaml index 230f171b3..ca5924973 100644 --- a/templates/definition/tariff/ckw.yaml +++ b/templates/definition/tariff/ckw.yaml @@ -23,7 +23,7 @@ params: default: 0.081 render: | type: custom - {{ include "tariff-base" . }} + {{ include "base" . }} forecast: source: http uri: https://e-ckw-public-data.de-c1.eu1.cloudhub.io/api/v1/netzinformationen/energie/dynamische-preise?tariff_type=integrated&tariff_name={{ .tariff }}&start_timestamp={{ `{{ (now.AddDate 0 0 -1).Format "2006-01-02T00:00:00Z" }}` }}&end_timestamp={{ `{{ (now.AddDate 0 0 3).Format "2006-01-02T00:00:00Z" }}` }} diff --git a/templates/definition/tariff/demo-dynamic-grid.yaml b/templates/definition/tariff/demo-dynamic-grid.yaml index ab5874645..d0a1de33a 100644 --- a/templates/definition/tariff/demo-dynamic-grid.yaml +++ b/templates/definition/tariff/demo-dynamic-grid.yaml @@ -44,7 +44,7 @@ params: render: | type: custom - {{ include "tariff-base" . }} + {{ include "base" . }} forecast: source: js script: | diff --git a/templates/definition/tariff/ekz.yaml b/templates/definition/tariff/ekz.yaml index a65ba35a5..f30003daa 100644 --- a/templates/definition/tariff/ekz.yaml +++ b/templates/definition/tariff/ekz.yaml @@ -29,7 +29,7 @@ params: advanced: true render: | type: custom - {{ include "tariff-base" . }} + {{ include "base" . }} forecast: source: http uri: https://api.tariffs.ekz.ch/v1/tariffs?tariff_name=integrated_{{ .tariff_name }}&start_timestamp={{ `{{ now.Local | date "2006-01-02T15" }}` }}:00:00%2B02%3A00&end_timestamp={{ `{{ addDate now.Local 0 0 2 | date "2006-01-02T15" }}` }}:00:00 diff --git a/templates/definition/tariff/elering.yaml b/templates/definition/tariff/elering.yaml index 99e4336ed..7e0530beb 100644 --- a/templates/definition/tariff/elering.yaml +++ b/templates/definition/tariff/elering.yaml @@ -18,4 +18,4 @@ params: render: | type: elering region: {{ .region }} - {{ include "tariff-base" . }} + {{ include "base" . }} diff --git a/templates/definition/tariff/energinet-price.yaml b/templates/definition/tariff/energinet-price.yaml index eb5a42897..663ea9675 100644 --- a/templates/definition/tariff/energinet-price.yaml +++ b/templates/definition/tariff/energinet-price.yaml @@ -15,7 +15,7 @@ params: - preset: tariff-base render: | type: custom - {{ include "tariff-base" . }} + {{ include "base" . }} forecast: source: http uri: https://api.energidataservice.dk/dataset/DayAheadPrices?start=now&filter={"PriceArea":["{{ .region }}"]} diff --git a/templates/definition/tariff/energinet.yaml b/templates/definition/tariff/energinet.yaml index 4a033609c..1be858da6 100644 --- a/templates/definition/tariff/energinet.yaml +++ b/templates/definition/tariff/energinet.yaml @@ -16,4 +16,4 @@ params: render: | type: energinet region: {{ .region }} - {{ include "tariff-base" . }} + {{ include "base" . }} diff --git a/templates/definition/tariff/energy-charts-api.yaml b/templates/definition/tariff/energy-charts-api.yaml index a3ba8825c..32d450354 100644 --- a/templates/definition/tariff/energy-charts-api.yaml +++ b/templates/definition/tariff/energy-charts-api.yaml @@ -45,8 +45,8 @@ params: - preset: tariff-features render: | type: custom - {{ include "tariff-base" . }} - {{ include "tariff-features" . }} + {{ include "base" . }} + {{ include "features" . }} forecast: source: http uri: https://api.energy-charts.info/price?bzn={{ .bzn }}&end={{ `{{ addDate now 0 0 1 | date "2006-01-02" }}` }} diff --git a/templates/definition/tariff/energyforecast.yaml b/templates/definition/tariff/energyforecast.yaml index 2e8bb183d..c683cc3c0 100644 --- a/templates/definition/tariff/energyforecast.yaml +++ b/templates/definition/tariff/energyforecast.yaml @@ -15,8 +15,8 @@ params: - preset: tariff-features render: | type: custom - {{ include "tariff-base" . }} - {{ include "tariff-features" . }} + {{ include "base" . }} + {{ include "features" . }} forecast: source: http uri: https://www.energyforecast.de/api/v1/predictions/next_96_hours?resolution=QUARTER_HOURLY&fixed_cost_cent=0&vat=0&token={{ .token }}{{ if .domain }}&market_zone={{ .domain }}{{ end }} diff --git a/templates/definition/tariff/energypriceforecast.yaml b/templates/definition/tariff/energypriceforecast.yaml index 0c814a001..dec09964d 100644 --- a/templates/definition/tariff/energypriceforecast.yaml +++ b/templates/definition/tariff/energypriceforecast.yaml @@ -30,8 +30,8 @@ params: - preset: tariff-features render: | type: custom - {{ include "tariff-base" . }} - {{ include "tariff-features" . }} + {{ include "base" . }} + {{ include "features" . }} forecast: source: http uri: https://api.energypriceforecast.eu/api/v1/evcc/tariff?country={{.region}}&type=grid¤cy={{.currency}}&hours={{.horizon}} diff --git a/templates/definition/tariff/enever.yaml b/templates/definition/tariff/enever.yaml index 2b3829df5..f1b68df3f 100644 --- a/templates/definition/tariff/enever.yaml +++ b/templates/definition/tariff/enever.yaml @@ -52,8 +52,8 @@ params: advanced: true render: | type: custom - {{ include "tariff-base" . }} - {{ include "tariff-features" . }} + {{ include "base" . }} + {{ include "features" . }} forecast: source: go script: | diff --git a/templates/definition/tariff/entsoe.yaml b/templates/definition/tariff/entsoe.yaml index 10e06b469..6e2d88537 100644 --- a/templates/definition/tariff/entsoe.yaml +++ b/templates/definition/tariff/entsoe.yaml @@ -118,5 +118,5 @@ render: | type: entsoe securitytoken: {{ .securitytoken }} domain: "{{ .domain }}" - {{ include "tariff-base" . }} - {{ include "tariff-features" . }} + {{ include "base" . }} + {{ include "features" . }} diff --git a/templates/definition/tariff/epex-predictor.yaml b/templates/definition/tariff/epex-predictor.yaml index f3bc9b57e..47cb5c74c 100644 --- a/templates/definition/tariff/epex-predictor.yaml +++ b/templates/definition/tariff/epex-predictor.yaml @@ -26,8 +26,8 @@ params: - preset: tariff-features render: | type: custom - {{ include "tariff-base" . }} - {{ include "tariff-features" . }} + {{ include "base" . }} + {{ include "features" . }} forecast: source: http uri: {{ .uri }}/prices?region={{ .region }}&unit=EUR_PER_KWH&timezone=UTC diff --git a/templates/definition/tariff/epexprijzen-nl.yaml b/templates/definition/tariff/epexprijzen-nl.yaml index 7572055fc..6aae45c98 100644 --- a/templates/definition/tariff/epexprijzen-nl.yaml +++ b/templates/definition/tariff/epexprijzen-nl.yaml @@ -54,7 +54,7 @@ params: advanced: true render: | type: custom - {{ include "tariff-base" . }} + {{ include "base" . }} forecast: source: http uri: https://epexprijzen.nl/api/v1/prices/{{ .provider }}/{{ index . "price-interval" }} diff --git a/templates/definition/tariff/esios.yaml b/templates/definition/tariff/esios.yaml index d8d99b8f6..24b2afc80 100644 --- a/templates/definition/tariff/esios.yaml +++ b/templates/definition/tariff/esios.yaml @@ -32,7 +32,7 @@ params: - preset: tariff-base render: | type: custom - {{ include "tariff-base" . }} + {{ include "base" . }} forecast: source: http uri: https://api.esios.ree.es/indicators/{{ .indicator }}?start_date={{ now | date "2006-01-02" }}T00:00&end_date={{ (now.AddDate 0 0 1) | date "2006-01-02" }}T23:59 diff --git a/util/templates/includes/tariff-features.tpl b/templates/definition/tariff/features.tpl similarity index 70% rename from util/templates/includes/tariff-features.tpl rename to templates/definition/tariff/features.tpl index 80d458b11..5748394eb 100644 --- a/util/templates/includes/tariff-features.tpl +++ b/templates/definition/tariff/features.tpl @@ -1,4 +1,4 @@ -{{ define "tariff-features" }} +{{ define "features" }} {{- if eq .average "true" }} features: ["average"] {{- end }} diff --git a/templates/definition/tariff/fixed-zones.yaml b/templates/definition/tariff/fixed-zones.yaml index 7e1c8e4fb..5e98ae031 100644 --- a/templates/definition/tariff/fixed-zones.yaml +++ b/templates/definition/tariff/fixed-zones.yaml @@ -37,4 +37,4 @@ render: | months: {{ .months }} {{- end }} {{- end }} - {{ include "tariff-base" . }} + {{ include "base" . }} diff --git a/templates/definition/tariff/fixed.yaml b/templates/definition/tariff/fixed.yaml index b0e5a6411..2e4aafcae 100644 --- a/templates/definition/tariff/fixed.yaml +++ b/templates/definition/tariff/fixed.yaml @@ -15,4 +15,4 @@ params: render: | type: fixed price: {{ .price }} - {{ include "tariff-base" . }} + {{ include "base" . }} diff --git a/templates/definition/tariff/groupe-e.yaml b/templates/definition/tariff/groupe-e.yaml index 77f6536db..881709416 100644 --- a/templates/definition/tariff/groupe-e.yaml +++ b/templates/definition/tariff/groupe-e.yaml @@ -12,7 +12,7 @@ params: render: | type: custom features: ["cacheable"] - {{ include "tariff-base" . }} + {{ include "base" . }} forecast: source: http uri: https://api.tariffs.groupe-e.ch/v2/tariffs?start_timestamp={{ `{{ now.UTC | mustDateModify "-1h" | date "2006-01-02T15" }}` }}:00:00Z&end_timestamp={{ `{{ addDate now.UTC 0 0 2 | date "2006-01-02T15" }}` }}:00:00Z diff --git a/templates/definition/tariff/nordpool.yaml b/templates/definition/tariff/nordpool.yaml index 3f1c12d65..df648b713 100644 --- a/templates/definition/tariff/nordpool.yaml +++ b/templates/definition/tariff/nordpool.yaml @@ -46,8 +46,8 @@ params: - preset: tariff-features render: | type: custom - {{ include "tariff-base" . }} - {{ include "tariff-features" . }} + {{ include "base" . }} + {{ include "features" . }} forecast: source: go script: | diff --git a/templates/definition/tariff/omie.yaml b/templates/definition/tariff/omie.yaml index 49e04f099..91e5cc8ce 100644 --- a/templates/definition/tariff/omie.yaml +++ b/templates/definition/tariff/omie.yaml @@ -27,8 +27,8 @@ params: - preset: tariff-features render: | type: custom - {{ include "tariff-base" . }} - {{ include "tariff-features" . }} + {{ include "base" . }} + {{ include "features" . }} forecast: source: http uri: https://www.omie.es/sites/default/files/dados/NUEVA_SECCION/INT_PBC_EV_H_ACUM.TXT diff --git a/templates/definition/tariff/pstryk.yaml b/templates/definition/tariff/pstryk.yaml index 8a3426619..f4fbdda2f 100644 --- a/templates/definition/tariff/pstryk.yaml +++ b/templates/definition/tariff/pstryk.yaml @@ -34,7 +34,7 @@ params: render: | type: custom - {{ include "tariff-base" . }} + {{ include "base" . }} forecast: source: http uri: https://api.pstryk.pl/integrations/meter-data/unified-metrics/?metrics=pricing&resolution=hour&window_start={{ `{{ now.UTC | mustDateModify "-24h" | date "2006-01-02" }}` }}:00:00Z&window_end={{ `{{ now.UTC | mustDateModify "+72h" | date "2006-01-02" }}` }}:00:00Z diff --git a/templates/definition/tariff/pun.yaml b/templates/definition/tariff/pun.yaml index 8736d59b1..871b6d162 100644 --- a/templates/definition/tariff/pun.yaml +++ b/templates/definition/tariff/pun.yaml @@ -21,5 +21,5 @@ params: en: Italian electricity market zone. If no zone is specified, the national unit price (PUN) is used. render: | type: pun - {{ include "tariff-base" . }} + {{ include "base" . }} zone: {{ .zone }} diff --git a/templates/definition/tariff/smartenergy.yaml b/templates/definition/tariff/smartenergy.yaml index 8ad4b9136..c9aebd287 100644 --- a/templates/definition/tariff/smartenergy.yaml +++ b/templates/definition/tariff/smartenergy.yaml @@ -11,4 +11,4 @@ params: - preset: tariff-base render: | type: smartenergy - {{ include "tariff-base" . }} + {{ include "base" . }} diff --git a/templates/definition/tariff/spottyenergy.yaml b/templates/definition/tariff/spottyenergy.yaml index cc61f1b63..baacdf00b 100644 --- a/templates/definition/tariff/spottyenergy.yaml +++ b/templates/definition/tariff/spottyenergy.yaml @@ -29,7 +29,7 @@ params: - preset: tariff-base render: | type: custom - {{ include "tariff-base" . }} + {{ include "base" . }} forecast: source: http uri: https://i.spottyenergie.at/api/prices/{{ .pricetype }}/{{ unquote .contractid }} diff --git a/templates/definition/tariff/stekker.yaml b/templates/definition/tariff/stekker.yaml index 6895f5242..15404489f 100644 --- a/templates/definition/tariff/stekker.yaml +++ b/templates/definition/tariff/stekker.yaml @@ -55,5 +55,5 @@ params: render: | type: stekker region: {{ .region }} - {{ include "tariff-base" . }} - {{ include "tariff-features" . }} + {{ include "base" . }} + {{ include "features" . }} diff --git a/templates/definition/tariff/stroomprijsprognose.yaml b/templates/definition/tariff/stroomprijsprognose.yaml index 0ecc96b0d..0e3d3fa5e 100644 --- a/templates/definition/tariff/stroomprijsprognose.yaml +++ b/templates/definition/tariff/stroomprijsprognose.yaml @@ -29,8 +29,8 @@ params: - preset: tariff-features render: | type: custom - {{ include "tariff-base" . }} - {{ include "tariff-features" . }} + {{ include "base" . }} + {{ include "features" . }} forecast: source: http uri: https://api.energypriceforecast.eu/api/v1/evcc/tariff?country={{.region}}&type=grid¤cy={{.currency}}&hours={{.horizon}} diff --git a/templates/definition/tariff/tibber.yaml b/templates/definition/tariff/tibber.yaml index 1180bf656..543a04534 100644 --- a/templates/definition/tariff/tibber.yaml +++ b/templates/definition/tariff/tibber.yaml @@ -26,4 +26,4 @@ render: | type: tibber token: {{ .token }} homeid: {{ .homeid }} - {{ include "tariff-base" . }} + {{ include "base" . }} diff --git a/templates/definition/tariff/voltcast.yaml b/templates/definition/tariff/voltcast.yaml index cdb8de1d4..5ac7d6370 100644 --- a/templates/definition/tariff/voltcast.yaml +++ b/templates/definition/tariff/voltcast.yaml @@ -74,8 +74,8 @@ params: render: | type: custom features: ["cacheable"] - {{ include "tariff-base" . }} - {{ include "tariff-features" . }} + {{ include "base" . }} + {{ include "features" . }} forecast: source: http uri: https://voltcast.com/api/v1/forecasts/{{ .zone }}?horizon=48h diff --git a/templates/definition/vehicle/aiways.yaml b/templates/definition/vehicle/aiways.yaml index a63913ab6..cbbd44ce3 100644 --- a/templates/definition/vehicle/aiways.yaml +++ b/templates/definition/vehicle/aiways.yaml @@ -8,5 +8,5 @@ params: required: true render: | type: aiways - {{ include "vehicle-base" . }} - {{ include "vehicle-features" . }} + {{ include "base" . }} + {{ include "features" . }} diff --git a/templates/definition/vehicle/alpine.yaml b/templates/definition/vehicle/alpine.yaml index bee710624..02ac911dc 100644 --- a/templates/definition/vehicle/alpine.yaml +++ b/templates/definition/vehicle/alpine.yaml @@ -6,5 +6,5 @@ params: - preset: vehicle-features render: | type: myalpine - {{ include "vehicle-base" . }} - {{ include "vehicle-features" . }} + {{ include "base" . }} + {{ include "features" . }} diff --git a/templates/definition/vehicle/audi.yaml b/templates/definition/vehicle/audi.yaml index 86ec3c24b..8e093c0e9 100644 --- a/templates/definition/vehicle/audi.yaml +++ b/templates/definition/vehicle/audi.yaml @@ -40,5 +40,5 @@ params: render: | type: drivesomethinggreater brand: Audi - {{ include "vehicle-base" . }} - {{ include "vehicle-features" (merge (dict "basefeatures" (list "streaming")) .) }} + {{ include "base" . }} + {{ include "features" (merge (dict "basefeatures" (list "streaming")) .) }} diff --git a/util/templates/includes/vehicle-base.tpl b/templates/definition/vehicle/base.tpl similarity index 65% rename from util/templates/includes/vehicle-base.tpl rename to templates/definition/vehicle/base.tpl index 45e98fd21..92a0bd544 100644 --- a/util/templates/includes/vehicle-base.tpl +++ b/templates/definition/vehicle/base.tpl @@ -1,8 +1,8 @@ -{{ define "vehicle-base" }} +{{ define "base" }} user: {{ .user }} password: {{ .password }} vin: {{ .vin }} -{{ template "vehicle-common" . }} +{{ template "common" . }} {{- if .cache }} cache: {{ .cache }} {{- end }} diff --git a/templates/definition/vehicle/bmw.yaml b/templates/definition/vehicle/bmw.yaml index 7fcc11acf..b9bb96f9b 100644 --- a/templates/definition/vehicle/bmw.yaml +++ b/templates/definition/vehicle/bmw.yaml @@ -29,9 +29,9 @@ params: - preset: vehicle-features render: | type: bmw - {{ include "vehicle-base" . }} + {{ include "base" . }} {{- if ne .region "EU" }} region: {{ .region }} {{- end }} - {{ include "vehicle-features" . }} + {{ include "features" . }} hcaptcha: {{ .hcaptcha }} diff --git a/templates/definition/vehicle/cardata.yaml b/templates/definition/vehicle/cardata.yaml index 34feb78f4..7506d507b 100644 --- a/templates/definition/vehicle/cardata.yaml +++ b/templates/definition/vehicle/cardata.yaml @@ -72,5 +72,5 @@ render: | type: cardata vin: {{ .vin }} clientid: {{ .clientid }} - {{ include "vehicle-common" . }} - {{ include "vehicle-features" . }} + {{ include "common" . }} + {{ include "features" . }} diff --git a/templates/definition/vehicle/carwings.yaml b/templates/definition/vehicle/carwings.yaml index c716f0be5..c623d0878 100644 --- a/templates/definition/vehicle/carwings.yaml +++ b/templates/definition/vehicle/carwings.yaml @@ -8,5 +8,5 @@ params: - preset: vehicle-online render: | type: carwings - {{ include "vehicle-base" . }} - {{ include "vehicle-features" . }} + {{ include "base" . }} + {{ include "features" . }} diff --git a/templates/definition/vehicle/citroen.yaml b/templates/definition/vehicle/citroen.yaml index 8d17ba686..d69dbc56a 100644 --- a/templates/definition/vehicle/citroen.yaml +++ b/templates/definition/vehicle/citroen.yaml @@ -29,6 +29,6 @@ render: | tokens: access: {{ .accessToken }} refresh: {{ .refreshToken }} - {{ include "vehicle-common" . }} + {{ include "common" . }} cache: {{ .cache }} - {{ include "vehicle-features" . }} + {{ include "features" . }} diff --git a/util/templates/includes/vehicle-common.tpl b/templates/definition/vehicle/common.tpl similarity index 95% rename from util/templates/includes/vehicle-common.tpl rename to templates/definition/vehicle/common.tpl index bad12e6e5..934d29838 100644 --- a/util/templates/includes/vehicle-common.tpl +++ b/templates/definition/vehicle/common.tpl @@ -1,4 +1,4 @@ -{{ define "vehicle-common" }} +{{ define "common" }} {{- if .title }} title: {{ .title }} {{- end }} diff --git a/templates/definition/vehicle/connected-cars.yaml b/templates/definition/vehicle/connected-cars.yaml index 55b753358..b9870ffba 100644 --- a/templates/definition/vehicle/connected-cars.yaml +++ b/templates/definition/vehicle/connected-cars.yaml @@ -40,6 +40,6 @@ render: | domain: {{ .domain }} namespace: {{ .namespace }} vin: {{ .vin }} - {{ include "vehicle-common" . }} + {{ include "common" . }} cache: {{ .cache }} - {{ include "vehicle-features" . }} + {{ include "features" . }} diff --git a/templates/definition/vehicle/dacia.yaml b/templates/definition/vehicle/dacia.yaml index 097fda573..7708f3bfc 100644 --- a/templates/definition/vehicle/dacia.yaml +++ b/templates/definition/vehicle/dacia.yaml @@ -6,5 +6,5 @@ params: - preset: vehicle-features render: | type: dacia - {{ include "vehicle-base" . }} - {{ include "vehicle-features" . }} + {{ include "base" . }} + {{ include "features" . }} diff --git a/templates/definition/vehicle/drivesomethinggreater.yaml b/templates/definition/vehicle/drivesomethinggreater.yaml index 98030744a..551aa0371 100644 --- a/templates/definition/vehicle/drivesomethinggreater.yaml +++ b/templates/definition/vehicle/drivesomethinggreater.yaml @@ -58,5 +58,5 @@ params: render: | type: drivesomethinggreater brand: {{ .brand }} - {{ include "vehicle-base" . }} - {{ include "vehicle-features" (merge (dict "basefeatures" (list "streaming")) .) }} + {{ include "base" . }} + {{ include "features" (merge (dict "basefeatures" (list "streaming")) .) }} diff --git a/templates/definition/vehicle/ds.yaml b/templates/definition/vehicle/ds.yaml index 1abbf6443..ece471620 100644 --- a/templates/definition/vehicle/ds.yaml +++ b/templates/definition/vehicle/ds.yaml @@ -29,6 +29,6 @@ render: | tokens: access: {{ .accessToken }} refresh: {{ .refreshToken }} - {{ include "vehicle-common" . }} + {{ include "common" . }} cache: {{ .cache }} - {{ include "vehicle-features" . }} + {{ include "features" . }} diff --git a/templates/definition/vehicle/evnotify.yaml b/templates/definition/vehicle/evnotify.yaml index 4c6d79f52..536904606 100644 --- a/templates/definition/vehicle/evnotify.yaml +++ b/templates/definition/vehicle/evnotify.yaml @@ -21,7 +21,7 @@ params: - preset: vehicle-online render: | type: custom - {{ include "vehicle-common" . }} + {{ include "common" . }} {{- if eq .apiversion "3" }} soc: source: http @@ -45,4 +45,4 @@ render: | uri: https://app.evnotify.de/extended?akey={{ urlEncode .akey }}&token={{ urlEncode .token }} jq: .charging {{- end }} - {{ include "vehicle-features" . }} + {{ include "features" . }} diff --git a/util/templates/includes/vehicle-features.tpl b/templates/definition/vehicle/features.tpl similarity index 95% rename from util/templates/includes/vehicle-features.tpl rename to templates/definition/vehicle/features.tpl index f7deac5b5..7324a1b53 100644 --- a/util/templates/includes/vehicle-features.tpl +++ b/templates/definition/vehicle/features.tpl @@ -1,4 +1,4 @@ -{{ define "vehicle-features" }} +{{ define "features" }} {{- if or .basefeatures (eq .coarsecurrent "true") (eq .welcomecharge "true") (eq .streaming "true") (eq .climaterdisabled "true") (eq .autodetectdisabled "true") (eq .wakeupdisabled "true") }} features: {{- range .basefeatures }} diff --git a/templates/definition/vehicle/fiat.yaml b/templates/definition/vehicle/fiat.yaml index b1db92d85..d633ee235 100644 --- a/templates/definition/vehicle/fiat.yaml +++ b/templates/definition/vehicle/fiat.yaml @@ -13,8 +13,8 @@ params: - preset: vehicle-features render: | type: fiat - {{ include "vehicle-base" . }} + {{ include "base" . }} {{- if .pin }} pin: {{ .pin }} # mandatory to wake up, deep refresh Soc & start/stop charge {{- end }} - {{ include "vehicle-features" . }} + {{ include "features" . }} diff --git a/templates/definition/vehicle/flobz.yaml b/templates/definition/vehicle/flobz.yaml index ebeb35bd7..98cc720e5 100644 --- a/templates/definition/vehicle/flobz.yaml +++ b/templates/definition/vehicle/flobz.yaml @@ -29,7 +29,7 @@ params: advanced: true render: | type: custom - {{- include "vehicle-common" . }} + {{- include "common" . }} soc: {{- include "source" . | indent 2 }} jq: .energy[0].level @@ -73,4 +73,4 @@ render: | uri: {{ trimSuffix "/" .url }}/get_vehicleinfo/{{ .vin }}?from_cache=1 {{- end }} {{- end }} - {{ include "vehicle-features" . }} + {{ include "features" . }} diff --git a/templates/definition/vehicle/ford-connect-query.yaml b/templates/definition/vehicle/ford-connect-query.yaml index bb0944d88..1bb2c4f87 100644 --- a/templates/definition/vehicle/ford-connect-query.yaml +++ b/templates/definition/vehicle/ford-connect-query.yaml @@ -39,6 +39,6 @@ render: | id: {{ .clientid }} secret: {{ .clientsecret }} redirecturi: {{ .redirecturi }} - {{ include "vehicle-common" . }} + {{ include "common" . }} cache: {{ .cache }} - {{ include "vehicle-features" . }} + {{ include "features" . }} diff --git a/templates/definition/vehicle/ford-connect.yaml b/templates/definition/vehicle/ford-connect.yaml index 96fa6dc58..5451716fe 100644 --- a/templates/definition/vehicle/ford-connect.yaml +++ b/templates/definition/vehicle/ford-connect.yaml @@ -36,6 +36,6 @@ render: | tokens: access: {{ .accessToken }} refresh: {{ .refreshToken }} - {{ include "vehicle-common" . }} + {{ include "common" . }} cache: {{ .cache }} - {{ include "vehicle-features" . }} + {{ include "features" . }} diff --git a/templates/definition/vehicle/genesis.yaml b/templates/definition/vehicle/genesis.yaml index 84df535a2..d61c21e18 100644 --- a/templates/definition/vehicle/genesis.yaml +++ b/templates/definition/vehicle/genesis.yaml @@ -15,6 +15,6 @@ params: - preset: vehicle-language render: | type: genesis - {{ include "vehicle-base" . }} - {{ include "vehicle-language" . }} - {{ include "vehicle-features" . }} + {{ include "base" . }} + {{ include "language" . }} + {{ include "features" . }} diff --git a/templates/definition/vehicle/homeassistant.yaml b/templates/definition/vehicle/homeassistant.yaml index 00be34be4..191c079da 100644 --- a/templates/definition/vehicle/homeassistant.yaml +++ b/templates/definition/vehicle/homeassistant.yaml @@ -158,8 +158,8 @@ params: default: true render: | type: homeassistant - {{ include "vehicle-common" . }} - {{ include "vehicle-features" . }} + {{ include "common" . }} + {{ include "features" . }} uri: {{ .uri }} home: {{ .home }} # deprecated insecure: {{ .insecure }} diff --git a/templates/definition/vehicle/hyundai-us.yaml b/templates/definition/vehicle/hyundai-us.yaml index f17e1bc6a..0ec72746c 100644 --- a/templates/definition/vehicle/hyundai-us.yaml +++ b/templates/definition/vehicle/hyundai-us.yaml @@ -13,6 +13,6 @@ params: de: Wird von Bluelink (US) benötigt, um Fernkommandos zu autorisieren. render: | type: hyundai-us - {{ include "vehicle-base" . }} + {{ include "base" . }} pin: {{ quote .pin }} - {{ include "vehicle-features" . }} + {{ include "features" . }} diff --git a/templates/definition/vehicle/hyundai.yaml b/templates/definition/vehicle/hyundai.yaml index a7240d6a6..fc40a78b9 100644 --- a/templates/definition/vehicle/hyundai.yaml +++ b/templates/definition/vehicle/hyundai.yaml @@ -26,6 +26,6 @@ params: render: | type: hyundai region: {{ .region }} - {{ include "vehicle-base" . }} - {{ include "vehicle-language" . }} - {{ include "vehicle-features" . }} + {{ include "base" . }} + {{ include "language" . }} + {{ include "features" . }} diff --git a/templates/definition/vehicle/ioBroker.bmw.yaml b/templates/definition/vehicle/ioBroker.bmw.yaml index caed183b7..233b66508 100644 --- a/templates/definition/vehicle/ioBroker.bmw.yaml +++ b/templates/definition/vehicle/ioBroker.bmw.yaml @@ -34,7 +34,7 @@ params: default: true render: | type: custom - {{ include "vehicle-common" . }} + {{ include "common" . }} soc: source: http uri: {{ .uri }}/getPlainValue/bmw.{{ .id }}.{{ .vin }}.stream.vehicle.drivetrain.batteryManagement.header.value?noStringify @@ -65,4 +65,4 @@ render: | source: http uri: {{ .uri }}/getPlainValue/bmw.{{ .id }}.{{ .vin }}.stream.vehicle.drivetrain.electricEngine.charging.timeRemaining.value?noStringify jq: if (. == null) or (. == 0) or (type != "number") or (. < 0) then null else (now + (. * 60) | strftime("%Y-%m-%dT%H:%M:%SZ")) end - {{ include "vehicle-features" . }} + {{ include "features" . }} diff --git a/templates/definition/vehicle/iso15118.yaml b/templates/definition/vehicle/iso15118.yaml index 3e44b5e06..6a148489f 100644 --- a/templates/definition/vehicle/iso15118.yaml +++ b/templates/definition/vehicle/iso15118.yaml @@ -24,8 +24,8 @@ params: deprecated: true render: | type: custom - {{ include "vehicle-common" . }} - {{ include "vehicle-features" (merge (dict "basefeatures" (list "offline")) .) }} + {{ include "common" . }} + {{ include "features" (merge (dict "basefeatures" (list "offline")) .) }} soc: source: const value: 0 diff --git a/templates/definition/vehicle/jaguar-landrover.yaml b/templates/definition/vehicle/jaguar-landrover.yaml index 6097ec09b..0692b5638 100644 --- a/templates/definition/vehicle/jaguar-landrover.yaml +++ b/templates/definition/vehicle/jaguar-landrover.yaml @@ -8,5 +8,5 @@ params: - preset: vehicle-online render: | type: jaguar - {{ include "vehicle-base" . }} - {{ include "vehicle-features" . }} + {{ include "base" . }} + {{ include "features" . }} diff --git a/templates/definition/vehicle/kia.yaml b/templates/definition/vehicle/kia.yaml index 0571afba2..3b6bc5f00 100644 --- a/templates/definition/vehicle/kia.yaml +++ b/templates/definition/vehicle/kia.yaml @@ -19,6 +19,6 @@ params: - preset: vehicle-language render: | type: kia - {{ include "vehicle-base" . }} - {{ include "vehicle-language" . }} - {{ include "vehicle-features" . }} + {{ include "base" . }} + {{ include "language" . }} + {{ include "features" . }} diff --git a/util/templates/includes/vehicle-language.tpl b/templates/definition/vehicle/language.tpl similarity index 53% rename from util/templates/includes/vehicle-language.tpl rename to templates/definition/vehicle/language.tpl index 03f37104d..260b2b194 100644 --- a/util/templates/includes/vehicle-language.tpl +++ b/templates/definition/vehicle/language.tpl @@ -1,3 +1,3 @@ -{{ define "vehicle-language" }} +{{ define "language" }} language: {{ .language }} {{- end }} diff --git a/templates/definition/vehicle/lexus.yaml b/templates/definition/vehicle/lexus.yaml index 8549c29b2..d1d32a250 100644 --- a/templates/definition/vehicle/lexus.yaml +++ b/templates/definition/vehicle/lexus.yaml @@ -20,9 +20,9 @@ params: default: 15m render: | type: lexus - {{ include "vehicle-common" . }} + {{ include "common" . }} user: {{ .user }} password: {{ .password }} vin: {{ .vin }} cache: {{ .cache }} - {{ include "vehicle-features" . }} + {{ include "features" . }} diff --git a/templates/definition/vehicle/mazda2mqtt.yaml b/templates/definition/vehicle/mazda2mqtt.yaml index a50323342..ad3305b9f 100644 --- a/templates/definition/vehicle/mazda2mqtt.yaml +++ b/templates/definition/vehicle/mazda2mqtt.yaml @@ -17,7 +17,7 @@ params: default: 720h render: | type: custom - {{- include "vehicle-common" . }} + {{- include "common" . }} soc: source: mqtt topic: mazda2mqtt/{{ .vin }}/chargeInfo/batteryLevelPercentage @@ -36,4 +36,4 @@ render: | source: mqtt topic: mazda2mqtt/{{ .vin }}/chargeInfo/drivingRangeKm timeout: {{ .timeout }} - {{ include "vehicle-features" (merge (dict "basefeatures" (list "streaming")) .) }} + {{ include "features" (merge (dict "basefeatures" (list "streaming")) .) }} diff --git a/templates/definition/vehicle/mercedes.yaml b/templates/definition/vehicle/mercedes.yaml index 68937632c..3eb57b007 100644 --- a/templates/definition/vehicle/mercedes.yaml +++ b/templates/definition/vehicle/mercedes.yaml @@ -33,5 +33,5 @@ render: | tokens: access: {{ .accessToken }} refresh: {{ .refreshToken }} - {{ include "vehicle-common" . }} - {{ include "vehicle-features" . }} + {{ include "common" . }} + {{ include "features" . }} diff --git a/templates/definition/vehicle/mg.yaml b/templates/definition/vehicle/mg.yaml index f3b2bef54..b0e40a5e5 100644 --- a/templates/definition/vehicle/mg.yaml +++ b/templates/definition/vehicle/mg.yaml @@ -17,6 +17,6 @@ params: advanced: true render: | type: mg - {{ include "vehicle-base" . }} + {{ include "base" . }} region: {{ .region }} - {{ include "vehicle-features" . }} + {{ include "features" . }} diff --git a/templates/definition/vehicle/mg2mqtt.yaml b/templates/definition/vehicle/mg2mqtt.yaml index 4ca66c9cb..e788d21f7 100644 --- a/templates/definition/vehicle/mg2mqtt.yaml +++ b/templates/definition/vehicle/mg2mqtt.yaml @@ -18,7 +18,7 @@ params: default: 1h render: | type: custom - {{- include "vehicle-common" . }} + {{- include "common" . }} soc: # battery soc (%) source: mqtt topic: saic/{{ .user }}/vehicles/{{ .vin }}/drivetrain/soc @@ -63,4 +63,4 @@ render: | source: mqtt topic: saic/{{ .user }}/vehicles/{{ .vin }}/refresh/mode/set timeout: {{ .timeout }} - {{ include "vehicle-features" . }} + {{ include "features" . }} diff --git a/templates/definition/vehicle/mini.yaml b/templates/definition/vehicle/mini.yaml index 02e595ebf..bb70fb3d9 100644 --- a/templates/definition/vehicle/mini.yaml +++ b/templates/definition/vehicle/mini.yaml @@ -29,9 +29,9 @@ params: - preset: vehicle-features render: | type: mini - {{ include "vehicle-base" . }} + {{ include "base" . }} {{- if ne .region "EU" }} region: {{ .region }} {{- end }} hcaptcha: {{ .hcaptcha }} - {{ include "vehicle-features" . }} + {{ include "features" . }} diff --git a/templates/definition/vehicle/mz2mqtt.yaml b/templates/definition/vehicle/mz2mqtt.yaml index e9c4d0a21..29a1c8b32 100644 --- a/templates/definition/vehicle/mz2mqtt.yaml +++ b/templates/definition/vehicle/mz2mqtt.yaml @@ -18,7 +18,7 @@ params: advanced: true render: | type: custom - {{- include "vehicle-common" . }} + {{- include "common" . }} soc: source: mqtt topic: mz2mqtt/{{ .vin }}/chargeInfo/batteryLevelPercentage @@ -37,4 +37,4 @@ render: | source: mqtt topic: mz2mqtt/{{ .vin }}/chargeInfo/drivingRangeKm timeout: {{ .timeout }} - {{ include "vehicle-features" . }} + {{ include "features" . }} diff --git a/templates/definition/vehicle/nissan-ariya.yaml b/templates/definition/vehicle/nissan-ariya.yaml index b470d1623..53b57c786 100644 --- a/templates/definition/vehicle/nissan-ariya.yaml +++ b/templates/definition/vehicle/nissan-ariya.yaml @@ -22,6 +22,6 @@ params: render: | type: nissan version: v2 - {{ include "vehicle-base" . }} + {{ include "base" . }} expiry: {{ .expiry }} - {{ include "vehicle-features" . }} + {{ include "features" . }} diff --git a/templates/definition/vehicle/nissan.yaml b/templates/definition/vehicle/nissan.yaml index df93545b9..ad06f7a61 100644 --- a/templates/definition/vehicle/nissan.yaml +++ b/templates/definition/vehicle/nissan.yaml @@ -8,5 +8,5 @@ params: - preset: vehicle-online render: | type: nissan - {{ include "vehicle-base" . }} - {{ include "vehicle-features" . }} + {{ include "base" . }} + {{ include "features" . }} diff --git a/templates/definition/vehicle/niu-e-scooter.yaml b/templates/definition/vehicle/niu-e-scooter.yaml index fdd3059aa..d3230a633 100644 --- a/templates/definition/vehicle/niu-e-scooter.yaml +++ b/templates/definition/vehicle/niu-e-scooter.yaml @@ -32,4 +32,4 @@ render: | password: {{ .password }} # NIU app password serial: {{ .serial }} # NIU E-Scooter serial number like shown in app capacity: {{ .capacity }} - {{ include "vehicle-features" . }} + {{ include "features" . }} diff --git a/templates/definition/vehicle/octopus-de.yaml b/templates/definition/vehicle/octopus-de.yaml index 5e4d45242..d333731e8 100644 --- a/templates/definition/vehicle/octopus-de.yaml +++ b/templates/definition/vehicle/octopus-de.yaml @@ -44,10 +44,10 @@ params: default: 15m render: | type: octopus-de - {{- include "vehicle-common" . }} + {{- include "common" . }} email: {{ .email }} password: {{ .password }} accountnumber: {{ .accountnumber }} device: {{ .device }} cache: {{ .cache }} - {{ include "vehicle-features" . }} + {{ include "features" . }} diff --git a/templates/definition/vehicle/octopus-it.yaml b/templates/definition/vehicle/octopus-it.yaml index 41394184c..c9bc30a47 100644 --- a/templates/definition/vehicle/octopus-it.yaml +++ b/templates/definition/vehicle/octopus-it.yaml @@ -44,10 +44,10 @@ params: default: 15m render: | type: octopus-it - {{- include "vehicle-common" . }} + {{- include "common" . }} email: {{ .email }} password: {{ .password }} accountnumber: {{ .accountnumber }} device: {{ .device }} cache: {{ .cache }} - {{ include "vehicle-features" . }} + {{ include "features" . }} diff --git a/templates/definition/vehicle/offline.yaml b/templates/definition/vehicle/offline.yaml index 6437c90e2..c4ce259ec 100644 --- a/templates/definition/vehicle/offline.yaml +++ b/templates/definition/vehicle/offline.yaml @@ -18,8 +18,8 @@ params: deprecated: true render: | type: custom - {{- include "vehicle-common" . }} - {{ include "vehicle-features" (merge (dict "basefeatures" (list "offline")) .) }} + {{- include "common" . }} + {{ include "features" (merge (dict "basefeatures" (list "offline")) .) }} soc: source: const value: 0 diff --git a/templates/definition/vehicle/opel.yaml b/templates/definition/vehicle/opel.yaml index 405d76bc9..74751a541 100644 --- a/templates/definition/vehicle/opel.yaml +++ b/templates/definition/vehicle/opel.yaml @@ -29,6 +29,6 @@ render: | tokens: access: {{ .accessToken }} refresh: {{ .refreshToken }} - {{ include "vehicle-common" . }} + {{ include "common" . }} cache: {{ .cache }} - {{ include "vehicle-features" . }} + {{ include "features" . }} diff --git a/templates/definition/vehicle/outlanderphev.yaml b/templates/definition/vehicle/outlanderphev.yaml index 0deae5558..c6403295d 100644 --- a/templates/definition/vehicle/outlanderphev.yaml +++ b/templates/definition/vehicle/outlanderphev.yaml @@ -15,7 +15,7 @@ params: advanced: true render: | type: custom - {{- include "vehicle-common" . }} + {{- include "common" . }} soc: source: mqtt topic: phev/battery/level @@ -38,4 +38,4 @@ render: | topic: phev/climate/status timeout: {{ .timeout }} jq: (. != "off") - {{ include "vehicle-features" . }} + {{ include "features" . }} diff --git a/templates/definition/vehicle/ovms.yaml b/templates/definition/vehicle/ovms.yaml index 37de9a0d3..88891771d 100644 --- a/templates/definition/vehicle/ovms.yaml +++ b/templates/definition/vehicle/ovms.yaml @@ -28,10 +28,10 @@ params: default: 15m render: | type: ovms - {{- include "vehicle-common" . }} + {{- include "common" . }} user: {{ .user }} password: {{ .password }} vehicleid: {{ .vehicleid }} server: {{ .server }} cache: {{ .cache }} - {{ include "vehicle-features" . }} + {{ include "features" . }} diff --git a/templates/definition/vehicle/peugeot.yaml b/templates/definition/vehicle/peugeot.yaml index d88bfb319..989c27e07 100644 --- a/templates/definition/vehicle/peugeot.yaml +++ b/templates/definition/vehicle/peugeot.yaml @@ -29,6 +29,6 @@ render: | tokens: access: {{ .accessToken }} refresh: {{ .refreshToken }} - {{ include "vehicle-common" . }} + {{ include "common" . }} cache: {{ .cache }} - {{ include "vehicle-features" . }} + {{ include "features" . }} diff --git a/templates/definition/vehicle/polestar.yaml b/templates/definition/vehicle/polestar.yaml index 5d906ca53..3d246bd06 100644 --- a/templates/definition/vehicle/polestar.yaml +++ b/templates/definition/vehicle/polestar.yaml @@ -10,5 +10,5 @@ params: example: LPSVS... render: | type: polestar-grpc - {{ include "vehicle-base" . }} - {{ include "vehicle-features" . }} + {{ include "base" . }} + {{ include "features" . }} diff --git a/templates/definition/vehicle/porsche.yaml b/templates/definition/vehicle/porsche.yaml index a71b44582..bd28f3168 100644 --- a/templates/definition/vehicle/porsche.yaml +++ b/templates/definition/vehicle/porsche.yaml @@ -7,5 +7,5 @@ params: - preset: vehicle-online render: | type: porsche - {{ include "vehicle-base" . }} - {{ include "vehicle-features" . }} + {{ include "base" . }} + {{ include "features" . }} diff --git a/templates/definition/vehicle/renault.yaml b/templates/definition/vehicle/renault.yaml index d3f103d53..9eb52d1f6 100644 --- a/templates/definition/vehicle/renault.yaml +++ b/templates/definition/vehicle/renault.yaml @@ -27,6 +27,6 @@ params: advanced: true render: | type: renault - {{ include "vehicle-base" . }} - {{ include "vehicle-features" . }} + {{ include "base" . }} + {{ include "features" . }} wakeupmode: {{ .wakeupmode }} diff --git a/templates/definition/vehicle/seat-cupra.yaml b/templates/definition/vehicle/seat-cupra.yaml index b4df187f8..06fa4528a 100644 --- a/templates/definition/vehicle/seat-cupra.yaml +++ b/templates/definition/vehicle/seat-cupra.yaml @@ -35,5 +35,5 @@ params: render: | type: drivesomethinggreater brand: Cupra - {{ include "vehicle-base" . }} - {{ include "vehicle-features" (merge (dict "basefeatures" (list "streaming")) .) }} + {{ include "base" . }} + {{ include "features" (merge (dict "basefeatures" (list "streaming")) .) }} diff --git a/templates/definition/vehicle/seat.yaml b/templates/definition/vehicle/seat.yaml index 539140f29..b448698ab 100644 --- a/templates/definition/vehicle/seat.yaml +++ b/templates/definition/vehicle/seat.yaml @@ -37,5 +37,5 @@ params: render: | type: drivesomethinggreater brand: Seat - {{ include "vehicle-base" . }} - {{ include "vehicle-features" (merge (dict "basefeatures" (list "streaming")) .) }} + {{ include "base" . }} + {{ include "features" (merge (dict "basefeatures" (list "streaming")) .) }} diff --git a/templates/definition/vehicle/skoda.yaml b/templates/definition/vehicle/skoda.yaml index 6b7ea1df3..0f8571ce1 100644 --- a/templates/definition/vehicle/skoda.yaml +++ b/templates/definition/vehicle/skoda.yaml @@ -12,6 +12,6 @@ params: - name: timeout render: | type: skoda - {{ include "vehicle-base" . }} + {{ include "base" . }} timeout: {{ .timeout }} - {{ include "vehicle-features" . }} + {{ include "features" . }} diff --git a/templates/definition/vehicle/smart-hello.yaml b/templates/definition/vehicle/smart-hello.yaml index ac9c56365..d0627b247 100644 --- a/templates/definition/vehicle/smart-hello.yaml +++ b/templates/definition/vehicle/smart-hello.yaml @@ -8,5 +8,5 @@ params: - preset: vehicle-features render: | type: smart-hello - {{ include "vehicle-base" . }} - {{ include "vehicle-features" . }} + {{ include "base" . }} + {{ include "features" . }} diff --git a/templates/definition/vehicle/smart.yaml b/templates/definition/vehicle/smart.yaml index 8e6bfecf6..7c1c71b28 100644 --- a/templates/definition/vehicle/smart.yaml +++ b/templates/definition/vehicle/smart.yaml @@ -35,5 +35,5 @@ render: | tokens: access: {{ .accessToken }} refresh: {{ .refreshToken }} - {{ include "vehicle-common" . }} - {{ include "vehicle-features" . }} + {{ include "common" . }} + {{ include "features" . }} diff --git a/templates/definition/vehicle/subaru.yaml b/templates/definition/vehicle/subaru.yaml index c8b8de379..aa9b9590c 100644 --- a/templates/definition/vehicle/subaru.yaml +++ b/templates/definition/vehicle/subaru.yaml @@ -12,5 +12,5 @@ params: example: JF... render: | type: subaru - {{ include "vehicle-base" . }} - {{ include "vehicle-features" . }} + {{ include "base" . }} + {{ include "features" . }} diff --git a/templates/definition/vehicle/tesla-ble.yaml b/templates/definition/vehicle/tesla-ble.yaml index 705fdcbca..11c64c607 100644 --- a/templates/definition/vehicle/tesla-ble.yaml +++ b/templates/definition/vehicle/tesla-ble.yaml @@ -31,7 +31,7 @@ params: default: 30s render: | type: custom - {{- include "vehicle-common" . }} + {{- include "common" . }} chargeEnable: source: http uri: {{ .url }}:{{ .port }}/api/1/vehicles/{{ .vin }}/command/{{`{{ if .chargeenable }}charge_start{{ else }}charge_stop{{ end }}`}} @@ -86,4 +86,4 @@ render: | else "A" end) timeout: {{ .timeout }} cache: 1s - {{ include "vehicle-features" . }} + {{ include "features" . }} diff --git a/templates/definition/vehicle/tesla.yaml b/templates/definition/vehicle/tesla.yaml index 2663b0ae0..435fda714 100644 --- a/templates/definition/vehicle/tesla.yaml +++ b/templates/definition/vehicle/tesla.yaml @@ -84,6 +84,6 @@ render: | refresh: {{ .refreshToken }} commandProxy: {{ .commandProxy }} proxyToken: {{ .proxyToken }} - {{ include "vehicle-common" . }} - {{ include "vehicle-features" (merge (dict "basefeatures" (list "coarsecurrent")) .) }} + {{ include "common" . }} + {{ include "features" (merge (dict "basefeatures" (list "coarsecurrent")) .) }} cache: {{ .cache }} diff --git a/templates/definition/vehicle/teslafi.yaml b/templates/definition/vehicle/teslafi.yaml index c10a6e045..f19afc6fb 100644 --- a/templates/definition/vehicle/teslafi.yaml +++ b/templates/definition/vehicle/teslafi.yaml @@ -16,7 +16,7 @@ params: advanced: true render: | type: custom - {{- include "vehicle-common" . }} + {{- include "common" . }} {{- define "teslafi_source" }} source: http uri: https://www.teslafi.com/feed.php?command=lastGood{{ if .vin }}&vin={{ .vin }}{{ end }} @@ -44,4 +44,4 @@ render: | limitsoc: {{- include "teslafi_source" . | indent 2 }} jq: .charge_limit_soc - {{ include "vehicle-features" (merge (dict "basefeatures" (list "coarsecurrent")) .) }} + {{ include "features" (merge (dict "basefeatures" (list "coarsecurrent")) .) }} diff --git a/templates/definition/vehicle/teslalogger.yaml b/templates/definition/vehicle/teslalogger.yaml index 6e2a85ab3..0be265f1d 100644 --- a/templates/definition/vehicle/teslalogger.yaml +++ b/templates/definition/vehicle/teslalogger.yaml @@ -22,7 +22,7 @@ params: default: 5000 render: | type: custom - {{- include "vehicle-common" . }} + {{- include "common" . }} soc: # battery soc (%) source: http uri: {{ .url }}:{{ .port }}/currentjson/{{ .id }} @@ -66,4 +66,4 @@ render: | maxcurrent: source: http uri: {{ .url }}:{{ .port }}/command/{{ .id }}/set_charging_amps?${maxcurrent} - {{ include "vehicle-features" . }} + {{ include "features" . }} diff --git a/templates/definition/vehicle/teslamate.yaml b/templates/definition/vehicle/teslamate.yaml index 2a680ab5c..825465d42 100644 --- a/templates/definition/vehicle/teslamate.yaml +++ b/templates/definition/vehicle/teslamate.yaml @@ -19,7 +19,7 @@ params: default: 720h # 30d render: | type: custom - {{- include "vehicle-common" . }} + {{- include "common" . }} soc: source: mqtt topic: teslamate/cars/{{ .id }}/usable_battery_level @@ -69,4 +69,4 @@ render: | source: mqtt topic: teslamate/cars/{{ .id }}/is_preconditioning timeout: {{ .timeout }} - {{ include "vehicle-features" (merge (dict "basefeatures" (list "coarsecurrent")) .) }} + {{ include "features" (merge (dict "basefeatures" (list "coarsecurrent")) .) }} diff --git a/templates/definition/vehicle/tessie.yaml b/templates/definition/vehicle/tessie.yaml index 1029846a6..5fb094915 100644 --- a/templates/definition/vehicle/tessie.yaml +++ b/templates/definition/vehicle/tessie.yaml @@ -22,7 +22,7 @@ params: required: true render: | type: custom - {{- include "vehicle-common" . }} + {{- include "common" . }} soc: # battery state of charge (%) source: http uri: https://api.tessie.com/{{ .vin }}/state?use_cache=true @@ -116,4 +116,4 @@ render: | headers: Authorization: Bearer {{ .token }} method: POST - {{ include "vehicle-features" (merge (dict "basefeatures" (list "coarsecurrent")) .) }} + {{ include "features" (merge (dict "basefeatures" (list "coarsecurrent")) .) }} diff --git a/templates/definition/vehicle/tibber.yaml b/templates/definition/vehicle/tibber.yaml index d3a089f83..e93b5d0c3 100644 --- a/templates/definition/vehicle/tibber.yaml +++ b/templates/definition/vehicle/tibber.yaml @@ -39,10 +39,10 @@ auth: params: [clientid, clientsecret, redirecturi] render: | type: tibber - {{- include "vehicle-common" . }} + {{- include "common" . }} clientid: {{ .clientid }} clientsecret: {{ .clientsecret }} redirecturi: {{ .redirecturi }} vin: {{ .vin }} cache: {{ .cache }} - {{ include "vehicle-features" . }} + {{ include "features" . }} diff --git a/templates/definition/vehicle/toyota.yaml b/templates/definition/vehicle/toyota.yaml index eda7fe0e9..084fe0825 100644 --- a/templates/definition/vehicle/toyota.yaml +++ b/templates/definition/vehicle/toyota.yaml @@ -20,9 +20,9 @@ params: default: 15m render: | type: toyota - {{ include "vehicle-common" . }} + {{ include "common" . }} user: {{ .user }} password: {{ .password }} vin: {{ .vin }} cache: {{ .cache }} - {{ include "vehicle-features" . }} + {{ include "features" . }} diff --git a/templates/definition/vehicle/tronity.yaml b/templates/definition/vehicle/tronity.yaml index c3efc0052..cdf1390ac 100644 --- a/templates/definition/vehicle/tronity.yaml +++ b/templates/definition/vehicle/tronity.yaml @@ -29,6 +29,6 @@ render: | credentials: id: {{ .clientid }} secret: {{ .clientsecret }} - {{ include "vehicle-common" . }} + {{ include "common" . }} cache: {{ .cache }} - {{ include "vehicle-features" . }} + {{ include "features" . }} diff --git a/templates/definition/vehicle/volvo-connected.yaml b/templates/definition/vehicle/volvo-connected.yaml index cac6429c1..9ade34afc 100644 --- a/templates/definition/vehicle/volvo-connected.yaml +++ b/templates/definition/vehicle/volvo-connected.yaml @@ -64,5 +64,5 @@ render: | secret: {{ .clientSecret }} redirecturi: {{ .redirectUri }} vin: {{ .vin }} - {{ include "vehicle-common" . }} - {{ include "vehicle-features" . }} + {{ include "common" . }} + {{ include "features" . }} diff --git a/templates/definition/vehicle/volvo2mqtt.yaml b/templates/definition/vehicle/volvo2mqtt.yaml index bbf79faed..55c900ebf 100644 --- a/templates/definition/vehicle/volvo2mqtt.yaml +++ b/templates/definition/vehicle/volvo2mqtt.yaml @@ -17,7 +17,7 @@ params: default: 720h render: | type: custom - {{- include "vehicle-common" . }} + {{- include "common" . }} soc: source: mqtt topic: homeassistant/sensor/{{ .vin }}_battery_charge_level/state @@ -26,4 +26,4 @@ render: | source: mqtt topic: homeassistant/sensor/{{ .vin }}_electric_range/state timeout: {{ .timeout }} - {{ include "vehicle-features" . }} + {{ include "features" . }} diff --git a/templates/definition/vehicle/vw.yaml b/templates/definition/vehicle/vw.yaml index 31c5ead90..319984606 100644 --- a/templates/definition/vehicle/vw.yaml +++ b/templates/definition/vehicle/vw.yaml @@ -44,5 +44,5 @@ params: render: | type: drivesomethinggreater brand: Volkswagen - {{ include "vehicle-base" . }} - {{ include "vehicle-features" (merge (dict "basefeatures" (list "streaming")) .) }} + {{ include "base" . }} + {{ include "features" (merge (dict "basefeatures" (list "streaming")) .) }} diff --git a/templates/definition/vehicle/zero.yaml b/templates/definition/vehicle/zero.yaml index 00010b7f1..081ef832c 100644 --- a/templates/definition/vehicle/zero.yaml +++ b/templates/definition/vehicle/zero.yaml @@ -6,5 +6,5 @@ params: - preset: vehicle-online render: | type: zero - {{ include "vehicle-base" . }} - {{ include "vehicle-features" . }} + {{ include "base" . }} + {{ include "features" . }} diff --git a/util/templates/formatvalue_test.go b/util/templates/formatvalue_test.go index 0e0c34493..2d9da377d 100644 --- a/util/templates/formatvalue_test.go +++ b/util/templates/formatvalue_test.go @@ -31,7 +31,7 @@ func TestRequiredLargeNumber(t *testing.T) { Params: []Param{{Name: "serial", Type: TypeInt, Required: true}}, } - _, _, err := tmpl.RenderResult(RenderModeUnitTest, map[string]any{ + _, _, err := tmpl.RenderResult(Meter, RenderModeUnitTest, map[string]any{ "serial": float64(3493601102), }) assert.NoError(t, err, "large serial supplied as JSON number") diff --git a/util/templates/init.go b/util/templates/init.go index 5e9cc3165..132f7847f 100644 --- a/util/templates/init.go +++ b/util/templates/init.go @@ -15,6 +15,9 @@ import ( "go.yaml.in/yaml/v4" ) +// IncludeExt is the file extension of class-local, include-only templates +const IncludeExt = ".tpl" + var ( //go:embed includes/*.tpl includeFS embed.FS @@ -22,6 +25,9 @@ var ( // baseTmpl holds all included template definitions baseTmpl *template.Template + // classTmpl holds baseTmpl plus the class-local include definitions + classTmpl = make(map[Class]*template.Template) + templates = make(map[Class][]Template) ConfigDefaults configDefaults mu sync.Mutex @@ -31,13 +37,35 @@ var ( func init() { ConfigDefaults.Load() - baseTmpl = template.Must(FuncMap(template.New("base")).ParseFS(includeFS, "includes/*.tpl")) + // the root template is replaced by the rendered template's body, so its name + // must not collide with an include name (`$` cannot appear in a file name) + baseTmpl = template.Must(FuncMap(template.New("$root")).ParseFS(includeFS, "includes/*.tpl")) for _, class := range []Class{Charger, Meter, Vehicle, Tariff, Messenger, Circuit, Hems} { + if err := loadIncludes(definition.TemplateIncludes, class); err != nil { + panic(err) + } load(class) } } +// loadIncludes parses the class-local include-only templates +func loadIncludes(fsys fs.FS, class Class) error { + files, err := fs.Glob(fsys, class.String()+"/*"+IncludeExt) + if err != nil || len(files) == 0 { + return err + } + + tmpl, err := FuncMap(template.Must(baseTmpl.Clone())).ParseFS(fsys, files...) + if err != nil { + return fmt.Errorf("processing %s includes failed: %w", class, err) + } + + classTmpl[class] = tmpl + + return nil +} + // Register adds a template file to the registry func Register(class Class, filepath string) error { b, err := os.ReadFile(filepath) diff --git a/util/templates/init_test.go b/util/templates/init_test.go new file mode 100644 index 000000000..cc1bc24f0 --- /dev/null +++ b/util/templates/init_test.go @@ -0,0 +1,36 @@ +package templates + +import ( + "testing" + "testing/fstest" + + "github.com/stretchr/testify/require" +) + +func TestClassLocalIncludes(t *testing.T) { + fsys := fstest.MapFS{ + "loadpoint/local" + IncludeExt: &fstest.MapFile{ + Data: []byte(`{{ define "local" }}power: {{ .foo }}{{ end }}`), + }, + } + + require.NoError(t, loadIncludes(fsys, Loadpoint)) + require.Contains(t, classTmpl, Loadpoint) + + // class without include files + require.NoError(t, loadIncludes(fsys, Circuit)) + require.NotContains(t, classTmpl, Circuit) + + tmpl := Template{ + Params: []Param{{Name: "foo", Default: "bar"}}, + Render: "type: custom\n{{ include \"local\" . }}", + } + + b, _, err := tmpl.RenderResult(Loadpoint, RenderModeInstance, map[string]any{}) + require.NoError(t, err) + require.Equal(t, "type: custom\npower: bar", string(b)) + + // class-local includes are not visible to other classes + _, _, err = tmpl.RenderResult(Circuit, RenderModeInstance, map[string]any{}) + require.Error(t, err) +} diff --git a/util/templates/render_instance.go b/util/templates/render_instance.go index ebde89e58..28f0379b6 100644 --- a/util/templates/render_instance.go +++ b/util/templates/render_instance.go @@ -31,7 +31,7 @@ func RenderInstance(class Class, other map[string]any) (*Instance, error) { return nil, err } - b, _, err := tmpl.RenderResult(RenderModeInstance, other) + b, _, err := tmpl.RenderResult(class, RenderModeInstance, other) if err != nil { return nil, util.NewConfigError(err) } diff --git a/util/templates/render_testing.go b/util/templates/render_testing.go index e43452a6d..ceeb4c365 100644 --- a/util/templates/render_testing.go +++ b/util/templates/render_testing.go @@ -13,10 +13,10 @@ import ( ) // test renders and instantiates plus yaml-parses the template per usage -func test(t *testing.T, tmpl Template, values map[string]any, cb func(values map[string]any)) { +func test(t *testing.T, class Class, tmpl Template, values map[string]any, cb func(values map[string]any)) { t.Helper() - b, _, err := tmpl.RenderResult(RenderModeInstance, values) + b, _, err := tmpl.RenderResult(class, RenderModeInstance, values) if err != nil { t.Log(string(b)) t.Error(err) @@ -108,7 +108,7 @@ func TestClass(t *testing.T, class Class, instantiate func(t *testing.T, values t.Run(tmpl.Template, func(t *testing.T) { t.Parallel() - test(t, tmpl, values, func(values map[string]any) { + test(t, class, tmpl, values, func(values map[string]any) { instantiate(t, values) }) }) @@ -125,7 +125,7 @@ func TestClass(t *testing.T, class Class, instantiate func(t *testing.T, values t.Run(tmpl.Template+"/"+u, func(t *testing.T) { t.Parallel() - test(t, tmpl, usageValues, func(values map[string]any) { + test(t, class, tmpl, usageValues, func(values map[string]any) { instantiate(t, values) }) }) diff --git a/util/templates/template.go b/util/templates/template.go index 57b04b377..19a6eb2fe 100644 --- a/util/templates/template.go +++ b/util/templates/template.go @@ -325,7 +325,7 @@ func formatValue(val any) string { } // RenderResult renders the result template to instantiate the proxy -func (t *Template) RenderResult(renderMode int, other map[string]any) ([]byte, map[string]any, error) { +func (t *Template) RenderResult(class Class, renderMode int, other map[string]any) ([]byte, map[string]any, error) { values := t.Defaults(renderMode) if err := mergeMaps(other, values); err != nil { return nil, values, err @@ -422,7 +422,13 @@ func (t *Template) RenderResult(renderMode int, other map[string]any) ([]byte, m } } - tmpl, err := FuncMap(template.Must(baseTmpl.Clone())).Parse(t.Render) + // class-local includes take precedence over the global ones + base, ok := classTmpl[class] + if !ok { + base = baseTmpl + } + + tmpl, err := FuncMap(template.Must(base.Clone())).Parse(t.Render) if err != nil { return nil, res, err } diff --git a/util/templates/template_modbus_test.go b/util/templates/template_modbus_test.go index 3c446f4a2..c6374f388 100644 --- a/util/templates/template_modbus_test.go +++ b/util/templates/template_modbus_test.go @@ -22,7 +22,7 @@ func TestModbusTemplateDefaultID(t *testing.T) { tmpl, err := ByName(Charger, "phoenix-ev-eth") require.NoError(t, err) - _, values, err := tmpl.RenderResult(mode, map[string]any{ + _, values, err := tmpl.RenderResult(Meter, mode, map[string]any{ "host": "192.168.0.8", "port": 502, }) @@ -40,7 +40,7 @@ func TestModbusTemplateUserIDOverridesTemplate(t *testing.T) { tmpl, err := ByName(Charger, "phoenix-ev-eth") require.NoError(t, err) - _, values, err := tmpl.RenderResult(mode, map[string]any{ + _, values, err := tmpl.RenderResult(Meter, mode, map[string]any{ "host": "192.168.0.8", "port": 502, "id": 42, @@ -62,7 +62,7 @@ func TestWallbeTemplateCoveredByPhoenix(t *testing.T) { require.NoError(t, err) assert.Equal(t, "phoenix-ev-eth", tmpl.Template) - _, values, err := tmpl.RenderResult(RenderModeInstance, map[string]any{ + _, values, err := tmpl.RenderResult(Meter, RenderModeInstance, map[string]any{ "host": "192.168.0.8", "port": 502, }) diff --git a/util/templates/template_test.go b/util/templates/template_test.go index 8c3172280..95d968932 100644 --- a/util/templates/template_test.go +++ b/util/templates/template_test.go @@ -42,22 +42,22 @@ func TestRequiredString(t *testing.T) { }, } - _, _, err := tmpl.RenderResult(RenderModeUnitTest, map[string]any{ + _, _, err := tmpl.RenderResult(Meter, RenderModeUnitTest, map[string]any{ "Param": "foo", }) assert.NoError(t, err, "test: required present") - _, _, err = tmpl.RenderResult(RenderModeUnitTest, map[string]any{ + _, _, err = tmpl.RenderResult(Meter, RenderModeUnitTest, map[string]any{ "Param": "", }) assert.Error(t, err, "test: required present but empty") - _, _, err = tmpl.RenderResult(RenderModeUnitTest, map[string]any{ + _, _, err = tmpl.RenderResult(Meter, RenderModeUnitTest, map[string]any{ "Param": nil, }) assert.Error(t, err, "test: required present but nil") - _, _, err = tmpl.RenderResult(RenderModeDocs, map[string]any{ + _, _, err = tmpl.RenderResult(Meter, RenderModeDocs, map[string]any{ "Param": nil, }) assert.NoError(t, err, "docs: required present but nil") @@ -74,27 +74,27 @@ func TestRequiredNumber(t *testing.T) { }, } - _, _, err := tmpl.RenderResult(RenderModeUnitTest, map[string]any{ + _, _, err := tmpl.RenderResult(Meter, RenderModeUnitTest, map[string]any{ "Param": "1", }) assert.NoError(t, err, "test: required present") - _, _, err = tmpl.RenderResult(RenderModeUnitTest, map[string]any{ + _, _, err = tmpl.RenderResult(Meter, RenderModeUnitTest, map[string]any{ "Param": "", }) assert.Error(t, err, "test: required present but empty") - _, _, err = tmpl.RenderResult(RenderModeUnitTest, map[string]any{ + _, _, err = tmpl.RenderResult(Meter, RenderModeUnitTest, map[string]any{ "Param": "0", }) assert.Error(t, err, "test: required present but zero value") - _, _, err = tmpl.RenderResult(RenderModeUnitTest, map[string]any{ + _, _, err = tmpl.RenderResult(Meter, RenderModeUnitTest, map[string]any{ "Param": nil, }) assert.Error(t, err, "test: required present but nil") - _, _, err = tmpl.RenderResult(RenderModeDocs, map[string]any{ + _, _, err = tmpl.RenderResult(Meter, RenderModeDocs, map[string]any{ "Param": nil, }) assert.NoError(t, err, "docs: required present but nil") @@ -111,22 +111,22 @@ func TestRequiredDeprecated(t *testing.T) { }, } - _, _, err := tmpl.RenderResult(RenderModeUnitTest, map[string]any{ + _, _, err := tmpl.RenderResult(Meter, RenderModeUnitTest, map[string]any{ "Param": "foo", }) assert.NoError(t, err, "test: required present") - _, _, err = tmpl.RenderResult(RenderModeUnitTest, map[string]any{ + _, _, err = tmpl.RenderResult(Meter, RenderModeUnitTest, map[string]any{ "Param": "", }) assert.NoError(t, err, "test: required present but empty") - _, _, err = tmpl.RenderResult(RenderModeUnitTest, map[string]any{ + _, _, err = tmpl.RenderResult(Meter, RenderModeUnitTest, map[string]any{ "Param": nil, }) assert.NoError(t, err, "test: required present but nil") - _, _, err = tmpl.RenderResult(RenderModeDocs, map[string]any{ + _, _, err = tmpl.RenderResult(Meter, RenderModeDocs, map[string]any{ "Param": nil, }) assert.NoError(t, err, "docs: required present but nil") @@ -146,25 +146,25 @@ func TestRequiredPerUsage(t *testing.T) { }, } - _, _, err := tmpl.RenderResult(RenderModeUnitTest, map[string]any{ + _, _, err := tmpl.RenderResult(Meter, RenderModeUnitTest, map[string]any{ "Param": nil, "Usage": nil, }) require.NoError(t, err) - _, _, err = tmpl.RenderResult(RenderModeUnitTest, map[string]any{ + _, _, err = tmpl.RenderResult(Meter, RenderModeUnitTest, map[string]any{ "Param": nil, "Usage": "pv", }) require.NoError(t, err) - _, _, err = tmpl.RenderResult(RenderModeUnitTest, map[string]any{ + _, _, err = tmpl.RenderResult(Meter, RenderModeUnitTest, map[string]any{ "Param": nil, "Usage": "battery", }) require.Error(t, err) - _, _, err = tmpl.RenderResult(RenderModeUnitTest, map[string]any{ + _, _, err = tmpl.RenderResult(Meter, RenderModeUnitTest, map[string]any{ "Param": "foo", "Usage": "battery", }) @@ -190,7 +190,7 @@ func TestValidatePattern(t *testing.T) { for _, tt := range tests { t.Run(tt.host, func(t *testing.T) { - _, _, err := tmpl.RenderResult(RenderModeInstance, map[string]any{"host": tt.host}) + _, _, err := tmpl.RenderResult(Meter, RenderModeInstance, map[string]any{"host": tt.host}) if tt.valid { require.NoError(t, err) } else { diff --git a/vehicle/template_test.go b/vehicle/template_test.go index 116657406..6461ec989 100644 --- a/vehicle/template_test.go +++ b/vehicle/template_test.go @@ -43,7 +43,7 @@ func TestTemplates(t *testing.T) { }) } -// onlineVehicleFeatures render via the shared vehicle-features include, so a +// onlineVehicleFeatures render via the shared vehicle features include, so a // stored config may carry them; dropping the param breaks reload (discussion #31291). var onlineVehicleFeatures = []string{"streaming", "coarsecurrent", "welcomecharge", "climaterdisabled", "autodetectdisabled", "wakeupdisabled"} @@ -60,7 +60,7 @@ var requiredFeatureParams = map[string][]string{ func TestVehicleFeatureParamsConsistent(t *testing.T) { for _, tmpl := range templates.ByClass(templates.Vehicle, templates.WithDeprecated()) { - if !strings.Contains(tmpl.Render, "vehicle-features") { + if !strings.Contains(tmpl.Render, `include "features"`) { continue } @@ -77,7 +77,7 @@ func TestVehicleFeatureParamsConsistent(t *testing.T) { values["template"] = tmpl.Template values[feat] = true - if _, _, err := tmpl.RenderResult(templates.RenderModeInstance, values); err != nil { + if _, _, err := tmpl.RenderResult(templates.Vehicle, templates.RenderModeInstance, values); err != nil { t.Errorf("%s: feature %q must stay a declared param: %v", tmpl.Template, feat, err) } } From 35ed5e5b91365248af53e05f330182c4b6bc258a Mon Sep 17 00:00:00 2001 From: andig Date: Tue, 18 Aug 2026 09:45:20 +0200 Subject: [PATCH 020/135] chore: add ship-go rollup with mdns re-announcement leak fix (#32940) --- go.mod | 2 ++ go.sum | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index bf53915a1..044665c48 100644 --- a/go.mod +++ b/go.mod @@ -269,3 +269,5 @@ replace github.com/lorenzodonini/ocpp-go => github.com/evcc-io/ocpp-go v0.0.0-20 replace github.com/enbility/spine-go => github.com/andig/spine-go v0.7.1-0.20260804073026-f9a8d8e8fd18 replace github.com/enbility/eebus-go => github.com/andig/eebus-go v0.0.0-20260725155950-e735091ff165 + +replace github.com/enbility/ship-go => github.com/andig/ship-go v0.6.1-0.20260818072438-bade5ec9fbd7 diff --git a/go.sum b/go.sum index 642b08740..8ee8c4fc8 100644 --- a/go.sum +++ b/go.sum @@ -30,6 +30,8 @@ github.com/andig/gosunspec v0.0.0-20260705113727-6d585e133512 h1:1y8dS4GaBB9WUu/ github.com/andig/gosunspec v0.0.0-20260705113727-6d585e133512/go.mod h1:c6P6szcR+ROkqZruOR4f6qbDKFjZX6OitPpj+yJ/r8k= github.com/andig/mbserver v0.0.0-20230310211055-1d29cbb5820e h1:m/NTP3JWpR7M0ljLxiQU4fzR25jjhe1LDtxLMNcoNJQ= github.com/andig/mbserver v0.0.0-20230310211055-1d29cbb5820e/go.mod h1:4VtYzTm//oUipwvO3yh0g/udTE7pYJM+U/kyAuFDsgM= +github.com/andig/ship-go v0.6.1-0.20260818072438-bade5ec9fbd7 h1:YXXxRvtVSTLATXSvMYBtf8PBNeVRBu7WzxfL4PCqN+8= +github.com/andig/ship-go v0.6.1-0.20260818072438-bade5ec9fbd7/go.mod h1:EvRFx83phCwPQrOuWw2CX5t0c6mcFVzIk6gQEz4Lijg= github.com/andig/spine-go v0.7.1-0.20260804073026-f9a8d8e8fd18 h1:FVKqjwJJLo8vV47jXt1lNN4EVJXCyV3LSTNFt9eeimU= github.com/andig/spine-go v0.7.1-0.20260804073026-f9a8d8e8fd18/go.mod h1:ddWZU5BQGyzRGF20Z7rUKFFzbCp+cfu9mZgYtOMW/fM= github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM= @@ -140,8 +142,6 @@ github.com/eclipse/paho.mqtt.golang v1.5.1 h1:/VSOv3oDLlpqR2Epjn1Q7b2bSTplJIeV2I github.com/eclipse/paho.mqtt.golang v1.5.1/go.mod h1:1/yJCneuyOoCOzKSsOTUc0AJfpsItBGWvYpBLimhArU= github.com/enbility/go-avahi v0.0.0-20240909195612-d5de6b280d7a h1:foChWb8lhzqa6lWDRs6COYMdp649YlUirFP8GqoT0JQ= github.com/enbility/go-avahi v0.0.0-20240909195612-d5de6b280d7a/go.mod h1:H64mhYcAQUGUUnVqMdZQf93kPecH4M79xwH95Lddt3U= -github.com/enbility/ship-go v0.6.1-0.20260720110450-0aa90f64ac76 h1:gDrV6vBG3luAgsyX4sXYShITT41CDKXrtyluLeRmglE= -github.com/enbility/ship-go v0.6.1-0.20260720110450-0aa90f64ac76/go.mod h1:EvRFx83phCwPQrOuWw2CX5t0c6mcFVzIk6gQEz4Lijg= github.com/enbility/zeroconf/v2 v2.0.0-20240920094356-be1cae74fda6 h1:XOYvxKtT1oxT37w/5oEiRLuPbm9FuJPt3fiYhX0h8Po= github.com/enbility/zeroconf/v2 v2.0.0-20240920094356-be1cae74fda6/go.mod h1:BszP9qFV14mPXgyIREbgIdQtWxbAj3OKqvK02HihMoM= github.com/evcc-io/modbus v0.0.0-20250501165638-8b6f1fbdb7ea h1:F6eyC8V8wvc3ranlsR4coAls+OPBkVvxyX0a2VqdlPc= From 8cf5a44a32eeb679c7a8b24a484036d5fd844ad1 Mon Sep 17 00:00:00 2001 From: Alexandre JARDON <28548335+webalexeu@users.noreply.github.com> Date: Tue, 18 Aug 2026 10:34:39 +0200 Subject: [PATCH 021/135] Marstek: add discharge and optimise batterymode (#32571) --- .../definition/meter/marstek-batterymode.tpl | 188 ++++++++++++++++++ .../definition/meter/marstek-venus-a.yaml | 154 +------------- .../definition/meter/marstek-venus-e.yaml | 154 +------------- 3 files changed, 194 insertions(+), 302 deletions(-) create mode 100644 templates/definition/meter/marstek-batterymode.tpl diff --git a/templates/definition/meter/marstek-batterymode.tpl b/templates/definition/meter/marstek-batterymode.tpl new file mode 100644 index 000000000..02ef38a45 --- /dev/null +++ b/templates/definition/meter/marstek-batterymode.tpl @@ -0,0 +1,188 @@ +{{- define "marstek-batterymode" }} +batterymode: + source: switch + switch: + - case: 1 # normal + set: + source: sequence + set: + # Restore Max Charge Power + - source: const + value: {{ .maxchargepower }} + set: + source: modbus + {{- include "modbus" . | indent 10 }} + register: + address: 44002 # Max Charge Power + type: writesingle + decode: uint16 + # Enable RS485 Control Mode + - source: const + value: 21930 + set: + source: modbus + {{- include "modbus" . | indent 10 }} + register: + address: 42000 # RS485 Control Mode = Enabled + type: writesingle + decode: uint16 + # Set User Work Mode + - source: const + value: {{ .work_mode_normal }} + set: + source: modbus + {{- include "modbus" . | indent 10 }} + register: + address: 43000 # User Work Mode + type: writesingle + decode: uint16 + # Disable RS485 Control Mode + - source: const + value: 21947 + set: + source: modbus + {{- include "modbus" . | indent 10 }} + register: + address: 42000 # RS485 Control Mode = Disabled + type: writesingle + decode: uint16 + - case: 2 # hold + set: + source: sequence + set: + # Enable RS485 Control Mode + - source: const + value: 21930 + set: + source: modbus + {{- include "modbus" . | indent 10 }} + register: + address: 42000 # RS485 Control Mode = Enabled + type: writesingle + decode: uint16 + # Set Force Charge/Discharge to Stop + - source: const + value: 0 + set: + source: modbus + {{- include "modbus" . | indent 10 }} + register: + address: 42010 # Force Charge/Discharge = Stop + type: writesingle + decode: uint16 + # Do not disable RS485 Control Mode because it will reset the device and let it charge/discharge again + - case: 3 # charge + set: + source: sequence + set: + # Enable RS485 Control Mode + - source: const + value: 21930 + set: + source: modbus + {{- include "modbus" . | indent 10 }} + register: + address: 42000 # RS485 Control Mode = Enabled + type: writesingle + decode: uint16 + # Set Force Charge/Discharge to Charge + - source: const + value: 1 + set: + source: modbus + {{- include "modbus" . | indent 10 }} + register: + address: 42010 # Force Charge/Discharge = Charge + type: writesingle + decode: uint16 + # Set Forcible Charge Power + - source: const + value: {{ .maxchargepower }} + set: + source: modbus + {{- include "modbus" . | indent 10 }} + register: + address: 42020 # Forcible Charge Power + type: writesingle + decode: uint16 + # Do not disable RS485 Control Mode because it will reset the device and let it charge/discharge again + - case: 4 # holdcharge -> no charging, discharging allowed + set: + source: sequence + set: + # Set Max Charge Power to 0 to prevent charging + - source: const + value: 0 + set: + source: modbus + {{- include "modbus" . | indent 10 }} + register: + address: 44002 # Max Charge Power + type: writesingle + decode: uint16 + # Enable RS485 Control Mode + - source: const + value: 21930 + set: + source: modbus + {{- include "modbus" . | indent 10 }} + register: + address: 42000 # RS485 Control Mode = Enabled + type: writesingle + decode: uint16 + # Set User Work Mode + - source: const + value: {{ .work_mode_normal }} + set: + source: modbus + {{- include "modbus" . | indent 10 }} + register: + address: 43000 # User Work Mode + type: writesingle + decode: uint16 + # Disable RS485 Control Mode + - source: const + value: 21947 + set: + source: modbus + {{- include "modbus" . | indent 10 }} + register: + address: 42000 # RS485 Control Mode = Disabled + type: writesingle + decode: uint16 + # Do not disable RS485 Control Mode because it will reset the device and let it charge/discharge again + - case: 5 # discharge -> forced discharge to grid (feed-in arbitrage) + set: + source: sequence + set: + # Enable RS485 Control Mode + - source: const + value: 21930 + set: + source: modbus + {{- include "modbus" . | indent 10 }} + register: + address: 42000 # RS485 Control Mode = Enabled + type: writesingle + decode: uint16 + # Set Force Charge/Discharge to Discharge + - source: const + value: 2 + set: + source: modbus + {{- include "modbus" . | indent 10 }} + register: + address: 42010 # Force Charge/Discharge = Discharge + type: writesingle + decode: uint16 + # Set Forcible Discharge Power + - source: const + value: {{ .maxdischargepower }} + set: + source: modbus + {{- include "modbus" . | indent 10 }} + register: + address: 42021 # Forcible Discharge Power + type: writesingle + decode: uint16 +{{- end }} diff --git a/templates/definition/meter/marstek-venus-a.yaml b/templates/definition/meter/marstek-venus-a.yaml index 9096a5a0d..6041c4dd8 100644 --- a/templates/definition/meter/marstek-venus-a.yaml +++ b/templates/definition/meter/marstek-venus-a.yaml @@ -30,6 +30,8 @@ params: help: de: Für erzwungenes Laden des Speichers. Venus A 1200 W, Venus D 2200 W en: For forced charging of the battery. Venus A 1200 W, Venus D 2200 W + - name: maxdischargepower + required: true - name: work_mode_normal default: 1 advanced: true @@ -98,156 +100,6 @@ render: | address: 32104 # Battery SOC (%) type: holding decode: uint16 - batterymode: - source: switch - switch: - - case: 1 # normal - set: - source: sequence - set: - # Restore Max Charge Power - - source: const - value: {{ .maxchargepower }} - set: - source: modbus - {{- include "modbus" . | indent 10 }} - register: - address: 44002 # Max Charge Power - type: writesingle - decode: uint16 - # Enable RS485 Control Mode - - source: const - value: 21930 - set: - source: modbus - {{- include "modbus" . | indent 10 }} - register: - address: 42000 # RS485 Control Mode = Enabled - type: writesingle - decode: uint16 - # Set User Work Mode - - source: const - value: {{ .work_mode_normal }} - set: - source: modbus - {{- include "modbus" . | indent 10 }} - register: - address: 43000 # User Work Mode - type: writesingle - decode: uint16 - # Disable RS485 Control Mode - - source: const - value: 21947 - set: - source: modbus - {{- include "modbus" . | indent 10 }} - register: - address: 42000 # RS485 Control Mode = Disabled - type: writesingle - decode: uint16 - - case: 2 # hold - set: - source: sequence - set: - # Enable RS485 Control Mode - - source: const - value: 21930 - set: - source: modbus - {{- include "modbus" . | indent 10 }} - register: - address: 42000 # RS485 Control Mode = Enabled - type: writesingle - decode: uint16 - # Set Force Charge/Discharge to Stop - - source: const - value: 0 - set: - source: modbus - {{- include "modbus" . | indent 10 }} - register: - address: 42010 # Force Charge/Discharge = Stop - type: writesingle - decode: uint16 - # Do not disable RS485 Control Mode because it will reset the device and let it charge/discharge again - - case: 3 # charge - set: - source: sequence - set: - # Enable RS485 Control Mode - - source: const - value: 21930 - set: - source: modbus - {{- include "modbus" . | indent 10 }} - register: - address: 42000 # RS485 Control Mode = Enabled - type: writesingle - decode: uint16 - # Set Force Charge/Discharge to Charge - - source: const - value: 1 - set: - source: modbus - {{- include "modbus" . | indent 10 }} - register: - address: 42010 # Force Charge/Discharge = Charge - type: writesingle - decode: uint16 - # Set Forcible Charge Power - - source: const - value: {{ .maxchargepower }} - set: - source: modbus - {{- include "modbus" . | indent 10 }} - register: - address: 42020 # Forcible Charge Power - type: writesingle - decode: uint16 - # Do not disable RS485 Control Mode because it will reset the device and let it charge/discharge again - - case: 4 # holdcharge -> no charging, discharging allowed - set: - source: sequence - set: - # Set Max Charge Power to 0 to prevent charging - - source: const - value: 0 - set: - source: modbus - {{- include "modbus" . | indent 10 }} - register: - address: 44002 # Max Charge Power - type: writesingle - decode: uint16 - # Enable RS485 Control Mode - - source: const - value: 21930 - set: - source: modbus - {{- include "modbus" . | indent 10 }} - register: - address: 42000 # RS485 Control Mode = Enabled - type: writesingle - decode: uint16 - # Set User Work Mode - - source: const - value: {{ .work_mode_normal }} - set: - source: modbus - {{- include "modbus" . | indent 10 }} - register: - address: 43000 # User Work Mode - type: writesingle - decode: uint16 - # Disable RS485 Control Mode - - source: const - value: 21947 - set: - source: modbus - {{- include "modbus" . | indent 10 }} - register: - address: 42000 # RS485 Control Mode = Disabled - type: writesingle - decode: uint16 + {{- include "marstek-batterymode" . }} {{- include "battery-params" . }} {{- end }} diff --git a/templates/definition/meter/marstek-venus-e.yaml b/templates/definition/meter/marstek-venus-e.yaml index 9aeed1b99..7b8ea432b 100644 --- a/templates/definition/meter/marstek-venus-e.yaml +++ b/templates/definition/meter/marstek-venus-e.yaml @@ -40,6 +40,8 @@ params: default: 100 - name: maxchargepower default: 2500 + - name: maxdischargepower + default: 800 - name: work_mode_normal default: 1 advanced: true @@ -90,155 +92,5 @@ render: | decode: uint16 scale: 1 {{- end }} - batterymode: - source: switch - switch: - - case: 1 # normal - set: - source: sequence - set: - # Restore Max Charge Power - - source: const - value: {{ .maxchargepower }} - set: - source: modbus - {{- include "modbus" . | indent 10 }} - register: - address: 44002 # Max Charge Power - type: writesingle - decode: uint16 - # Enable RS485 Control Mode - - source: const - value: 21930 - set: - source: modbus - {{- include "modbus" . | indent 10 }} - register: - address: 42000 # RS485 Control Mode = Enabled - type: writesingle - decode: uint16 - # Set User Work Mode - - source: const - value: {{ .work_mode_normal }} - set: - source: modbus - {{- include "modbus" . | indent 10 }} - register: - address: 43000 # User Work Mode - type: writesingle - decode: uint16 - # Disable RS485 Control Mode - - source: const - value: 21947 - set: - source: modbus - {{- include "modbus" . | indent 10 }} - register: - address: 42000 # RS485 Control Mode = Disabled - type: writesingle - decode: uint16 - - case: 2 # hold - set: - source: sequence - set: - # Enable RS485 Control Mode - - source: const - value: 21930 - set: - source: modbus - {{- include "modbus" . | indent 10 }} - register: - address: 42000 # RS485 Control Mode = Enabled - type: writesingle - decode: uint16 - # Set Force Charge/Discharge to Stop - - source: const - value: 0 - set: - source: modbus - {{- include "modbus" . | indent 10 }} - register: - address: 42010 # Force Charge/Discharge = Stop - type: writesingle - decode: uint16 - # Do not disable RS485 Control Mode because it will reset the device and let it charge/discharge again - - case: 3 # charge - set: - source: sequence - set: - # Enable RS485 Control Mode - - source: const - value: 21930 - set: - source: modbus - {{- include "modbus" . | indent 10 }} - register: - address: 42000 # RS485 Control Mode = Enabled - type: writesingle - decode: uint16 - # Set Force Charge/Discharge to Charge - - source: const - value: 1 - set: - source: modbus - {{- include "modbus" . | indent 10 }} - register: - address: 42010 # Force Charge/Discharge = Charge - type: writesingle - decode: uint16 - # Set Forcible Charge Power - - source: const - value: {{ .maxchargepower }} - set: - source: modbus - {{- include "modbus" . | indent 10 }} - register: - address: 42020 # Forcible Charge Power - type: writesingle - decode: uint16 - # Do not disable RS485 Control Mode because it will reset the device and let it charge/discharge again - - case: 4 # holdcharge -> no charging, discharging allowed - set: - source: sequence - set: - # Set Max Charge Power to 0 to prevent charging - - source: const - value: 0 - set: - source: modbus - {{- include "modbus" . | indent 10 }} - register: - address: 44002 # Max Charge Power - type: writesingle - decode: uint16 - # Enable RS485 Control Mode - - source: const - value: 21930 - set: - source: modbus - {{- include "modbus" . | indent 10 }} - register: - address: 42000 # RS485 Control Mode = Enabled - type: writesingle - decode: uint16 - # Set User Work Mode - - source: const - value: {{ .work_mode_normal }} - set: - source: modbus - {{- include "modbus" . | indent 10 }} - register: - address: 43000 # User Work Mode - type: writesingle - decode: uint16 - # Disable RS485 Control Mode - - source: const - value: 21947 - set: - source: modbus - {{- include "modbus" . | indent 10 }} - register: - address: 42000 # RS485 Control Mode = Disabled - type: writesingle - decode: uint16 + {{- include "marstek-batterymode" . }} {{- include "battery-params" . }} From adfbd057b0c137842eb19eadc7bf3404afa007c8 Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Tue, 18 Aug 2026 10:41:47 +0200 Subject: [PATCH 022/135] Vehicle: don't instantiate disabled vehicles (#32919) --- cmd/setup.go | 11 ++++++++--- core/loadpoint.go | 3 ++- core/loadpoint_test.go | 25 +++++++++++++++++++++++++ core/site_vehicles.go | 13 +++++-------- tests/config-disable.spec.ts | 34 ++++++++++++++++++++++++++++++++++ 5 files changed, 74 insertions(+), 12 deletions(-) diff --git a/cmd/setup.go b/cmd/setup.go index 6a77f1c24..4cc932170 100644 --- a/cmd/setup.go +++ b/cmd/setup.go @@ -538,9 +538,14 @@ func configureVehicles(static []config.Named, names ...string) error { } } - instance, err := vehicleInstance(cc) - if err != nil { - return fmt.Errorf("cannot create vehicle '%s': %w", cc.Name, err) + // disabled vehicles are not instantiated + var instance api.Vehicle + if !conf.Disable { + var err error + instance, err = vehicleInstance(cc) + if err != nil { + return fmt.Errorf("cannot create vehicle '%s': %w", cc.Name, err) + } } mu.Lock() diff --git a/core/loadpoint.go b/core/loadpoint.go index 154686ac0..13f2e6e0e 100644 --- a/core/loadpoint.go +++ b/core/loadpoint.go @@ -253,7 +253,8 @@ func NewLoadpointFromConfig(log *util.Logger, settings settings.Settings, collec } lp.defaultVehicle = dev.Instance() if lp.defaultVehicle == nil { - return lp, errors.New("missing default vehicle instance") + // disabled vehicle + lp.log.DEBUG.Printf("default vehicle '%s' is disabled", lp.VehicleRef) } } diff --git a/core/loadpoint_test.go b/core/loadpoint_test.go index 59f55c5c9..d427b0f1d 100644 --- a/core/loadpoint_test.go +++ b/core/loadpoint_test.go @@ -12,7 +12,9 @@ import ( "github.com/evcc-io/evcc/core/soc" "github.com/evcc-io/evcc/messenger" "github.com/evcc-io/evcc/util" + "github.com/evcc-io/evcc/util/config" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" ) @@ -882,3 +884,26 @@ func TestBatteryBoostHold(t *testing.T) { // which is what keeps the sitePower priority adjustment applied to the loadpoint assert.NotEqual(t, boostDisabled, lp.GetBatteryBoost(), "hold is active") } + +// default vehicle referencing a disabled vehicle must not fail loadpoint creation +func TestNewLoadpointFromConfigDisabledVehicle(t *testing.T) { + config.Reset() + t.Cleanup(config.Reset) + + ctrl := gomock.NewController(t) + + // disabled vehicle: device registered without instance + var vehicle api.Vehicle + require.NoError(t, config.Vehicles().Add(config.NewStaticDevice(config.Named{Name: "vehicle"}, vehicle))) + require.NoError(t, config.Chargers().Add(config.NewStaticDevice(config.Named{Name: "charger"}, api.Charger(api.NewMockCharger(ctrl))))) + + lp, err := NewLoadpointFromConfig(util.NewLogger("foo"), nil, nil, map[string]any{ + "charger": "charger", + "vehicle": "vehicle", + }) + require.NoError(t, err) + require.Nil(t, lp.defaultVehicle) + + // disabled vehicle is filtered from instances + require.Empty(t, config.Instances(config.Vehicles().Devices())) +} diff --git a/core/site_vehicles.go b/core/site_vehicles.go index aa457808c..cedccb264 100644 --- a/core/site_vehicles.go +++ b/core/site_vehicles.go @@ -104,14 +104,7 @@ type vehicles struct { } func (vv *vehicles) Instances() []api.Vehicle { - devs := config.Vehicles().Devices() - - res := make([]api.Vehicle, 0, len(devs)) - for _, dev := range devs { - res = append(res, dev.Instance()) - } - - return res + return config.Instances(config.Vehicles().Devices()) } func (vv *vehicles) Settings() []vehicle.API { @@ -119,6 +112,10 @@ func (vv *vehicles) Settings() []vehicle.API { res := make([]vehicle.API, 0, len(devs)) for _, dev := range devs { + // skip disabled vehicles + if dev.Instance() == nil { + continue + } res = append(res, vehicle.Adapter(vv.log, dev)) } diff --git a/tests/config-disable.spec.ts b/tests/config-disable.spec.ts index 744bb07e7..5f5b66e19 100644 --- a/tests/config-disable.spec.ts +++ b/tests/config-disable.spec.ts @@ -239,6 +239,10 @@ test.describe("disable / enable", async () => { await start(); await page.goto("/#/config"); + // loadpoint so the vehicle shows up on the dashboard + await createLoadpoint(page, "Carport"); + await page.reload(); + // add user-defined vehicle await page.getByTestId("add-vehicle").click(); const modal = page.getByTestId("vehicle-modal"); @@ -277,6 +281,23 @@ soc: await expectNoFatal(page); await expect(disabledBadge(vehicleCard)).toBeVisible(); + // disabled vehicle is hidden from main ui + await page.goto("/"); + await expect(page.getByTestId("loadpoint")).toBeVisible(); + await expect( + page.getByTestId("change-vehicle").getByRole("option", { name: "blue Honda" }) + ).toHaveCount(0); + + // no vehicles entry in more menu + const moreTab = page.getByTestId("tab-more"); + await moreTab.click(); + await expect(moreTab.getByRole("button", { name: "User Interface" })).toBeVisible(); + await expect(moreTab.getByRole("button", { name: "Vehicles" })).not.toBeVisible(); + // close more menu + await moreTab.click(); + + await page.goto("/#/config"); + // re-enable by clicking the disabled card await disabledBadge(vehicleCard).click(); await expect(disabledBadge(vehicleCard)).toHaveCount(0); @@ -286,6 +307,19 @@ soc: await page.reload(); await expectNoFatal(page); await expect(vehicleCard).toContainText("blue Honda"); + + // vehicle is selectable on the dashboard again + await page.goto("/"); + await expect( + page.getByTestId("change-vehicle").getByRole("option", { name: "blue Honda" }) + ).toHaveCount(1); + + // vehicle is back in the more menu vehicles modal + await moreTab.click(); + await moreTab.getByRole("button", { name: "Vehicles" }).click(); + const settingsModal = page.getByTestId("vehicle-settings-modal"); + await expectModalVisible(settingsModal); + await expect(settingsModal.getByRole("group", { name: "blue Honda" })).toBeVisible(); }); }); From 50ff9ab77715e52e93acd87ef8890f743cd5af55 Mon Sep 17 00:00:00 2001 From: andig Date: Tue, 18 Aug 2026 10:42:01 +0200 Subject: [PATCH 023/135] chore: wait for the pull request to merge in the backport command (#32942) --- .github/workflows/command-backport.yml | 32 ++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/.github/workflows/command-backport.yml b/.github/workflows/command-backport.yml index 138a8836a..4b3e610b2 100644 --- a/.github/workflows/command-backport.yml +++ b/.github/workflows/command-backport.yml @@ -57,11 +57,33 @@ jobs: core.setFailed(message); }; - const { data: pr } = await github.rest.pulls.get({ - owner, - repo, - pull_number: context.payload.issue.number, - }); + // the command is usually posted right before hitting merge, so give the + // pull request time to land instead of failing on the race + const deadline = Date.now() + 5 * 60 * 1000; + let pr; + for (;;) { + try { + ({ data: pr } = await github.rest.pulls.get({ + owner, + repo, + pull_number: context.payload.issue.number, + })); + // a closed pull request will never merge, no point in waiting it out + if (pr.merged || pr.state === 'closed') { + break; + } + } catch (err) { + // a blip while polling must not fail the command, but a request that + // is still failing when the time is up is a real error + if (Date.now() >= deadline) { + throw err; + } + } + if (Date.now() >= deadline) { + break; + } + await new Promise((resolve) => setTimeout(resolve, 15000)); + } if (!pr.merged) { fail(`pull request #${pr.number} is not merged`); return; From cc92f3938a9513b370033c271f9dba9452aa3a95 Mon Sep 17 00:00:00 2001 From: mggevaer <41551215+mggevaer@users.noreply.github.com> Date: Tue, 18 Aug 2026 11:43:39 +0200 Subject: [PATCH 024/135] Fix site.pvPower estimation for battery without pv (#32930) --- core/site.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/core/site.go b/core/site.go index d0a7e36b2..e558110a6 100644 --- a/core/site.go +++ b/core/site.go @@ -1054,9 +1054,10 @@ func (site *Site) sitePower(totalChargePower, flexiblePower float64) (float64, b residualPower = 100 // W } - // allow using grid and charge as estimate for pv power - if site.pvMeters == nil { - site.pvPower = totalChargePower - site.gridPower + residualPower + // allow using grid, charge and battery power as estimate for pv power + // needs a grid meter, otherwise grid power is itself derived from pv power (see above) + if site.pvMeters == nil && site.gridMeter != nil { + site.pvPower = totalChargePower - site.gridPower - site.battery.Power + residualPower if site.pvPower < 0 { site.pvPower = 0 } From f4b289f745e84e121814c3d35487becac9416043 Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Tue, 18 Aug 2026 12:02:29 +0200 Subject: [PATCH 025/135] Sponsor: include device serial in hardware check metadata (#32927) --- util/sponsor/hemspro_linux.go | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/util/sponsor/hemspro_linux.go b/util/sponsor/hemspro_linux.go index c2e3d144e..7180441af 100644 --- a/util/sponsor/hemspro_linux.go +++ b/util/sponsor/hemspro_linux.go @@ -20,11 +20,23 @@ package sponsor // SOFTWARE. import ( + "os" + "strings" + i2c "github.com/d2r2/go-i2c" ) const hemspro = "hemspro" +// deviceSerial returns the device tree serial number or empty string +func deviceSerial() string { + b, err := os.ReadFile("/sys/firmware/devicetree/base/serial-number") + if err != nil { + return "" + } + return strings.TrimSpace(strings.Trim(string(b), "\x00")) +} + // checkHemsPro checks if the hardware is a supported HEMS Pro device and returns sponsor subject func checkHemsPro() string { const ( @@ -49,5 +61,7 @@ func checkHemsPro() string { } // I2C succeeded — verify with server - return checkHardware(hemspro, nil) + return checkHardware(hemspro, map[string]string{ + "serial": deviceSerial(), + }) } From 88b641301a4b7f47055995b334403d83a10e5031 Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Tue, 18 Aug 2026 12:35:22 +0200 Subject: [PATCH 026/135] chore: sign and notarize macOS release binaries (#32946) --- .github/workflows/release.yml | 6 ++++++ .goreleaser.yml | 20 +++++++++++++------- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 09c2fd088..914b468be 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -144,6 +144,12 @@ jobs: # a bugfix release of an older line must not move the homebrew formula # or the Github latest release marker MAKE_LATEST: ${{ needs.guard.outputs.latest }} + # macOS code signing and notarization + MACOS_SIGN_P12: ${{ secrets.MACOS_SIGN_P12 }} + MACOS_SIGN_PASSWORD: ${{ secrets.MACOS_SIGN_PASSWORD }} + MACOS_NOTARY_KEY: ${{ secrets.MACOS_NOTARY_KEY }} + MACOS_NOTARY_KEY_ID: ${{ secrets.MACOS_NOTARY_KEY_ID }} + MACOS_NOTARY_ISSUER_ID: ${{ secrets.MACOS_NOTARY_ISSUER_ID }} - uses: actions/setup-python@v6 with: diff --git a/.goreleaser.yml b/.goreleaser.yml index ff4d59c91..8c1d08ea3 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -60,6 +60,19 @@ archives: universal_binaries: - replace: true +notarize: + macos: + - enabled: '{{ isEnvSet "MACOS_SIGN_P12" }}' + sign: + certificate: "{{ .Env.MACOS_SIGN_P12 }}" + password: "{{ .Env.MACOS_SIGN_PASSWORD }}" + notarize: + issuer_id: "{{ .Env.MACOS_NOTARY_ISSUER_ID }}" + key_id: "{{ .Env.MACOS_NOTARY_KEY_ID }}" + key: "{{ .Env.MACOS_NOTARY_KEY }}" + wait: true + timeout: 20m + checksum: name_template: "checksums.txt" @@ -136,10 +149,3 @@ homebrew_casks: description: "Sonne tanken ☀️🚘" binaries: - evcc - hooks: - post: - # the binaries are not notarized, so gatekeeper would refuse to run them - install: | - if OS.mac? - system_command "/usr/bin/xattr", args: ["-dr", "com.apple.quarantine", "#{staged_path}/evcc"] - end From 6d276526a6fdc334b3d9514770b8c38fbd431aae Mon Sep 17 00:00:00 2001 From: andig Date: Tue, 18 Aug 2026 12:38:26 +0200 Subject: [PATCH 027/135] Revert "PV: reserve only min power for higher-priority loadpoints starting up" (#32947) --- core/loadpoint.go | 11 +++-------- core/site.go | 4 +--- core/site_prioritize_test.go | 37 ++++++++---------------------------- 3 files changed, 12 insertions(+), 40 deletions(-) diff --git a/core/loadpoint.go b/core/loadpoint.go index 13f2e6e0e..ee0884870 100644 --- a/core/loadpoint.go +++ b/core/loadpoint.go @@ -1062,20 +1062,15 @@ func (lp *Loadpoint) charging() bool { func (lp *Loadpoint) PvChargeStarting() bool { lp.RLock() enabled := lp.enabled - pvTimer := lp.pvTimer + pvTimerRunning := !lp.pvTimer.IsZero() lp.RUnlock() if lp.GetMode() != api.ModePV || !lp.connected() || lp.chargeGoalReached(enabled) { return false } - if enabled || pvTimer.IsZero() { - return false - } - - // a timer restarting on every surplus dip never starts the loadpoint, hence - // only claim surplus once it has survived half of the enable delay (#32778) - return lp.clock.Since(pvTimer) >= lp.GetEnableDelay()/2 + // enable timer running (not yet enabled) + return !enabled && pvTimerRunning } // chargeGoalReached reports whether the loadpoint will not draw more: enabled diff --git a/core/site.go b/core/site.go index e558110a6..f1c47d0a0 100644 --- a/core/site.go +++ b/core/site.go @@ -1140,9 +1140,7 @@ func (site *Site) reservedPVPower(lp updater) float64 { continue } if other.EffectivePriority() > prio && other.PvChargeStarting() { - // min power is what the loadpoint needs to start, reserving its max - // power would starve this loadpoint beyond that (#32778) - reserved += other.EffectiveMinPower() + reserved += other.EffectiveMaxPower() } } diff --git a/core/site_prioritize_test.go b/core/site_prioritize_test.go index b6d4bcb20..aeaae88b8 100644 --- a/core/site_prioritize_test.go +++ b/core/site_prioritize_test.go @@ -10,10 +10,8 @@ import ( "github.com/evcc-io/evcc/util" ) -const testEnableDelay = time.Minute - func newPVLoadpoint(prio int, mode api.ChargeMode, status api.ChargeStatus, enabled bool, timer time.Time) *Loadpoint { - lp := &Loadpoint{ + return &Loadpoint{ log: util.NewLogger("lp"), clock: clock.NewMock(), minCurrent: minA, @@ -25,21 +23,13 @@ func newPVLoadpoint(prio int, mode api.ChargeMode, status api.ChargeStatus, enab pvTimer: timer, priority: prio, } - lp.Enable.Delay = testEnableDelay - return lp -} - -// pvTimerStarted returns a timer start that has been running for the given duration -func pvTimerStarted(running time.Duration) time.Time { - return clock.NewMock().Now().Add(-running) } func TestPvChargeStarting(t *testing.T) { now := clock.NewMock().Now() - settled := pvTimerStarted(testEnableDelay / 2) // enable timer running but car already full (soc at default 100% limit): not starting up - enablePendingFull := newPVLoadpoint(0, api.ModePV, api.StatusB, false, settled) + enablePendingFull := newPVLoadpoint(0, api.ModePV, api.StatusB, false, now) enablePendingFull.vehicleSoc = 100 tc := []struct { @@ -47,13 +37,12 @@ func TestPvChargeStarting(t *testing.T) { lp *Loadpoint starting bool }{ - {"enable timer running", newPVLoadpoint(0, api.ModePV, api.StatusB, false, settled), true}, - {"enable timer just restarted", newPVLoadpoint(0, api.ModePV, api.StatusB, false, now), false}, + {"enable timer running", newPVLoadpoint(0, api.ModePV, api.StatusB, false, now), true}, {"enabled not charging", newPVLoadpoint(0, api.ModePV, api.StatusB, true, time.Time{}), false}, {"enabled and charging", newPVLoadpoint(0, api.ModePV, api.StatusC, true, time.Time{}), false}, {"disabled idle", newPVLoadpoint(0, api.ModePV, api.StatusB, false, time.Time{}), false}, - {"disconnected", newPVLoadpoint(0, api.ModePV, api.StatusA, false, settled), false}, - {"not pv mode", newPVLoadpoint(0, api.ModeNow, api.StatusB, false, settled), false}, + {"disconnected", newPVLoadpoint(0, api.ModePV, api.StatusA, false, now), false}, + {"not pv mode", newPVLoadpoint(0, api.ModeNow, api.StatusB, false, now), false}, {"enable pending but car full", enablePendingFull, false}, } @@ -68,7 +57,7 @@ func TestReservedPVPower(t *testing.T) { Voltage = 230 // higher-priority loadpoint (prio 1) starting up - high := newPVLoadpoint(1, api.ModePV, api.StatusB, false, pvTimerStarted(testEnableDelay/2)) + high := newPVLoadpoint(1, api.ModePV, api.StatusB, false, clock.NewMock().Now()) // lower-priority loadpoint (prio 0) in PV mode low := newPVLoadpoint(0, api.ModePV, api.StatusB, false, time.Time{}) @@ -77,20 +66,10 @@ func TestReservedPVPower(t *testing.T) { loadpoints: []*Loadpoint{high, low}, } - // low reserves the power high needs to start, not its max power (#32778) - if got, want := site.reservedPVPower(low), high.EffectiveMinPower(); got != want { + // low reserves high's anticipated max power while high is starting up + if got, want := site.reservedPVPower(low), high.EffectiveMaxPower(); got != want { t.Errorf("low: want %.0f, got %.0f", want, got) } - if high.EffectiveMinPower() == high.EffectiveMaxPower() { - t.Fatal("test requires min and max power to differ") - } - - // a timer restarting on every surplus dip must not reserve at all (#32778) - high.pvTimer = clock.NewMock().Now() - if got := site.reservedPVPower(low); got != 0 { - t.Errorf("low while high timer restarts: want 0, got %.0f", got) - } - high.pvTimer = pvTimerStarted(testEnableDelay / 2) // high (top priority) reserves nothing if got := site.reservedPVPower(high); got != 0 { From 2524b46204aadc774e06b365045dc599c81124f0 Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Tue, 18 Aug 2026 12:53:55 +0200 Subject: [PATCH 028/135] Translations update from Hosted Weblate (#32432) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Kejserjorn Co-authored-by: Richard E Co-authored-by: FraBoCH <19388397+FraBoCH@users.noreply.github.com> Co-authored-by: RTTTC <94727758+RTTTC@users.noreply.github.com> Co-authored-by: Joni Lehtola Co-authored-by: shpokas Co-authored-by: Filipe Lima Co-authored-by: Dominik Trzebiński Co-authored-by: Eduard Costa Co-authored-by: Jonas Gate Co-authored-by: dingoduffel Co-authored-by: aaronchase64 Co-authored-by: Fernando Lopes Co-authored-by: Dario Cambié Co-authored-by: Алексей Новиков Co-authored-by: amtssp Co-authored-by: Tomasz Cielecki Co-authored-by: Łukasz Jernaś Co-authored-by: 19atlas Co-authored-by: Pere Montpeó Co-authored-by: premultiply <4681172+premultiply@users.noreply.github.com> --- i18n/bg.json | 2 - i18n/bs.json | 5 +- i18n/ca.json | 1800 ++++++++++++++++++++++++++++++++++++++++----- i18n/cs.json | 2 - i18n/da.json | 164 ++++- i18n/de.json | 4 +- i18n/el.json | 2 - i18n/es.json | 5 +- i18n/fi.json | 268 +++++-- i18n/fr.json | 34 +- i18n/hr.json | 2 - i18n/hu.json | 2 - i18n/it.json | 98 ++- i18n/ja.json | 2 - i18n/lb.json | 34 +- i18n/lt.json | 52 +- i18n/lv.json | 48 +- i18n/nl.json | 189 +++-- i18n/no.json | 7 - i18n/pl.json | 99 ++- i18n/pt.json | 239 ++++-- i18n/ro.json | 2 - i18n/ru.json | 944 +++++++++++++++++++----- i18n/sk.json | 56 +- i18n/sl.json | 2 - i18n/sv.json | 171 ++++- i18n/ta.json | 2 - i18n/tr.json | 27 +- i18n/uk.json | 2 - i18n/zh-Hans.json | 2 - 30 files changed, 3630 insertions(+), 636 deletions(-) diff --git a/i18n/bg.json b/i18n/bg.json index 2eb43a989..952fd4583 100644 --- a/i18n/bg.json +++ b/i18n/bg.json @@ -847,7 +847,6 @@ }, "planNumber": "План {number}", "precondition": { - "description": "Зареди за {duration} преди тръгване, за да бъде кондиционирана батерията на автомобила.", "label": "Късно зареждане", "optionAll": "всичко", "optionNo": "не" @@ -857,7 +856,6 @@ "repeatingPlans": "Повтарящи се планове", "selectAll": "Изберете всички", "strategyDisabledDescription": "Зареждането започва възможно най-късно, за да приключи точно навреме за тръгване. С динамични цени на електроенергията или тарифа за CO₂, тук са налице повече опции.", - "strategySettings": "Настройки на стратегията", "time": "Време", "title": "План", "unsavedChanges": "Има незаписани промени. Приложи сега?", diff --git a/i18n/bs.json b/i18n/bs.json index b540382fe..b8b284665 100644 --- a/i18n/bs.json +++ b/i18n/bs.json @@ -375,10 +375,7 @@ }, "main": { "chargingPlan": { - "planNumber": "Plan {number}", - "precondition": { - "description": "Punite {duration} prije polaska kako biste pripremili bateriju." - } + "planNumber": "Plan {number}" }, "energyflow": { "batteryTooltip": "{energy} od {total} ({soc})", diff --git a/i18n/ca.json b/i18n/ca.json index 20bb330ff..6c6f81834 100644 --- a/i18n/ca.json +++ b/i18n/ca.json @@ -1,230 +1,1517 @@ { "authProviders": { - "authCode": "Codi d'autenticació", + "authCode": "Codi d’autenticació", "authCodeHelp": "Copieu aquest codi i utilitzeu-lo al pas següent. Vàlid durant {duration}.", - "authorizationFailed": "L'autorització ha fallat", + "authorizationFailed": "Autorització fallida", "authorizationRequired": "Cal autorització", "authorizationSuccessful": "Autorització reeixida", - "buttonConnect": "Connectar a {provider}", - "buttonDisconnect": "Desconnectar", - "confirmLogout": "Estàs segur que vols desconnectar {title}?", + "buttonConnect": "Connecta a {provider}", + "buttonDisconnect": "Desconnecta", + "confirmLogout": "Esteu segur que voleu desconnectar {title}?", "connect": "connectar", "disconnect": "desconnectar", - "loggedOut": "Sortida reeixida", - "logoutFailed": "Sortida de la sessió fallida", - "modalDescriptionLogin": "Completeu el procés d'autorització per establir la connexió amb {provider}.", - "modalDescriptionLogout": "Això desconnectarà el vostre compte de {provider} i suprimirà l'accés a les seves dades.", - "success": "{title} ja està connectat i a punt per utilitzar.", - "successCloseModal": "Ara ja pots tancar aquest diàleg.", - "successCloseTab": "Ara ja pots tancar aquesta pestanya.", - "title": "Estat d'autorització" + "loggedOut": "S’ha tancat la sessió correctament", + "logoutFailed": "No s’ha pogut tancar la sessió", + "modalDescriptionLogin": "Completeu el procés d’autorització per establir la connexió amb {provider}.", + "modalDescriptionLogout": "Això desconnectarà el vostre compte de {provider} i suprimirà l’accés a les seves dades.", + "success": "{title} ja està connectat i a punt per utilitzar-se.", + "successCloseModal": "Ara ja podeu tancar aquest diàleg.", + "successCloseTab": "Ara ja podeu tancar aquesta pestanya.", + "title": "Estat d’autorització" }, "battery": { "card": { - "charging": "Carregant", - "discharging": "Descarregant", + "charging": "carregant", + "discharging": "descarregant", "energy": "Energia", - "gridCharge": "Carrega de la xarxa elècrtica", + "gridCharge": "càrrega de xarxa", "hold": "descàrrega bloquejada", "holdCharge": "càrrega bloquejada", "ofTotal": "de {total}", - "power": "Energia", + "power": "Potència", "soc": "Nivell de bateria" }, "combined": "Combinada", "config": { - "buffer": "Nivell de bateria de la casa per sobre de {soc} pot ser utilitzada per a mantenir la carrega i retardar l'apagat del mode solar. Inicia el mode de càrrega solar autimaticament {start}.", - "bufferNone": "L'energia de les bateries de casa no s'utilitza per a la càrrega. Indica valor inferior a {soc} per a habilitar el suport.", + "buffer": "L’energia de la bateria domèstica per sobre de {soc} es pot utilitzar per a donar suport als punts de recàrrega i retarda l’aturada en mode solar. La càrrega solar s’inicia automàticament {start}.", + "bufferNone": "L’energia de la bateria domèstica no s’utilitza per als punts de recàrrega. Establiu un valor inferior a {soc} per a habilitar el suport.", "bufferStart": { "above": "quan la bateria es troba per sobre de {soc}", "full": "quan la bateria està plena", - "never": "només amb suficient excedent" + "never": "només amb prou excedent" }, - "bufferTitle": "Bateria com a buffer de càrrega", - "discharge": "Evita la descàrrega de la bateria de casa en mode ràpid i durant càrregues planificades.", - "gridDischarge": "Permet a la bateria de casa exportar a la xarxa elèctrica." + "bufferTitle": "Bateria com a reserva de càrrega", + "discharge": "Evita la descàrrega de la bateria domèstica en mode ràpid i durant càrregues planificades.", + "gridDischarge": "Permet que la bateria domèstica exporti a la xarxa elèctrica.", + "priority": "La bateria domèstica té prioritat mentre estigui per sota de {soc}. Aleshores els punts de recàrrega reben l’excedent.", + "priorityNone": "L’excedent va directament als punts de recàrrega. Establiu un valor per sobre de {soc} per a prioritzar la bateria domèstica.", + "priorityTitle": "On va primer l’excedent?" + }, + "optimizer": { + "action": { + "charge": "Carrega de la xarxa", + "discharge": "Descarrega a la xarxa", + "hold": "Evita la descàrrega", + "holdcharge": "Evita la càrrega", + "normal": "Funcionament normal" + }, + "empty": "buida", + "full": "plena", + "highest": "Màxim ({value})", + "lowest": "Mínim ({value})", + "suggestion": "Suggeriment" } }, "batterySettings": { "batteryLevel": "Nivell de bateria", "bufferStart": { - "full": "quan gairebé ple {soc}.", - "never": "només amb prous excedents." + "above": "quan està per sobre de {soc}.", + "full": "quan arriba a {soc}.", + "never": "només amb prou excedent." }, "capacity": "{energy} de {total}", + "control": "Control de la bateria", + "discharge": "Evita la descàrrega en mode ràpid i càrrega planificada.", "disclaimerHint": "Nota:", - "disclaimerText": "aquests paràmetres només afecten el mode solar. El comportament de càrrega s'ajusta en conseqüència.", - "legendBottomName": "prioritat de la casa", + "disclaimerText": "Aquests paràmetres només afecten el mode solar. El comportament de càrrega s’ajusta en conseqüència.", + "gridChargeTab": "Càrrega de xarxa", + "legendBottomName": "Prioritat per a la bateria domèstica", "legendBottomSubline": "fins que arriba a {soc}.", - "legendMiddleName": "primer, el vehicle", - "legendMiddleSubline": "quan la bateria de casa està per sobre {soc}.", - "legendTopAutostart": "s'inicia automaticament", - "legendTopName": "càrrega soportada per bateria", - "legendTopSubline": "quan la bateria de casa està per sobre {soc}.", - "modalTitle": "Configuració bateria" + "legendMiddleName": "Prioritat per al vehicle", + "legendMiddleSubline": "quan la bateria domèstica està per sobre de {soc}.", + "legendTopAutostart": "S’inicia automàticament", + "legendTopName": "Càrrega del vehicle amb suport de bateria", + "legendTopSubline": "quan la bateria domèstica està per sobre de {soc}.", + "legendTopSublineAbove": "quan està per sobre de {soc}", + "legendTopSublineDisabled": "és {soc}.", + "legendTopSublineDisabledState": "desactivat", + "modalTitle": "Bateria domèstica", + "noBattery": "No hi ha bateries configurades.", + "usageTab": "Ús de la bateria" + }, + "colors": { + "auto": "Automàtic", + "hex": "Color en hexadecimal" + }, + "config": { + "adminPassword": { + "description": "El complement de script executa una ordre a escala de sistema amb els permisos del procés d’evcc. Torneu a introduir la contrasenya d’administrador per a continuar.", + "invalid": "La contrasenya no és vàlida. Torneu-ho a provar.", + "title": "Operació potencialment perillosa" + }, + "apiKey": { + "description": "Dona accés segur als scripts i a tasques d’automatització com les còpies de seguretat programades, sense compartir la contrasenya d’administrador.", + "exampleLabel": "Proveu-ho: descarregueu una còpia de seguretat amb curl", + "generate": "Genera la clau API", + "generateConfirm": "Introduïu la contrasenya d’administrador per a generar una clau API nova.", + "keyLabel": "Clau API", + "regenerate": "Regenera la clau API", + "regenerateConfirm": "Introduïu la contrasenya d’administrador per a regenerar la clau API.", + "regenerateLink": "Regenera", + "regenerateWarning": "Regenerar invalidarà la clau existent i qualsevol automatització que la utilitzi deixarà de funcionar. Voleu continuar?", + "revealSuccess": "La clau API nova ja es pot utilitzar.", + "revealTitle": "Clau API creada", + "shownOnce": "Aquesta clau es mostra només una vegada. Deseu-la en un lloc segur. No la podreu tornar a consultar després.", + "title": "Clau API" + }, + "aux": { + "description": "Dispositiu que ajusta el seu consum segons l’excedent disponible (com els escalfadors d’aigua intel·ligents). evcc espera que aquest dispositiu redueixi el seu consum si cal.", + "titleAdd": "Afegeix consumidor autoregulat", + "titleEdit": "Edita consumidor autoregulat" + }, + "battery": { + "title": "Bateria", + "titleAdd": "Afegeix bateria", + "titleEdit": "Edita bateria" + }, + "charge": { + "titleAdd": "Afegeix comptador de càrrega", + "titleEdit": "Edita comptador de càrrega" + }, + "charger": { + "chargers": "Carregadors de vehicles elèctrics", + "generic": "Integracions genèriques", + "heatingdevices": "Dispositius de calefacció", + "ocppConfirmContinue": "El vostre carregador encara no s’ha connectat a evcc. Esteu segur que voleu continuar?", + "ocppConnected": "Connectat!", + "ocppDescription": "evcc té un servidor OCPP incorporat. Seguiu aquests passos:", + "ocppHelp": "Copieu aquesta URL a la configuració del carregador. Comproveu el manual del fabricant per als detalls. S’espera que el carregador afegeixi automàticament el seu identificador únic (ID de l’estació) a la URL. En casos poc freqüents, pot ser que hàgiu d’afegir manualment l’identificador. Exemple: `{url}`", + "ocppLabel": "URL del servidor OCPP", + "ocppNextStep": "Següent pas", + "ocppStep1": "Configureu el vostre carregador per a utilitzar evcc com a servidor OCPP.", + "ocppStep2": "Espereu que el vostre carregador es connecti a evcc.", + "ocppStep3": "Continueu i completeu la configuració.", + "ocppWaiting": "Esperant la connexió", + "switchsockets": "Endolls controlables", + "template": "Fabricant", + "titleAdd": { + "charging": "Afegeix carregador", + "heating": "Afegeix dispositiu de calefacció" + }, + "titleEdit": { + "charging": "Edita carregador", + "heating": "Edita dispositiu de calefacció" + }, + "type": { + "custom": { + "charging": "Carregador definit per l’usuari", + "heating": "Dispositiu de calefacció definit per l’usuari" + }, + "heatpump": "Bomba de calor definida per l’usuari", + "sgready": "Bomba de calor definida per l’usuari (amb funció sg-ready a través de complements)", + "sgready-boost": "Bomba de calor definida per l’usuari (amb funció sg-ready-boost, obsolet)", + "sgready-relay": "Bomba de calor definida per l’usuari (amb funció sg-ready a través de relés)", + "switchsocket": "Endoll controlable definit per l’usuari" + } + }, + "circuits": { + "description": "Assegura que la suma de tots els punts de recàrrega connectats a un circuit no superi els límits de potència i corrent configurats. Els circuits es poden imbricar per a crear jerarquies.", + "title": "Gestió de càrregues", + "usableMeters": "Referències dels comptadors utilitzables" + }, + "consumer": { + "description": "Fa el seguiment de dispositius petits no controlables (rentadora, nevera…). Dona una millor visió general del consum de la llar.", + "titleAdd": "Afegeix comptador de consum", + "titleEdit": "Edita comptador de consum" + }, + "control": { + "description": "Normalment els valors per defecte són correctes. Canvieu-los només si sabeu què esteu fent.", + "descriptionInterval": "Cicle d’actualització en segons. Defineix la freqüència amb què evcc llegeix les dades dels comptadors i ajusta la càrrega. El valor per defecte de 30 segons és una opció segura. Els dispositius com vehicles, carregadors i inversors solen necessitar alguns segons per a ajustar el seu comportament. Si els vostres components reaccionen ràpidament, podeu utilitzar valors més baixos. No recomanem en cap cas baixar dels 10 segons. Si observeu un comportament erràtic o valors de potència irregulars, trieu un interval més gran.", + "descriptionResidualPower": "Canvia el punt de funcionament del bucle de control. Si teniu una bateria a la instal·lació, es recomana establir-hi un valor de 100 W. D’aquesta manera la bateria tindrà una lleugera prioritat respecte a la xarxa elèctrica.", + "labelInterval": "Interval d’actualització", + "labelResidualPower": "Potència residual", + "title": "Configuració del control" + }, + "currency": { + "description": "S’utilitza per a formatar els preus de l’energia, els costos i els estalvis segons la vostra tarifa.", + "example": "La càrrega ha costat {price}. Heu estalviat {amount}.", + "label": "Divisa", + "title": "Divisa" + }, + "deviceValue": { + "activeClients": "Clients actius", + "amount": "Import", + "asleep": "adormit", + "broker": "Broker", + "bucket": "Bucket", + "capacity": "Capacitat", + "chargeStatus": "Estat", + "chargeStatusA": "no connectat", + "chargeStatusB": "connectat", + "chargeStatusC": "carregant", + "chargeStatusE": "sense energia", + "chargeStatusF": "error", + "chargedEnergy": "Carregat", + "co2": "CO₂ de la xarxa", + "configured": "Configurat", + "connected": "Connectat", + "connections": "Connexions", + "controllable": "Controlable", + "currency": "Divisa", + "current": "Corrent", + "currentRange": "Corrent", + "curtailLimit": "Límit d’injecció", + "curtailable": "Reduïble", + "curtailed": "Injecció limitada", + "detected": "Detectat", + "dimLimit": "Límit de consum", + "dimmed": "Consum limitat", + "enabled": "Habilitat", + "energy": "Energia", + "events": "Esdeveniments", + "exportLimit": "Límit d’exportació a la xarxa", + "feedinPrice": "Preu d’injecció a la xarxa", + "forecast": "Previsió", + "gridPrice": "Preu de l’energia de la xarxa", + "heaterTempLimit": "Límit de l’escalfador", + "identifier": "Identificador RFID", + "loginBlocked": "Límit d’intents d’inici de sessió assolit", + "max": "màxim", + "messengers": "Serveis", + "no": "no", + "odometer": "Comptaquilòmetres", + "org": "Organització", + "outdoorTemp": "Temperatura exterior", + "phaseCurrents": "Corrent", + "phasePowers": "Potència", + "phaseVoltages": "Voltatge", + "phases1p3p": "Interruptor de fase", + "power": "Potència", + "powerRange": "Potència", + "price": "Preu", + "productionLimited": "Producció limitada", + "range": "Autonomia", + "remoteEnabled": "Habilitat", + "returnEnergy": "Energia (inversa)", + "singlePhase": "Monofàsic", + "soc": "Càrrega", + "solarForecast": "Previsió solar", + "switchDevice": "Control de corrent", + "temp": "Temperatura", + "topic": "Tema", + "url": "URL", + "vehicleLimitSoc": "Límit de càrrega", + "yes": "sí" + }, + "deviceValueChargeStatus": { + "A": "A (no connectat)", + "B": "B (connectat)", + "C": "C (carregant)" + }, + "devices": { + "auxMeter": "Consumidor intel·ligent", + "batteryStorage": "Emmagatzematge en bateria", + "consumer": "Consumidor", + "solarSystem": "Sistema fotovoltaic" + }, + "editor": { + "loading": "Carregant editor YAML…" + }, + "eebus": { + "certificate": { + "private": "Clau privada", + "public": "Certificat públic", + "title": "Certificats" + }, + "configuredDevices": "Dispositius configurats", + "description": "Configuració que permet a evcc comunicar-se amb dispositius compatibles amb EEBus, com carregadors o una unitat de control de l’operador de xarxa. Tota la inicialització i generació de certificats es realitza automàticament en el primer inici.", + "descriptionAdvanced": "No cal fer cap canvi. Feu-ho només si sabeu realment què esteu fent. Si modifiqueu el SHIP-ID o els certificats, haureu de tornar a aparellar els dispositius.", + "interfaces": "Interfícies", + "interfacesHelp": "Limiteu les interfícies de xarxa que EEBus ha d’utilitzar per a evitar problemes de comunicació. Deixeu el camp en blanc per a utilitzar-les totes. Una entrada per línia.", + "noConfiguredDevices": "Cap dispositiu configurat per SKI.", + "noPairings": "Cap dispositiu aparellat.", + "pairings": "Dispositius aparellats", + "port": "Port", + "portHelp": "El port que s’ha d’utilitzar.", + "qr": "Codi d’aparellament", + "qrExplain": "Escanegeu-lo des d’un altre dispositiu EEBus durant la instal·lació per a aparellar automàticament, sense introduir el codi SKI manualment.", + "removeConfirm": "Tota la configuració EEBus s’eliminarà. Es crearan certificats i identificadors nous en el pròxim inici. N’esteu segur?", + "removePairing": "Suprimeix l’aparellament", + "removePairingConfirm": "El dispositiu es desconnectarà i s’haurà de tornar a aparellar. N’esteu segur?", + "shipid": "SHIP-ID", + "shipidExplain": "Identificador permanent del dispositiu dins la xarxa EEBus.", + "shipidHelp": "Aquest SHIP-ID està enllaçat amb els certificats que es mostren a continuació.", + "ski": "SKI", + "skiExplain": "Identificador de seguretat únic per a aparellar dispositius EEBus.", + "title": "EEBus" + }, + "experimental": { + "description": "Proporciona accés anticipat a funcions que encara s’estan provant. Poden ser inestables i canviar o desaparèixer en versions futures.", + "title": "Experimental" + }, + "ext": { + "description": "Fa el seguiment de comptadors que no s’utilitzen per a la regulació ni es mostren a la interfície (subquadres, comptadors de xarxa o solars alternatius…). Les seves dades es registren i s’exporten igual que les de la resta de comptadors.", + "titleAdd": "Afegeix comptador addicional", + "titleEdit": "Edita comptador addicional" + }, + "form": { + "danger": "Perill", + "deprecated": "obsolet", + "durationUnit": "Unitat de temps de {label}", + "example": "Exemple", + "optional": "opcional" + }, + "general": { + "applyAndClose": "Aplica i tanca", + "authPerform": "Connecta’t amb {provider}", + "authPerformHint": "S’obrirà en una pestanya nova. Torneu aquí per a continuar.", + "authPrepare": "Prepara la connexió", + "cancel": "Cancel·la", + "change": "Canvia", + "clear": "Neteja", + "close": "Tanca", + "confirmDisable": "Voleu inhabilitar aquest dispositiu? S’ignorarà després del pròxim reinici.", + "confirmEnable": "Voleu tornar a habilitar aquest dispositiu?", + "confirmSave": "Hi ha canvis sense desar. Voleu desar-los ara?", + "copied": "Copiat!", + "copy": "Copia", + "customHelp": "Crea un dispositiu definit per l’usuari mitjançant el sistema de complements d’evcc.", + "customOption": "Dispositiu definit per l’usuari", + "delete": "Suprimeix", + "disable": "Inhabilita", + "disabled": "inhabilitat", + "dismiss": "Descarta", + "docsLink": "Consulteu la documentació.", + "dragHandle": "Nansa d’arrossegament", + "dragItem": "Arrossegable: {title}", + "dragList": "Llista reordenable", + "enable": "Habilita", + "error": "Error", + "experimental": "Experimental", + "forceSave": "Desa igualment", + "fromYamlHint": "Nota: configurat mitjançant evcc.yaml. Elimineu l’entrada del fitxer per a poder-ho editar aquí.", + "hideAdvancedSettings": "Amaga els paràmetres avançats", + "invalidFileSelected": "Fitxer seleccionat no vàlid", + "legacy": "antic", + "noFileSelected": "Cap fitxer seleccionat.", + "off": "desactivat", + "on": "activat", + "readFromFile": "Llegeix des d’un fitxer", + "remove": "Elimina", + "required": "obligatori", + "reset": "Restableix", + "save": "Desa", + "saved": "Desat.", + "saving": "S’està desant…", + "selectFile": "Explora", + "showAdvancedSettings": "Mostra els paràmetres avançats", + "telemetry": "Telemetria", + "templateLoading": "S’està carregant…", + "title": "Títol", + "typeDeprecated": "El tipus «{type}» està desactualitzat i s’eliminarà en una versió futura. Comproveu el registre de canvis i torneu a crear aquest dispositiu.", + "validateSave": "Valida i desa" + }, + "grid": { + "title": "Comptador de xarxa", + "titleAdd": "Afegeix comptador de xarxa", + "titleEdit": "Edita comptador de xarxa" + }, + "hems": { + "changeConfirm": "La configuració existent se suprimirà. Voleu continuar?", + "csv": { + "created": "Creat", + "finished": "Finalitzat", + "gridpower": "Potència de xarxa (kW)", + "limitpower": "Límit (kW)", + "type": "Tipus" + }, + "customOption": "Integració definida per l’usuari", + "description": "El llindar de limitació real s’ha de configurar a cada inversor.", + "download": "Descarregueu", + "dynamicLimits": "Límits dinàmics", + "eventsRecorded": "S’han registrat {count} esdeveniments de limitació de xarxa.", + "exportLimit": "Límit d’exportació a la xarxa", + "exportLimitDescription": "L’optimitzador de bateries respecta aquest límit per a evitar pèrdues solars. Un senyal de limitació actiu d’una integració de límits dinàmics té prioritat.", + "exportLimitHint": "Límit estàtic d’injecció al punt de connexió a la xarxa.", + "generic": "Integracions genèriques", + "integrations": "Integracions", + "lastEvent": "Esdeveniment més recent: {timeAgo}.", + "noEvents": "Encara no s’ha registrat cap esdeveniment.", + "recordedEvents": "Esdeveniments registrats", + "staticLimits": "Límits estàtics", + "template": "Integració", + "title": "Límit extern", + "type": { + "custom": "Integració definida per l’usuari", + "relay": "Integració definida per l’usuari (relé)" + } + }, + "icon": { + "change": "canvia", + "label": "Icona" + }, + "influx": { + "description": "Escriu les dades de càrrega i altres mètriques a InfluxDB. Utilitzeu Grafana o altres eines per a visualitzar les dades.", + "descriptionToken": "Consulteu la documentació d’InfluxDB per a saber com crear-ne un. https://docs.influxdata.com/influxdb/v2/admin/", + "labelBucket": "Bucket", + "labelCheckInsecure": "Permet els certificats autosignats", + "labelDatabase": "Base de dades", + "labelInsecure": "Validació de certificats", + "labelOrg": "Organització", + "labelPassword": "Contrasenya", + "labelToken": "Testimoni API", + "labelUrl": "URL", + "labelUser": "Nom d’usuari", + "title": "InfluxDB", + "v1Support": "Necessiteu compatibilitat amb InfluxDB 1.x?", + "v2Support": "Torna a InfluxDB 2.x" + }, + "loadpoint": { + "addCharger": { + "charging": "Afegeix un carregador", + "heating": "Afegeix un dispositiu de calefacció" + }, + "addMeter": "Afegeix un comptador d’energia dedicat", + "advancedConfig": "Configuració avançada", + "cancel": "Cancel·la", + "chargerError": { + "charging": "Cal configurar un carregador.", + "heating": "Cal configurar un dispositiu de calefacció." + }, + "chargerLabel": { + "charging": "Carregador", + "heating": "Dispositiu de calefacció" + }, + "chargerPower11kw": "11 kW", + "chargerPower11kwHelp": "Utilitzarà un interval de corrent de 6 a 16 A.", + "chargerPower22kw": "22 kW", + "chargerPower22kwHelp": "Utilitzarà un interval de corrent de 6 a 32 A.", + "chargerPowerCustom": "altre", + "chargerPowerCustomHelp": "Defineix un interval de corrent personalitzat.", + "chargerTypeLabel": "Tipus de carregador", + "chargingTitle": "Comportament", + "circuitHelp": "Assignació a la gestió de càrregues per a assegurar que no se superin els límits de potència i corrent.", + "circuitInvalid": "El circuit no existeix", + "circuitLabel": "Circuit", + "circuitUnassigned": "sense assignar", + "created": "«{name}» s’ha creat amb valors per defecte raonables.", + "createdTitle": { + "charging": "Punt de recàrrega creat", + "heating": "Escalfador creat" + }, + "creating": { + "charging": "Carregador desat. S’està creant el punt de recàrrega…", + "heating": "Dispositiu de calefacció desat. S’està creant l’escalfador…" + }, + "defaultModeHelp": { + "charging": "Mode de càrrega en connectar el vehicle.", + "heating": "S’estableix a l’inici del sistema." + }, + "defaultModeHelpKeep": "Manté l’últim mode seleccionat.", + "defaultModeKeep": "manté l’actual", + "defaultModeLabel": "Mode per defecte", + "delete": "Suprimeix", + "electricalSubtitle": "En cas de dubte, consulteu el vostre electricista.", + "electricalTitle": "Electricitat", + "energyMeterHelp": "Comptador addicional si el carregador no en té un d’integrat.", + "energyMeterLabel": "Comptador d’energia", + "estimateLabel": "Interpola el nivell de càrrega entre actualitzacions de l’API", + "maxCurrentHelp": "Ha de ser superior al corrent mínim.", + "maxCurrentLabel": "Corrent màxim", + "maxTempHelp": "Ha de ser superior a la temperatura mínima.", + "maxTempLabel": "Temperatura màxima", + "minCurrentHelp": "Baixeu de 6 A només si sabeu què esteu fent.", + "minCurrentLabel": "Corrent mínim", + "minTempLabel": "Temperatura mínima", + "noVehicles": "No hi ha cap vehicle configurat.", + "option": { + "charging": "Afegeix un punt de recàrrega", + "heating": "Afegeix un escalfador" + }, + "phases1p": "Monofàsic", + "phases3p": "Trifàsic", + "phasesAutomatic": "Fases automàtiques", + "phasesAutomaticHelp": "El vostre carregador admet el canvi automàtic entre càrrega monofàsica i trifàsica. A la pantalla principal podeu ajustar el comportament de les fases durant la càrrega.", + "phasesHelp": "Nombre de fases connectades.", + "phasesLabel": "Fases", + "pollIntervalDanger": "Consultar el vehicle regularment pot descarregar la bateria del vehicle. Alguns fabricants poden bloquejar activament la càrrega en aquest cas. No recomanat! Utilitzeu-ho només si esteu al corrent dels riscos.", + "pollIntervalHelp": "Temps entre actualitzacions de l’API del vehicle. Intervals curts poden descarregar la bateria del vehicle.", + "pollIntervalLabel": "Interval d’actualització", + "pollModeAlways": "sempre", + "pollModeAlwaysHelp": "Demana actualitzacions d’estat a intervals regulars sempre.", + "pollModeCharging": "carregant", + "pollModeChargingHelp": "Demana actualitzacions de l’estat del vehicle només durant la càrrega.", + "pollModeConnected": "connectat", + "pollModeConnectedHelp": "Actualitza l’estat del vehicle a intervals regulars quan està connectat.", + "pollModeLabel": "Comportament d’actualització", + "priorityHelp": "Una prioritat més alta obté accés preferent a l’excedent solar.", + "priorityLabel": "Prioritat", + "save": "Desa", + "solarBehaviorCustomHelp": "Defineix els vostres propis llindars i retards d’activació i desactivació.", + "solarBehaviorDefaultHelp": "Inicia després de {enableDelay} d’excedent suficient. Atura quan no hi ha excedent suficient durant {disableDelay}.", + "solarBehaviorLabel": "Solar", + "solarModeCustom": "personalitzat", + "solarModeMaximum": "solar màxim", + "temperatureRangeHelp": "Interval de visualització de la barra de temperatura. No afecta el control.", + "temperatureRangeTitle": "Interval de temperatura", + "thresholdDisableDelayLabel": "Retard de desactivació", + "thresholdDisableHelpInvalid": "Utilitzeu un valor positiu.", + "thresholdDisableHelpPositive": "Atura quan s’utilitzen més de {power} de la xarxa durant {delay}.", + "thresholdDisableHelpZero": "Atura quan la potència mínima requerida no es pot satisfer durant {delay}.", + "thresholdDisableLabel": "Potència de xarxa de desactivació", + "thresholdEnableDelayLabel": "Retard d’activació", + "thresholdEnableHelpInvalid": "Utilitzeu un valor negatiu.", + "thresholdEnableHelpNegative": "Inicia quan hi hagi un excedent de {surplus} disponible durant {delay}.", + "thresholdEnableHelpZero": "Inicia quan la potència mínima requerida es pugui satisfer durant {delay}.", + "thresholdEnableLabel": "Potència de xarxa d’activació", + "titleAdd": { + "charging": "Afegeix punt de recàrrega", + "heating": "Afegeix escalfador", + "unknown": "Afegeix punt de recàrrega o escalfador" + }, + "titleEdit": { + "charging": "Edita punt de recàrrega", + "heating": "Edita escalfador", + "unknown": "Edita punt de recàrrega o escalfador" + }, + "titleExample": { + "charging": "Garatge, aparcament cobert, etc.", + "heating": "Bomba de calor, escalfador, etc." + }, + "titleLabel": "Títol", + "vehicleAutoDetection": "detecció automàtica", + "vehicleHelpAutoDetection": "Selecciona automàticament el vehicle més probable. Es pot canviar manualment.", + "vehicleHelpDefault": "Assumeix sempre que aquest vehicle carrega aquí. Detecció automàtica desactivada. Es pot canviar manualment.", + "vehicleInvalid": "El vehicle no existeix", + "vehicleLabel": "Vehicle per defecte", + "vehiclesTitle": "Vehicles" + }, + "main": { + "addAdditional": "Afegeix comptador addicional", + "addConsumer": "Afegeix consumidor", + "addGrid": "Afegeix comptador de xarxa", + "addLoadpoint": "Afegeix punt de recàrrega o escalfador", + "addPvBattery": "Afegeix solar o bateria", + "addTariffs": "Afegeix tarifes", + "addVehicle": "Afegeix vehicle", + "configured": "configurat", + "edit": "edita", + "loadpointRequired": "Cal configurar com a mínim un punt de recàrrega.", + "name": "Nom", + "title": "Configuració", + "unconfigured": "no configurat", + "vehicles": "Els meus vehicles", + "welcomeBannerText": "Comenceu creant com a mínim un **carregador**, un **escalfador**, un comptador de **xarxa**, un comptador **solar**, un comptador de **bateria** o un comptador **addicional**. Si només voleu fer una prova, trieu un **dispositiu de demostració**.", + "welcomeBannerTitle": "Configureu el vostre sistema!" + }, + "mcp": { + "description": "Exposa un servidor Model Context Protocol que permet a assistents d’IA com Claude llegir l’estat del vostre sistema i controlar la càrrega.", + "exampleLabel": "Exemple: Claude CLI", + "restartHint": "Estarà disponible després de reiniciar.", + "title": "Servidor MCP", + "url": "Endpoint MCP" + }, + "messaging": { + "addMessenger": "Afegeix servei", + "description": "Rep notificacions sobre les vostres sessions de càrrega.", + "event": { + "asleep": { + "messageDefault": "Alliberament de càrrega, el vehicle {vehicleName} no carrega.", + "title": "Quan s’espera el vehicle", + "titleDefault": "Vehicle en repòs" + }, + "connect": { + "messageDefault": "Vehicle connectat amb {pvPower} kW de PV", + "title": "Quan es connecta un vehicle", + "titleDefault": "Vehicle connectat" + }, + "disconnect": { + "messageDefault": "Vehicle desconnectat després de {connectedDuration}", + "title": "Quan es desconnecta un vehicle", + "titleDefault": "Vehicle desconnectat" + }, + "guest": { + "messageDefault": "Vehicle desconegut, s’ha connectat un convidat?", + "title": "Quan es connecta un vehicle desconegut", + "titleDefault": "Vehicle desconegut" + }, + "planoverrun": { + "messageDefault": "{vehicleTitle}: el pla no es complirà a temps.", + "title": "Quan el pla de càrrega no es complirà a temps", + "titleDefault": "Pla superat" + }, + "soc": { + "messageDefault": "Bateria carregada al {vehicleSoc}%", + "title": "Actualització del nivell de càrrega", + "titleDefault": "Nivell de càrrega actualitzat" + }, + "start": { + "messageDefault": "S’ha iniciat la càrrega en mode {mode}.", + "title": "Quan comença la càrrega", + "titleDefault": "Càrrega iniciada" + }, + "stop": { + "messageDefault": "S’ha acabat de carregar {chargedEnergy} kWh en {chargeDuration}.", + "title": "Quan s’atura la càrrega", + "titleDefault": "Càrrega finalitzada" + }, + "suggestion": { + "messageDefault": "Suggeriment de l’optimitzador per a {suggestionTitle}: si el control de l’optimitzador estigués actiu, ara aplicaria «{suggestionAction}». Només informatiu, no s’ha canviat res. Experimental; envieu comentaris a https://github.com/evcc-io/evcc/issues/31903", + "title": "Quan canvia l’acció orientativa de l’optimitzador", + "titleDefault": "Suggeriment de l’optimitzador" + } + }, + "eventMessage": "Missatge", + "eventTitle": "Títol", + "events": "Esdeveniments", + "legacyWarning": "Hi ha una nova configuració de notificacions disponible. Elimineu i deseu la configuració aquí per a utilitzar el nou procés guiat.", + "messengers": "Serveis", + "seePlaceholders": "vegeu els marcadors", + "title": "Notificacions" + }, + "messenger": { + "custom": "Servei definit per l’usuari", + "generic": "Servei genèric", + "primary": "Servei específic", + "template": "Servei", + "titleAdd": "Afegeix servei", + "titleEdit": "Edita servei" + }, + "meter": { + "cancel": "Cancel·la", + "convertToConsumer": { + "confirm": "Converteix aquest «comptador addicional» en un «consumidor».", + "link": "Converteix en consumidor" + }, + "delete": "Suprimeix", + "generic": "Integracions genèriques", + "option": { + "aux": "Afegeix consumidor autoregulat", + "battery": "Afegeix comptador de bateria", + "consumer": "Afegeix consumidor normal", + "pv": "Afegeix comptador solar" + }, + "save": "Desa", + "specific": "Integracions específiques", + "template": "Fabricant", + "titleChoice": "Què voleu afegir?", + "titleLabel": "Títol", + "usage": { + "aux": "Consumidor autoregulat", + "battery": "Bateria", + "charge": "Carregador", + "grid": "Xarxa", + "label": "Ús", + "pv": "Producció" + }, + "validateSave": "Valida i desa" + }, + "modbus": { + "baudrate": "Velocitat en bauds", + "comset": "ComSet", + "connection": "Connexió Modbus", + "connectionHintSerial": "El dispositiu està connectat directament mitjançant RS485 (o un adaptador USB a RS485).", + "connectionHintTcpip": "El dispositiu és accessible per xarxa (LAN/wifi).", + "connectionValueSerial": "RS485", + "connectionValueTcpip": "Xarxa", + "delay": "Retard", + "delayHint": "Temps d’espera opcional abans de cada accés als registres. Només cal per a dispositius lents.", + "device": "Nom del dispositiu", + "deviceHint": "Exemple: /dev/ttyUSB0", + "host": "Adreça IP o nom de l’equip", + "hostHint": "Exemple: 192.0.2.2", + "id": "ID de Modbus", + "port": "Port", + "protocol": "Protocol Modbus", + "protocolHintRtu": "Connexió mitjançant un adaptador RS485 a Ethernet sense traducció de protocol.", + "protocolHintTcp": "El dispositiu té compatibilitat nativa LAN/wifi o està connectat mitjançant un adaptador RS485 a Ethernet amb traducció de protocol.", + "protocolValueRtu": "RTU", + "protocolValueTcp": "TCP", + "timeout": "Temps d’espera", + "timeoutHint": "Temps màxim opcional d’espera de la resposta d’un dispositiu." + }, + "modbusproxy": { + "add": "Afegeix connexió de servidor intermediari", + "connection": "Connexió núm. {number}", + "description": "Alguns dispositius Modbus només admeten una sola connexió o molt poques. evcc pot actuar com a servidor intermediari i permetre l’accés simultani de diversos clients (domòtica, scripts, etc.).", + "device": "Dispositiu", + "option": { + "deny": "error", + "false": "no", + "true": "silenciós" + }, + "readonly": { + "help": { + "deny": "L’accés d’escriptura es bloqueja amb un error de Modbus.", + "false": "L’accés d’escriptura es reenvia.", + "true": "L’accés d’escriptura es bloqueja sense resposta." + }, + "label": "Només lectura" + }, + "sourcePortHelp": "Port per a les connexions de clients entrants. Ha d’estar disponible.", + "title": "Proxy de Modbus" + }, + "mqtt": { + "authentication": "Autenticació", + "description": "Es connecta a un broker MQTT per a intercanviar dades amb altres sistemes de la vostra xarxa.", + "descriptionClientId": "Autor dels missatges. Si està buit s’utilitza `evcc-[rand]`.", + "descriptionTopic": "Deixeu-ho buit per a desactivar la publicació.", + "labelBroker": "Broker", + "labelCaCert": "Certificat del servidor (CA)", + "labelCheckInsecure": "Permet els certificats autosignats", + "labelClientCert": "Certificat de client", + "labelClientId": "ID de client", + "labelClientKey": "Clau de client", + "labelInsecure": "Validació de certificats", + "labelPassword": "Contrasenya", + "labelTopic": "Tema", + "labelUser": "Nom d’usuari", + "publishing": "Publicació", + "title": "MQTT" + }, + "network": { + "descriptionExternalUrl": "Adreça per a altres dispositius que es vulguin connectar a evcc i per al descobriment automàtic de l’app d’evcc.", + "descriptionHost": "S’utilitza per a anunciar evcc a la vostra xarxa local.", + "descriptionInternalUrl": "Adreça d’evcc a la xarxa local.", + "descriptionPort": "Port per a la interfície web i l’API. Si el canvieu, haureu d’actualitzar l’URL del navegador.", + "labelExternalUrl": "URL externa", + "labelHost": "Nom d’equip mDNS", + "labelInternalUrl": "URL interna", + "labelPort": "Port", + "title": "Xarxa", + "warningUrlPath": "Normalment l’URL no necessita cap camí. Esteu segur que és correcte?" + }, + "ocpp": { + "forwardingConfigured": "Reenviament configurat", + "forwardingError": "Error de reenviament", + "forwardingOff": "Cap reenviament configurat", + "noChargers": "No s’ha detectat cap carregador OCPP.", + "stations": "ID d’estació", + "stationsHelp": "Carregadors OCPP configurats i detectats. A més del control local, podeu reenviar missatges a un servidor extern (p. ex. per a facturació).", + "status": { + "configured": "No connectat", + "connected": "Connectat", + "unknown": "Desconegut" + }, + "title": "Servidor OCPP", + "url": "URL del servidor", + "urlHelp": "Copieu aquesta URL a la configuració del carregador. Consulteu el manual del fabricant per als detalls. S’espera que el carregador afegeixi automàticament el seu identificador únic (ID d’estació) a la URL. En casos rars, pot caldre afegir l’identificador manualment. Exemple: `{url}`" + }, + "ocppforwarder": { + "description": "Reenvia els missatges OCPP d’aquest carregador a un servidor extern (plataforma de facturació, distribuïdora, etc.) a més d’evcc.", + "editTitle": "Reenviament OCPP", + "labelCaCert": "Certificat del servidor (CA)", + "labelCheckInsecure": "Permet els certificats autosignats", + "labelInsecure": "Validació de certificats", + "password": "Contrasenya", + "passwordHelp": "Per a l’autenticació bàsica HTTP amb el servidor de destí.", + "readOnly": { + "check": "Bloqueja les ordres del servidor de destí", + "help": { + "false": "El servidor de destí pot enviar ordres al carregador a través d’evcc.", + "true": "El servidor de destí rep tots els missatges del carregador, però no pot enviar ordres. evcc manté el control exclusiu." + }, + "label": "Ordres del servidor de destí" + }, + "status": "Estat", + "upstreamStationId": "ID d’estació", + "upstreamStationIdHelp": "Identificador del carregador enviat al servidor de destí.", + "upstreamUrl": "URL del servidor de destí", + "upstreamUrlHelp": "URL WebSocket OCPP del servidor extern.", + "username": "Nom d’usuari", + "usernameHelp": "Per a l’autenticació bàsica HTTP amb el servidor de destí." + }, + "optimizer": { + "description": "Analitza la previsió solar, els preus de l’electricitat i els vostres patrons de consum per a optimitzar l’estratègia de bateria i càrrega. Les dades s’envien al servei d’optimització d’evcc per al seu processament. Actualment només calcula i visualitza. Encara no controla dispositius.", + "enable": "Habilita l’optimitzador", + "info": "Pot trigar uns minuts a aparèixer l’entrada de menú de l’optimitzador. En instal·lacions noves, poden passar fins a 24 hores fins que evcc hagi recopilat prou dades.", + "title": "Optimitzador" + }, + "options": { + "boolean": { + "no": "no", + "yes": "sí" + }, + "endianness": { + "big": "big-endian", + "little": "little-endian" + }, + "operationMode": { + "heating": "Calefacció", + "standby": "En espera" + }, + "schema": { + "http": "HTTP (sense xifrar)", + "https": "HTTPS (xifrat)" + }, + "status": { + "A": "A (no connectat)", + "B": "B (connectat)", + "C": "C (carregant)" + } + }, + "pv": { + "titleAdd": "Afegeix comptador solar", + "titleEdit": "Edita comptador solar" + }, + "remote": { + "active": "Actiu", + "addClient": "Afegeix client", + "addClientDescription": "Les credencials s’emmagatzemen i es verifiquen només localment a la vostra instància d’evcc.", + "addClientTitle": "Afegeix client remot", + "clientCreated": "Client creat", + "clients": "Clients", + "confirmDelete": "Voleu suprimir el client?", + "connected": "Connectat", + "createClient": "Crea client", + "description": "Accediu a la vostra instal·lació d’evcc des de qualsevol lloc amb l’app mòbil d’evcc. No cal reenviament de ports ni VPN.", + "deviceName": "Nom del dispositiu", + "disconnected": "Desconnectat", + "done": "Fet", + "enableLabel": "Habilita l’accés remot", + "expiration": "Venciment", + "expirationNone": "Mai", + "expired": "vençut", + "expiresIn": "venç {time}", + "lastActive": "actiu {time}", + "loginBlocked": "Els inicis de sessió remots estan blocats durant un minut per massa intents fallits.", + "manualLogin": "O inicieu sessió manualment a {url} al navegador amb aquestes credencials:", + "noClients": "Encara no hi ha clients. Ningú no es pot connectar encara.", + "password": "Contrasenya", + "passwordOnce": "Aquesta contrasenya es mostra una sola vegada. Escanegeu el codi QR o copieu-la ara. No la tornareu a veure.", + "qrInstall": "Instal·leu l’app d’evcc per a {ios} o {android}.", + "qrScan": "Escanegeu el codi amb la càmera del mòbil per a connectar-vos. Feu-hi clic si ja utilitzeu el mòbil.", + "removeClient": "Elimina client", + "title": "Accés remot", + "url": "URL pública", + "username": "Nom d’usuari" + }, + "section": { + "additionalMeter": "Comptadors addicionals", + "consumers": "Consumidors", + "general": "General", + "grid": "Xarxa", + "integrations": "Integracions", + "loadpoints": "Punts de recàrrega i escalfadors", + "meter": "Solar i bateria", + "services": "Serveis", + "system": "Sistema", + "vehicles": "Vehicles" + }, + "security": { + "authDisabledHint": "L’autenticació està desactivada. La gestió de credencials no està disponible.", + "description": "Gestioneu l’autenticació i les credencials d’aquesta instància.", + "passwordDescription": "Actualitzeu la contrasenya utilitzada per a iniciar la sessió a la interfície de configuració.", + "passwordTitle": "Contrasenya d’administrador", + "title": "Seguretat", + "updatePassword": "Actualitza la contrasenya" + }, + "shm": { + "cardTitle": "Sunny Home Manager", + "description": "evcc incorpora la integració amb l’SMA Sunny Home Manager (SHM) mitjançant el protocol SEMP. Si funciona a la mateixa xarxa, després d’iniciar la sessió al vostre compte de Sunny Portal, hauríeu de veure l’opció d’afegir automàticament tots els carregadors configurats a evcc com a consumidors nous detectats. Tot hauria d’estar a punt per a utilitzar immediatament, sense cap ajust aquí sota.", + "descriptionDeviceId": "Cadena hexadecimal de 12 caràcters. Prefix per a tots els dispositius (punt de recàrrega…).", + "descriptionDeviceSerial": "Cadena hexadecimal de 12 caràcters. Número de sèrie base per a tots els dispositius (punt de recàrrega…). Per defecte, evcc el dedueix de l’adreça MAC de l’equip.", + "descriptionIdPattern": "Patró d’identificador", + "descriptionIds": "Al Sunny Portal cada dispositiu consumidor necessita un identificador únic. evcc genera un identificador únic basat en el vostre maquinari. Si migreu evcc a un altre maquinari, aquests identificadors poden canviar. Si voleu mantenir l’historial, podeu sobreescriure aquí els identificadors generats. Obriu la URL SEMP (/semp) per a comprovar els identificadors actuals.", + "descriptionSempUrl": "URL SEMP", + "descriptionVendorId": "Cadena hexadecimal de 8 caràcters. Prefix general de totes les entitats. Per defecte, evcc utilitzarà el seu propi ID de proveïdor intern.", + "labelDeviceId": "ID del dispositiu", + "labelDeviceSerial": "Número de sèrie del dispositiu", + "labelVendorId": "ID de proveïdor", + "title": "SMA Sunny Home Manager" + }, + "sponsor": { + "addToken": "Introduïu el testimoni", + "changeToken": "Canvia el testimoni", + "description": "El model de patrocini ens ajuda a mantenir el projecte i a desenvolupar de manera sostenible funcions noves i interessants. Com a patrocinador teniu accés a totes les implementacions de carregadors.", + "descriptionToken": "Com a GitHub Sponsor trobareu el vostre testimoni a {url}. Per a començar, oferim un {trialToken}.", + "enterYourToken": "El vostre testimoni de patrocinador", + "error": "El testimoni de patrocinador no és vàlid.", + "invalid": "no vàlid", + "labelToken": "Testimoni de patrocinador", + "title": "Patrocini", + "tokenRequired": "Heu de configurar un testimoni de patrocinador abans de poder crear aquest dispositiu.", + "tokenRequiredFeature": "Aquesta funció requereix un testimoni de patrocinador.", + "tokenRequiredLearnMore": "Més informació.", + "tokenRequiredShort": "Cap testimoni de patrocinador configurat.", + "trialToken": "testimoni de prova", + "viaYaml": "mitjançant evcc.yaml", + "yourToken": "Testimoni de patrocinador" + }, + "system": { + "backupRestore": { + "backup": { + "action": "Descarrega la còpia de seguretat…", + "confirmationButton": "Descarrega la còpia de seguretat", + "confirmationText": "Descarregueu el fitxer de la base de dades.", + "description": "Feu una còpia de seguretat de les vostres dades en un fitxer. Aquest fitxer es pot utilitzar per a restaurar les dades en cas de fallada del sistema.", + "title": "Còpia de seguretat" + }, + "cancel": "Cancel·la", + "description": "Feu còpies de seguretat, restaureu-les i restabliu les vostres dades. Útil si voleu moure les dades a un altre sistema.", + "note": "Nota: totes les accions anteriors només afecten les dades de la base de dades. El fitxer de configuració evcc.yaml no es modifica.", + "reset": { + "action": "Restableix…", + "confirmationButton": "Restableix i reinicia", + "confirmationText": "Se suprimiran permanentment les dades seleccionades. Assegureu-vos d’haver descarregat abans una còpia de seguretat.", + "description": "Teniu problemes de configuració i voleu començar de zero? Suprimiu totes les dades i comenceu de nou.", + "sessions": "Sessions de càrrega", + "sessionsDescription": "Suprimeix l’historial de les vostres sessions de càrrega.", + "settings": "Configuració i paràmetres", + "settingsDescription": "Suprimeix tots els dispositius, serveis, plans, memòries cau, etc. configurats.", + "title": "Restabliment" + }, + "restore": { + "action": "Restaura…", + "confirmationButton": "Restaura i reinicia", + "confirmationText": "Se sobreescriurà completament la base de dades. Assegureu-vos d’haver descarregat abans una còpia de seguretat.", + "description": "Restaureu les vostres dades des d’un fitxer de còpia de seguretat. Se sobreescriuran totes les dades actuals.", + "labelFile": "Fitxer de còpia de seguretat", + "title": "Restauració" + }, + "title": "Còpia de seguretat i restauració" + }, + "logs": "Registres", + "restart": "Reinicia", + "restartRequiredDescription": "Reinicieu per a veure l’efecte.", + "restartRequiredMessage": "La configuració ha canviat.", + "restartingDescription": "Espereu…", + "restartingMessage": "S’està reiniciant evcc." + }, + "tariff": { + "addForecast": "Afegeix previsió", + "addTariff": "Afegeix tarifa", + "co2": { + "description": "Previsió d’intensitat de CO₂ de l’electricitat de xarxa. Per a la càrrega optimitzada per CO₂ i el càlcul d’estalvis d’emissions.", + "titleAdd": "Afegeix previsió de CO₂", + "titleEdit": "Edita previsió de CO₂" + }, + "co2Services": "Serveis de CO₂", + "customForecast": "Previsió definida per l’usuari", + "customTariff": "Tarifa definida per l’usuari", + "description": "Configureu les vostres tarifes i previsions d’energia. Utilitzeu la configuració basada en dispositius per a la gestió dinàmica o YAML per a paràmetres estàtics.", + "feedIn": { + "description": "Compensació per l’electricitat exportada a la xarxa. S’utilitza per a calcular els costos reals de càrrega.", + "titleAdd": "Afegeix tarifa d’exportació a la xarxa", + "titleEdit": "Edita tarifa d’exportació a la xarxa" + }, + "generic": "Integracions genèriques", + "grid": { + "description": "Preu de l’electricitat del consum de xarxa. Per a calcular els costos reals de càrrega i la càrrega optimitzada per preu de vehicles, dispositius de calefacció o càrrega de la bateria domèstica des de la xarxa.", + "titleAdd": "Afegeix tarifa de consum de xarxa", + "titleEdit": "Edita tarifa de consum de xarxa" + }, + "legacyWarning": "Hi ha una nova configuració de tarifes disponible. Elimineu i deseu les vostres tarifes aquí per a utilitzar el nou procés guiat.", + "option": { + "co2": "Afegeix previsió de CO₂", + "feedIn": "Afegeix tarifa d’exportació a la xarxa", + "grid": "Afegeix tarifa de consum de xarxa", + "planner": "Afegeix previsió del planificador", + "solar": "Afegeix previsió solar", + "temperature": "Afegeix previsió de temperatura" + }, + "planner": { + "description": "Paràmetre avançat. Normalment, no cal, ja que les tarifes elèctriques dinàmiques o les previsions de CO₂ s’utilitzen automàticament. Habilita una font de dades addicional que només s’utilitza per a la planificació de càrrega, no per a les estadístiques ni els càlculs de preus.", + "titleAdd": "Afegeix previsió del planificador", + "titleEdit": "Edita previsió del planificador" + }, + "services": "Serveis", + "solar": { + "description": "Previsió de producció solar del vostre sistema fotovoltaic. Es mostra a la interfície i en el futur s’utilitzarà per als algorismes d’optimització.", + "titleAdd": "Afegeix previsió solar", + "titleEdit": "Edita previsió solar" + }, + "temperature": { + "description": "Previsió de temperatura exterior de la vostra ubicació. Es mostra a la interfície i s’utilitza per a predir la demanda de calefacció.", + "titleAdd": "Afegeix previsió de temperatura", + "titleEdit": "Edita previsió de temperatura" + }, + "template": "Proveïdor", + "title": "Tarifes i previsions", + "titleChoice": "Què voleu afegir?", + "type": { + "co2": "Intensitat de CO₂", + "feedIn": "Preu d’exportació a la xarxa", + "grid": "Preu de consum de xarxa", + "planner": "Planificador", + "solar": "Solar", + "temperature": "Previsió de temperatura" + }, + "zones": { + "add": "Afegeix zona", + "allDays": "Tots els dies", + "allMonths": "Tots els mesos", + "allTimes": "Totes les hores", + "cancel": "Cancel·la", + "charge": "Càrrega", + "days": "Dies", + "edit": "Edita", + "hours": "Hores", + "months": "Mesos", + "price": "Preu", + "priceRequired": "Cal indicar el preu", + "remove": "Elimina zona", + "save": "Desa", + "selectAll": "Tots els dies", + "timeFrom": "Des de", + "timeRangeError": "L’hora d’inici ha de ser anterior a la de fi. Per a abastar la mitjanit, creeu dues zones separades.", + "timeTo": "Fins a", + "weekdays": "Dies feiners" + } + }, + "telemetry": { + "description": "Configureu la compartició de dades per a ajudar a millorar evcc. La vostra privadesa és important per a nosaltres i la participació és completament opcional.", + "title": "Telemetria" + }, + "title": { + "description": "Es mostra a la pantalla principal i a la pestanya del navegador.", + "label": "Títol", + "title": "Edita el títol" + }, + "validation": { + "failed": "fallit", + "label": "Estat", + "running": "s’està validant…", + "success": "correcte", + "unknown": "desconegut", + "validate": "valida" + }, + "vehicle": { + "generic": "Altres integracions", + "online": "Vehicles amb API en línia", + "primary": "Integracions genèriques", + "scooter": "Escúter", + "template": "Fabricant", + "titleAdd": "Afegeix vehicle", + "titleEdit": "Edita vehicle" + } }, "footer": { "community": { - "greenEnergy": "Energia Solar", + "greenEnergy": "Energia solar", "greenEnergySub1": "Carregats amb evcc", - "greenEnergySub2": "desde octubre de 2022", + "greenEnergySub2": "des d’octubre de 2022", "greenShare": "Part solar", - "greenShareSub1": "energía proporcionada per", - "greenShareSub2": "fotovoltàica i emmagatzematge de bateries", - "power": "potencia de carrega", - "powerSub1": "{activeClients} de {totalClients} usuaris", - "powerSub2": "carregant...", + "greenShareSub1": "potència proporcionada per", + "greenShareSub2": "fotovoltaica i emmagatzematge en bateries", + "power": "Potència de càrrega", + "powerSub1": "{activeClients} de {totalClients} participants", + "powerSub2": "carregant.", "tabTitle": "Comunitat en viu" }, "savings": { - "footerLong": "{percent} Autoconsum", - "footerShort": "{percent} Sol", - "modalTitle": "Avaluació de l'energia de càrrega", + "co2Saved": "{value} estalviats", + "co2Title": "Emissions de CO₂", + "configurePriceCo2": "Apreneu a configurar les dades de preu i CO₂.", + "footerLong": "{percent} energia solar", + "footerShort": "{percent} solar", + "indicator": { + "co2": "emissions de CO₂", + "co2saved": "CO₂ estalviat", + "none": "cap", + "price": "preu de l’energia", + "savings": "estalvis", + "solar": "energia solar" + }, + "indicatorLabel": "Informació de la capçalera", + "modalTitle": "Resum de l’energia de càrrega", + "moneySaved": "{value} estalviats", "percentGrid": "{grid} kWh xarxa", "percentSelf": "{self} kWh solar", "percentTitle": "Energia solar", - "priceTitle": "Preu de l'energia", + "period": { + "30d": "últims 30 dies", + "365d": "últims 365 dies", + "thisYear": "enguany", + "total": "històric complet" + }, + "periodLabel": "Període", + "priceTitle": "Preu de l’energia", + "referenceGrid": "xarxa", + "referenceLabel": "Dades de referència", + "sessionInfo": "Basat en les sessions de càrrega finalitzades.", "tabTitle": "Les meves dades" }, "sponsor": { - "becomeSponsor": "Convertir-se en patrocinador", - "confetti": "¿Vols una mica de confeti?", - "confettiPromise": "També hi ha adhesius i confeti digital", - "sticker": "...o adhesius de evcc?", - "supportUs": "La nostre missió: fer que repostar del sol sigui la norma. Ajuda'ns i dona suport econòmic a evcc!", - "thanks": "Gràcies pel teu patrocini, {sponsor}! Això ens ajudarà a continuar desenvolupant evcc.", - "titleNoSponsor": "Dona'ns suport", - "titleSponsor": "Ets un seguidor" + "becomeSponsor": "Esdevén patrocinador", + "becomeSponsorExtended": "Doneu-nos suport directament per a obtenir adhesius.", + "confetti": "Preparat pel confeti?", + "confettiPromise": "Rebeu adhesius i confeti digital", + "sticker": "…o adhesius d’evcc?", + "supportUs": "La nostra missió és fer que la càrrega solar sigui la norma. Ajudeu evcc pagant allò que val per a vosaltres.", + "thanks": "Gràcies pel vostre patrocini, {sponsor}! Això ens ajudarà a continuar desenvolupant evcc.", + "titleNoSponsor": "Doneu-nos suport", + "titleSponsor": "Sou un patrocinador", + "titleTrial": "Mode de prova", + "titleVictron": "Patrocinat per Victron Energy", + "trial": "Esteu en mode de prova i podeu utilitzar totes les funcions. Considereu donar suport al projecte.", + "victron": "Utilitzeu evcc en maquinari de Victron Energy i teniu accés a totes les funcions." }, "telemetry": { - "optIn": "També voldria contribuir amb les meves dades.", + "optIn": "Vull contribuir amb les meves dades.", "optInMoreDetails": "Més detalls {0}.", "optInMoreDetailsLink": "aquí", "optInSponsorship": "Es requereix patrocini." }, "version": { - "availableLong": "Actualitzacions disponibles", - "modalCancel": "Cancel·lar", - "modalDownload": "Descarregar", + "availableLong": "nova versió disponible", + "community": "comunitat d’evcc", + "labelEmail": "Correu electrònic", + "labelPhone": "Telèfon", + "labelRelease": "Llançament", + "labelVersion": "Versió", + "labelWebsite": "Lloc web", + "latestVersion": "última versió", + "madeByCommunity": "Fet per la {0}.", + "modalCancel": "Cancel·la", + "modalDownload": "Descarrega", "modalInstalledVersion": "Versió instal·lada", - "modalNoReleaseNotes": "No hi ha notes de llançament disponibles. Pots trobar més informació sobre la nova versió aquí:", - "modalTitle": "Actualitzacions disponibles", - "modalUpdate": "Instal·lar", - "modalUpdateNow": "Instal·lar ara", - "modalUpdateStarted": "Després de l'actualització, evcc es reiniciarà.", - "modalUpdateStatusStart": "Instal·lació iniciada:" + "modalLatest": "Esteu utilitzant l’última versió.", + "modalNextRelease": "Novetats de la pròxima versió", + "modalNoReleaseNotes": "No hi ha notes de llançament disponibles. Més informació sobre la nova versió:", + "modalTitle": "Nova versió disponible", + "modalUpdate": "Instal·la", + "modalUpdateNow": "Instal·la ara", + "modalUpdateStarted": "S’està iniciant la nova versió d’evcc…", + "modalUpdateStatusStart": "Instal·lació iniciada:", + "modalViewOnGitHub": "Vegeu a GitHub", + "openSource": "codi obert", + "poweredByOpenSource": "Impulsat per {0}." } }, + "forecast": { + "co2": { + "average": "Mitjana", + "constant": "Intensitat de CO₂", + "lowestHour": "Hora més neta", + "range": "Interval" + }, + "empty": { + "co2": "Vegeu quan l’energia de xarxa a la vostra regió és neta. Els plans de càrrega s’optimitzaran per a baixes emissions i calcularan els estalvis de CO₂.", + "price": "Configureu la vostra tarifa elèctrica dinàmica per a optimitzar automàticament els plans de càrrega i calcular els estalvis.", + "setup": "Configura tarifes i previsions", + "solar": "Vegeu la producció solar prevista per a avui i els pròxims dies. En el futur també s’utilitzarà per a l’optimització automàtica de la càrrega.", + "temperature": "Vegeu la temperatura exterior prevista per a la vostra ubicació. S’utilitza per a predir la demanda de calefacció." + }, + "hideLine": "Amaga la línia", + "modalTitle": "Previsió", + "price": { + "average": "Mitjana", + "constant": "Preu", + "lowestHour": "Hora més barata", + "range": "Interval" + }, + "priceZoom": "Amplia", + "showLine": "Mostra la línia", + "solar": { + "dayAfterTomorrow": "Demà passat", + "partly": "parcial", + "remaining": "restant", + "today": "Avui", + "tomorrow": "Demà" + }, + "solarAdjust": "Ajusta la previsió solar segons les dades reals de producció{percent}.", + "solarAdjustMedium": "ajust amb dades reals", + "solarAdjustShort": "ajusta", + "temperature": { + "average": "Mitjana", + "range": "Interval" + }, + "type": { + "co2": "Emissions de CO₂", + "price": "Preu de xarxa", + "solar": "Producció solar", + "temperature": "Temperatura exterior" + } + }, + "general": { + "back": "Enrere", + "download": "Descarrega", + "error": "Error", + "none": "cap", + "note": "Nota:", + "warning": "Avís" + }, "header": { - "about": "Sobre", + "about": "Quant a", "blog": "Blog", "docs": "Documentació", "github": "GitHub", - "needHelp": "Necessites ajuda?", - "sessions": "Processos de càrrega" + "logout": "Tanca la sessió", + "nativeSettings": "Canvia de servidor", + "needHelp": "Necessiteu ajuda?", + "sessions": "Sessions de càrrega" }, "help": { - "discussionsButton": "Debats en GitHub", + "contactButton": "Contacta amb nosaltres", + "discussionsButton": "Debats de GitHub", "documentationButton": "Documentació", - "issueButton": "Informar d'un error", - "issueDescription": "¿Has trobat un comportament extrany o incorrecte?", - "modalTitle": "Necessites ajuda?", - "primaryActions": "Si quelcom no funciona com hauria, podeu obtenir ajuda aqui.", + "issueButton": "Notifica un problema", + "issueDescription": "Heu trobat un comportament estrany o incorrecte?", + "logsButton": "Vegeu els registres", + "logsDescription": "Comproveu si hi ha errors als registres.", + "modalTitle": "Necessiteu ajuda?", + "primaryActions": "Alguna cosa no funciona com caldria? Aquests són bons llocs per a obtenir ajuda.", "restart": { - "cancel": "Cancel·lar", - "confirm": "¡Sí, reinicia!", - "description": "Sota circumstàncies normals no hauria de ser necessari reiniciar. Considereu sisplau la possibilitat de reportar un error si necessiteu reiniciar-lo regularment.", - "disclaimer": "Nota: evcc es pararà i dependrà del sistema operatiu per reiniciar el servei", - "modalTitle": "¿Segur que vols tornar començar?" + "cancel": "Cancel·la", + "confirm": "Sí, reinicieu!", + "description": "En circumstàncies normals no hauria de caldre reiniciar. Si necessiteu reiniciar-lo regularment, considereu informar d’un error.", + "disclaimer": "Nota: evcc s’aturarà i dependrà del sistema operatiu per a reiniciar el servei.", + "modalTitle": "Segur que voleu reiniciar?" }, - "restartButton": "Reiniciar", - "restartDescription": "¿Has provat d'apagar i tornar a engegar?", - "secondaryActions": "¿Segeixes sense poder resoldre el teu problema? Aquí tens altres opcions més contundents." + "restartButton": "Reinicia", + "restartDescription": "Heu provat d’apagar i tornar a engegar?", + "secondaryActions": "Encara no heu pogut resoldre el problema? Aquí teniu altres opcions més contundents." + }, + "issue": { + "additional": { + "description": "Incloeu configuració i registres per a ajudar-nos a reproduir el problema ràpidament. Us recomanem compartir tot el possible. Normalment no cal l’estat.", + "include": "inclou", + "lines": "línies", + "logs": "Registres", + "logsDescription": "Entrades de registre recents que poden ajudar a identificar el problema.", + "showDetails": "mostra els detalls", + "source": "Codi font", + "state": "Estat", + "stateDescription": "Estat complet d’execució amb la informació del punt de recàrrega, els dispositius i l’energia. Incloeu-lo només si us ho demanen.", + "title": "Informació addicional", + "uiConfig": "Configuració (UI)", + "uiConfigDescription": "Paràmetres de configuració fets mitjançant la interfície web.", + "yamlConfig": "Configuració (YAML)", + "yamlConfigDescription": "El vostre fitxer de configuració complet." + }, + "additionalContext": "Context addicional", + "additionalContextPlaceholder": "Qualsevol informació addicional que pugui ser útil…\n- Detalls de configuració\n- Què heu provat\n- Detalls de l’entorn", + "createButtonDiscussion": "Inicia un debat a GitHub…", + "createButtonEmail": "Envia la sol·licitud per correu", + "createButtonIssue": "Crea una incidència a GitHub…", + "description": "La vostra instal·lació no funciona com espereu? Utilitzeu aquesta pàgina per a obtenir ajuda o notificar problemes. Proporcioneu prou detalls per a ajudar-nos a entendre i reproduir el problema, mantenint la descripció concisa, clara i fàcil de seguir.", + "downloadButton": "Descarrega la informació de depuració", + "downloadHint": "Adjunteu aquest fitxer al correu si cal més informació.", + "helpType": { + "discussion": "Necessito ajuda amb la meva configuració", + "discussionDescription": "Els debats de la comunitat tenen respostes.", + "issue": "He trobat un error", + "issueDescription": "Estic segur que alguna cosa falla i s’ha d’arreglar.", + "title": "De quin problema parlem?" + }, + "issueDescription": "Descripció", + "issueTitle": "Títol", + "stepsToReproduce": "Passos per a reproduir", + "subTitleDiscussion": "Descriviu el vostre problema", + "subTitleEmail": "Descriviu el vostre problema", + "subTitleIssue": "Descriviu la incidència", + "summary": { + "confirmationButtonDiscussion": "Inicia un debat a GitHub", + "confirmationButtonIssue": "Crea una incidència a GitHub", + "copied": "Copiat!", + "copyButton": "Copia la informació addicional", + "instructions": "A causa de les limitacions de mida d’URL de GitHub, aquest és un procés en dos passos:", + "singleStepDescription": "Feu clic al botó de sota per a obrir GitHub amb un formulari preomplert amb els detalls del vostre problema. Les dades sensibles s’han ocultat automàticament, però reviseu-les abans de compartir-les.", + "step1Description": "Feu clic al botó de sota per a crear una entrada bàsica a GitHub amb el vostre títol, descripció i detalls.", + "step2Description": "Després de crear l’entrada, torneu aquí per a copiar la informació addicional de sota i enganxar-la al formulari de GitHub. Les dades sensibles s’han ocultat, però reviseu-les abans de compartir-les.", + "stepOneDiscussion": "Pas 1: creeu el debat bàsic", + "stepOneIssue": "Pas 1: creeu la incidència bàsica", + "stepTwo": "Pas 2: copieu la informació addicional", + "title": "Resum del problema per a GitHub" + }, + "system": "Sistema", + "timezone": "Zona horària", + "title": "Notifica un problema", + "version": "Versió" + }, + "log": { + "areaLabel": "Filtra per àrea", + "areas": "Totes les àrees", + "download": "Descarrega el registre complet", + "levelLabel": "Filtra per nivell de registre", + "nAreas": "{count} àrees", + "noResults": "Cap entrada de registre coincident.", + "search": "Cerca", + "selectAll": "selecciona-ho tot", + "showAll": "Mostra totes les entrades", + "title": "Registres", + "update": "Actualització automàtica" + }, + "loginModal": { + "cancel": "Cancel·la", + "demoMode": "L’inici de sessió no està disponible en mode de demostració.", + "iframeHint": "Obriu evcc en una pestanya nova.", + "iframeIssue": "La contrasenya és correcta, però sembla que el navegador ha descartat la galeta d’autenticació. Pot passar si executeu evcc en un iframe per HTTP.", + "invalid": "La contrasenya no és vàlida.", + "login": "Inicia la sessió", + "password": "Contrasenya d’administrador", + "reset": "Restabliu la contrasenya?", + "title": "Autenticació" }, "main": { "chargingPlan": { - "modalTitle": "Plà de càrrega", - "none": "no definit", - "title": "Plà" + "active": "Actiu", + "addRepeatingPlan": "Afegeix un pla repetitiu", + "day": "Dia", + "goal": "Objectiu de càrrega", + "modalTitle": "Pla de càrrega", + "none": "cap", + "once": "una vegada", + "optimization": { + "cheapest": "més barat", + "cheapestDescription": "Selecciona les franges més barates. Pot provocar diverses arrencades i aturades.", + "cleanest": "més net", + "cleanestDescription": "Selecciona les franges més netes. Pot provocar diverses arrencades i aturades.", + "continuous": "continu", + "continuousCo2Description": "Selecciona la sèrie més neta i ininterrompuda de franges.", + "continuousPriceDescription": "Selecciona la sèrie més barata i ininterrompuda de franges.", + "label": "Optimització" + }, + "planNumber": "Pla {number}", + "precondition": { + "description": "Mou part de la càrrega just abans de la sortida: bateria calenta, menys temps amb nivell de càrrega alt.", + "label": "Càrrega tardana", + "optionAll": "tot", + "optionNo": "no", + "summary": "{precondition} abans" + }, + "remove": "Elimina", + "repeating": "repetitiu", + "repeatingPlans": "Plans repetitius", + "selectAll": "Selecciona-ho tot", + "strategy": { + "label": "Estratègia" + }, + "strategyDisabledDescription": "La càrrega comença tan tard com sigui possible per a acabar just a temps de la sortida. Amb preus dinàmics de xarxa o tarifa de CO₂, hi ha més opcions disponibles.", + "time": "Hora", + "title": "Pla", + "unsavedChanges": "Hi ha canvis sense aplicar. Voleu aplicar-los ara?", + "update": "Aplica", + "weekdays": "Dies" + }, + "continuousStatus": { + "charging": "Càrrega ràpida activa.", + "connected": "Funcionament normal.", + "waitForVehicle": "Càrrega ràpida sol·licitada…" }, "energyflow": { "battery": "Bateria", - "batteryCharge": "Carregar la bateria", - "batteryDischarge": "Descarregar la bateria", + "batteryCharge": "Càrrega de la bateria", + "batteryChargeHold": "Bateria (càrrega bloquejada)", + "batteryDischarge": "Descàrrega de la bateria", + "batteryDischargeHold": "Bateria (descàrrega bloquejada)", + "batteryGridChargeActive": "Càrrega de xarxa: activa", + "batteryGridChargeLimit": "Càrrega de xarxa: quan", "batteryTooltip": "{energy} de {total} ({soc})", - "gridImport": "Consum de xarxa", + "forecast": "Previsió", + "forecastTooltip": "previsió: producció solar restant avui", + "gridImport": "Importació de la xarxa", "homePower": "Consum", - "loadpoints": "Punt de càrrega | Punt de càrrega | {count} Punts de càrrega", + "loadpoints": "Punt de recàrrega | Punt de recàrrega | {count} punts de recàrrega", + "loadpointsLimit": "límit de {limit}", "noEnergy": "Sense lectures", - "pvExport": "Excedent", + "pv": "Sistema fotovoltaic", + "pvExport": "Exportació a la xarxa", "pvProduction": "Producció", - "selfConsumption": "Autoconsum" + "selfConsumption": "Autoconsum", + "toggleDetails": "Detalls del flux d’energia" + }, + "heatingStatus": { + "charging": "Escalfant…", + "connected": "En espera.", + "minTemp": "Calefacció mínima fins a {temp}.", + "suggestionStartTooltip": "L’optimitzador recomana escalfar ara.", + "suggestionStopTooltip": "L’optimitzador recomana aturar la calefacció.", + "vehicleLimit": "Límit de l’escalfador", + "waitForVehicle": "A punt per a escalfar…" + }, + "hemsWarning": { + "consumption": "Consum", + "feedIn": "Injecció", + "title": "Límit extern actiu" + }, + "history": { + "direction": { + "battery": { + "energy": "carregada", + "returnEnergy": "descarregada" + }, + "grid": { + "energy": "importada", + "returnEnergy": "exportada" + }, + "meter": { + "energy": "Energia", + "returnEnergy": "Energia (inversa)" + } + }, + "download": "Descarrega", + "empty": "No hi ha dades d’historial per a aquest interval de temps.", + "group": { + "battery": "Bateria", + "consumer": "Consum", + "forecast": "Previsió", + "grid": "Xarxa", + "loadpoint": "Càrrega i calefacció", + "meter": "Comptadors addicionals", + "pv": "Producció" + }, + "otherConsumers": "Altres", + "title": "Historial" }, "loadpoint": { "avgPrice": "Preu ⌀", "charged": "Carregat", "co2": "CO₂ ⌀", - "duration": "Duració", - "fallbackName": "Punt de càrrega", + "duration": "Durada", + "emission": "Emissió", + "fallbackName": "Punt de recàrrega", + "finished": "Hora de finalització", + "last24hEnergy": "Últimes 24 h", + "last7dEnergy": "Últims 7 dies", "power": "Potència", - "price": "Σ Preu", - "solar": "Solar" + "price": "Cost", + "remaining": "Restant", + "remoteDisabledHard": "{source}: desactivat", + "remoteDisabledSoft": "{source}: càrrega solar adaptativa desactivada", + "solar": "Solar", + "todayEnergy": "Avui" }, "loadpointSettings": { + "batteryBoost": { + "description": "Permet la càrrega ràpida des de la bateria domèstica fins que arribi a {limit}.", + "descriptionDisabled": "Seleccioneu un límit per a permetre la càrrega ràpida des de la bateria domèstica.", + "disabled": "Desactivat", + "label": "Càrrega ràpida de bateria", + "mode": "Només disponible en mode solar i min+solar.", + "once": "Càrrega ràpida activa per a aquesta sessió de càrrega.", + "stateActive": "Càrrega ràpida de bateria activa", + "stateBelowLimit": "Bateria massa baixa per a la càrrega ràpida", + "stateHold": "Bateria bloquejada", + "stateReady": "Càrrega ràpida de bateria a punt" + }, + "batteryUsage": "Bateria domèstica", "currents": "Corrent de càrrega", - "default": "defecte", + "default": "per defecte", + "heating": "Calefacció", "maxCurrent": { - "label": "Corrent de carrega màxima" + "label": "Corrent màxim" }, "minCurrent": { - "label": "Corriente de carrega mínima" + "label": "Corrent mínim" + }, + "minTemp": { + "description": "Per sota de {0}, en mode solar també s’utilitzen la bateria i la potència de xarxa per a mantenir aquesta temperatura.", + "label": "Temperatura mínima" }, "phasesConfigured": { "label": "Fases", - "phases_0": "Canvi automàtic", + "no1p3pSupport": "Com està connectat el vostre carregador?", + "phases_0": "canvi automàtic", "phases_1": "monofàsica", "phases_1_hint": "({min} a {max})", "phases_3": "trifàsica", "phases_3_hint": "({min} a {max})" }, - "title": "Configuracions {0}", + "smartCostCheap": "Càrrega barata de xarxa", + "smartCostClean": "Càrrega neta de xarxa", + "title": "Configuració {0}", "vehicle": "Vehicle" }, "mode": { - "minpv": "Min+Sol", + "minpv": "Mín+Solar", "now": "Ràpid", "off": "Apagat", - "pv": "Solar" + "pv": "Solar", + "smart": "Intel·ligent" }, "provider": { - "login": "iniciar sessió", - "logout": "tancar sessió" + "login": "inicieu la sessió", + "logout": "tanqueu la sessió" }, + "startConfiguration": "Comencem amb la configuració", "targetCharge": { - "activate": "Activar", - "co2Limit": "Límit de CO₂ del {co2}", + "activate": "Activa", + "co2Limit": "Límit de CO₂ de {co2}", "costLimitIgnore": "El {limit} configurat serà ignorat durant aquest període.", - "descriptionEnergy": "Fins quan s'ha d'haver carregat {targetEnergy} el vehicle?", - "descriptionSoc": "Fins quan s'ha d'haver carregat el vehicle al {targetSoc}?", - "inactiveLabel": "hora objectiu", + "currentPlan": "Pla actiu", + "descriptionEnergy": "Per a quan cal haver carregat {targetEnergy} al vehicle?", + "descriptionSoc": "Fins quan s’ha d’haver carregat el vehicle al {targetSoc}?", + "goalReached": "Objectiu ja assolit", + "inactiveLabel": "Hora objectiu", + "nextPlan": "Proper pla", + "notReachableInTime": "L’objectiu s’assolirà {overrun} més tard.", + "onlyInPvMode": "El pla de càrrega només funciona en mode solar.", "planDuration": "Temps de càrrega", "planPeriodLabel": "Període", "planPeriodValue": "de {start} a {end}", - "planUnknown": "encara no se sap", - "priceLimit": "límit en el preu de {price}", + "planUnknown": "encara es desconeix", + "preview": "Previsualitza el pla", + "priceLimit": "límit de preu de {price}", + "remove": "Elimina", "setTargetTime": "cap", - "targetIsInThePast": "Tria un temps en el futur, Marty.", - "targetIsTooFarInTheFuture": "Ajustarém el pla quan sapiguém més sobre el futur.", - "title": "hora objectiu", + "targetIsAboveLimit": "El límit de càrrega configurat de {limit} s’ignorarà durant aquest període.", + "targetIsAboveVehicleLimit": "El límit del vehicle és inferior a l’objectiu de càrrega.", + "targetIsInThePast": "Trieu una hora en el futur, Marty.", + "targetIsTooFarInTheFuture": "Ajustarem el pla quan sapiguem més sobre el futur.", + "title": "Hora objectiu", "today": "avui", - "tomorrow": "matí", - "update": "Actualitzar" + "tomorrow": "demà", + "update": "Actualitza", + "vehicleCapacityDocs": "Apreneu a configurar-lo.", + "vehicleCapacityRequired": "Cal la capacitat de la bateria del vehicle per a estimar el temps de càrrega." }, "targetChargePlan": { "chargeDuration": "Temps de càrrega", "co2Label": "Emissió de CO₂ ⌀", - "priceLabel": "Preu de l'energia", - "timeRange": "{day} {range} hora", + "priceLabel": "Preu de l’energia", + "timeRange": "{day} {range} h", "unknownPrice": "encara desconegut" }, "targetEnergy": { @@ -232,95 +1519,232 @@ "noLimit": "cap" }, "vehicle": { - "addVehicle": "Afegir un vehicle", + "addVehicle": "Afegeix un vehicle", "changeVehicle": "Canvia de vehicle", - "detectionActive": "Reconeixent vehicle...", + "detectionActive": "Detectant el vehicle…", "fallbackName": "Vehicle", "moreActions": "Més accions", "none": "Cap vehicle", + "notReachable": "El vehicle no era accessible. Proveu de reiniciar evcc.", "targetSoc": "Objectiu de càrrega", + "temp": "Temp.", + "tempLimit": "Límit de temp.", "unknown": "Vehicle convidat", "vehicleSoc": "Nivell de càrrega" }, + "vehicleSettings": { + "connectedTo": "connectat a {0}", + "description": "Aquests valors per defecte s’apliquen quan el vehicle es connecta a un punt de recàrrega.", + "editHint": "Afegiu o editeu vehicles a {0}.", + "keepAsIs": "Tal com està", + "learnMore": "Més informació.", + "limitSoc": "Límit per defecte", + "limitSocDescription": "S’aplica en connectar el vehicle.", + "menu": "Vehicles", + "minSoc": "Càrrega mínima", + "minSocDescription": "Càrrega ràpida fins a aquest nivell en mode solar.", + "mode": "Mode per defecte", + "notConnected": "no connectat", + "title": "Paràmetres del vehicle" + }, "vehicleStatus": { - "charging": "Carregant...", - "cheapEnergyCharging": "Energia barata disponible. Carregant...", - "cleanEnergyCharging": "Energia neta disponible. Carregant...", + "awaitingAuthorization": "Esperant l’autorització.", + "batteryBoost": "Càrrega ràpida de bateria activa.", + "batteryBoostBelowLimit": "Bateria massa baixa per a la càrrega ràpida.", + "batteryBoostDisabled": "Càrrega ràpida de bateria desactivada.", + "batteryBoostEnabled": "Càrrega ràpida fins que la bateria arribi a {limit}.", + "batteryBoostHold": "Bateria bloquejada. Càrrega ràpida no disponible.", + "charging": "Carregant…", + "cheapEnergyCharging": "Hi ha energia barata disponible.", + "cheapEnergyNextStart": "Energia barata en {duration}.", + "cheapEnergySet": "Límit de preu establert.", + "cleanEnergyCharging": "Hi ha energia neta disponible.", + "cleanEnergyNextStart": "Energia neta en {duration}.", + "cleanEnergySet": "Límit de CO₂ establert.", "climating": "Precondicionament detectat.", "connected": "Connectat.", + "disconnectRequired": "Sessió finalitzada. Torneu a connectar.", "disconnected": "Desconnectat.", + "feedinPriorityNextStart": "Les tarifes altes d’injecció comencen en {duration}.", + "feedinPriorityPausing": "Càrrega solar en pausa per a maximitzar la injecció.", + "finished": "Finalitzat.", "minCharge": "Càrrega mínima fins a {soc}.", - "pvDisable": "Excedent insuficient. Pausa en {remaining}...", - "pvEnable": "Excedent disponible. Començar en {remaining}...", - "scale1p": "Reduint a càrrega monofàsica a {remaining}...", - "scale3p": "Augmentant la càrrega trifàsica en {remaining}...", - "targetChargeActive": "Càrrega objectiu activa. Finalització estimada en {duration}.", - "targetChargePlanned": "La càrrega objectiu comença en {duration}.", - "targetChargeWaitForVehicle": "Càrrega objectiu llesta. Esperar vehicle.", - "vehicleLimitReached": "Límit de vehicle {soc} assolit.", - "waitForVehicle": "A punt per carregar. Esperant vehicle." + "pvDisable": "No hi ha prou excedent. S’aturarà aviat.", + "pvEnable": "Hi ha excedent disponible. S’iniciarà aviat.", + "scale1p": "Aviat es reduirà a càrrega monofàsica.", + "scale3p": "Aviat s’incrementarà a càrrega trifàsica.", + "suggestionStartTooltip": "L’optimitzador recomana carregar ara.", + "suggestionStopTooltip": "L’optimitzador recomana aturar la càrrega.", + "targetChargeActive": "Pla de càrrega actiu. Finalització estimada en {duration}.", + "targetChargePlanned": "El pla de càrrega comença en {duration}.", + "targetChargeWaitForVehicle": "Pla de càrrega enllestit. Esperant vehicle…", + "vehicleLimit": "Límit del vehicle", + "vehicleLimitReached": "Límit del vehicle assolit.", + "waitForAuthorization": "Connectat. Esperant l’autorització…", + "waitForVehicle": "A punt per a carregar. Esperant vehicle…", + "welcome": "Càrrega inicial breu per a confirmar la connexió." }, - "vehicles": "Estacionament" + "vehicles": "Aparcament", + "welcome": "Benvingut a bord!" }, "notifications": { - "dismissAll": "Eliminar notificacions", + "dismissAll": "Descarta-les totes", + "logs": "Vegeu el registre complet", "modalTitle": "Notificacions" }, "offline": { - "message": "No connectat amb servidor." + "configurationError": "Error durant l’inici. Comproveu la configuració i reinicieu.", + "message": "No hi ha connexió amb el servidor.", + "restart": "Reinicia", + "restartNeeded": "Cal reiniciar per a aplicar els canvis.", + "restarting": "El servidor tornarà d’aquí a un moment.", + "starting": "S’està iniciant el servidor…" + }, + "passwordModal": { + "description": "Establiu una contrasenya per a protegir els paràmetres de configuració. Podeu continuar utilitzant la pantalla principal sense iniciar la sessió.", + "empty": "La contrasenya no pot ser buida", + "labelCurrent": "Contrasenya actual", + "labelNew": "Contrasenya nova", + "labelRepeat": "Confirmació de la contrasenya", + "newPassword": "Crea una contrasenya", + "noMatch": "Les contrasenyes no coincideixen", + "titleNew": "Estableix la contrasenya d’administrador", + "titleUpdate": "Actualitza la contrasenya d’administrador", + "updatePassword": "Actualitza la contrasenya" }, "session": { - "cancel": "Cancelar", + "addOdometer": "Afegeix un valor", + "addedRange": "Autonomia afegida", + "cancel": "Cancel·la", "co2": "CO₂", - "date": "Período", - "delete": "Borrar", - "finished": "Finalizó", - "meter": "Contador", - "meterstart": "Iniciar el medidor", - "meterstop": "Parar el medidor", - "odometer": "Kilometraje", - "price": "Preu", - "started": "Comenzó", - "title": "Cargando la sesión" + "date": "Període", + "delete": "Suprimeix", + "finished": "Finalitzada", + "meter": "Comptador", + "meterstart": "Lectura inicial", + "meterstop": "Lectura final", + "odometer": "Quilometratge", + "price": "Cost", + "soc": "Càrrega", + "started": "Iniciada", + "title": "Sessió de càrrega" }, "sessions": { + "addedRange": "Autonomia", + "avgPower": "Potència ⌀", "avgPrice": "Preu ⌀", - "co2": "CO₂", + "chargeDuration": "Durada", + "chargedSoc": "Càrrega", + "chartTitle": { + "avgCo2ByGroup": "CO₂ ⌀ {byGroup}", + "avgPriceByGroup": "Preu ⌀ {byGroup}", + "byGroupLoadpoint": "per punt de recàrrega", + "byGroupVehicle": "per vehicle", + "energy": "Energia carregada", + "energyGrouped": "Energia solar i de xarxa", + "energyGroupedByGroup": "Energia {byGroup}", + "energySubSolar": "{value} solar", + "energySubTotal": "{value} total", + "groupedCo2ByGroup": "Quantitat de CO₂ {byGroup}", + "groupedPriceByGroup": "Cost total {byGroup}", + "historyCo2": "Emissions de CO₂", + "historyCo2Sub": "{value} total", + "historyPrice": "Costos de càrrega", + "historyPriceSub": "{value} total", + "solar": "Part solar durant l’any", + "solarByGroup": "Part solar {byGroup}" + }, + "co2": "CO₂ ⌀", "csv": { + "addedrange": "Autonomia afegida (km)", "chargedenergy": "Energia (kWh)", - "created": "Hora d'inici", + "chargeduration": "Durada", + "co2perkwh": "CO₂/kWh", + "created": "Hora d’inici", "finished": "Hora de finalització", "identifier": "Identificador", - "loadpoint": "Punt de càrrega", - "meterstart": "Mesura inicial (kWh)", - "meterstop": "Mesura final (kWh)", - "odometer": "Kilometratge (km)", + "loadpoint": "Punt de recàrrega", + "meterstart": "Lectura inicial (kWh)", + "meterstop": "Lectura final (kWh)", + "odometer": "Quilometratge (km)", + "price": "Cost", + "priceperkwh": "Preu/kWh", + "socend": "SoC final (%)", + "socstart": "SoC inicial (%)", + "solarpercentage": "Solar (%)", "vehicle": "Vehicle" }, - "date": "Període", + "date": "Inici", + "download": "Descarrega {period}", + "downloadTotal": "Descarrega el total", "energy": "Carregada", + "filter": { + "allLoadpoints": "tots els punts de recàrrega", + "allVehicles": "tots els vehicles", + "filter": "Filtre" + }, + "group": { + "co2": "Emissions", + "grid": "Xarxa", + "price": "Cost", + "self": "Solar" + }, + "groupBy": { + "loadpoint": "Punt de recàrrega", + "none": "Solar", + "vehicle": "Vehicle" + }, "loadpoint": "Punt de recàrrega", - "price": "Preu", - "reallyDelete": "Realment vols esborrar aquesta sessió?", + "noData": "Cap sessió de càrrega aquest mes.", + "odometer": "Quilometratge", + "overview": "Resum", + "period": { + "day": "Dia", + "month": "Mes", + "total": "Total", + "year": "Any" + }, + "price": "Cost", + "reallyDelete": "Esteu segur que voleu suprimir aquesta sessió?", + "showIndividualEntries": "Mostra les sessions individuals", "solar": "Solar", "title": "Sessions de càrrega", + "total": "Total", + "type": { + "co2": "CO₂", + "price": "Cost", + "solar": "Energia" + }, "vehicle": "Vehicle" }, "settings": { + "deviceInfo": "Els paràmetres d’aquest diàleg només afecten aquest dispositiu.", + "fullscreen": { + "enter": "Activa la pantalla completa", + "exit": "Desactiva la pantalla completa", + "label": "Pantalla completa" + }, "hiddenFeatures": { - "label": "En proves", - "value": "Mostra les característiques experimentals de la interfície d'usuari." + "label": "Experimental", + "value": "Habilita les funcions experimentals." }, "language": { "auto": "Automàtic", "label": "Idioma" }, + "loadpoints": { + "disabledDevice": "desactivat", + "help": "Canvieu l’ordre i la visibilitat a la interfície.", + "hide": "Amaga {title}", + "label": "Punts de recàrrega", + "show": "Mostra {title}" + }, "sponsorToken": { - "expires": "El teu token de patrocinador expira en {inXDays}.", - "expiresUpdateUi": "{getNewToken} i actualitza el teu fitxer de configuració.", - "expiresUpdateYaml": "{getNewToken} i actualitza'l al teu evcc.yaml.", - "getNewToken": "Agafa'n un de nou", - "hint": "Nota: automatitzarém això en el futur." + "expires": "El vostre testimoni de patrocinador venç {inXDays}.", + "expiresUpdateUi": "{getNewToken} i actualitzeu-lo aquí.", + "expiresUpdateYaml": "{getNewToken} i actualitzeu-lo al vostre evcc.yaml.", + "getNewToken": "Aconsegueix-ne un de nou", + "hint": "Nota: ho automatitzarem en el futur." }, "telemetry": { "label": "Telemetria" @@ -331,7 +1755,12 @@ "label": "Tema", "light": "clar" }, - "title": "Configuració", + "time": { + "12h": "12 h", + "24h": "24 h", + "label": "Format de l’hora" + }, + "title": "Interfície d’usuari", "unit": { "km": "km", "label": "Unitats", @@ -340,25 +1769,50 @@ }, "smartCost": { "activeHours": "{active} de {total}", - "activeHoursLabel": "Hores funcionant", + "activeHoursLabel": "Temps de funcionament", + "applyToAll": "Voleu aplicar-ho a tot arreu?", + "batteryDescription": "Carrega la bateria domèstica amb energia de la xarxa.", + "cheapTitle": "Càrrega barata de xarxa", + "cleanTitle": "Càrrega neta de xarxa", "co2Label": "Emissions de CO₂", "co2Limit": "Límit de CO₂", - "loadpointDescription": "Habilita temporalment la càrrega ràpida en mode solar", - "modalTitle": "Xarxa intel·ligent de càrrega", + "enable": "Habilita el límit", + "loadpointDescription": "Habilita temporalment la càrrega ràpida en mode solar.", + "modalTitle": "Càrrega intel·ligent amb la xarxa", "none": "cap", - "priceLabel": "Preu de l'energia", - "priceLimit": "Límit de preu" + "priceLabel": "Preu de l’energia", + "priceLimit": "Límit de preu", + "resetAction": "Elimina el límit", + "resetWarning": "No hi ha cap preu dinàmic de xarxa ni font de CO₂ configurats. Tanmateix, encara hi ha un límit de {limit}. Voleu netejar la configuració?", + "saved": "Desat." + }, + "smartFeedInPriority": { + "activeHoursLabel": "Temps en pausa", + "description": "Posa en pausa la càrrega durant els preus alts per a prioritzar la injecció rendible a la xarxa.", + "priceLabel": "Tarifa d’injecció", + "priceLimit": "Límit d’injecció", + "resetWarning": "No hi ha cap tarifa dinàmica d’injecció configurada. Tanmateix, encara hi ha un límit de {limit}. Voleu netejar la configuració?", + "title": "Prioritat d’injecció" }, "startupError": { - "configFile": "Arxiu de configuració utilitzat:", + "configFile": "Fitxer de configuració utilitzat:", "configuration": "Configuració", - "description": "Sisplau reviseu el vostre fitxer de configuració. Si el missatge d'error no us ajuda, busqueu una solució al nostre {0}.", - "discussions": "Debats en GitHub", - "fixAndRestart": "Soluciona sisplau el problema i reinicia el servidor.", - "hint": "Nota: També pot ser que tinguis un aparell avariat (inversor, mesura, ...). Comprova les seves connexions a la xarxa.", - "lineError": "Error en {0}.", - "lineErrorLink": "Línia {0}", - "restartButton": "Reiniciar", - "title": "Error a l'inici" + "description": "Comproveu el fitxer de configuració. Si el missatge d’error no us ajuda, consulteu el {0}.", + "discussions": "GitHub Discussions", + "editConfiguration": "Edita la configuració", + "fixAndRestart": "Solucioneu el problema i reinicieu el servidor.", + "hint": "Nota: també pot ser que tingueu un dispositiu avariat (inversor, comptador…). Comproveu les connexions de xarxa.", + "lineError": "Error a {0}.", + "lineErrorLink": "línia {0}", + "restartButton": "Reinicia", + "title": "Error a l’inici" + }, + "tabBar": { + "battery": "Bateria", + "charge": "Càrrega", + "comingSoon": "Aquesta pàgina està en construcció.", + "forecast": "Previsió", + "more": "Més", + "sessions": "Sessions" } } diff --git a/i18n/cs.json b/i18n/cs.json index 7c7022321..41fcfbde2 100644 --- a/i18n/cs.json +++ b/i18n/cs.json @@ -1171,7 +1171,6 @@ }, "planNumber": "Plán {number}", "precondition": { - "description": "Nabíjení bude dokončeno {duration} před časem odjezdu. Vhodné v zimě pro zahřátí baterie či její šetření při vyšším stavu nabití.", "label": "Odložené nabíjení", "optionAll": "všechen", "optionNo": "ne" @@ -1181,7 +1180,6 @@ "repeatingPlans": "Opakující se plány", "selectAll": "Vybrat vše", "strategyDisabledDescription": "Nabíjení začíná co nejpozději, aby bylo dokončeno právě včas před odjezdem. S dynamickými cenami elektřiny nebo tarifem CO₂ je zde k dispozici více možností.", - "strategySettings": "Nastavení strategie", "time": "Čas", "title": "Plán", "unsavedChanges": "Jsou zde neuložené změny. Použít nyní?", diff --git a/i18n/da.json b/i18n/da.json index ff5a07e5b..4c51b782c 100644 --- a/i18n/da.json +++ b/i18n/da.json @@ -24,9 +24,9 @@ "charging": "lader", "discharging": "aflader", "energy": "Energi", - "gridCharge": "lader fra nettet", + "gridCharge": "netladning", "hold": "afladning låst", - "holdCharge": "opladning låst", + "holdCharge": "ladning låst", "ofTotal": "af {total}", "power": "Effekt", "soc": "Batteriniveau" @@ -42,13 +42,14 @@ }, "bufferTitle": "Batteri som opladningsbuffer", "discharge": "Forhindrer hjemmebatteriet at aflade i hurtig mode og ved planlagt opladning.", + "gridDischarge": "Tillad hjemmebatteriet at aflade til elnettet.", "priority": "Hjemmebatteriet har prioritet, mens det er under {soc}. Derefter får ladepunkterne overskuddet.", "priorityNone": "Soloverskud går direkte til ladepunkterne. Sæt en værdi over {soc} for at prioritere hjemmebatteriet.", "priorityTitle": "Hvor sendes overskuddet først hen?" }, "optimizer": { "action": { - "charge": "opladning fra nettet", + "charge": "Opladning fra nettet", "discharge": "Aflader til nettet", "hold": "Bloker afladning", "holdcharge": "Bloker opladning", @@ -119,6 +120,7 @@ "titleEdit": "Rediger selvregulerende forbrugsenhed" }, "battery": { + "title": "Batteri", "titleAdd": "Tilføj batteri", "titleEdit": "Rediger batteri" }, @@ -189,6 +191,7 @@ "deviceValue": { "activeClients": "Aktive klienter", "amount": "Mængde", + "asleep": "sover", "broker": "Broker", "bucket": "Bucket", "capacity": "Kapacitet", @@ -216,6 +219,7 @@ "enabled": "Aktiveret", "energy": "Energi", "events": "Begivenheder", + "exportLimit": "Eksportgrænse til nettet", "feedinPrice": "Indfødningstarif", "forecast": "Prognose", "gridPrice": "God pris", @@ -227,6 +231,7 @@ "no": "nej", "odometer": "Kilometertæller", "org": "Organisation", + "outdoorTemp": "Udendørstemperatur", "phaseCurrents": "Strømstyrke", "phasePowers": "Effekt", "phaseVoltages": "Spænding", @@ -236,6 +241,7 @@ "price": "Pris", "productionLimited": "Produktionsgrænse", "range": "Rækkevidde", + "remoteEnabled": "Aktiveret", "returnEnergy": "Energi (baglæns)", "singlePhase": "Enkelt fase", "soc": "Oplader", @@ -301,6 +307,7 @@ "form": { "danger": "Pas på", "deprecated": "forældet", + "durationUnit": "Tidsenhed for {label}", "example": "Eksempel", "optional": "valgfrit" }, @@ -313,17 +320,22 @@ "change": "Ændre", "clear": "Slet", "close": "Luk", + "confirmDisable": "Deaktiver denne enhed? Den ignoreres efter næste genstart.", + "confirmEnable": "Aktiver denne enhed igen?", "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.", "customOption": "Brugerdefineret enhed", "delete": "Slet", + "disable": "Deaktivér", + "disabled": "deaktiveret", "dismiss": "Afvis", "docsLink": "Se dokumentation.", "dragHandle": "Træk-håndtag", "dragItem": "Kan trækkes: {title}", "dragList": "Rækkefølgen kan ændres", + "enable": "Aktiver", "error": "Fejl", "experimental": "Eksperimentel", "forceSave": "Gem alligevel", @@ -365,12 +377,24 @@ }, "customOption": "Brugerdefineret integration", "description": "Effektbegrænsning via eksterne systemer (f.eks. §14a EnWG, §9 EEG-grænseflade eller overordnet energistyringssystem). Fungerer sammen med belastningsstyringsfunktionen. Begrænsning af solproduktionen understøttes også for udvalgte enheder. Den faktiske begrænsningsgrænse skal konfigureres for hver enkelt inverter.", + "download": "Download", + "dynamicLimits": "Dynamisk grænse", "eventsRecorded": "Registrerede {count} netbegrænsningshændelser.", + "exportLimit": "Eksportgrænse til nettet", + "exportLimitDescription": "Batterioptimeringen respekterer denne grænse for at undgå tab af solenergi. En aktivt begrænsning fra en integration med en dynamisk grænse har forrang.", + "exportLimitHint": "Statisk grænse for indfødning ved nettilslutningspunktet.", "generic": "Generelle integrationer", "integrations": "Integrationer", "lastEvent": "Sidst {timeAgo}.", + "noEvents": "Endnu ingen begivenheder gemt.", + "recordedEvents": "Gemte begivenheder", + "staticLimits": "Statisk grænse", "template": "Integration", - "title": "Ekstern grænse" + "title": "Ekstern grænse", + "type": { + "custom": "Brugerdefineret integration", + "relay": "Brugerdefineret integration (relæ)" + } }, "icon": { "change": "ændre", @@ -398,10 +422,11 @@ "heating": "Tilføj varmelegeme" }, "addMeter": "Tilføj dedikeret energimåler", + "advancedConfig": "Avanceret konfiguration", "cancel": "Afbryd", "chargerError": { "charging": "Konfiguration af en oplader er påkrævet.", - "heating": "Varmelegemet skal konfigureres." + "heating": "Et varmelegeme skal konfigureres." }, "chargerLabel": { "charging": "Oplader", @@ -419,11 +444,21 @@ "circuitInvalid": "Kredsløbet findes ikke", "circuitLabel": "Kredsløb", "circuitUnassigned": "ikke tildelt", + "created": "“{name}” blev oprettet og bruger fornuftige standardindstillinger.", + "createdTitle": { + "charging": "Ladepunkt oprettet", + "heating": "Varmelegeme oprettet" + }, + "creating": { + "charging": "Lader gemt. Opretter ladepunkt…", + "heating": "Varmelegeme gemt. Opretter varmelegeme…" + }, "defaultModeHelp": { "charging": "Opladningstilstand når køretøjet forbindes.", "heating": "Indstilles ved systemstart." }, "defaultModeHelpKeep": "Gemmer den senest valgte tilstand.", + "defaultModeKeep": "Gem som det er", "defaultModeLabel": "Standardtilstand", "delete": "Slet", "electricalSubtitle": "Ved tvivl, spørg din elektriker.", @@ -536,18 +571,18 @@ "titleDefault": "Køretøjet sover" }, "connect": { - "messageDefault": "Bil forbundet med {pvPower}kW PV", - "title": "Når bilen forbindes", - "titleDefault": "Bil er forbundet" + "messageDefault": "Køretøj forbundet med {pvPower}kW PV", + "title": "Når køretøjet forbindes", + "titleDefault": "Køretøj forbundet" }, "disconnect": { - "messageDefault": "Bilen er frakoblet efter {connectedDuration}", - "title": "Når bilen frakobles", - "titleDefault": "Bilen er frakoblet" + "messageDefault": "Køretøjet er frakoblet efter {connectedDuration}", + "title": "Når køretøjet frakobles", + "titleDefault": "Køretøjet er frakoblet" }, "guest": { "messageDefault": "Ukendt køretøj, er et gæstekøretøj forbundet?", - "title": "Når en ukendt bil forbindes", + "title": "Når et ukendt køretøj forbindes", "titleDefault": "Ukendt køretøj" }, "planoverrun": { @@ -569,6 +604,11 @@ "messageDefault": "Opladning er afsluttet {chargedEnergy}kWh på {chargeDuration}.", "title": "Når opladning stopper", "titleDefault": "Opladning er færdig" + }, + "suggestion": { + "messageDefault": "Optimeringsforslag for {suggestionTitle}: Hvis optimeringsstyring var aktiv, ville den nu udføre handlingen '{suggestionAction}'. Dette er kun en anbefaling. Der er ikke foretaget nogen ændringer. Eksperimentel funktion. Feedback modtages gerne på https://github.com/evcc-io/evcc/issues/31903", + "title": "Når optimeringens anbefalede handling ændres", + "titleDefault": "Optimeringsforslag" } }, "eventMessage": "Besked", @@ -624,6 +664,8 @@ "connectionHintTcpip": "Enheden er tilgængelig via netværk (LAN/WiFi).", "connectionValueSerial": "RS485", "connectionValueTcpip": "Netværk", + "delay": "Forsinkelse", + "delayHint": "Valgfri ventetid før hver registeradgang. Er kun nødvendig for langsomme enheder.", "device": "Enhedsnavn", "deviceHint": "Eksempel: /dev/ttyUSB0", "host": "IP-adresse eller værtsnavn", @@ -634,7 +676,9 @@ "protocolHintRtu": "Forbindelse gennem en RS485 til Ethernet-adapter uden protokoloversættelse.", "protocolHintTcp": "Enheden har indbygget LAN/Wifi-understøttelse eller er forbundet via en RS485 til Ethernet-adapter med protokoloversættelse.", "protocolValueRtu": "RTU", - "protocolValueTcp": "TCP" + "protocolValueTcp": "TCP", + "timeout": "Timeout", + "timeoutHint": "Valgfri maksimal ventetid på svar fra enheden." }, "modbusproxy": { "add": "Tilføj proxyforbindelse", @@ -798,7 +842,7 @@ "general": "Generelle", "grid": "Elnettet", "integrations": "Integrationer", - "loadpoints": "Lader og varmelegemer", + "loadpoints": "Ladere og varmelegemer", "meter": "Sol og batteri", "services": "Tjenester", "system": "System", @@ -913,7 +957,8 @@ "feedIn": "Tilføj elnet eksport tarif", "grid": "Tilføj elnet import tarif", "planner": "Tilføj planlægnings prognose", - "solar": "Tilføj sol prognose" + "solar": "Tilføj sol prognose", + "temperature": "Tilføj temperaturprognose" }, "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.", @@ -926,6 +971,11 @@ "titleAdd": "Tilføj sol prognose", "titleEdit": "Juster sol prognose" }, + "temperature": { + "description": "Udendørs temperaturprognose for din placering. Vises i brugerfladen og bruges til at forudsige varmebehovet.", + "titleAdd": "Tilføj temperaturprognose", + "titleEdit": "Rediger temperaturprognose" + }, "template": "Udbyder", "title": "Tariffer og prognoser", "titleChoice": "Hvad ønsker du at tilføje?", @@ -934,7 +984,8 @@ "feedIn": "Indfødningspris", "grid": "Købspris fra elnettet", "planner": "Planlægning", - "solar": "Sol" + "solar": "Sol", + "temperature": "Temperaturprognose" }, "zones": { "add": "Tilføj zone", @@ -1055,6 +1106,8 @@ "version": { "availableLong": "ny version tilgængelig", "community": "evcc-fællesskabet", + "labelEmail": "Email", + "labelPhone": "Telefon", "labelRelease": "Udgivet", "labelVersion": "Version", "labelWebsite": "Hjemmeside", @@ -1087,7 +1140,8 @@ "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." + "solar": "Vis forventet solenergi produceret i dag og de nærmeste dage. Bliver også brugt til automatisk optimering af opladninger i fremtiden.", + "temperature": "Se den forventede udendørstemperatur for din placering. Bruges til at forudsige varmebehovet." }, "hideLine": "skjul linjen", "modalTitle": "Prognose", @@ -1109,14 +1163,24 @@ "solarAdjust": "Juster solprognosen på baggrund af produktions data{percent}.", "solarAdjustMedium": "Justering baseret på faktiske data", "solarAdjustShort": "justering", + "temperature": { + "average": "Gennemsnit", + "range": "Rækkevidde" + }, "type": { "co2": "CO₂ Emissioner", "price": "Strømpris", - "solar": "Solenergiproduktion" + "solar": "Solenergiproduktion", + "temperature": "Udendørstemperatur" } }, "general": { - "note": "Note:" + "back": "Tilbage", + "download": "Download", + "error": "Fejl", + "none": "ingen", + "note": "Note:", + "warning": "Advarsel" }, "header": { "about": "Om", @@ -1129,6 +1193,7 @@ "sessions": "Opladnings sessioner" }, "help": { + "contactButton": "Kontakt os", "discussionsButton": "GitHub diskussioner", "documentationButton": "Dokumentation", "issueButton": "Indberet et problem", @@ -1168,8 +1233,11 @@ "additionalContext": "Supplerende oplysninger", "additionalContextPlaceholder": "Eventuelle yderligere oplysninger, der kan være nyttige...\"\n- Konfigurationsdetaljer\n- Hvad du har prøvet\n- Platform- og systemoplysninger", "createButtonDiscussion": "Start en GitHub diskussion...", + "createButtonEmail": "Send anmodning via e-mail", "createButtonIssue": "Opret GitHub-issue…", "description": "Virker din installation ikke som forventet? Brug denne side til at få hjælp eller rapportere problemer. Giv tilstrækkelige oplysninger, så vi kan forstå og genskabe problemet, samtidig med at din beskrivelse holdes kort, klar og let at følge.", + "downloadButton": "Download debug information", + "downloadHint": "Tilføj denne fil til e-mailen hvis mere information er nødvendig.", "helpType": { "discussion": "Har brug for hjælp til min opsætning", "discussionDescription": "Diskussioner i fællesskabet giver svar.", @@ -1181,6 +1249,7 @@ "issueTitle": "Titel", "stepsToReproduce": "Trin til at genskabe problemet", "subTitleDiscussion": "Beskriv dit problem", + "subTitleEmail": "Beskriv problemet", "subTitleIssue": "Beskriv problemet", "summary": { "confirmationButtonDiscussion": "Start en GitHub-diskussion", @@ -1233,24 +1302,33 @@ "goal": "Opladnings mål", "modalTitle": "Lade plan", "none": "ingen", + "once": "èn gang", "optimization": { "cheapest": "billigst", + "cheapestDescription": "Vælger de billigste tidspunkter. Kan medføre flere start og stop.", + "cleanest": "renest", + "cleanestDescription": "Vælger de reneste tidsrum. Kan medføre flere start og stop.", "continuous": "kontinuerlig", + "continuousCo2Description": "Vælger det reneste sammenhængende tidsrum.", + "continuousPriceDescription": "Vælger det billigste sammenhængende tidsrum.", "label": "Optimering" }, "planNumber": "Plan {number}", "precondition": { - "description": "Oplad {duration} før afgang til batterikonditionering.", + "description": "Flytter en del af opladningen til lige før afgang: Varmt batteri og mindre tid ved høj ladestand.", "label": "Sen opladning", "optionAll": "alt", - "optionNo": "nej" + "optionNo": "nej", + "summary": "{precondition} før" }, "remove": "Fjern", "repeating": "Gentager", "repeatingPlans": "Gentager plan", "selectAll": "Vælg alt", + "strategy": { + "label": "Strategi" + }, "strategyDisabledDescription": "Opladningen påbegyndes så sent som muligt og afsluttes præcis ved afgang. Med dynamiske elpriser eller CO₂-tariffer er der flere valgmuligheder.", - "strategySettings": "Indstillinger for strategi", "time": "Tid", "title": "Plan", "unsavedChanges": "Der er ændringer som ikke er gemt. Skal de benyttes nu?", @@ -1281,11 +1359,15 @@ "pv": "Solcelleanlæg", "pvExport": "Eksport til elnet", "pvProduction": "Produktion", - "selfConsumption": "Eget forbrug" + "selfConsumption": "Eget forbrug", + "toggleDetails": "Detaljer om energiflow" }, "heatingStatus": { "charging": "Opvarmer…", "connected": "Standby.", + "minTemp": "Minimum opvarmning til {temp}.", + "suggestionStartTooltip": "Optimering anbefaler opvarmning nu.", + "suggestionStopTooltip": "Optimering anbefaler at pause opvarmningen.", "vehicleLimit": "Opvarmning begrænsning", "waitForVehicle": "Klar til at varme…" }, @@ -1309,6 +1391,7 @@ "returnEnergy": "Energi (eksport)" } }, + "download": "Download", "empty": "Ingen historik i dette tidsinterval.", "group": { "battery": "Batteri", @@ -1330,12 +1413,15 @@ "emission": "Udledning", "fallbackName": "Ladepunkt", "finished": "Sluttidspunkt", + "last24hEnergy": "Sidste 24 timer", + "last7dEnergy": "Sidste 7 dage", "power": "Effekt", "price": "Pris", "remaining": "Resterende tid", "remoteDisabledHard": "{source}: slukket", "remoteDisabledSoft": "{source}: Adaptiv sol-opladning er slukket", - "solar": "solenergi" + "solar": "solenergi", + "todayEnergy": "I dag" }, "loadpointSettings": { "batteryBoost": { @@ -1353,12 +1439,17 @@ "batteryUsage": "Husbatteri", "currents": "Ladestrøm", "default": "standard", + "heating": "Opvarmning", "maxCurrent": { "label": "Max. Nuværende" }, "minCurrent": { "label": "Min. Nuværende" }, + "minTemp": { + "description": "Under {0} bruges batteri- og netstrøm også i soltilstand for at opretholde denne temperatur.", + "label": "Min. temperatur" + }, "phasesConfigured": { "label": "Faser", "no1p3pSupport": "Hvordan er din oplader forbundet?", @@ -1441,6 +1532,21 @@ "unknown": "Gæstekøretøj", "vehicleSoc": "Oplader" }, + "vehicleSettings": { + "connectedTo": "Forbundet til {0}", + "description": "Disse standarder benyttes, når køretøjet forbindes til et ladepunkt.", + "editHint": "Tilføj eller rediger køretøjer i {0}.", + "keepAsIs": "Gem som det er", + "learnMore": "Lær mere.", + "limitSoc": "Standardgrænse", + "limitSocDescription": "Anvendes, når køretøjet er tilsluttet.", + "menu": "Køretøjer", + "minSoc": "Minimum opladning", + "minSocDescription": "Hurtig opladning til dette niveau i soltilstand.", + "mode": "Standardtilstand", + "notConnected": "Ikke tilsluttet", + "title": "Køretøjsindstillinger" + }, "vehicleStatus": { "awaitingAuthorization": "Venter på godkendelse.", "batteryBoost": "Boost fra batteri er aktiv.", @@ -1467,6 +1573,8 @@ "pvEnable": "Overskud tilgængeligt. Starter snart.", "scale1p": "Reducerer snart til 1-faset opladning.", "scale3p": "Øger snart til 3-faset opladning.", + "suggestionStartTooltip": "Optimering anbefaler opladning nu.", + "suggestionStopTooltip": "Optimering anbefaler at pause opladningen.", "targetChargeActive": "Opladningsplan er aktiv. Forventes afsluttet om {duration}.", "targetChargePlanned": "Opladningsplan starter om {duration}.", "targetChargeWaitForVehicle": "Plan for opladning er klar. Venter på køretøj…", @@ -1506,6 +1614,7 @@ }, "session": { "addOdometer": "Tilføj distance", + "addedRange": "Tilføjet range", "cancel": "Afbryd", "co2": "CO2", "date": "Tidsrum", @@ -1516,11 +1625,12 @@ "meterstop": "Meter slut", "odometer": "Kilometerstand", "price": "Omkostning", - "soc": "Køretøjets SoC", + "soc": "Opladning", "started": "Startet", "title": "Lade session" }, "sessions": { + "addedRange": "Rækkevidde", "avgPower": "⌀ Effekt", "avgPrice": "⌀ pris", "chargeDuration": "Varighed", @@ -1546,6 +1656,7 @@ }, "co2": "⌀ CO₂", "csv": { + "addedrange": "Tilføjet rækkevidde (km)", "chargedenergy": "Energi (kWh)", "chargeduration": "Varighed", "co2perkwh": "CO₂/kWh", @@ -1564,6 +1675,8 @@ "vehicle": "Køretøj" }, "date": "Start", + "download": "Download {period}", + "downloadTotal": "Download total", "energy": "Opladet", "filter": { "allLoadpoints": "alle ladepunkter", @@ -1620,6 +1733,7 @@ "label": "Sprog" }, "loadpoints": { + "disabledDevice": "deaktiveret", "help": "Tilpas rækkefølge og synlighed i brugerfladen.", "hide": "Skjul {title}", "label": "Opladningspunkter", diff --git a/i18n/de.json b/i18n/de.json index 2b6a02b83..4ef54b677 100644 --- a/i18n/de.json +++ b/i18n/de.json @@ -1235,9 +1235,9 @@ "createButtonDiscussion": "GitHub Diskussion starten...", "createButtonEmail": "Anfrage per E-Mail senden", "createButtonIssue": "GitHub Issue erstellen...", + "description": "Deine Installation funktioniert nicht wie erwartet? Nutze diese Seite um Hilfe zu bekommen oder Probleme zu melden. Gib genügend Details an, damit wir das Problem verstehen und reproduzieren können. Halte deine Beschreibung prägnant, klar und leicht verständlich.", "downloadButton": "Debug-Informationen herunterladen", "downloadHint": "Hänge diese Datei an deine E-Mail an, falls weitere Informationen benötigt werden.", - "description": "Deine Installation funktioniert nicht wie erwartet? Nutze diese Seite um Hilfe zu bekommen oder Probleme zu melden. Gib genügend Details an, damit wir das Problem verstehen und reproduzieren können. Halte deine Beschreibung prägnant, klar und leicht verständlich.", "helpType": { "discussion": "Brauche Hilfe bei meiner Einrichtung", "discussionDescription": "Community-Diskussionen bieten Antworten.", @@ -1625,7 +1625,7 @@ "meterstop": "Endzählerstand", "odometer": "Kilometerstand", "price": "Kosten", - "soc": "Ladestand", + "soc": "Ladung", "started": "Startzeit", "title": "Ladevorgang" }, diff --git a/i18n/el.json b/i18n/el.json index e53958372..6d607d86b 100644 --- a/i18n/el.json +++ b/i18n/el.json @@ -847,7 +847,6 @@ }, "planNumber": "Πρόγραμμα {number}", "precondition": { - "description": "Φορτίστε {duration} πριν από την αναχώρηση για προετοιμασία μπαταρίας.", "label": "Καθυστερημένη φόρτιση", "optionAll": "Όλα", "optionNo": "Όχι" @@ -857,7 +856,6 @@ "repeatingPlans": "Επαναλαμβανόμενα προγράμματα", "selectAll": "Επιλογή όλων", "strategyDisabledDescription": "Η φόρτιση ξεκινά όσο το δυνατόν αργότερα, ώστε να ολοκληρωθεί ακριβώς πριν την αναχώρηση. Με τις δυναμικές τιμές δικτύου ή το τιμολόγιο CO₂, υπάρχουν περισσότερες επιλογές διαθέσιμες εδώ.", - "strategySettings": "Ρυθμίσεις στρατηγικής", "time": "Ώρα", "title": "Πρόγραμμα", "unsavedChanges": "Υπάρχουν μη αποθηκευμένες αλλαγές. Υποβολή τώρα;", diff --git a/i18n/es.json b/i18n/es.json index c67ee09f1..0c2548c81 100644 --- a/i18n/es.json +++ b/i18n/es.json @@ -471,8 +471,7 @@ }, "start": { "messageDefault": "Carga iniciada en modo {mode}.", - "title": "Cuando empieza la carga", - "titleDefault": "Carga iniciada en modo {mode}." + "title": "Cuando empieza la carga" }, "stop": { "messageDefault": "Carga finalizada: {chargedEnergy} kWh en {chargeDuration}.", @@ -979,7 +978,6 @@ }, "planNumber": "Plan {number}", "precondition": { - "description": "Cargue {duration} antes de la salida para preacondicionar la batería.", "label": "Cobro tardío", "optionAll": "todo", "optionNo": "no" @@ -989,7 +987,6 @@ "repeatingPlans": "Planes recurrentes", "selectAll": "Seleccionar todo", "strategyDisabledDescription": "La recarga comienza lo más tarde posible para terminar justo a tiempo para la salida. Con los precios dinámicos de la red o la tarifa de CO₂, hay más opciones disponibles aquí.", - "strategySettings": "Configuración de la estrategia", "time": "Tiempo", "title": "Plan", "unsavedChanges": "Hay cambios sin guardar. ¿Aplicar ahora?", diff --git a/i18n/fi.json b/i18n/fi.json index cdcdeaee0..4f83ccbcf 100644 --- a/i18n/fi.json +++ b/i18n/fi.json @@ -19,6 +19,49 @@ "successCloseTab": "Voit sulkea tämän välilehden.", "title": "Valtuutuksen tila" }, + "battery": { + "card": { + "charging": "lataa", + "discharging": "käytetään", + "energy": "Energia", + "gridCharge": "lataus verkosta", + "hold": "purku estetty", + "holdCharge": "lataus estetty", + "ofTotal": "{total}:sta", + "power": "Teho", + "soc": "Akun taso" + }, + "combined": "Yhdistetty", + "config": { + "buffer": "Kotiakun varaus, joka ylittää {soc} voidaan käyttää lataukseen, ja viivästyttää PV-tilan latauksen päättymistä. Aurinkolataus alkaa automaattisesti {start}.", + "bufferNone": "Kotiakun energiaa ei käytetä latauspisteisiin. Aseta arvo alle {soc} hyödyntääksesi sitä.", + "bufferStart": { + "above": "kun akun varaus on yli {soc}", + "full": "kun akku on täynnä", + "never": "vain riittävällä ylijäämäsähköllä" + }, + "bufferTitle": "Akku latauspuskurina", + "discharge": "Estä kotiakun purkautuminen välittömässä-tilassa ja suunnitellun latauksen tilassa.", + "gridDischarge": "Salli kotiakun purkaa varaustaan sähköverkkoon.", + "priority": "Kotiakulla on prioriteetti ollessaan alle {soc}. Silloin latauspisteet saavat vain ylijäämän.", + "priorityNone": "Aurinkoylijäämä käytetään suoraan latauspisteisiin. Aseta arvo yli {soc} priorisoidaksesi kotiakkua.", + "priorityTitle": "Minne aurinkoylijäämä käytetään ensisijaisesti?" + }, + "optimizer": { + "action": { + "charge": "Lataa verkosta", + "discharge": "Pura verkkoon", + "hold": "Estä purkaminen", + "holdcharge": "Estä lataus", + "normal": "Normaali toiminta" + }, + "empty": "tyhjä", + "full": "täynnä", + "highest": "Korkein ({value})", + "lowest": "Alhaisin ({value})", + "suggestion": "Ehdotus" + } + }, "batterySettings": { "batteryLevel": "Akun varaustaso", "bufferStart": { @@ -51,6 +94,11 @@ "hex": "heksa-värikoodi" }, "config": { + "adminPassword": { + "description": "Skripti-liitännäinen suorittaa järjestelmätason komennon joka ajetaan evcc-prosessin oikeuksissa. Syötä hallintasalasanasi jatkaaksesi.", + "invalid": "Salasana väärin. Yritä uudelleen.", + "title": "Mahdollisesti vaarallinen toiminto" + }, "apiKey": { "description": "Salli skriptien ja automaatioiden, kuten ajastettujen varmuuskopioiden, käyttää järjestelmää turvallisesti ilman ylläpitäjän salasanan jakamista.", "exampleLabel": "Kokeile sitä: lataa varmuuskopio curl:lla", @@ -72,6 +120,7 @@ "titleEdit": "Muokkaa kulutustaan itsesäätelevää sähkölaitetta" }, "battery": { + "title": "Akku", "titleAdd": "Lisää akku", "titleEdit": "Muokkaa akkua" }, @@ -97,16 +146,16 @@ "template": "Valmistaja", "titleAdd": { "charging": "Lisää laturi", - "heating": "Lisää lämmitin" + "heating": "Lisää lämmityslaite" }, "titleEdit": { "charging": "Muokkaa laturia", - "heating": "Muokkaa lämmitintä" + "heating": "Muokkaa lämmityslaitetta" }, "type": { "custom": { "charging": "Itse määritelty laturi", - "heating": "Itse määritelty lämmitin" + "heating": "Itse määritelty lämmityslaite" }, "heatpump": "Itse määritelty lämpöpumppu", "sgready": "Itse määritelty lämpöpumppu (sg-ready pluginien kautta)", @@ -142,6 +191,7 @@ "deviceValue": { "activeClients": "Aktiiviset asiakasyhteydet", "amount": "Määrä", + "asleep": "nukkuu", "broker": "Välittäjä (Broker)", "bucket": "Tietosäiliö (Bucket)", "capacity": "Kapasiteetti", @@ -169,6 +219,7 @@ "enabled": "Käytössä", "energy": "Energia", "events": "Tapahtumat", + "exportLimit": "Sähköverkkoon vientiraja", "feedinPrice": "Sähköverkkoon myynninhinta", "forecast": "Ennuste", "gridPrice": "Verkosta ostonhinta", @@ -180,6 +231,7 @@ "no": "ei", "odometer": "Matkamittari", "org": "Organisaatio", + "outdoorTemp": "Ulkolämpötila", "phaseCurrents": "Virta", "phasePowers": "Teho", "phaseVoltages": "Jännite", @@ -189,6 +241,7 @@ "price": "Hinta", "productionLimited": "Tuotanto rajoitettu", "range": "Toimintasäde", + "remoteEnabled": "Päällä", "returnEnergy": "Annettu energia", "singlePhase": "Yksivaiheinen", "soc": "Lataus", @@ -220,15 +273,21 @@ "public": "Julkinen sertifikaatti", "title": "Sertifikaatit" }, + "configuredDevices": "Määritellyt laitteet", "description": "Konfiguraatio, jonka avulla evcc voi kommunikoida muiden EEBus-yhteensopivien laitteiden kanssa, kuten laturit tai sähköverkko-operaattorin hallintalaitteet. Kaikki tarvittavat yhteydenmuodostukset ja yhteyssertifikaatin luonti tehdään automaattisesti ensimmäisessä käynnistyksessä.", "descriptionAdvanced": "Muutoksia ei tarvita. Tee muutoksia näihin asetuksiin vain jos todella tiedät mitä olet tekemässä. Jos muutat SHIP-ID:tä tai sertifikaatteja sinun tulee parittaa laitteesi uudelleen.", "interfaces": "Verkkoyhteydet", "interfacesHelp": "Rajaa verkkoyhteyksiä, joita EEBus voi käyttää, jotta vältytään kommunikointiongelmilta. Jätä tyhjäksi käyttääksesi kaikki verkkoyhteyksiä. Yksi laite per rivi.", + "noConfiguredDevices": "SKI-laitteita ei ole määritelty.", + "noPairings": "Ei paritettuja laitteita.", + "pairings": "Paritetut laitteet", "port": "Portti", "portHelp": "Käytettävä porttinumero.", "qr": "Parituskoodi", "qrExplain": "Skannaa toisesta EEBus-laitteesta asennuksen aikana parittaaksesi automaattisesti ilman SKI:n käsinsyöttämistä.", "removeConfirm": "Kaikki EEBus-asetukset poistetaan. Uudet sertifikaatit ja tunnisteet generoidaan seuraavan järjetestelmän käynnistyksessä. Oletko varma?", + "removePairing": "Poista paritus", + "removePairingConfirm": "Laitteen yhteys katkeaa ja se täytyy parittaa uudelleen. Oletko varma?", "shipid": "SHIP-ID", "shipidExplain": "Pysyvä laitteen tunnistetieto EEBus-yhteydessä laitteen yksilöimiseksi.", "shipidHelp": "Alla olevat sertifikaatit ovat yhdistetty tähän SHIP-ID:seen.", @@ -241,13 +300,14 @@ "title": "Kokeellinen" }, "ext": { - "description": "Tallentaa ei-hallittujen virrankuluttajien (kuten jääkaapin, pesukoneen) käyttämää energiaa tilastointiin. Voidaan käyttää myös kuormanhallintaan (esim. osittaiskuormana).", - "titleAdd": "Lisää kulutusmittari", - "titleEdit": "Muokkaa kulutusmittaria" + "description": "Tallentaa mittareita, joita ei käytetä energian ohjaukseen eikä niitä näytetä käyttöliittymässä (aliverkot, vaihtoehtoverkot, aurinkopaneelien mittarit, ...). Data lokitetaan ja on liitettävissä ulkoisiin lähteisiin kuten muutkin mittarit.", + "titleAdd": "Lisää ylimääräinen mittari", + "titleEdit": "Muokkaa ylimääräistä mittaria" }, "form": { "danger": "Vaara", "deprecated": "vanhentunut", + "durationUnit": "Aikayksikkö {label}:lle", "example": "Esimerkiksi", "optional": "valinnainen" }, @@ -260,17 +320,22 @@ "change": "Muuta", "clear": "Tyhjennä", "close": "Sulje", + "confirmDisable": "Poista tämä laite käytöstä? Laitetta ei huomioida seuraavan käynnistyksen jälkeen.", + "confirmEnable": "Ota laite uudelleen käyttöön?", "confirmSave": "Asetuksien muutoksia on tallentamatta. Tallennetaanko nyt heti?", "copied": "Kopioitu!", "copy": "Kopioi", "customHelp": "Luo itsemääritelty laite käyttäen EVCC:n lisäosaa.", "customOption": "Itsemääritelty laite", "delete": "Poista", + "disable": "Poista käytöstä", + "disabled": "ei käytössä", "dismiss": "Ohita", "docsLink": "Katso dokumentaatio.", "dragHandle": "Kahva", "dragItem": "Raahattavissa: {title}", "dragList": "Järjestettävä lista", + "enable": "Ota käyttöön", "error": "Virhe", "experimental": "Kokeellinen", "forceSave": "Tallenna joka tapauksessa", @@ -302,6 +367,7 @@ "titleEdit": "Muokkaa sähköverkonmittaria" }, "hems": { + "changeConfirm": "Nykyinen määritys poistetaan. Jatka?", "csv": { "created": "Luotu", "finished": "Valmis", @@ -309,10 +375,26 @@ "limitpower": "Raja (kW)", "type": "Tyyppi" }, - "description": "Ulkoisten järjestelmien (esim. saksalaisen energialainsäädännön §14a EnWG, §9 EEG-rajapinta tai ylemmän tason energianhallintajärjestelmä) suorittama tehonrajoitus. Toimii yhdessä kuormituksenhallintaominaisuuden kanssa.", + "customOption": "Itsemääritelty integraatio", + "description": "Ulkoisten järjestelmien (esim. saksalaisen energialainsäädännön §14a EnWG, §9 EEG-rajapinta tai ylemmän tason energianhallintajärjestelmä) suorittama tehonrajoitus. Toimii yhdessä kuormituksenhallintaominaisuuden kanssa. Aurinkotuotannon ylityksen käyttö mahdollista ja tuettu valikoiduille laitteille. Sallittu ylitys pitää määritellä invertterin asetuksessa.", + "download": "Lataa", + "dynamicLimits": "Mukautuvat rajat", "eventsRecorded": "Tallennettu {count} verkon rajoitustapahtumaa.", + "exportLimit": "Sähköverkkoon vientiraja", + "exportLimitDescription": "Akkuoptimoija huomioi tämän rajan välttääkseen aurinkotuoton hävikin. Mukautuvan rajan integraation aktiivinen tehonrajoitussignaali ohittaa tämän.", + "exportLimitHint": "Kiinteä syöttörajoite sähköverkon liittymispisteessä.", + "generic": "Yleiset integraatiot", + "integrations": "Integraatiot", "lastEvent": "Viimeisin {timeAgo}.", - "title": "Ulkoinen raja" + "noEvents": "Ei vielä tallennettuja tapahtumia.", + "recordedEvents": "Tallennetut tapahtumat", + "staticLimits": "Kiinteät rajat", + "template": "Integraatio", + "title": "Ulkoinen raja", + "type": { + "custom": "Itsemääritelty integraatio", + "relay": "Itsemääritelty integraatio (välitys)" + } }, "icon": { "change": "vaihda", @@ -337,17 +419,18 @@ "loadpoint": { "addCharger": { "charging": "Lisää laturi", - "heating": "Lisää lämmitin" + "heating": "Lisää lämmityslaite" }, "addMeter": "Lisää erillinen kulutusmittari", + "advancedConfig": "Lisäasetukset", "cancel": "Peruuta", "chargerError": { "charging": "Vaaditaan käyttöönottoasetusten määrittely laturille.", - "heating": "Vaaditaan lämmittimen asetuksien määrittely." + "heating": "Vaaditaan lämmityslaitteen asetuksien määrittely." }, "chargerLabel": { "charging": "Laturi", - "heating": "Lämmitin" + "heating": "Lämmityslaite" }, "chargerPower11kw": "11 kW", "chargerPower11kwHelp": "Käyttää 6-16 A:n virta-aluetta.", @@ -361,11 +444,21 @@ "circuitInvalid": "Piiriä ei ole olemassa", "circuitLabel": "Piiri", "circuitUnassigned": "määräämätön", + "created": "\"{name}\" luotiin ja käyttää vakioasetuksia.", + "createdTitle": { + "charging": "Latauspiste luotiin", + "heating": "Lämmitin luotiin" + }, + "creating": { + "charging": "Latauslaite tallennettu. Luodaan latauspiste …", + "heating": "Lämmityslaite tallennettu. Luodaan lämmitin …" + }, "defaultModeHelp": { "charging": "Lataustila ajoneuvoa kytkettäessä.", "heating": "Asetetaan päälle järjestelmän käynnistyessä." }, "defaultModeHelpKeep": "Säilyttää viimeksi valitun lataus/käyttötilan.", + "defaultModeKeep": "pidä nykyisellään", "defaultModeLabel": "Oletustila", "delete": "Poista", "electricalSubtitle": "Jos olet epävarma, kysy sähköasentajaltasi.", @@ -375,12 +468,15 @@ "estimateLabel": "Arvioi lataustaso API-päivitysten välillä", "maxCurrentHelp": "On oltava minimivirtaa suurempi.", "maxCurrentLabel": "Maksimivirta", + "maxTempHelp": "Pitää olla suurempi kuin minimilämpötila.", + "maxTempLabel": "Maksimilämpötila", "minCurrentHelp": "Valitse alle 6 A vain, jos tiedät mitä olet tekemässä.", "minCurrentLabel": "Minimivirta", + "minTempLabel": "Minimilämpötila", "noVehicles": "Ajoneuvoja ei ole määritetty.", "option": { "charging": "Lisää latauspiste", - "heating": "Lisää lämmityslaite" + "heating": "Lisää lämmitin" }, "phases1p": "1-vaihe", "phases3p": "3-vaihe", @@ -406,6 +502,7 @@ "solarBehaviorLabel": "Aurinkoenergia", "solarModeCustom": "muokattu", "solarModeMaximum": "maksimi PV", + "temperatureRangeHelp": "Näkymäasetus lämpötila-asteikolle. Ei vaikuta ohjaukseen.", "temperatureRangeTitle": "Lämpötila-alue", "thresholdDisableDelayLabel": "Poista viive käytöstä", "thresholdDisableHelpInvalid": "Käytä positiivista arvoa.", @@ -419,13 +516,13 @@ "thresholdEnableLabel": "Ota verkkoenergia käyttöön", "titleAdd": { "charging": "Lisää latauspiste", - "heating": "Lisää lämmityslaite", - "unknown": "Lisää latauslaite tai lämmitin" + "heating": "Lisää lämmitin", + "unknown": "Lisää latauspiste tai lämmitin" }, "titleEdit": { "charging": "Muokkaa latauspistettä", - "heating": "Muokkaa latauslaitetta", - "unknown": "Muokkaa latauslaitetta tai lämmitintä" + "heating": "Muokkaa lämmitintä", + "unknown": "Muokkaa latauspistettä tai lämmitintä" }, "titleExample": { "charging": "Autotalli, autokatos, yms.", @@ -443,7 +540,7 @@ "addAdditional": "Lisää lisämittari", "addConsumer": "Lisää kuluttaja", "addGrid": "Lisää verkkomittari", - "addLoadpoint": "Lisää latauslaite tai lämmitin", + "addLoadpoint": "Lisää latauspiste tai lämmityspistettä", "addPvBattery": "Lisää aurinkovoimala tai akusto", "addTariffs": "Lisää tariffit", "addVehicle": "Lisää ajoneuvo", @@ -474,14 +571,14 @@ "titleDefault": "Ajoneuvo nukuksissa" }, "connect": { - "messageDefault": "Auto yhdistettynä aurinko-{pvPower}kW", - "title": "Kun auto yhdistetään", - "titleDefault": "Auto yhdistettynä" + "messageDefault": "Ajoneuvo yhdistettynä aurinko-{pvPower}kW", + "title": "Kun ajoneuvo yhdistetään", + "titleDefault": "Ajoneuvo yhdistettynä" }, "disconnect": { - "messageDefault": "Auto irroitettu {connectedDuration} jälkeen", - "title": "Autoa irroitettaessa", - "titleDefault": "Auto irroitettu" + "messageDefault": "Ajoneuvo irroitettu {connectedDuration} jälkeen", + "title": "Ajoneuvoa irroitettaessa", + "titleDefault": "Ajoneuvo irroitettu" }, "guest": { "messageDefault": "Tuntematon ajoneuvo, vieras yhdistettynä?", @@ -507,6 +604,11 @@ "messageDefault": "Lataus päättynyt {chargedEnergy}kWh ajassa {chargeDuration}.", "title": "Latauksen päättyessä", "titleDefault": "Lataus päättynyt" + }, + "suggestion": { + "messageDefault": "Optimoijan suositus {suggestionTitle}lle: Jos optimointiohjaus olisi aktiviinen, se suorittaisi nyt '{suggestionAction}'. Tämä on vain suositus, mitään ei ole muutettu. Kokeellinen — palaute tervetullutta keskusteluun https://github.com/evcc-io/evcc/issues/31903", + "title": "Kun optimoidan suositustoimenpide muuttuu", + "titleDefault": "Optimoijan suositus" } }, "eventMessage": "Viesti", @@ -536,6 +638,7 @@ "option": { "aux": "Lisää kulutustaan itsesäätelevä laite", "battery": "Lisää akun tilan mittari", + "consumer": "Lisää tavanomainen kuluttaja", "pv": "Lisää aurinkovoimalan mittari" }, "save": "Tallenna", @@ -546,7 +649,7 @@ "usage": { "aux": "Kulutusta säätelevä käyttäjä", "battery": "Akku", - "charge": "Virrankuluttaja / Laturi", + "charge": "Laturi", "grid": "Jakeluverkko", "label": "Kulutus", "pv": "Tuotanto" @@ -561,6 +664,8 @@ "connectionHintTcpip": "Laite on saavutettavissa lähiverkosta (LAN/WiFi-yhteydellä).", "connectionValueSerial": "RS485 (sarjaportti)", "connectionValueTcpip": "Verkko", + "delay": "Viive", + "delayHint": "Valinnainen odotusaika ennen jokaista rekisterilukua. Tarvitaan vain hitaasti vastaaville laitteille.", "device": "Laitenimi", "deviceHint": "Esim. /dev/ttyUSB0", "host": "IP-osoite tai verkkotunnus", @@ -571,7 +676,9 @@ "protocolHintRtu": "RS485-yhteys Ethernet-sovittimen kautta ilman protokollamuunnosta.", "protocolHintTcp": "Laitteessa on sisäänrakennettu LAN/WiFi-tuki tai RS485-yhteydessä Ethernet-sovittimen kautta ilman protokollamuunnosta.", "protocolValueRtu": "RTU", - "protocolValueTcp": "TCP" + "protocolValueTcp": "TCP", + "timeout": "Ei vastaa", + "timeoutHint": "Valinnainen maksimiodotusaika, joka odotetaan laitteen vastausta." }, "modbusproxy": { "add": "Lisää välityspalvelimellinen (proxy) yhteys", @@ -731,12 +838,12 @@ }, "section": { "additionalMeter": "Ylimääräiset mittarit", - "consumers": "Kuluttajat", + "consumers": "Sähkönkuluttajat", "general": "Yleinen", "grid": "Sähköverkko", "integrations": "Integraatiot", - "loadpoints": "Lataus- ja lämmityspisteet", - "meter": "Aurinko & Akku", + "loadpoints": "Lataus- ja lämmitin", + "meter": "Aurinko & akku", "services": "Palvelut", "system": "Järjestelmä", "vehicles": "Ajoneuvot" @@ -850,7 +957,8 @@ "feedIn": "Lisää sähkön myyntihinta", "grid": "Lisää sähkön ostohinta", "planner": "Lisää ennuste suunnitelmalle", - "solar": "Lisää aurinkotuotantoennuste" + "solar": "Lisää aurinkotuotantoennuste", + "temperature": "Lisää lämpötilaennuste" }, "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.", @@ -863,15 +971,21 @@ "titleAdd": "Lisää aurinkotuotantoennuste", "titleEdit": "Muokkaa aurinkotuotantoennustetta" }, + "temperature": { + "description": "Ulkolämpötilaennuste sijainnillesi. Näytetään käyttöliittymässä ja sitä käytetään ennakoimaan lämmitystarvetta.", + "titleAdd": "Lisää lämpötilaennuste", + "titleEdit": "Muuta lämpötilaennustetta" + }, "template": "Lähde", - "title": "Tariffit & Ennusteet", + "title": "Tariffit & ennusteet", "titleChoice": "Mitä haluat lisätä?", "type": { "co2": "CO₂-intensiteetti", "feedIn": "Sähkön myyntihinta verkkoon", "grid": "Verkkosähkön ostohinta", "planner": "Suunnitelma", - "solar": "Aurinko" + "solar": "Aurinko", + "temperature": "Lämpötilaennuste" }, "zones": { "add": "Lisää hintajakso", @@ -992,6 +1106,8 @@ "version": { "availableLong": "uusi versio saatavilla", "community": "evcc-yhteisö", + "labelEmail": "Sähköposti", + "labelPhone": "Puhelin", "labelRelease": "Julkaisu", "labelVersion": "Versio", "labelWebsite": "Verkkosivu", @@ -1018,13 +1134,14 @@ "average": "Keskimäärin", "constant": "CO₂-kerroin", "lowestHour": "Puhtain tunti", - "range": "Toimintamatka" + "range": "Vaihteluväli" }, "empty": { "co2": "Näytä milloin sähköverkon energia on puhdasta. Lataussuunnitelmissa optimoidaan alhaiset päästöt ja laskelmoidut CO₂-säästöt.", "price": "Määritä vaihteleva energian hintasi optimoidaksesi automaattisesti lataussuunnitelmasi ja laskeaksesi käytetyn energian kustannukset.", "setup": "Määritä hinnat ja ennusteet", - "solar": "Katso aurinkotuotannon ennuste täksi ja tuleviksi päiviksi. Käytetään tulevaisuudessa myös latauksien optimoitiin." + "solar": "Katso aurinkotuotannon ennuste täksi ja tuleviksi päiviksi. Käytetään tulevaisuudessa myös latauksien optimoitiin.", + "temperature": "Näe ulkolämpötilaennuste sijainnissasi. Sitä käytetään ennakkoimaan lämmitystarvetta." }, "hideLine": "piilota kuvaaja", "modalTitle": "Ennuste", @@ -1032,7 +1149,7 @@ "average": "Keskimääräinen", "constant": "Hinta", "lowestHour": "Edullisin tunti", - "range": "Toimintamatka" + "range": "Vaihteluväli" }, "priceZoom": "lähennä", "showLine": "näytä kuvaaja", @@ -1046,14 +1163,24 @@ "solarAdjust": "Mukauta aurinkosääennustettu todellisten tuotantotietojen perusteella{percent}.", "solarAdjustMedium": "mitatun datan korjaus", "solarAdjustShort": "korjaus", + "temperature": { + "average": "Keskiarvo", + "range": "Vaihteluväli" + }, "type": { "co2": "CO₂-päästöt", "price": "Sähkön hinta", - "solar": "Aurinkotuotanto" + "solar": "Aurinkotuotanto", + "temperature": "Ulkolämpötila" } }, "general": { - "note": "Huomautus:" + "back": "Takaisin", + "download": "Lataa", + "error": "Virhe", + "none": "ei mitään", + "note": "Huomautus:", + "warning": "Varoitus" }, "header": { "about": "Tietoa", @@ -1066,6 +1193,7 @@ "sessions": "Lataustapahtumat" }, "help": { + "contactButton": "Ota yhteyttä", "discussionsButton": "GitHub keskustelut", "documentationButton": "Dokumentaatio", "issueButton": "Ilmoita virheestä", @@ -1105,8 +1233,11 @@ "additionalContext": "Tilannetiedot", "additionalContextPlaceholder": "Mitä tahansa lisätietoja, jotka voivat olla höydyllisiä, kuten\n- asetuksien yksityiskohtia\n- mitä olet yrittänyt tehdä\n- asennusympäristön yksityiskohtia", "createButtonDiscussion": "Aloita GitHub keskustelu...", + "createButtonEmail": "Lähetä pyyntö sähköpostitse", "createButtonIssue": "Luo GitHub -tiketti...", "description": "Asennuksesi ei toimi kuten oletit? Tällä sivulla voit saada apua tai raportoida uudesta ongelmatilanteesta. Välitä tarpeeksi yksityiskohtia, jotta voimme ymmärtää ja toisintaa ongelman, kuitenkin pitäen selityksen napakkana, selkeänä ja helppona seurata.", + "downloadButton": "Lataa virheenkorjaustietoja", + "downloadHint": "Liitä tämä tiedosto sähköpostiisi, jos tarvitaan lisätietoja.", "helpType": { "discussion": "Tarvitsen apua ympäristöni kanssa", "discussionDescription": "Yhteisökeskustelu tarjoaa vastauksia.", @@ -1118,6 +1249,7 @@ "issueTitle": "Otsikko", "stepsToReproduce": "Toisintamisvaiheet", "subTitleDiscussion": "Kuvaile ongelmasi", + "subTitleEmail": "Kuvaile ongelmasi", "subTitleIssue": "Kuvaile ongelma", "summary": { "confirmationButtonDiscussion": "Aloita GitHub-keskustelu", @@ -1170,24 +1302,33 @@ "goal": "Lataus tavoite", "modalTitle": "Lataussuunnitelma", "none": "ei mitään", + "once": "kerran", "optimization": { "cheapest": "halvin", + "cheapestDescription": "Valitsee halvimmat aikajaksot. Voi aiheuttaa useita aloituksia ja lopetuksia.", + "cleanest": "puhtain", + "cleanestDescription": "Valitsee puhtaimmat aikajaksot. Voi aiheuttaa useita aloituksia ja lopetuksia.", "continuous": "yhtenäinen", + "continuousCo2Description": "Valitsee puhtaimman yhteisen aikajakson.", + "continuousPriceDescription": "Valitsee halvimman yhtenäisen aikajakson.", "label": "Optimointi" }, "planNumber": "Suunnitelma {number}", "precondition": { - "description": "Ladataan {duration} ennen lähtöä akunesilämmitystä varten.", + "description": "Siirtää osan latauksesta juuri ennen lähtöaikaa: lämmittää akkua, vähemmän aikaa korkealla lataustasolla.", "label": "Viimehetken lataus", "optionAll": "koko latausmäärä", - "optionNo": "ei" + "optionNo": "ei", + "summary": "{precondition} ennen" }, "remove": "Poista", "repeating": "toistetaan", "repeatingPlans": "Toistuvat suunnitelmat", "selectAll": "Valitse kaikki", + "strategy": { + "label": "Strategia" + }, "strategyDisabledDescription": "Lataus aloitetaan mahdollisimman myöhään, jotta se valmistuu juuri ennen lähtöä. Jos dynaaminen sähköverkkohinnoittelu tai CO₂-tariffit ovat asetetut, on tässä tarjolla enemmän valintoja.", - "strategySettings": "Strategiamääritykset", "time": "Aika", "title": "Suunnitelma", "unsavedChanges": "Tallentamattomia muutoksia. Asetetaanko nyt?", @@ -1218,11 +1359,15 @@ "pv": "Aurinkosähköjärjestelmä", "pvExport": "Sähköverkkoon vienti", "pvProduction": "Tuotanto", - "selfConsumption": "Tuotannon kulutus" + "selfConsumption": "Tuotannon kulutus", + "toggleDetails": "Energiavirran yksityiskohdat" }, "heatingStatus": { "charging": "Lämmitys…", "connected": "Valmiustila.", + "minTemp": "Vähimmäislämmitys {temp}:een.", + "suggestionStartTooltip": "Optimoija suosittelee lämmitystä nyt.", + "suggestionStopTooltip": "Optimoija suosittelee keskeyttämään lämmityksen.", "vehicleLimit": "Lämmittimen rajoitin", "waitForVehicle": "Lämmitystä aloitetaan…" }, @@ -1240,11 +1385,17 @@ "grid": { "energy": "ladattu verkosta", "returnEnergy": "syötetty verkkoon" + }, + "meter": { + "energy": "Energia", + "returnEnergy": "Energia (käänteinen)" } }, + "download": "Lataa", "empty": "Tälle ajanjaksolle ei ole saatavilla historiadataa.", "group": { "battery": "Akku", + "consumer": "Kulutus", "forecast": "Ennuste", "grid": "Sähköverkko", "loadpoint": "Lataus & lämmitys", @@ -1262,12 +1413,15 @@ "emission": "Päästöt", "fallbackName": "Latauspiste", "finished": "Valmistumisaika", + "last24hEnergy": "Viimeinen 24h", + "last7dEnergy": "Viimeinen 7 päivää", "power": "Teho", "price": "Kustannus", "remaining": "Jäljellä", "remoteDisabledHard": "{source}: sammui", "remoteDisabledSoft": "{source}: sammutti adaptiivisen aurinkolatauksen", - "solar": "Aurinkoenergia" + "solar": "Aurinkoenergia", + "todayEnergy": "Tänään" }, "loadpointSettings": { "batteryBoost": { @@ -1285,12 +1439,17 @@ "batteryUsage": "Kodin akku", "currents": "Latausvirta", "default": "oletus", + "heating": "Lämmitys", "maxCurrent": { "label": "Maksimivirta" }, "minCurrent": { "label": "Minimivirta" }, + "minTemp": { + "description": "Alle {0} akun ja sähköverkon tehoa käytetään myös aurinko/PV-tilassa ylläpitämään tätä lämpötilaa.", + "label": "Min. lämpötila" + }, "phasesConfigured": { "label": "Vaihetila", "no1p3pSupport": "Kuinka laturisi on kytketty?", @@ -1373,6 +1532,21 @@ "unknown": "Vieras ajoneuvo", "vehicleSoc": "Lataus" }, + "vehicleSettings": { + "connectedTo": "kytketty {0}", + "description": "Näitä asetuksia käytetään kun ajoneuvo kytketään latauspisteeseen.", + "editHint": "Lisää tai muokkaa ajoneuvoja {0}:ssa.", + "keepAsIs": "Pidä nykyisenä", + "learnMore": "Kerro lisää.", + "limitSoc": "Vakioraja", + "limitSocDescription": "Käytössä kun ajoneuvo kytketään.", + "menu": "Ajoneuvot", + "minSoc": "Vähimmäislataus", + "minSocDescription": "Välitön lataus tälle tasolle aurinko/PV-tilassa.", + "mode": "Vakiotila", + "notConnected": "ei kytketty", + "title": "Ajoneuvon asetukset" + }, "vehicleStatus": { "awaitingAuthorization": "Odotetaan valtuutusta.", "batteryBoost": "Akun tehostus aktiivinen.", @@ -1399,6 +1573,8 @@ "pvEnable": "Ylijäämää saatavilla. Lataus alkaa pian.", "scale1p": "Siirrytään pian 1-vaihe lataukseen.", "scale3p": "Siirrytään pian 3-vaihe lataukseen.", + "suggestionStartTooltip": "Optimoija suosittelee lataamaan nyt.", + "suggestionStopTooltip": "Optimoija suosittelee keskeyttämään latauksen.", "targetChargeActive": "Lataussuunnitelma aktiivinen. Arvioitu valmistuminen {duration} kuluttua.", "targetChargePlanned": "Lataussuunnitelma alkaa {duration} kuluttua.", "targetChargeWaitForVehicle": "Lataussuunnitelma valmis. Odotetaan ajoneuvoa…", @@ -1438,6 +1614,7 @@ }, "session": { "addOdometer": "Lisää arvo", + "addedRange": "Ladattu toimintamatka", "cancel": "Peruuta", "co2": "CO₂", "date": "Ajanjakso", @@ -1448,11 +1625,12 @@ "meterstop": "Mittarin pysähdys", "odometer": "Ajokilometrit", "price": "Hinta", - "soc": "Ajoneuvon lataustila", + "soc": "Lataustila", "started": "Alkoi", "title": "Latausistunto" }, "sessions": { + "addedRange": "Toimintamatka", "avgPower": "⌀ Teho", "avgPrice": "⌀ Hinta", "chargeDuration": "Kesto", @@ -1478,6 +1656,7 @@ }, "co2": "⌀ CO₂", "csv": { + "addedrange": "Lisääntynyt toimintamatka (km)", "chargedenergy": "Energia (kWh)", "chargeduration": "Kesto", "co2perkwh": "CO₂/kWh", @@ -1496,6 +1675,8 @@ "vehicle": "Ajoneuvo" }, "date": "Aloitettu", + "download": "Lataa {period}", + "downloadTotal": "Lataa kaikki", "energy": "Ladattu", "filter": { "allLoadpoints": "kaikki latauspisteet", @@ -1532,7 +1713,7 @@ "type": { "co2": "CO₂", "price": "Hinta", - "solar": "Aurinko" + "solar": "Energia" }, "vehicle": "Ajoneuvo" }, @@ -1552,6 +1733,7 @@ "label": "Kieli" }, "loadpoints": { + "disabledDevice": "ei käytössä", "help": "Muuta käyttöliittymän järjestystä ja näkyvyyttä.", "hide": "Piilota {title}", "label": "Latauspisteet", diff --git a/i18n/fr.json b/i18n/fr.json index 3575b8ec2..9540c63a6 100644 --- a/i18n/fr.json +++ b/i18n/fr.json @@ -219,6 +219,7 @@ "enabled": "Activé", "energy": "Énergie", "events": "Evénements", + "exportLimit": "Limite d'exportation du réseau", "feedinPrice": "Prix de vente", "forecast": "Prévisions", "gridPrice": "Prix du réseau", @@ -306,6 +307,7 @@ "form": { "danger": "Attention", "deprecated": "obsolète", + "durationUnit": "Unité de temps pour {label}", "example": "Exemple", "optional": "optionnel" }, @@ -371,12 +373,17 @@ "customOption": "Intégration personnalisée", "description": "Limitation de puissance par des systèmes externes (par exemple, §14a de l'EnWG, §9 de l'EEG ou un système de gestion de l'énergie de niveau supérieur). Fonctionne conjointement avec la fonctionnalité de gestion de la charge. La limitation de la production solaire est également prise en charge pour certains appareils. La limite de limitation effective doit être définie lors de la configuration de chaque onduleur.", "download": "Télécharger", + "dynamicLimits": "Limites dynamiques", "eventsRecorded": "Enregistrement de {count} événements liés à la limitation de la grille.", + "exportLimit": "Limite d'exportation du réseau", + "exportLimitDescription": "L'optimiseur de batterie respecte cette limite afin d'éviter les pertes d'énergie solaire. Un signal de limitation actif issu d'une intégration dynamique des limites prévaut.", + "exportLimitHint": "Limite statique d'injection au point de raccordement au réseau.", "generic": "Intégrations génériques", "integrations": "Intégrations", "lastEvent": "Le plus récent {timeAgo}.", "noEvents": "Aucun événement n'a encore été enregistré.", "recordedEvents": "Événements enregistrés", + "staticLimits": "Limites statiques", "template": "Intégration", "title": "Limite externe", "type": { @@ -1094,6 +1101,8 @@ "version": { "availableLong": "mise à jour disponible", "community": "Communauté evcc", + "labelEmail": "E-mail", + "labelPhone": "Téléphone", "labelRelease": "Lancement", "labelVersion": "Version", "labelWebsite": "Site web", @@ -1176,6 +1185,7 @@ "sessions": "Sessions de charge" }, "help": { + "contactButton": "Contactez-nous", "discussionsButton": "Discussions GitHub", "documentationButton": "Documentation", "issueButton": "Signaler un problème", @@ -1215,8 +1225,11 @@ "additionalContext": "Contexte additionnel", "additionalContextPlaceholder": "Toute information utile...\n- Détails de configuration\n- Ce que vous avez essayé\n- Détails de l’environnement", "createButtonDiscussion": "Démarrer une discussion sur GitHub…", + "createButtonEmail": "Envoyer une demande par e-mail", "createButtonIssue": "Créer un problème sur GitHub…", "description": "Votre installation ne fonctionne pas comme attendu ? Utilisez cette page pour obtenir de l’aide ou signaler un problème. Fournissez suffisamment de détails pour nous permettre de comprendre et de reproduire le problème, tout en restant concis, clair et facile à suivre.", + "downloadButton": "Télécharger les informations de débogage", + "downloadHint": "Si vous avez besoin de plus d'informations, veuillez joindre ce fichier à votre e-mail.", "helpType": { "discussion": "J’ai besoin d’aide avec mon installation", "discussionDescription": "Les discussions de la communauté fournissent des réponses.", @@ -1228,6 +1241,7 @@ "issueTitle": "Titre", "stepsToReproduce": "Étapes permettant de reproduire le problème", "subTitleDiscussion": "Décrivez votre problème", + "subTitleEmail": "Décrivez votre problème", "subTitleIssue": "Décrivez le problème", "summary": { "confirmationButtonDiscussion": "Démarrer une Discussion sur GitHub", @@ -1280,24 +1294,33 @@ "goal": "Objectif de charge", "modalTitle": "Planification de la charge", "none": "aucune", + "once": "une fois", "optimization": { "cheapest": "le moins cher", + "cheapestDescription": "Sélectionne les créneaux horaires les moins chers. Cela peut entraîner plusieurs démarrages et arrêts.", + "cleanest": "le plus propre", + "cleanestDescription": "Sélectionne les créneaux les plus propres. Cela peut entraîner plusieurs démarrages et arrêts.", "continuous": "continu", + "continuousCo2Description": "Sélectionne la série de créneaux la plus propre et sans interruption.", + "continuousPriceDescription": "Sélectionne la série de créneaux horaires sans interruption et la moins chère.", "label": "Optimisation" }, "planNumber": "Plan {number}", "precondition": { - "description": "Charge {duration} avant le départ pour pré-conditionner la batterie.", + "description": "Effectuer une partie de la recharge juste avant le départ : la batterie est chaude et reste moins longtemps à un niveau de charge élevé.", "label": "Chargement tardif", "optionAll": "tous", - "optionNo": "non" + "optionNo": "non", + "summary": "{precondition} avant" }, "remove": "Enlever", "repeating": "récurrent", "repeatingPlans": "Plans récurrents", "selectAll": "Sélect. tous", + "strategy": { + "label": "Stratégie" + }, "strategyDisabledDescription": "La charge commence le plus tard possible afin de se terminer juste à temps pour le départ. Avec les prix dynamiques du réseau ou la tarification CO₂, davantage d'options sont disponibles ici.", - "strategySettings": "Paramètres de stratégie", "time": "Heure", "title": "Planification", "unsavedChanges": "Il y a des modifications non sauvegardées. Appliquer ?", @@ -1582,6 +1605,7 @@ }, "session": { "addOdometer": "Ajouter une valeur", + "addedRange": "Autonomie ajoutée", "cancel": "Annuler", "co2": "CO₂", "date": "Période", @@ -1592,11 +1616,12 @@ "meterstop": "Fin du Compteur", "odometer": "Kilométrage", "price": "Coût", - "soc": "Etat de Charge du véhicule", + "soc": "Charge", "started": "Démarré", "title": "Session de charge" }, "sessions": { + "addedRange": "Autonomie", "avgPower": "Puissance ⌀", "avgPrice": "Prix ⌀", "chargeDuration": "Durée", @@ -1622,6 +1647,7 @@ }, "co2": "CO₂ ⌀", "csv": { + "addedrange": "Autonomie ajoutée (km)", "chargedenergy": "Énergie (kWh)", "chargeduration": "Durée", "co2perkwh": "CO₂/kWh", diff --git a/i18n/hr.json b/i18n/hr.json index 7e88148c1..ba3dcbbac 100644 --- a/i18n/hr.json +++ b/i18n/hr.json @@ -847,7 +847,6 @@ }, "planNumber": "Plan {number}", "precondition": { - "description": "Punjenje {duration} prije polaska kako bi se izvršilo predkondicioniranje baterije.", "label": "Kasnije punjenje", "optionAll": "sve", "optionNo": "ne" @@ -857,7 +856,6 @@ "repeatingPlans": "Ponavljajući planovi", "selectAll": "Odaberi sve", "strategyDisabledDescription": "Punjenje počinje što kasnije kako bi se završilo upravo na vrijeme za polazak. Uz dinamične cijene mreže ili CO₂ tarifu ovdje su dostupne dodatne opcije.", - "strategySettings": "Postavke strategije", "time": "Vrijeme", "title": "Plan", "unsavedChanges": "Postoje promjene koje nisu spremljene. Spremiti odmah?", diff --git a/i18n/hu.json b/i18n/hu.json index edae2dd2a..541ab4978 100644 --- a/i18n/hu.json +++ b/i18n/hu.json @@ -858,7 +858,6 @@ }, "planNumber": "Terv {number}", "precondition": { - "description": "Töltse {duration} indulás előtt az akkumulátor előkondícionálásához.", "label": "Késleltetett Töltés", "optionAll": "minden", "optionNo": "nem" @@ -868,7 +867,6 @@ "repeatingPlans": "Ismétlődő tervek", "selectAll": "Összes kiválasztása", "strategyDisabledDescription": "A töltés a lehető legkésőbb kezdődik, hogy pontosan az indulás előtt befejeződjön. Dinamikus hálózati árak vagy CO₂-tarifa esetén itt több lehetőség áll rendelkezésre.", - "strategySettings": "Stratégiai beállítások", "time": "Idő", "title": "Terv", "unsavedChanges": "Vannak nem mentett módosítások. Alkalmazza most?", diff --git a/i18n/it.json b/i18n/it.json index f9b0fefb0..31de67d2c 100644 --- a/i18n/it.json +++ b/i18n/it.json @@ -25,11 +25,13 @@ "discharging": "Scarica", "energy": "Energia", "gridCharge": "Carica da rete", - "holdCharge": "Ricarica chiusa", + "hold": "Scarica inibita", + "holdCharge": "Ricarica inibita", "ofTotal": "di {total}", "power": "Potenza batteria", "soc": "% batteria" }, + "combined": "Combinata", "config": { "buffer": "L'energia accumulata nella batteria domestica al di sopra del valore {soc} può essere utilizzata per alimentare i punti di ricarica e ritardare lo spegnimento in modalità solare. Avvia automaticamente la ricarica solare {start}.", "bufferNone": "L'energia della batteria domestica non viene utilizzata per i punti di ricarica. Impostare un valore inferiore a {soc} per abilitare il supporto.", @@ -40,6 +42,7 @@ }, "bufferTitle": "Batteria come tampone di carica", "discharge": "Prevenire la scarica della batteria domestica in modalità rapida e durante la ricarica programmata.", + "gridDischarge": "Consenti alla batteria domestica di fornire energia alla rete.", "priority": "La batteria domestica ha la priorità finché il suo livello di carica è inferiore a {soc}. Successivamente, le colonnine di ricarica ricevono l'energia in eccesso.", "priorityNone": "L'energia solare in eccesso viene inviata direttamente ai punti di ricarica. Imposta un valore superiore a {soc} per dare priorità alla batteria domestica.", "priorityTitle": "Dove va a finire prima il surplus?" @@ -49,11 +52,14 @@ "charge": "Carica dalla rete", "discharge": "Scarico in rete", "hold": "Previeni scarica batteria", + "holdcharge": "Impedisci la ricarica", "normal": "Funzionamento normale" }, "empty": "Vuoto", "full": "pieno", - "highest": "Più alto ({value})" + "highest": "Più alto ({value})", + "lowest": "Più basso ({value})", + "suggestion": "Suggerimento" } }, "batterySettings": { @@ -76,17 +82,45 @@ "legendTopAutostart": "Parti automaticamente", "legendTopName": "Ricarica del veicolo assistita dalla batteria", "legendTopSubline": "quando la batteria supera il {soc}.", + "legendTopSublineAbove": "quando sopra il {soc}", + "legendTopSublineDisabled": "è {soc}.", + "legendTopSublineDisabledState": "disabilitato", "modalTitle": "Batteria di casa", "noBattery": "Nessuna batteria configurata.", "usageTab": "Uso della batteria" }, + "colors": { + "auto": "Automatico", + "hex": "Codice colore esadecimale" + }, "config": { + "adminPassword": { + "description": "Lo script plugin esegue un comando a livello di sistema con i permessi del processo evcc. Reinserisci la password di amministratore per continuare.", + "invalid": "Password non valida. Per favore ritenta.", + "title": "Operazione potenzialmente danno" + }, + "apiKey": { + "description": "Consenti a script e attività di automazione, come i backup programmati, di accedere in modo sicuro senza dover condividere la password di amministratore.", + "exampleLabel": "Provalo: scarica un backup con curl", + "generate": "Genera chiave API", + "generateConfirm": "Inserisci la tua password di amministratore per generare una nuova chiave API.", + "keyLabel": "Chiave API", + "regenerate": "Rigenera chiave API", + "regenerateConfirm": "Inserisci la tua password di amministratore per rigenerare la chiave API.", + "regenerateLink": "Rigenera", + "regenerateWarning": "La rigenerazione invaliderà le chiavi esistenti e tutte le automazioni che le utilizzano cesserano di funzionare. Continuare?", + "revealSuccess": "La tua nuova chiave API è pronta per essere usata.", + "revealTitle": "Chiave API creata", + "shownOnce": "Questa chiave è mostrata una sola volta. Conservala in un luogo sicuro. Non potrai recuperarla in seguito.", + "title": "Chiave API" + }, "aux": { "description": "Dispositivo che modifica il suo consumo sulla base del surplus di produzione disponibile (es: boiler smart). evcc si aspetta che questo dispositivo riduca il suo consumo se richiesto.", "titleAdd": "Aggiungi consumatore autoregolato", "titleEdit": "Modifica dispositivo con consumo auto-regolato" }, "battery": { + "title": "Batteria", "titleAdd": "Aggiungi Batteria", "titleEdit": "Modifica Batteria" }, @@ -135,6 +169,11 @@ "title": "Gestione del carico", "usableMeters": "Riferimenti dei contatori utilizzabili" }, + "consumer": { + "description": "Monitora elettrodomestici non controllabili (frigorifero, lavatrice, ecc.). Ti offre una migliore panoramica dei consumi domestici.", + "titleAdd": "Aggiungi contatore di consumo", + "titleEdit": "Modifica contatore di consumo" + }, "control": { "description": "Normalmente le impostazioni di default sono adeguate. Modificale solo se sai cosa stai facendo.", "descriptionInterval": "Ciclo di aggiornamento in secondi. Definisce la frequenza con cui evcc legge i dati del contatore e regola la ricarica. L'impostazione predefinita di 30 secondi è una scelta sicura. Dispositivi come veicoli, wallbox e inverter richiedono in genere diversi secondi per regolare il loro comportamento. Se i componenti reagiscono rapidamente, è possibile utilizzare valori inferiori. Si consiglia vivamente di non scendere al di sotto dei 10 secondi. Se si osserva un comportamento di controllo irregolare o valori di potenza instabili, scegliere un intervallo maggiore.", @@ -145,13 +184,14 @@ }, "currency": { "description": "Utilizzato per formattare i prezzi dell'energia, i costi e i risparmi in base alla tua tariffa.", - "example": "Il prezzo della ricarica era {price}. Hai risparmiato {amount}.", + "example": "Il prezzo della ricarica è stato {price}. Hai risparmiato {amount}.", "label": "Valuta", "title": "Valuta" }, "deviceValue": { "activeClients": "Client attivi", "amount": "Quantità", + "asleep": "inattivo", "broker": "Broker", "bucket": "Bucket", "capacity": "Capacità", @@ -170,12 +210,16 @@ "currency": "Valuta", "current": "Corrente", "currentRange": "Corrente", + "curtailLimit": "Limite di immissione", + "curtailable": "Limitabile", "curtailed": "Alimentazione limitata", "detected": "Rilevato", + "dimLimit": "Limite di consumo", "dimmed": "Consumi limitati", "enabled": "Abilitato", "energy": "Energia", "events": "Stati", + "exportLimit": "Limite esportazione in rete", "feedinPrice": "Prezzo di vendita", "forecast": "Previsione", "gridPrice": "Prezzo d'acquisto", @@ -187,6 +231,7 @@ "no": "no", "odometer": "Chilometraggio", "org": "Organizzazione", + "outdoorTemp": "Temperatura esterna", "phaseCurrents": "Corrente", "phasePowers": "Potenza", "phaseVoltages": "Voltaggio", @@ -194,7 +239,10 @@ "power": "Potenza", "powerRange": "Potenza", "price": "Prezzo", + "productionLimited": "Produzione limitata", "range": "Intervallo", + "remoteEnabled": "Abilitato", + "returnEnergy": "Energia (inversa)", "singlePhase": "Singola fase", "soc": "Carica", "solarForecast": "Previsioni solari", @@ -224,13 +272,21 @@ "public": "Certificato pubblico", "title": "Certificati" }, + "configuredDevices": "Dispositivi configurati", "description": "Configurazione che consente a evcc di comunicare con dispositivi compatibili con EEBus, come i caricabatterie o un'unità di controllo del gestore della rete. Tutte le inizializzazioni e la generazione dei certificati necessari vengono eseguite automaticamente al primo avvio.", "descriptionAdvanced": "Non sono necessarie modifiche. Apportate modifiche solo se sapete esattamente cosa state facendo. Se modificate lo SHIP-id o i certificati, dovrete associare nuovamente i dispositivi.", "interfaces": "Interfacce", "interfacesHelp": "Limita le interfacce di rete che EEBus deve utilizzare per evitare problemi di comunicazione. Lascia il campo vuoto per utilizzare tutte le interfacce. Un solo valore per riga.", + "noConfiguredDevices": "Nessun dispositivo configurato da SKI.", + "noPairings": "Nessun dispositivo associato.", + "pairings": "Dispositivi associati", "port": "Porta", "portHelp": "La porta da usare.", + "qr": "Codice di abbinamento", + "qrExplain": "Esegui la scansione da un altro dispositivo EEBus durante l'installazione per associare automaticamente il dispositivo, senza dover inserire manualmente l'identificativo dell'apparecchio SKI.", "removeConfirm": "Tutte le configurazioni di EEBus verranno rimosse. Al prossimo avvio verranno generati nuovi certificati e identificativi. Sei sicuro?", + "removePairing": "Rimuovi associazione", + "removePairingConfirm": "Il dispositivo sarà disconnesso e dovrà essere associato nuovamente. Sei sicuro?", "shipid": "SHIP-ID", "shipidExplain": "Identificativo permanente del dispositivo per l'identificazione nella rete EEBus.", "shipidHelp": "Questo SHIP-ID è collegato ai certificati riportati di seguito.", @@ -250,6 +306,7 @@ "form": { "danger": "Pericolo", "deprecated": "deprecato", + "durationUnit": "Unità di tempo per {label}", "example": "Esempio", "optional": "facoltativo" }, @@ -303,6 +360,7 @@ "titleEdit": "Modifica contatore di rete" }, "hems": { + "changeConfirm": "La configurazione esistente sarà eliminata. Continuare?", "csv": { "created": "Creato", "finished": "Finito", @@ -310,10 +368,26 @@ "limitpower": "Limite (kW)", "type": "Tipo" }, + "customOption": "Integrazione definite dall'utente", "description": "Limitazione della potenza da parte di sistemi esterni (ad es. §14a EnWG, interfaccia §9 EEG o sistema di gestione dell'energia di livello superiore). Funziona in combinazione con la funzione di gestione del carico.", + "download": "Scarica", + "dynamicLimits": "Limiti dinamici", "eventsRecorded": "Registrati {count} eventi di limitazione di prelievo dalla rete.", + "exportLimit": "Limite di immissione in rete", + "exportLimitDescription": "L'ottimizzatore della batteria rispetta questo limite per evitare perdite solari. Un segnale di limitazione attivo derivante da un'integrazione con limite dinamico ha la precedenza.", + "exportLimitHint": "Limite statico di immissione in rete al punto di connessione.", + "generic": "Integrazioni generiche", + "integrations": "Integrazioni", "lastEvent": "Più recente {timeAgo}.", - "title": "Limite prelievo da rete" + "noEvents": "Nessun evento registrato finora.", + "recordedEvents": "Eventi registrati", + "staticLimits": "Limiti statici", + "template": "Integrazione", + "title": "Limite prelievo da rete", + "type": { + "custom": "Integrazioni definite dall'utente", + "relay": "Integrazione definita dall'utente (relè)" + } }, "icon": { "change": "cambia", @@ -341,6 +415,7 @@ "heating": "Aggiungi riscaldatore" }, "addMeter": "Aggiungi contatore energetico dedicato", + "advancedConfig": "Configurazione avanzata", "cancel": "Annulla", "chargerError": { "charging": "È necessario configurare un caricatore.", @@ -362,11 +437,21 @@ "circuitInvalid": "Il circuito non esiste", "circuitLabel": "Circuito", "circuitUnassigned": "non assegnato", + "created": "“{name}” è stato creato e utilizza impostazioni predefinite di buon senso.", + "createdTitle": { + "charging": "Punto di ricarica creato", + "heating": "Riscaldatore creato" + }, + "creating": { + "charging": "Caricabatterie salvato. Creazione del punto di ricarica in corso…", + "heating": "Dispositivo di riscaldamento salvato. Creazione del riscaldatore in corso…" + }, "defaultModeHelp": { "charging": "Modalità di ricarica quando si collega il veicolo.", "heating": "Viene impostato all'avvio del sistema." }, "defaultModeHelpKeep": "Mantiene l'ultima modalità selezionata.", + "defaultModeKeep": "mantieni com'è", "defaultModeLabel": "Modalità predefinita", "delete": "Elimina", "electricalSubtitle": "Se non sei sicuro, chiedi al tuo elettricista.", @@ -376,8 +461,11 @@ "estimateLabel": "Stima il livello di carica tra gli aggiornamenti dell'API", "maxCurrentHelp": "Deve essere maggiore della corrente minima.", "maxCurrentLabel": "Corrente massima", + "maxTempHelp": "Deve essere maggiore della temperatura minima.", + "maxTempLabel": "Temperature massima", "minCurrentHelp": "Scendere al di sotto dei 6A solo se si sa quello che si sta facendo.", "minCurrentLabel": "Corrente minima", + "minTempLabel": "Temperatura minima", "noVehicles": "Nessun veicolo configurato.", "option": { "charging": "Aggiungi punto di ricarica", @@ -1126,7 +1214,6 @@ }, "planNumber": "Piano {number}", "precondition": { - "description": "Carica {duration} prima della partenza per pre-condizionare la batteria.", "label": "Carica ritardata", "optionAll": "tutto", "optionNo": "no" @@ -1136,7 +1223,6 @@ "repeatingPlans": "Piani ricorrenti", "selectAll": "Seleziona tutti", "strategyDisabledDescription": "La ricarica inizia il più tardi possibile per terminare appena in tempo per la partenza. Con i prezzi dinamici della rete elettrica o la tariffa CO₂, sono disponibili ulteriori opzioni.", - "strategySettings": "Impostazioni strategia", "time": "Ora", "title": "Piano", "unsavedChanges": "Ci sono modifiche non salvate. Vuoi salvarle adesso?", diff --git a/i18n/ja.json b/i18n/ja.json index 55186e39d..869f54a90 100644 --- a/i18n/ja.json +++ b/i18n/ja.json @@ -1101,7 +1101,6 @@ }, "planNumber": "プラン {number}", "precondition": { - "description": "バッテリーのプレコンディショニングのため、出発の {duration} 前から充電します。", "label": "遅延充電", "optionAll": "すべて", "optionNo": "いいえ" @@ -1111,7 +1110,6 @@ "repeatingPlans": "繰り返しプラン", "selectAll": "すべて選択", "strategyDisabledDescription": "出発時刻に間に合うよう、できるだけ遅い時間帯に充電を開始します。市場連動型の料金プランやCO₂データが設定されている場合、さらに高度なオプションが利用可能になります。", - "strategySettings": "戦略設定", "time": "時間", "title": "プラン", "unsavedChanges": "保存されていない変更があります。今すぐ適用しますか?", diff --git a/i18n/lb.json b/i18n/lb.json index fbbda591f..02bda632a 100644 --- a/i18n/lb.json +++ b/i18n/lb.json @@ -219,6 +219,7 @@ "enabled": "Ageschalt", "energy": "Energie", "events": "Evenementer", + "exportLimit": "Limitt vum Netz-Export", "feedinPrice": "Präis fir anzespeisen", "forecast": "Previsioun", "gridPrice": "Netz Präis", @@ -306,6 +307,7 @@ "form": { "danger": "Oppassen", "deprecated": "obsolet", + "durationUnit": "Zäiteenheet fir {label}", "example": "Beispill", "optional": "optionell" }, @@ -371,12 +373,17 @@ "customOption": "Benotzerdefinéiert Integratioun", "description": "Kraaftbegrenzung duerch extern Systemer (z. B. §14a EnWG, §9 EEG-Schnittstell oder e méi héichgestallt Energiemanagementsystem). Arbecht zesumme mat der Laaschtmanagement-Funktioun. D'Begrenzung vun der Solarproduktioun gëtt och fir ausgewielte Geräter ënnerstëtzt. Den tatsächleche Begrenzungswäert muss an der Konfiguratioun vun all Inverter festgeluecht ginn.", "download": "Eroflueden", + "dynamicLimits": "Dynamesch Limitten", "eventsRecorded": "{count} gespäichert Limitatiounen vum Netz.", + "exportLimit": "Limitt vum Netz-Export", + "exportLimitDescription": "De Batterieoptimiséierer respektéiert dës Limitte fir Solarveeloschter ze vermeiden. En aktiven Ofschalt-Signal aus enger dynamescher Limitt-Integratioun huet Prioritéit.", + "exportLimitHint": "Statesche Einspeise-Limitt um Netzuschlosspunkt.", "generic": "Generesch Integratiounen", "integrations": "Integratiounen", "lastEvent": "Méi rezent {timeAgo}.", "noEvents": "Nach keng Evenementer opgezeechent.", "recordedEvents": "Opgezeechent Evenementer", + "staticLimits": "Statesch Limitte", "template": "Integratioun", "title": "Äusserlech Grenz", "type": { @@ -1094,6 +1101,8 @@ "version": { "availableLong": "nei Versioun verfügbar", "community": "evcc Communautéit", + "labelEmail": "E-Mail", + "labelPhone": "Telefon", "labelRelease": "Verëffentlechung", "labelVersion": "Versioun", "labelWebsite": "Websäit", @@ -1176,6 +1185,7 @@ "sessions": "Opluedsessiounen" }, "help": { + "contactButton": "Kontaktéiert eis", "discussionsButton": "GitHub Diskussiounen", "documentationButton": "Dokumentatioun", "issueButton": "Problem mellen", @@ -1215,8 +1225,11 @@ "additionalContext": "Zousätzleche Kontext", "additionalContextPlaceholder": "All zousätzlech Informatiounen, déi hëllefräich kéinte sinn...\n- Konfiguratiounsdetailer\n- Wat Dir probéiert hutt\n- Ëmfelddetailer", "createButtonDiscussion": "GitHub Diskussioun starten...", + "createButtonEmail": "Ufro per E-Mail schécken", "createButtonIssue": "GitHub Issue erstellen...", "description": "Funktionéiert Är Installatioun net wéi erwaart? Benotzt dës Säit fir Hëllef ze kréien oder Problemer ze mellen. Gitt genuch Detailer fir eis ze hëllefen de Problem ze verstoen a reproduzéieren, wärend Är Beschreiwung präzis, kloer an einfach ze verstoen ass.", + "downloadButton": "Debug-Informatioun eroflueden", + "downloadHint": "Füügt dës Datei bei Är E-Mail derbäi, wa méi Informatiounen néideg sinn.", "helpType": { "discussion": "Brauch Hëllef mat menger Installatioun", "discussionDescription": "Gemeinschaftsdiskussiounen liwweren Äntwerten.", @@ -1228,6 +1241,7 @@ "issueTitle": "Titel", "stepsToReproduce": "Schrëtt fir de Problem ze reproduzéieren", "subTitleDiscussion": "Beschreif däi Problem", + "subTitleEmail": "Beschreif däi Problem", "subTitleIssue": "Beschreif de Problem", "summary": { "confirmationButtonDiscussion": "GitHub Diskussioun starten", @@ -1280,24 +1294,33 @@ "goal": "Luedzil", "modalTitle": "Luedplanifikatioun", "none": "keng", + "once": "eemol", "optimization": { "cheapest": "am bëllegsten", + "cheapestDescription": "Wielt déi bëllegst Slots aus. Kann zu méi Starten a Stoppe féieren.", + "cleanest": "am proppersten", + "cleanestDescription": "Wielt déi propperst Slots aus. Kann zu méi Starts a Stops féieren.", "continuous": "kontinuéierlech", + "continuousCo2Description": "Wielt déi propperest onënnerbrach Serie vu Slots.", + "continuousPriceDescription": "Wielt déi bëllegst onënnerbrach Serie vu Slots aus.", "label": "Optimisatioun" }, "planNumber": "Planifkatioun {number}", "precondition": { - "description": "Luetzäit {duration} virum Fortfuere fir Batterie-Prekonditionéierung.", + "description": "Verleeë e Deel vum Opluede direkt virum Depart: waarm Batterie, manner Zäit op engem héijen Opluede-Niveau.", "label": "Verspéit Lueden", "optionAll": "Alles", - "optionNo": "Nee" + "optionNo": "Nee", + "summary": "{precondition} virdrun" }, "remove": "Ewechhuelen", "repeating": "widderhuelend", "repeatingPlans": "Widderhuelend Planifikatiounen", "selectAll": "All auswielen", + "strategy": { + "label": "Strategie" + }, "strategyDisabledDescription": "D'Laden fänkt sou spéit wéi méiglech un, fir grad rechtzäiteg virum Depart ofzeschléissen. Mat dynamesche Netzpräisser oder engem CO₂-Tarif stinn hei nach méi Optiounen zur Verfügung.", - "strategySettings": "Strategie-Astellungen", "time": "Zäit", "title": "Planifikatioun", "unsavedChanges": "Net gespäichert Ännerungen leie vir. Elo uwennen?", @@ -1582,6 +1605,7 @@ }, "session": { "addOdometer": "Wäert derbäisetzen", + "addedRange": "Zousätzlech Autonomie", "cancel": "Ofbriechen", "co2": "CO₂", "date": "Zäitraum", @@ -1592,11 +1616,12 @@ "meterstop": "Last Meter Liesung", "odometer": "Kilometerzähler", "price": "Käschten", - "soc": "Gefier-SoC", + "soc": "Ladung", "started": "Startzeit", "title": "Charging Sessioun" }, "sessions": { + "addedRange": "Autonomie", "avgPower": "⌀ Leeschtung", "avgPrice": "⌀ Präis", "chargeDuration": "Oplueddauer", @@ -1622,6 +1647,7 @@ }, "co2": "⌀ CO₂", "csv": { + "addedrange": "Zousätzlech Distanz (km)", "chargedenergy": "Energie (kWh)", "chargeduration": "Oplueddauer", "co2perkwh": "CO₂/kWh", diff --git a/i18n/lt.json b/i18n/lt.json index 7d887ed4c..62a4fe247 100644 --- a/i18n/lt.json +++ b/i18n/lt.json @@ -120,8 +120,6 @@ "titleEdit": "Redaguoti Savireguliuojantį vartotoją" }, "battery": { - "description": "Eksperimentiniai kaupiklio valdikliai. Šios funkcijos vis dar testuojamos ir gali būti pakeistos arba pašalintos būsimose versijose.", - "gridDischarge": "Leiskite kaupikliui išsikrauti į tinklą", "title": "Kaupiklis", "titleAdd": "Pridėti kaupiklį", "titleEdit": "Redaguoti kaupiklį" @@ -221,6 +219,7 @@ "enabled": "Aktyvuotas", "energy": "Energija (paimta)", "events": "Įvykiai", + "exportLimit": "Atidavimo į tinklą limitas", "feedinPrice": "Tiekimo į tinklą kaina", "forecast": "Prognozė", "gridPrice": "Energijos kaina", @@ -308,6 +307,7 @@ "form": { "danger": "Pavojus", "deprecated": "nebenaudojama", + "durationUnit": "Laiko vienetas, skirtas {label}", "example": "Pavyzdys", "optional": "pasirinktinai" }, @@ -320,17 +320,22 @@ "change": "Pakeisti", "clear": "Išvalyti", "close": "Uždaryti", + "confirmDisable": "Išjungti šį įrenginį? Jis bus ignoruojamas po evcc paleidimo iš naujo.", + "confirmEnable": "Vėl įjungti šį įrenginį?", "confirmSave": "Yra neišsaugotų pakeitimų. Išsaugoti dabar ?", "copied": "Nukopijuota!", "copy": "Kopijuoti", "customHelp": "Sukurkite vartotojo apibrėžtą įrenginį naudodami evcc įskiepių sistemą.", "customOption": "Vartotojo apibrėžtas įrenginys", "delete": "Ištrinti", + "disable": "Išjungti", + "disabled": "Išjungta", "dismiss": "Atmesti", "docsLink": "Žiūrėkite dokumentaciją.", "dragHandle": "Vilkti", "dragItem": "Galima vilkti: {title}", "dragList": "Galima sukeisti vietomis", + "enable": "Įjungti", "error": "Klaida", "experimental": "Eksperimentinės", "forceSave": "Vistiek įrašyti", @@ -373,12 +378,17 @@ "customOption": "Vartotojo sukurta integracija", "description": "Galios ribojimas išorinėmis sistemomis (pvz., §14a EnWG, §9 EEG sąsaja arba aukštesnio lygio energijos valdymo sistema). Veikia kartu su apkrovos valdymo funkcija. Kai kuriems inverteriams taip pat galima apriboti gamybos galią, ji nustatoma kiekvieno inverterio konfigūracijoje.", "download": "Atsisiųsti", + "dynamicLimits": "Dinaminės ribos", "eventsRecorded": "Užfiksuota {count} tinklo galios apribojimo įvykių.", + "exportLimit": "Tinklo eksporto limitas", + "exportLimitDescription": "Kaupiklio optimizatorius laikosi šios ribos, kad išvengtų saulės nuostolių. Pirmenybė teikiama aktyviam ribojimo signalui iš dinaminės ribos integravimo.", + "exportLimitHint": "Tinklo eksporto galios riba.", "generic": "Standartinės integracijos", "integrations": "Integracijos", "lastEvent": "Naujausias {timeAgo}.", "noEvents": "Kol kas neužfiksuota jokių įvykių.", "recordedEvents": "Įrašyti įvykiai", + "staticLimits": "Statinės ribos", "template": "Integracija", "title": "Išorinė riba", "type": { @@ -832,7 +842,7 @@ "general": "Pagrindiniai", "grid": "Elektros tinklas", "integrations": "Integracijos", - "loadpoints": "Įkrovikliai ir Šildytuvai", + "loadpoints": "Įkrovikliai ir šildytuvai", "meter": "Saulės elektrinės ir kaupikliai", "services": "Paslaugos", "system": "Sistema", @@ -967,7 +977,7 @@ "titleEdit": "Redaguoti Temperatūros Prognozę" }, "template": "Tiekėjas", - "title": "Tarifai ir Prognozės", + "title": "Tarifai ir prognozės", "titleChoice": "Ką norite pridėti?", "type": { "co2": "CO₂ Intensyvumas", @@ -1096,6 +1106,8 @@ "version": { "availableLong": "(yra naujesnė versija)", "community": "evcc bendruomenė", + "labelEmail": "El. paštas", + "labelPhone": "Telefonas", "labelRelease": "Leidimas", "labelVersion": "Versija", "labelWebsite": "Svetainė", @@ -1163,9 +1175,12 @@ } }, "general": { + "back": "Atgal", "download": "Atsisiųsti", + "error": "Klaida", "none": "nė vienas", - "note": "Pastaba:" + "note": "Pastaba:", + "warning": "Perspėjimas" }, "header": { "about": "Apie", @@ -1178,6 +1193,7 @@ "sessions": "Įkrovimo Sesijos" }, "help": { + "contactButton": "Susisiekite su mumis", "discussionsButton": "GitHub diskusijos", "documentationButton": "Dokumentacija", "issueButton": "Pranešti apie problemą", @@ -1217,8 +1233,11 @@ "additionalContext": "Papildomas kontekstas", "additionalContextPlaceholder": "Bet kokia papildoma informacija, kuri galėtų būti naudinga...\n- Konfigūracijos informacija\n- Ką bandėte\n- Sistemos informacija", "createButtonDiscussion": "Pradėti GitHub diskusiją...", + "createButtonEmail": "Siųsti užklausą el. paštu", "createButtonIssue": "Sukurti GitHub pranešimą apie problemą...", "description": "Jūsų diegimas neveikia taip, kaip tikėtasi? Šiame puslapyje galite gauti pagalbos arba pranešti apie problemas. Pateikite pakankamai informacijos, kad galėtume suprasti ir atkartoti problemą, o aprašymas turi būti glaustas, aiškus ir lengvai suprantamas.", + "downloadButton": "Atsisiųskite klaidų šalinimo informaciją", + "downloadHint": "Pridėkite šį failą prie savo el. laiško, jei reikia daugiau informacijos.", "helpType": { "discussion": "Reikia pagalbos", "discussionDescription": "Bendruomenės diskusijose randami atsakymai.", @@ -1230,6 +1249,7 @@ "issueTitle": "Pavadinimas", "stepsToReproduce": "Žingsniai atkartojimui", "subTitleDiscussion": "Aprašykite savo problemą", + "subTitleEmail": "Apibūdinkite problemą", "subTitleIssue": "Aprašykite nesklandumus", "summary": { "confirmationButtonDiscussion": "Pradėti diskusiją GitHub", @@ -1282,24 +1302,33 @@ "goal": "Įkrovimo tikslas", "modalTitle": "Įkrovimo planas", "none": "nėra", + "once": "vieną kartą", "optimization": { "cheapest": "pigiausias", + "cheapestDescription": "Parenka pigiausius laikotarpius. Gali būti daugiau paleidimų ir sustojimų.", + "cleanest": "švariausia", + "cleanestDescription": "Parenka švariausius laikotarpius. Gali būti daugiau paleidimų ir sustojimų.", "continuous": "be pertraukos", + "continuousCo2Description": "Parenka švariausią nepertraukiamą laikotarpių seriją.", + "continuousPriceDescription": "Pasirenkama pigiausia ištisinė laikotarpių serija.", "label": "Optimizacija" }, "planNumber": "Planas {number}", "precondition": { - "description": "Įkrovimas {duration} prieš išvykstant, baterijos paruošimui.", + "description": "Dalis įkrovimo perkeliama prieš pat išvykstant: pašyla baterija, trumpiau būna su aukštu įkrovimo lygiu.", "label": "Vėlyvas įkrovimas", "optionAll": "viskas", - "optionNo": "ne" + "optionNo": "ne", + "summary": "{precondition} prieš" }, "remove": "Pašalinti", "repeating": "kartojasi", "repeatingPlans": "Pasikartojantys planai", "selectAll": "Pasirinkti visus", + "strategy": { + "label": "Strategija" + }, "strategyDisabledDescription": "Įkrovimas pradedamas kuo vėliau, kad būtų baigtas prieš pat išvykimą. Jei naudojate 15 minučių biržos planą (arba CO₂ tarifą), čia galite rinktis iš daugiau variantų.", - "strategySettings": "Strategijos pasirinkimai", "time": "Laikas", "title": "Planas", "unsavedChanges": "Yra neišsaugotų pakeitimų. Išsaugoti dabar?", @@ -1510,6 +1539,7 @@ "keepAsIs": "Palikti kaip yra", "learnMore": "Sužinoti daugiau.", "limitSoc": "Numatytasis limitas", + "limitSocDescription": "Taikoma, kai prijungiamas automobilis.", "menu": "Automobiliai", "minSoc": "Minimali įkrova", "minSocDescription": "Greitas įkrovimas iki šio lygio saulės režime.", @@ -1584,6 +1614,7 @@ }, "session": { "addOdometer": "Įrašyti skaičių", + "addedRange": "Įkrautas atstumas", "cancel": "Atšaukti", "co2": "CO₂", "date": "Periodas", @@ -1594,11 +1625,12 @@ "meterstop": "Skaitiklis pabaiga", "odometer": "Odometras", "price": "Išlaidos", - "soc": "Automobilio įkrova", + "soc": "Įkrova", "started": "Pradėta", "title": "Įkrovimo sesija" }, "sessions": { + "addedRange": "Atstumas", "avgPower": "⌀ Galia", "avgPrice": "⌀ Kaina", "chargeDuration": "Trukmė", @@ -1624,6 +1656,7 @@ }, "co2": "⌀ CO₂", "csv": { + "addedrange": "Įkrautas atstumas (km)", "chargedenergy": "Energija (kWh)", "chargeduration": "Trukmė", "co2perkwh": "CO₂/kWh", @@ -1700,6 +1733,7 @@ "label": "Kalba" }, "loadpoints": { + "disabledDevice": "išjungtas", "help": "Pakeiskite vartotojo sąsajos išdėstymą ir matomumą.", "hide": "Paslėpti {title}", "label": "Įkrovikliai", diff --git a/i18n/lv.json b/i18n/lv.json index 15648985b..81f0e68dc 100644 --- a/i18n/lv.json +++ b/i18n/lv.json @@ -120,8 +120,6 @@ "titleEdit": "Rediģēt pašregulējošu patērētāju" }, "battery": { - "description": "Eksperimentāla akumulatora vadība. Šīs funkcijas vēl tiek testētas un var tikt mainītas vai noņemtas nākamajās versijās.", - "gridDischarge": "Atļaut akumulatora izlādi tīklā", "title": "Akumulators", "titleAdd": "Pievienot akumulatoru", "titleEdit": "Rediģēt akumulatoru" @@ -221,6 +219,7 @@ "enabled": "Iespējots", "energy": "Enerģija", "events": "Notikumi", + "exportLimit": "Tīkla atdeves limits", "feedinPrice": "Atdeves cena", "forecast": "Prognoze", "gridPrice": "Tīkla cena", @@ -308,6 +307,7 @@ "form": { "danger": "Bīstami", "deprecated": "novecojis", + "durationUnit": "Laika vienība vienumam {label}", "example": "Piemērs", "optional": "neobligāti" }, @@ -320,17 +320,22 @@ "change": "Mainīt", "clear": "Notīrīt", "close": "Aizvērt", + "confirmDisable": "Atspējot šo ierīci? Tā tiks ignorēta pēc nākamās restartēšanas.", + "confirmEnable": "Vai atkal iespējot šo ierīci?", "confirmSave": "Ir nesaglabātas izmaiņas. Saglabāt tagad?", "copied": "Nokopēts!", "copy": "Kopēt", "customHelp": "Izveidot lietotāja definētu ierīci, izmantojot evcc spraudņu sistēmu.", "customOption": "Lietotāja definēta ierīce", "delete": "Dzēst", + "disable": "Atspējot", + "disabled": "atspējots", "dismiss": "Noraidīt", "docsLink": "Skatīt dokumentāciju.", "dragHandle": "Vilkt", "dragItem": "Velkams: {title}", "dragList": "Pārkārtojams saraksts", + "enable": "Iespējot", "error": "Kļūda", "experimental": "Eksperimentāla versija", "forceSave": "Saglabāt tik un tā", @@ -373,12 +378,17 @@ "customOption": "Lietotāja definēta integrācija", "description": "Jaudas ierobežošana no ārējām sistēmām (piem. §14a EnWG §9 EEG saskarne vai augstāka līmeņa enerģijas pārvaldības sistēma). Darbojas kopā ar slodzes pārvaldības funkciju. Saules ražošanas ierobežošana ir pieejama arī atlasītām ierīcēm. Faktiskais atdeves ierobežojums jāiestata katrā invertora konfigurācijā.", "download": "Lejupielādēt", + "dynamicLimits": "Dinamiskie ierobežojumi", "eventsRecorded": "Saglabāti {count} tīkla ierobežošanas notikumi.", + "exportLimit": "Atdeves tīklā ierobežojums", + "exportLimitDescription": "Akumulatora optimizētājs ievēro šo ierobežojumu, lai novērstu saules enerģijas zudumus. Aktīvs ierobežošanas signāls no dinamisko ierobežojumu integrācijas ir prioritārs.", + "exportLimitHint": "Statisks atdeves ierobežojums pieslēguma punktā tīklam.", "generic": "Vispārīgās integrācijas", "integrations": "Integrācijas", "lastEvent": "Pēdējais pirms {timeAgo}.", "noEvents": "Vēl nav reģistrēts neviens notikums.", "recordedEvents": "Reģistrētie notikumi", + "staticLimits": "Statiskie ierobežojumi", "template": "Integrācija", "title": "Ārējais ierobežojums", "type": { @@ -1096,6 +1106,8 @@ "version": { "availableLong": "pieejama jauna versija", "community": "evcc kopiena", + "labelEmail": "E-pasts", + "labelPhone": "Tālrunis", "labelRelease": "Laidiens", "labelVersion": "Versija", "labelWebsite": "Tīmekļa vietne", @@ -1163,9 +1175,12 @@ } }, "general": { + "back": "Atpakaļ", "download": "Lejupielādēt", + "error": "Kļūda", "none": "nav", - "note": "Piezīme:" + "note": "Piezīme:", + "warning": "Brīdinājums" }, "header": { "about": "Par", @@ -1178,6 +1193,7 @@ "sessions": "Uzlādes sesijas" }, "help": { + "contactButton": "Sazinieties ar mums", "discussionsButton": "GitHub diskusijas", "documentationButton": "Dokumentācija", "issueButton": "Ziņot par problēmu", @@ -1217,8 +1233,11 @@ "additionalContext": "Papildu konteksts", "additionalContextPlaceholder": "Jebkāda papildu informācija, kas varētu būt noderīga...\n- Konfigurācijas detaļas\n- Ko mēģinājāt\n- Vides informācija", "createButtonDiscussion": "Sākt GitHub diskusiju...", + "createButtonEmail": "Nosūtīt pieprasījumu e-pastā", "createButtonIssue": "Izveidot GitHub problēmu...", "description": "Jūsu instalācija nedarbojas kā paredzēts? Izmantojiet šo lapu, lai saņemtu palīdzību vai ziņotu par problēmām. Sniedziet pietiekami daudz detaļu, lai mēs varētu saprast un reproducēt problēmu, vienlaikus saglabājot aprakstu kodolīgu skaidru un viegli izsekojamu.", + "downloadButton": "Lejupielādēt atkļūdošanas informāciju", + "downloadHint": "Ja nepieciešama papildu informācija, pievienojiet šo datni savam e-pastam.", "helpType": { "discussion": "Nepieciešama palīdzība ar manu iestatīšanu", "discussionDescription": "Kopienas diskusijas sniedz atbildes.", @@ -1230,6 +1249,7 @@ "issueTitle": "Nosaukums", "stepsToReproduce": "Soļi reproducēšanai", "subTitleDiscussion": "Aprakstiet savu problēmu", + "subTitleEmail": "Aprakstiet savu problēmu", "subTitleIssue": "Aprakstiet problēmu", "summary": { "confirmationButtonDiscussion": "Sākt GitHub diskusiju", @@ -1282,24 +1302,33 @@ "goal": "Uzlādes mērķis", "modalTitle": "Uzlādes plāns", "none": "nav", + "once": "vienu reizi", "optimization": { "cheapest": "lētākais", + "cheapestDescription": "Atlasa vislētākos laika posmus. Var rasties vairākas uzlādes pauzes un atsākšanas.", + "cleanest": "vistīrākā", + "cleanestDescription": "Atlasa viszaļākos laika posmus. Var izraisīt vairākas uzlādes pārtraukumus.", "continuous": "nepārtraukts", + "continuousCo2Description": "Atlasa nepārtrauktu vistīrāko laika posmu virkni.", + "continuousPriceDescription": "Atlasa nepārtrauktu vislētāko laika posmu virkni.", "label": "Optimizācija" }, "planNumber": "Plāns {number}", "precondition": { - "description": "Uzlādēt {duration} pirms izbraukšanas akumulatora priekškondicionēšanai.", + "description": "Daļu no uzlādes veic tieši pirms izbraukšanas: slitāks akumulators, mazāk laika ar augstu uzlādes līmeni.", "label": "Vēlā uzlāde", "optionAll": "viss", - "optionNo": "nē" + "optionNo": "nē", + "summary": "{precondition} pirms" }, "remove": "Noņemt", "repeating": "atkārtojošs", "repeatingPlans": "Atkārtojošie plāni", "selectAll": "Izvēlēties visus", + "strategy": { + "label": "Stratēģija" + }, "strategyDisabledDescription": "Uzlāde sākas cik vien vēlu iespējams, lai pabeigtu tieši pirms izbraukšanas. Ar dinamiskiem tīkla tarifiem vai CO₂ tarifu šeit ir pieejamas papildus iespējas.", - "strategySettings": "Stratēģijas iestatījumi", "time": "Laiks", "title": "Plāns", "unsavedChanges": "Ir nesaglabātas izmaiņas. Piemērot tagad?", @@ -1510,6 +1539,7 @@ "keepAsIs": "Atstāt kā ir", "learnMore": "Uzzināt vairāk.", "limitSoc": "Noklusējuma ierobežojums", + "limitSocDescription": "Tiek piemērots, kad transportlīdzeklis ir pievienots.", "menu": "Transportlīdzekļi", "minSoc": "Minimālā uzlāde", "minSocDescription": "Ātrā uzlāde līdz šim līmenim saules režīmā.", @@ -1584,6 +1614,7 @@ }, "session": { "addOdometer": "Pievienot vērtību", + "addedRange": "Pievienotais nobraukums", "cancel": "Atcelt", "co2": "CO₂", "date": "Periods", @@ -1594,11 +1625,12 @@ "meterstop": "Skaitītāja beigas", "odometer": "Nobraukums", "price": "Izmaksas", - "soc": "Transp. uzlādes līmenis", + "soc": "Uzlāde", "started": "Sākts", "title": "Uzlādes sesija" }, "sessions": { + "addedRange": "Diapazons", "avgPower": "⌀ Jauda", "avgPrice": "⌀ Cena", "chargeDuration": "Ilgums", @@ -1624,6 +1656,7 @@ }, "co2": "⌀ CO₂", "csv": { + "addedrange": "Pievienotais nobraukums (km)", "chargedenergy": "Enerģija (kWh)", "chargeduration": "Ilgums", "co2perkwh": "CO₂/kWh", @@ -1700,6 +1733,7 @@ "label": "Valoda" }, "loadpoints": { + "disabledDevice": "atspējots", "help": "Mainīt secību un redzamību lietotāja saskarnē.", "hide": "Slēpt {title}", "label": "Uzlādes punkti", diff --git a/i18n/nl.json b/i18n/nl.json index 44317714a..9159383f9 100644 --- a/i18n/nl.json +++ b/i18n/nl.json @@ -45,14 +45,14 @@ "gridDischarge": "Sta ontladen van de batterij naar het net toe.", "priority": "Thuisbatterij heeft voorrang tot {soc} gehaald wordt. Na het behalen van het ingestelde percentage gaat het overschot naar de laadpunten.", "priorityNone": "Zon-overschot gaat rechtstreeks naar de laadpunten. Stel een waarde boven {soc} in om de thuisbatterij voorrang te geven.", - "priorityTitle": "Voorrangsregeling voor zon-overschot" + "priorityTitle": "Waar gaat het overschot als eerst heen?" }, "optimizer": { "action": { "charge": "Laden met netstroom", "discharge": "Ontladen naar het net", - "hold": "Verhinder ontladen", - "holdcharge": "Verhinder ontladen", + "hold": "Blokkeer ontladen", + "holdcharge": "Blokkeer laden", "normal": "Normale werking" }, "empty": "leeg", @@ -155,7 +155,7 @@ "type": { "custom": { "charging": "Gebruikersgedefinieerde lader", - "heating": "Gebruikersgedefinieerde verwarmer" + "heating": "Gebruikergedefinieerde verwarmer" }, "heatpump": "Gebruikersgedefinieerde warmtepomp", "sgready": "Gebruikersgedefinieerde warmtepomp (sg-ready via plugins)", @@ -299,13 +299,14 @@ "title": "Experimenteel" }, "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": "Verbruiksmeter toevoegen", - "titleEdit": "Verbruiksmeter wijzigen" + "description": "Registreert meters die niet gebruikt worden voor sturing of worden getoond in de UI (onderverdelers, pv-meters) voor statistieken. Deze data is gelogd en kan worden geëxporteerd zoals andere meters.", + "titleAdd": "Voeg extra meter toe", + "titleEdit": "Extra meter wijzigen" }, "form": { "danger": "Pas op", "deprecated": "verouderd", + "durationUnit": "tijdseenheid voor {label}", "example": "Voorbeeld", "optional": "optioneel" }, @@ -324,11 +325,14 @@ "customHelp": "Creëer een gebruiker-gedefinieerd apparaat met evcc’s plugin systeem.", "customOption": "Gebruiker-gedefinieerd apparaat", "delete": "Wissen", + "disable": "Uitschakelen", + "disabled": "uitgeschakeld", "dismiss": "Negeren", "docsLink": "Zie documentatie.", "dragHandle": "Schuifknop", "dragItem": "Verschuifbaar:{title}", "dragList": "Sorteerbare lijst", + "enable": "Inschakelen", "error": "Fout", "experimental": "Experimenteel", "forceSave": "Toch opslaan", @@ -369,13 +373,21 @@ "type": "Type" }, "customOption": "Door de gebruiker aangemaakte integratie", - "description": "Vermogensbeperking door externe systemen (bijv. §14a EnWG, §9 EEG-interface of hoger niveau energiebeheersysteem). Werkt samen met de functie voor Load Balancing.", + "description": "Vermogensbeperking door externe systemen (bijv. §14a EnWG, §9 EEG-interface of hoger niveau energiebeheersysteem). Werkt samen met de functie voor Load Balancing. Sommige meters ondersteunen ook curtailment van de zonne-energieopbrengst. Het curtailmentlimiet moet op elke omvormerconfiguratie ingesteld worden.", "download": "Download", + "dynamicLimits": "Dynamische limieten", "eventsRecorded": "{count} gebeurtenissen met rasterbeperkingen geregistreerd.", "generic": "Algemene integraties", "integrations": "integraties", "lastEvent": "Meest recent {timeAgo}.", - "title": "Externe limiet" + "noEvents": "Er zijn nog geen gebeurtenissen opgeslagen.", + "recordedEvents": "Opgeslagen gebeurtenissen", + "template": "Integratie", + "title": "Externe limiet", + "type": { + "custom": "Gebruikergedefinieerde integratie", + "relay": "Gebruikergedefinieerde integratie (relais)" + } }, "icon": { "change": "wijzig", @@ -400,13 +412,14 @@ "loadpoint": { "addCharger": { "charging": "Laadpunt toevoegen", - "heating": "Verwarmer toevoegen" + "heating": "Verwarmingstoestel toevoegen" }, "addMeter": "Toevoegen van een individuele energiemeter", + "advancedConfig": "Geavanceerde configuratie", "cancel": "Annuleren", "chargerError": { "charging": "Het configureren van een lader is vereist.", - "heating": "Een verwarmer instellen is vereist." + "heating": "Een verwarmingstoestel instellen is vereist." }, "chargerLabel": { "charging": "Oplader", @@ -424,11 +437,21 @@ "circuitInvalid": "Het circuit bestaat niet", "circuitLabel": "Groep", "circuitUnassigned": "niet toegewezen", + "created": "\"{name}\" is gecreëerd en gebruikt zinvolle standaardwaarden.", + "createdTitle": { + "charging": "Laadpunt gecreëerd", + "heating": "Verwarmer gecreëerd" + }, + "creating": { + "charging": "Lader opgeslagen. Laadpunt wordt gecreëerd…", + "heating": "Verwarmingsapparaat opgeslagen. Verwarmer wordt gecreëerd…" + }, "defaultModeHelp": { "charging": "Laadmodus bij het aansluiten van een voertuig.", "heating": "Wordt ingesteld bij opstarten van het systeem." }, "defaultModeHelpKeep": "Behoud de laatst-geselecteerde modus.", + "defaultModeKeep": "laat het zoals het is", "defaultModeLabel": "Standaard modus", "delete": "Wissen", "electricalSubtitle": "Bij twijfel, raadpleeg een elektricien.", @@ -438,12 +461,15 @@ "estimateLabel": "Interpoleer batterijpercentage tussen API updates", "maxCurrentHelp": "Moet groter zijn dan de minimum stroom.", "maxCurrentLabel": "Maximale stroom", + "maxTempHelp": "Moet groter zijn dan de minimumtemperatuur.", + "maxTempLabel": "Maximumtemperatuur", "minCurrentHelp": "Ga alleen onder 6 A als je weet wat je aan het doen bent.", "minCurrentLabel": "Minimale stroom", + "minTempLabel": "Minimumtemperatuur", "noVehicles": "Geen voertuigen ingesteld.", "option": { "charging": "Laadpunt toevoegen", - "heating": "Verwarmingstoestel toevoegen" + "heating": "Verwarmer toevoegen" }, "phases1p": "1-fase", "phases3p": "3-fase", @@ -469,6 +495,8 @@ "solarBehaviorLabel": "Zonne-energie", "solarModeCustom": "aangepast", "solarModeMaximum": "Maximale zonne-energie", + "temperatureRangeHelp": "Toont de range voor de temperatuurbalk. Heeft geen effect op de regeling.", + "temperatureRangeTitle": "Tempartuurbereik", "thresholdDisableDelayLabel": "Uitschakelvertraging", "thresholdDisableHelpInvalid": "Gebruik alstublieft een positieve waarde.", "thresholdDisableHelpPositive": "Stop wanneer er meer dan {power} van het net wordt gebruikt voor {delay}.", @@ -481,13 +509,13 @@ "thresholdEnableLabel": "Netstroom inschakelen", "titleAdd": { "charging": "Laadpunt Toevoegen", - "heating": "Verwarmingstoestel toevoegen", - "unknown": "Lader of verwarmingsapparaat toevoegen" + "heating": "Verwarmer toevoegen", + "unknown": "Laadpunt of verwarmer toevoegen" }, "titleEdit": { "charging": "Wijzig laadpunt", - "heating": "Wijzig verwarmingsapparaat", - "unknown": "Wijzig lader of verwarmingsapparaat" + "heating": "Wijzig verwarmer", + "unknown": "Wijzig laadpunt of verwarmer" }, "titleExample": { "charging": "Garage, carport, etc.", @@ -503,8 +531,9 @@ }, "main": { "addAdditional": "Extra meter toevoegen", + "addConsumer": "Voeg verbruiker toe", "addGrid": "Netmeter toevoegen", - "addLoadpoint": "Voeg lader of verwarmingsapparaat toe", + "addLoadpoint": "Voeg laadpunt of verwarmer toe", "addPvBattery": "Zonnepanelen of batterij toevoegen", "addTariffs": "Tarieven toevoegen", "addVehicle": "Voeg voertuig toe", @@ -535,18 +564,18 @@ "titleDefault": "Voertuig in slaapstand" }, "connect": { - "messageDefault": "Auto aangesloten op {pvPower}kW PV", - "title": "Wanneer een auto verbinding maakt", - "titleDefault": "Auto verbonden" + "messageDefault": "Voertuig aangesloten op {pvPower}kW PV", + "title": "Wanneer een voertuig verbinding maakt", + "titleDefault": "Voertuig verbonden" }, "disconnect": { - "messageDefault": "Auto losgekoppeld na {connectedDuration}", - "title": "Wanneer een auto de verbinding verbreekt", - "titleDefault": "Auto losgekoppeld" + "messageDefault": "Voertuig losgekoppeld na {connectedDuration}", + "title": "Wanneer een voertuig de verbinding verbreekt", + "titleDefault": "Voertuig losgekoppeld" }, "guest": { "messageDefault": "Onbekend voertuig, gast verbonden?", - "title": "Wanneer een onbekende auto verbinding maakt", + "title": "Wanneer een onbekend voertuig verbinding maakt", "titleDefault": "Onbekend voertuig" }, "planoverrun": { @@ -568,6 +597,11 @@ "messageDefault": "Het opladen van {chargedEnergy}kWh is voltooid in {chargeDuration}.", "title": "Wanneer het opladen stopt", "titleDefault": "Laadsessie voltooid" + }, + "suggestion": { + "messageDefault": "Optimizer suggestie voor {suggestionTitle}: als de optimizer actief zou zijn, zou het nu '{suggestionAction}' toepassen. Dit alleen een advies, er is niet aangepast. Experimenteel — feedback is welkom op https://github.com/evcc-io/evcc/issues/31903", + "title": "Wanneer de optimizer zijn aanbeveling toepast", + "titleDefault": "Aanbeveling van de optimizer" } }, "eventMessage": "Bericht", @@ -588,11 +622,16 @@ }, "meter": { "cancel": "Annuleren", + "convertToConsumer": { + "confirm": "Converteer deze \"extra meter\" naar een \"verbruiker\".", + "link": "Converteer naar verbruiker" + }, "delete": "Verwijder", "generic": "Generieke integraties", "option": { "aux": "Zelfregulerende consument toevoegen", "battery": "Batterijmeter toevoegen", + "consumer": "Standaard verbruiker toevoegen", "pv": "Zonne-energie meter toevoegen" }, "save": "Opslaan", @@ -603,7 +642,7 @@ "usage": { "aux": "Zelf regulerende verbruiker", "battery": "Batterij", - "charge": "Verbruiker / Lader", + "charge": "Lader", "grid": "Netwerk", "label": "Verbruik", "pv": "Productie" @@ -618,6 +657,8 @@ "connectionHintTcpip": "Het apparaat is bereikbaar via het netwerk (LAN/WiFi).", "connectionValueSerial": "RS485", "connectionValueTcpip": "Netwerk", + "delay": "Vertraging", + "delayHint": "Optionele wachttijd voor elke registerraadpleging. Alleen nodig voor langzame apparaten.", "device": "Apparaatnaam", "deviceHint": "Voorbeeld: /dev/ttyUSB0", "host": "IP-adres of hostnaam", @@ -628,7 +669,9 @@ "protocolHintRtu": "Verbinding via een RS485 naar Ethernet-adapter zonder protocolvertaling.", "protocolHintTcp": "Apparaat heeft ingebouwde LAN/WiFi-ondersteuning of is verbonden via een RS485 naar Ethernet-adapter met protocolvertaling.", "protocolValueRtu": "RTU", - "protocolValueTcp": "TCP" + "protocolValueTcp": "TCP", + "timeout": "Time-out", + "timeoutHint": "Optionele maximale wachttijd op antwoord van het apparaat." }, "modbusproxy": { "add": "Voeg een proxy verbinding toe", @@ -788,10 +831,11 @@ }, "section": { "additionalMeter": "Extra meters", + "consumers": "Verbruikers", "general": "Algemeen", "grid": "Netaansluiting", "integrations": "Integraties", - "loadpoints": "Laden en verwarmen", + "loadpoints": "Laadpunten en verwarmers", "meter": "Zonnepanelen & Batterij", "services": "Diensten", "system": "Systeem", @@ -906,7 +950,8 @@ "feedIn": "Net-export tarief toevoegen", "grid": "Net-import tarief toevoegen", "planner": "Planner voorspelling toevoegen", - "solar": "Zonne-energie voorspelling toevoegen" + "solar": "Zonne-energie voorspelling toevoegen", + "temperature": "Voeg temperatuurvoorspelling toe" }, "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.", @@ -919,6 +964,11 @@ "titleAdd": "Zonne-energie Voorspelling Toevoegen", "titleEdit": "Zonne-energie Voorspelling Aanpassen" }, + "temperature": { + "description": "Buitentemperatuurvoorspelling voor jouw locatie. Wordt weergegeven in de interface en wordt gebruikt om de vewarmingsbehoefte te bepalen.", + "titleAdd": "Temperatuurvoorspelling toevoegen", + "titleEdit": "Temperatuurvoorspelling bewerken" + }, "template": "Aanbieder", "title": "Tarieven & Voorspelingen", "titleChoice": "Wat wilt u toevoegen?", @@ -927,7 +977,8 @@ "feedIn": "Terugleververgoeding", "grid": "Grid import prijs", "planner": "Planner", - "solar": "PV" + "solar": "PV", + "temperature": "Temperatuurvoorspelling" }, "zones": { "add": "Zone toevoegen", @@ -1080,7 +1131,8 @@ "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." + "solar": "Bekijk de verwachtte zonne opbrengt voor vandaag en de komende dagen. Wordt ook gebruikt voor automatische laadplan optimalisatie in de toekomst.", + "temperature": "Bekijk de buitentemperatuurverwachting voor jouw locatie. Wordt gebruikt om de verwarmingsbehoefte te bepalen." }, "hideLine": "regel verbergen", "modalTitle": "Voorspelling", @@ -1102,14 +1154,23 @@ "solarAdjust": "Pas de zonne-energie voorspelling aan op basis van echte productiegegevens{percent}.", "solarAdjustMedium": "pas aan op basis van gemeten data", "solarAdjustShort": "aanpassen", + "temperature": { + "average": "Gemiddeld", + "range": "Bereik" + }, "type": { "co2": "CO₂ Uitstoot", "price": "Net-Prijs", - "solar": "Zonne-energie opbrengst" + "solar": "Zonne-energie opbrengst", + "temperature": "Buitentemperatuur" } }, "general": { - "note": "Let op:" + "back": "Terug", + "download": "Download", + "none": "geen", + "note": "Let op:", + "warning": "Waarschuwing" }, "header": { "about": "Over", @@ -1226,14 +1287,17 @@ "goal": "Laaddoel", "modalTitle": "Laad Plan", "none": "geen", + "once": "eenmalig", "optimization": { "cheapest": "goedkoopste", + "cheapestDescription": "Selecteer de goedkoopste laadmomenten. Kan resulteren in meerdere starts en stops.", + "cleanest": "Schoonste", + "cleanestDescription": "Selecteer de schoonste laadmomenten. Kan resulteren in meerdere starts en stops.", "continuous": "continu", "label": "Optimalisatie" }, "planNumber": "Schema {number}", "precondition": { - "description": "Laadtijd {duration} voor vertrek voor accu voorconditionering.", "label": "Laat Opladen", "optionAll": "alles", "optionNo": "nee" @@ -1243,7 +1307,6 @@ "repeatingPlans": "Herhalende schema's", "selectAll": "Alles selectreren", "strategyDisabledDescription": "Het opladen begint zo laat mogelijk, zodat het precies op tijd voor vertrek klaar is. Met dynamische netprijzen of een CO₂-tarief zijn er meer mogelijkheden.", - "strategySettings": "Strategie-instellingen", "time": "Tijd", "title": "Plan", "unsavedChanges": "Wijzigingen nog niet opgeslagen. Nu toepassen?", @@ -1274,11 +1337,15 @@ "pv": "Zonnepanelen systeem", "pvExport": "Netinjectie", "pvProduction": "Productie", - "selfConsumption": "Zelfverbruik" + "selfConsumption": "Zelfverbruik", + "toggleDetails": "Energiestroom details" }, "heatingStatus": { "charging": "Bezig met verwarmen…", "connected": "Stand-by.", + "minTemp": "Minimale verwarming tot {temp}.", + "suggestionStartTooltip": "De optimizer beveelt aan om nu te verwarmen.", + "suggestionStopTooltip": "De optimizer beveelt aan om het verwarmen te pauzeren.", "vehicleLimit": "Verwarming limiet", "waitForVehicle": "Klaar om te verwarmen…" }, @@ -1296,15 +1363,21 @@ "grid": { "energy": "geimporteerd", "returnEnergy": "geexporteerd" + }, + "meter": { + "energy": "Energie", + "returnEnergy": "Energie (omgekeerd)" } }, + "download": "Download", "empty": "Er is voor deze tijdspanne geen data beschikbaar.", "group": { "battery": "Batterij", + "consumer": "Verbruik", "forecast": "Voorspelling", "grid": "Net", "loadpoint": "Laden & Verwarmen", - "meter": "Consumptie", + "meter": "Extra meters", "pv": "Productie" }, "otherConsumers": "Overig", @@ -1318,12 +1391,15 @@ "emission": "Uitstoot", "fallbackName": "Laadpunt", "finished": "Eindtijd", + "last24hEnergy": "Laatste 24h", + "last7dEnergy": "Laatste 7 dagen", "power": "Vermogen", "price": "Kosten", "remaining": "Resterend", "remoteDisabledHard": "{source}: uitgeschakeld", "remoteDisabledSoft": "{source}: adaptief laden van zonnepanelen uitgeschakeld", - "solar": "Zon" + "solar": "Zon", + "todayEnergy": "Vandaag" }, "loadpointSettings": { "batteryBoost": { @@ -1341,12 +1417,17 @@ "batteryUsage": "Thuisbatterij", "currents": "Laadstroom", "default": "standaard", + "heating": "Verwarming", "maxCurrent": { "label": "Max. Stroomsterkte" }, "minCurrent": { "label": "Min. Stroomsterkte" }, + "minTemp": { + "description": "Onder {0} worden accu en netstroom ook gebruikt in pv-mode om deze temperatuur te behouden.", + "label": "Min. temperatuur" + }, "phasesConfigured": { "label": "Fasen", "no1p3pSupport": "Hoe is je lader aangesloten?", @@ -1429,6 +1510,20 @@ "unknown": "Gast voertuig", "vehicleSoc": "Lading" }, + "vehicleSettings": { + "connectedTo": "verbonden met {0}", + "description": "Deze standaardwaarden worden toegepast wanneer een voertuig wordt verbonden met een laadpunt.", + "editHint": "Voertuig onder {0} toevoegen of bewerken.", + "keepAsIs": "Laat zoals het is", + "learnMore": "Leer meer.", + "limitSoc": "Standaardlimiet", + "menu": "Voertuigen", + "minSoc": "Minimale lading", + "minSocDescription": "Snelladen tot dit niveau in pv modus.", + "mode": "Standaardmodus", + "notConnected": "niet verbonden", + "title": "Voertuiginstellingen" + }, "vehicleStatus": { "awaitingAuthorization": "Wachten op autorisatie.", "batteryBoost": "Batterijboost actief.", @@ -1455,6 +1550,8 @@ "pvEnable": "Voldoende PV overschot beschikbaar. Laden wordt binnenkort gestart.", "scale1p": "Wordt gereduceerd naar laden op 1 fase.", "scale3p": "Wordt verhoogd naar laden op 3 fasen.", + "suggestionStartTooltip": "Optimizer beveelt aan om nu op te laden.", + "suggestionStopTooltip": "Optimizer beveelt aan om het opladen te pauzeren.", "targetChargeActive": "Laadplan actief. Geschat einde over {duration}.", "targetChargePlanned": "Laadplan begint over {duration}.", "targetChargeWaitForVehicle": "Laadplan gereed. Wachten op voertuig…", @@ -1494,6 +1591,7 @@ }, "session": { "addOdometer": "Voeg waarde toe", + "addedRange": "Toegevoegd bereik", "cancel": "Annuleer", "co2": "CO₂", "date": "Periode", @@ -1504,11 +1602,12 @@ "meterstop": "Eindwaarde meter", "odometer": "Kilometerstand", "price": "Kostprijs", - "soc": "Voertuig SoC", + "soc": "Laadstatus", "started": "Gestart", "title": "Oplaadsessie" }, "sessions": { + "addedRange": "Bereik", "avgPower": "⌀ Vermogen", "avgPrice": "⌀ Prijs", "chargeDuration": "Duur", @@ -1534,6 +1633,7 @@ }, "co2": "⌀ CO₂", "csv": { + "addedrange": "Toegevoegd bereik (km)", "chargedenergy": "Energie (kWh)", "chargeduration": "Duur", "co2perkwh": "CO₂/kWh", @@ -1544,7 +1644,7 @@ "meterstart": "Beginwaarde meter (kWh)", "meterstop": "Eindwaarde meter (kWh)", "odometer": "Kilometerstand (km)", - "price": "Prijs", + "price": "Kosten", "priceperkwh": "Prijs/kWh", "socend": "SoC eind (%)", "socstart": "SoC start (%)", @@ -1552,6 +1652,8 @@ "vehicle": "Voertuig" }, "date": "Start", + "download": "Download {period}", + "downloadTotal": "Download alles", "energy": "Geladen", "filter": { "allLoadpoints": "alle oplaadpunten", @@ -1561,12 +1663,12 @@ "group": { "co2": "Emissies", "grid": "Net", - "price": "Prijs", + "price": "Kosten", "self": "Zon" }, "groupBy": { "loadpoint": "Laadpunt", - "none": "Totaal", + "none": "PV", "vehicle": "Voertuig" }, "loadpoint": "Oplaadpunt", @@ -1587,8 +1689,8 @@ "total": "Totaal", "type": { "co2": "CO₂", - "price": "Prijs", - "solar": "Zon" + "price": "Kosten", + "solar": "Energie" }, "vehicle": "Voertuig" }, @@ -1608,6 +1710,7 @@ "label": "Taal" }, "loadpoints": { + "disabledDevice": "uitgeschakeld", "help": "Wijzig de volgorde en zichtbaarheid in de gebruikersinterface.", "hide": "{title} verbergen", "label": "Laadpunten", diff --git a/i18n/no.json b/i18n/no.json index a116e0603..ee39cf8e8 100644 --- a/i18n/no.json +++ b/i18n/no.json @@ -896,7 +896,6 @@ }, "planNumber": "Plan {number}", "precondition": { - "description": "Lad {duration} før avreise for å forberede batteriet.", "label": "Sen betaling", "optionAll": "alt", "optionNo": "nei" @@ -906,7 +905,6 @@ "repeatingPlans": "Gjentatte planer", "selectAll": "Velg alt", "strategyDisabledDescription": "Ladingen starter så sent som mulig for å være ferdig akkurat i tide til avreise. Med dynamiske nettpriser eller CO₂-tariff er det flere alternativer tilgjengelig her.", - "strategySettings": "Strategiinnstillinger", "time": "Tid", "title": "Plan", "unsavedChanges": "Det er ubehandlede endringer. Vil du bruke dem nå?", @@ -1071,15 +1069,10 @@ "feedinPriorityPausing": "Solcellelading pauset for å maksimere innmatingen.", "finished": "Ferdig.", "minCharge": "Minimumslading til {soc}.", - "pvDisable": "Ikke nok overskudd. Pause i {remaining}...", - "pvEnable": "Overskudd tilgjengelig. Starter om {remaining}...", - "scale1p": "Reduserer til 1-fase strøm i {remaining}...", - "scale3p": "Øker til 3-fase strøm i {remaining}...", "targetChargeActive": "Målladning aktiv. Estimert ferdig om {duration}.", "targetChargePlanned": "Mållading starter på {duration}.", "targetChargeWaitForVehicle": "Ladingsplan klar. Venter på kjøretøy…", "vehicleLimit": "Kjøretøybegrensning", - "vehicleLimitReached": "Kjøretøygrense på {soc} nådd.", "waitForVehicle": "Klar. Venter på kjøretøy…", "welcome": "Kort innledende ladning for å bekrefte tilkoblingen." }, diff --git a/i18n/pl.json b/i18n/pl.json index ec43a9638..6ff7292de 100644 --- a/i18n/pl.json +++ b/i18n/pl.json @@ -19,6 +19,49 @@ "successCloseTab": "Możesz teraz zamknąć tę kartę.", "title": "Status autoryzacji" }, + "battery": { + "card": { + "charging": "ładowanie", + "discharging": "rozładowywanie", + "energy": "Energia", + "gridCharge": "ładowanie sieciowe", + "hold": "rozładowywanie zablokowane", + "holdCharge": "ładowanie zablokowane", + "ofTotal": "z {total}", + "power": "Moc", + "soc": "Poziom naładowania magazynu" + }, + "combined": "Łącznie", + "config": { + "buffer": "Energia z domowego magazynu energii powyżej {soc} może być wykorzystana do zasilania punktów obciążenia i opóźnień w wyłączaniu w trybie solarnym. Automatyczne uruchamianie ładowania słonecznego {start}.", + "bufferNone": "Energia z domowego magazynu energii nie jest wykorzystywana do zasilania punktów obciążenia. Ustaw wartość poniżej {soc}, aby włączyć obsługę.", + "bufferStart": { + "above": "gdy magazyn energii ma powyżej {soc}", + "full": "gdy magazyn energii jest pełen", + "never": "tylko z wystarczającą nadwyżką" + }, + "bufferTitle": "Magazyn energii jako bufor ładowania", + "discharge": "Zapobiegaj rozładowywaniu domowego magazynu energii w trybie szybkim i w zaplanowanym ładowaniu.", + "gridDischarge": "Pozwól na rozładowywanie domowego magazynu energii do sieci.", + "priority": "Domowy magazyn energii ma priorytet, gdy jego stan jest poniżej {soc}. Wtedy punkty obciążenia otrzymują nadwyżkę.", + "priorityNone": "Nadwyżka energii słonecznej trafia bezpośrednio do punktów obciążenia. Ustaw wartość powyżej {soc}, aby nadać priorytet domowemu magazynowi energii.", + "priorityTitle": "Gdzie najpierw trafia nadwyżka?" + }, + "optimizer": { + "action": { + "charge": "Ładowanie z sieci", + "discharge": "Rozładowywanie do sieci", + "hold": "Zapobiegaj rozładowywaniu", + "holdcharge": "Zapobiegaj ładowaniu", + "normal": "Normalna praca" + }, + "empty": "pusty", + "full": "pełny", + "highest": "Najwyższa ({value})", + "lowest": "Najniższa ({value})", + "suggestion": "Sugestia" + } + }, "batterySettings": { "batteryLevel": "Stan naładowania magazynu energii", "bufferStart": { @@ -28,7 +71,7 @@ }, "capacity": "{energy} z {total}", "control": "Kontrola akumulatora", - "discharge": "Zapobiegaj rozładowaniu w trybie szybkim i zaplanowanym ładowaniu.", + "discharge": "Zapobiegaj rozładowywaniu w trybie szybkim i zaplanowanym ładowaniu.", "disclaimerHint": "Uwaga:", "disclaimerText": "Te ustawienia mają wpływ tylko na tryb solarny. Sposób ładowania jest odpowiednio dostosowywany.", "gridChargeTab": "Ładowanie z sieci", @@ -51,12 +94,29 @@ "hex": "Kolor Hex" }, "config": { + "adminPassword": { + "invalid": "Błędne hasło. Proszę spróbować ponownie.", + "title": "Możliwa niebezpieczna czynność" + }, + "apiKey": { + "generate": "Stwórz klucz API", + "keyLabel": "Klucz API", + "regenerate": "Utwórz ponownie klucz API", + "regenerateConfirm": "Należy podać hasło administratora, aby ponownie utworzyć klucz API.", + "regenerateLink": "Utwórz ponownie", + "regenerateWarning": "Ponowne utworzenie unieważni istniejący klucz i wszelkie automatyzacje używające go przestaną działać. Kontynuować?", + "revealSuccess": "Nowe klucz API jest gotowy do użycia.", + "revealTitle": "Utworzono klucz API", + "shownOnce": "Klucz jest wyświetlany tylko raz. Należy zapisać go w bezpiecznym miejscu, gdyż nie będzie można go pobrać ponownie.", + "title": "Klucz API" + }, "aux": { "description": "Urządzenie, które dostosowuje swój pobór energii na podstawie dostępnej nadwyżki (np. inteligentne podgrzewacze wody). evcc zakłada, że urządzenie to zmniejszy zużycie energii, jeśli zajdzie taka potrzeba.", "titleAdd": "Dodaj samoregulujące się urządzenie konsumujące", "titleEdit": "Edytuj samoregulujące się urządzenie konsumujące" }, "battery": { + "title": "Bateria", "titleAdd": "Dodaj magazyn energii", "titleEdit": "Edytuj magazyn energii" }, @@ -125,7 +185,9 @@ "title": "Waluta" }, "deviceValue": { + "activeClients": "Aktywni klienci", "amount": "Ilość", + "asleep": "śpi", "broker": "Broker", "bucket": "Bucket", "capacity": "Pojemność", @@ -335,11 +397,20 @@ "circuitInvalid": "Obwód nie istnieje", "circuitLabel": "Obwód", "circuitUnassigned": "nieprzypisany", + "created": "Utworzono \"{name}\" z domyślnymi wartościami.", + "createdTitle": { + "charging": "Utworzono punkt ładowania", + "heating": "Utworzono grzejnik" + }, + "creating": { + "charging": "Zapisano ładowarkę. Tworzenie punktu ładowania…" + }, "defaultModeHelp": { "charging": "Tryb ładowania po podłączeniu pojazdu.", "heating": "Jest ustawiane podczas uruchamiania systemu." }, "defaultModeHelpKeep": "Zachowuje ostatnio wybrany tryb.", + "defaultModeKeep": "zostaw jak jest", "defaultModeLabel": "Tryb domyślny", "delete": "Usuń", "electricalSubtitle": "W razie wątpliwości skonsultuj się z elektrykiem.", @@ -380,6 +451,7 @@ "solarBehaviorLabel": "Słońce", "solarModeCustom": "własne", "solarModeMaximum": "maksymalnie słońce", + "temperatureRangeTitle": "Zakres temperatur", "thresholdDisableDelayLabel": "Wyłącz opóźnienie", "thresholdDisableHelpInvalid": "Proszę użyć wartości dodatniej.", "thresholdDisableHelpPositive": "Zatrzymaj, gdy z sieci zostanie zużyte więcej niż {power} przez {delay}.", @@ -414,6 +486,7 @@ }, "main": { "addAdditional": "Dodaj dodatkowy licznik", + "addConsumer": "Dodaj konsumenta", "addGrid": "Dodaj licznik sieciowy", "addLoadpoint": "Dodaj ładowarkę lub podgrzewacz", "addPvBattery": "Dodaj licznik energii słonecznej lub magazynu energii", @@ -430,7 +503,10 @@ "welcomeBannerTitle": "Skonfigurujmy Twój system!" }, "mcp": { - "description": "Udostępnia serwer protokołu Model Context Protocol, umożliwiając asystentom AI, takim jak Claude, odczytywanie stanu systemu i kontrolowanie ładowania." + "description": "Udostępnia serwer protokołu Model Context Protocol, umożliwiając asystentom AI, takim jak Claude, odczytywanie stanu systemu i kontrolowanie ładowania.", + "exampleLabel": "Np. Claude CLI", + "title": "Serwer MCP", + "url": "Adres MCP" }, "messaging": { "addMessenger": "Dodaj usługę", @@ -475,6 +551,9 @@ "messageDefault": "Zakończono ładowanie {chargedEnergy}kWh w ciągu {chargeDuration}.", "title": "Gdy ładowanie się zatrzyma", "titleDefault": "Ładowanie zakończone" + }, + "suggestion": { + "titleDefault": "Sugestia optymalizatora" } }, "eventMessage": "Wiadomość", @@ -1034,7 +1113,6 @@ }, "planNumber": "Plan {number}", "precondition": { - "description": "Naładuj {duration} przed wyjazdem dla wstępnego kondycjonowania akumulatora.", "label": "Późniejsze ładowanie", "optionAll": "wszystko", "optionNo": "nie" @@ -1044,7 +1122,6 @@ "repeatingPlans": "Powtarzające się plany", "selectAll": "Wybierz wszystko", "strategyDisabledDescription": "Ładowanie rozpoczyna się jak najpóźniej, aby zakończyć się tuż przed wyjazdem. Dzięki dynamicznym cenom energii elektrycznej lub taryfie CO₂ dostępnych jest więcej opcji.", - "strategySettings": "Ustawienia strategii", "time": "Czas", "title": "Plan", "unsavedChanges": "Są niezapisane zmiany. Zastosować teraz?", @@ -1142,6 +1219,9 @@ "minCurrent": { "label": "Min. Prąd" }, + "minTemp": { + "label": "Min. temperatura" + }, "phasesConfigured": { "label": "Fazy", "no1p3pSupport": "Jak podłączone jest urządzenie?", @@ -1224,6 +1304,16 @@ "unknown": "Pojazd gościa", "vehicleSoc": "Poziom naładowania" }, + "vehicleSettings": { + "connectedTo": "podłączono do {0}", + "editHint": "Dodaj lub usuļ pojazdy w {0}.", + "keepAsIs": "Zostaw jak jest", + "learnMore": "Dowiedz się więcej.", + "limitSoc": "Domyślne ograniczenie", + "menu": "Pojazdy", + "minSoc": "Minimalny poziom naładowania", + "title": "Ustawienia pojazdu" + }, "vehicleStatus": { "awaitingAuthorization": "Oczekiwanie na zezwolenie.", "batteryBoost": "Turbo ładowanie aktywne.", @@ -1477,6 +1567,7 @@ "tabBar": { "battery": "Magazyn energii", "charge": "Obciążenie", + "comingSoon": "Strona jest w trakcie budowy.", "forecast": "Prognoza", "more": "Więcej", "sessions": "Sesje" diff --git a/i18n/pt.json b/i18n/pt.json index 8ec6e6318..84f5c6133 100644 --- a/i18n/pt.json +++ b/i18n/pt.json @@ -1,22 +1,22 @@ { "authProviders": { - "authCode": "Código de autorização", + "authCode": "Código de autenticação", "authCodeHelp": "Copie este código e utilize-o na próxima etapa. Válido por {duration}.", "authorizationFailed": "A autorização falhou", - "authorizationRequired": "Autorização necessária", - "authorizationSuccessful": "Autorização bem-sucedida", - "buttonConnect": "Conectar-se ao {provider}", - "buttonDisconnect": "Desconectar", - "confirmLogout": "Tem a certeza de que deseja desconectar {title}?", - "connect": "conectar", - "disconnect": "desconectar", + "authorizationRequired": "Necessário autorização", + "authorizationSuccessful": "Autorização com sucesso", + "buttonConnect": "Ligar-se ao {provider}", + "buttonDisconnect": "Desligar", + "confirmLogout": "Tem a certeza de que deseja desligar {title}?", + "connect": "Ligar", + "disconnect": "Desligar", "loggedOut": "Sessão encerrada com sucesso", "logoutFailed": "Não foi possível sair da conta", "modalDescriptionLogin": "Conclua o processo de autorização para estabelecer a ligação com {provider}.", "modalDescriptionLogout": "Isso irá desconectar a sua conta {provider} e remover o acesso aos seus dados.", - "success": "{title} está agora conectado e pronto a ser utilizado.", - "successCloseModal": "Agora pode fechar esta caixa de diálogo.", - "successCloseTab": "Pode agora fechar esta guia.", + "success": "{title} está agora ligado e pronto a ser utilizado.", + "successCloseModal": "Pode fechar esta caixa de diálogo.", + "successCloseTab": "Pode fechar esta guia.", "title": "Estado da autorização" }, "battery": { @@ -30,6 +30,36 @@ "ofTotal": "de {total}", "power": "Potência", "soc": "Nível da bateria" + }, + "combined": "Combinado", + "config": { + "buffer": "A energia da bateria doméstica acima de {soc} pode ser utilizada para alimentar os pontos de carregamento e retardar o desligamento no modo solar. Iniciar o carregamento solar automaticamente {start}.", + "bufferNone": "A energia da bateria doméstica não é utilizada para os pontos de carregamento. Defina um valor inferior a {soc} para ativar esta funcionalidade.", + "bufferStart": { + "above": "assim que a bateria estiver acima de {soc}", + "full": "assim que a bateria estiver cheia", + "never": "apenas se houver excedente suficiente" + }, + "bufferTitle": "Bateria como reserva de carga", + "discharge": "Evite a descarga da bateria doméstica no modo rápido e durante o carregamento programado.", + "gridDischarge": "Permitir que a bateria doméstica descarregue para a rede elétrica.", + "priority": "A bateria doméstica tem prioridade enquanto o seu nível de carga (SOC) estiver abaixo de {soc}. Depois, os pontos de carregamento recebem o excedente.", + "priorityNone": "O excedente de energia solar é encaminhado diretamente para os pontos de carregamento. Defina um valor superior a {soc} para dar prioridade à bateria doméstica.", + "priorityTitle": "Para onde vai o excedente primeiro?" + }, + "optimizer": { + "action": { + "charge": "Carregar a partir da rede elétrica", + "discharge": "Descarga para a rede", + "hold": "Evitar a descarga", + "holdcharge": "Impedir o carregamento", + "normal": "Funcionamento normal" + }, + "empty": "vazio", + "full": "cheio", + "highest": "Mais elevado ({value})", + "lowest": "Mais baixo ({value})", + "suggestion": "Sugestão" } }, "batterySettings": { @@ -71,7 +101,7 @@ }, "apiKey": { "description": "Conceda acesso seguro a scripts e tarefas de automatização, como cópias de segurança programadas, sem partilhar a sua palavra-passe de administrador.", - "exampleLabel": "Experimenta: faz o download de uma cópia de segurança com o curl", + "exampleLabel": "Experimente: faça o download de uma cópia de segurança com o curl", "generate": "Gerar chave API", "generateConfirm": "Introduza a sua palavra-passe de administrador para gerar uma nova chave API.", "keyLabel": "Chave API", @@ -90,6 +120,7 @@ "titleEdit": "Editar Equipamento com capacidade de auto-ajuste de consumos" }, "battery": { + "title": "Bateria", "titleAdd": "Adicionar Bateria", "titleEdit": "Editar Bateria" }, @@ -110,21 +141,21 @@ "ocppStep1": "Configure o seu carregador para utilizar o evcc como servidor OCPP.", "ocppStep2": "Aguarde até que o carregador se conecte ao evcc.", "ocppStep3": "Continue e conclua a configuração.", - "ocppWaiting": "Aguardando conexão", + "ocppWaiting": "A aguardar ligação", "switchsockets": "Tomadas comandadas", "template": "Fabricante", "titleAdd": { "charging": "Adicionar Carregador", - "heating": "Adicionar aquecedor" + "heating": "Adicionar dispositivo de aquecimento" }, "titleEdit": { "charging": "Editar Carregador", - "heating": "Editar o aquecimento" + "heating": "Editar dispositivo de aquecimento" }, "type": { "custom": { "charging": "Carregador personalizado", - "heating": "Aquecimento personalizado" + "heating": "Dispositivo de aquecimento definido pelo utilizador" }, "heatpump": "Bomba de calor personalizada", "sgready": "Bomba de calor personalizada (sg-ready via plugins)", @@ -154,12 +185,13 @@ "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" + "label": "Moeda", + "title": "Moeda" }, "deviceValue": { "activeClients": "Clientes ativos", "amount": "Quantidade", + "asleep": "em repouso", "broker": "Broker", "bucket": "Bucket", "capacity": "Capacidade", @@ -187,6 +219,7 @@ "enabled": "Ativado", "energy": "Energia", "events": "Eventos", + "exportLimit": "Limite de injeção na Rede", "feedinPrice": "Preço de injeção na rede", "forecast": "Previsões", "gridPrice": "Preço da Rede", @@ -196,8 +229,9 @@ "max": "max", "messengers": "Serviços", "no": "não", - "odometer": "Totalizador Km", + "odometer": "Conta Quilómetros", "org": "Organização", + "outdoorTemp": "Temperatura exterior", "phaseCurrents": "Corrente", "phasePowers": "Energia", "phaseVoltages": "Voltagem", @@ -207,6 +241,7 @@ "price": "Preço", "productionLimited": "Produção limitada", "range": "Autonomia", + "remoteEnabled": "Ativado", "returnEnergy": "Energia (inversa)", "singlePhase": "Monofásico", "soc": "Carga", @@ -238,15 +273,21 @@ "public": "Certificado publico", "title": "Certificados" }, + "configuredDevices": "Dispositivos configurados", "description": "Configuração que permite o evcc comunicar com outros dispositivos compatíveis com EEBus, como carregadores ou unidades de controlo do operador de rede. Toda a inicialização e geração de certificados é feita automaticamente na primeira inicialização.", "descriptionAdvanced": "Não são necessárias alterações. Somente altere se souber realmente o que está a fazer. Se mudar quer o SHIP-id ou os certificados, terá que emparelhar novamente os seus dispositivos.", "interfaces": "Interfaces", "interfacesHelp": "Limitar as interfaces que o EEBus pode usar para evitar problemas de comunicação. Deixar em branco para usar todas as interfaces. Um entrada por linha.", + "noConfiguredDevices": "Não existem dispositivos configurados pela SKI.", + "noPairings": "Não existem dispositivos emparelhados.", + "pairings": "Dispositivos emparelhados", "port": "Porta", "portHelp": "Porta a usar.", "qr": "Código de emparelhamento", "qrExplain": "Durante a instalação, procure outro dispositivo EEBus para emparelhar automaticamente, sem ter de introduzir manualmente o SKI.", "removeConfirm": "Toda a configuração EEBus será eliminada. Serão gerados novos certificados e identificadores na próxima inicialização. Tem a certeza?", + "removePairing": "Remover emparelhamento", + "removePairingConfirm": "O dispositivo será desligado e terá de ser emparelhado novamente. Tem a certeza?", "shipid": "SHIP-ID", "shipidExplain": "Identificador permanente do dispositivo a ser usado dentro da rede EEBus.", "shipidHelp": "Este SHIP-ID está vinculado ao certificado abaixo.", @@ -265,13 +306,14 @@ }, "form": { "danger": "Perigo", - "deprecated": "obsoleto", + "deprecated": "descontinuado", + "durationUnit": "Unidade de tempo para {label}", "example": "Exemplo", "optional": "opcional" }, "general": { "applyAndClose": "Aplicar e fechar", - "authPerform": "Se conectar a {provider}", + "authPerform": "Ligar-se a {provider}", "authPerformHint": "Será aberto numa nova janela. Volte aqui para continuar.", "authPrepare": "Preparar a ligação", "cancel": "Cancelar", @@ -330,12 +372,24 @@ }, "customOption": "Integração definida pelo utilizador", "description": "Limitação de potência por sistemas externos (por exemplo, §14a da EnWG, interface §9 da EEG ou sistema de gestão de energia de nível superior). Funciona em conjunto com a funcionalidade de gestão de carga. A redução da produção solar também é suportada para determinados dispositivos. O limite de redução efetivo deve ser definido na configuração de cada inversor.", + "download": "Descarregar", + "dynamicLimits": "Limites dinâmicos", "eventsRecorded": "Registou {count} eventos de limitação da rede.", + "exportLimit": "Limite de exportação da rede", + "exportLimitDescription": "O otimizador da bateria respeita este limite para evitar perdas de energia solar. Um sinal de restrição ativo proveniente de uma integração de limites dinâmicos tem prioridade.", + "exportLimitHint": "Limite estático de injeção no ponto de ligação à rede.", "generic": "Integrações genéricas", "integrations": "Integrações", "lastEvent": "Mais recente {timeAgo}.", + "noEvents": "Ainda não há eventos registados.", + "recordedEvents": "Eventos registados", + "staticLimits": "Limites estáticos", "template": "Integração", - "title": "Limite externo" + "title": "Limite externo", + "type": { + "custom": "Integração definida pelo utilizador", + "relay": "Integração definida pelo utilizador (relé)" + } }, "icon": { "change": "alterar", @@ -360,17 +414,18 @@ "loadpoint": { "addCharger": { "charging": "Adicionar Carregador", - "heating": "Adicionar aquecedor" + "heating": "Adicionar dispositivo de aquecimento" }, "addMeter": "Adicionar contador de energia dedicado", + "advancedConfig": "Configuração avançada", "cancel": "Cancelar", "chargerError": { "charging": "É necessário configurar um carregador.", - "heating": "A configuração do aquecimento é necessária." + "heating": "É necessário configurar um dispositivo de aquecimento." }, "chargerLabel": { "charging": "Carregador", - "heating": "Aquecedor" + "heating": "Dispositivo de aquecimento" }, "chargerPower11kw": "11 kW", "chargerPower11kwHelp": "Utilização de uma gama de correntes de 6 a 16 A.", @@ -384,11 +439,21 @@ "circuitInvalid": "O circuito não existe", "circuitLabel": "Circuito", "circuitUnassigned": "não atribuído", + "created": "O «{name}» foi criado e utiliza valores predefinidos adequados.", + "createdTitle": { + "charging": "Ponto de carregamento criado", + "heating": "Aquecedor criado" + }, + "creating": { + "charging": "Carregador guardado. A criar ponto de carregamento…", + "heating": "Dispositivo de aquecimento guardado. A criar o aquecedor…" + }, "defaultModeHelp": { "charging": "Modo de carregamento ao ligar o veículo.", "heating": "É definido no arranque do sistema." }, "defaultModeHelpKeep": "Mantém o último modo selecionado.", + "defaultModeKeep": "manter tal como está", "defaultModeLabel": "Modo predefinido", "delete": "Apagar", "electricalSubtitle": "Em caso de dúvida, pergunte ao seu eletricista.", @@ -406,7 +471,7 @@ "noVehicles": "Nenhum veículo configurado.", "option": { "charging": "Adicionar um ponto de carga", - "heating": "Adicionar dispositivo de aquecimento" + "heating": "Adicionar aquecedor" }, "phases1p": "Monofásico", "phases3p": "Trifásico", @@ -446,13 +511,13 @@ "thresholdEnableLabel": "Ativar a energia da Rede", "titleAdd": { "charging": "Adicionar ponto de carregamento", - "heating": "Adicionar dispositivo de aquecimento", - "unknown": "Adicionar carregador ou aquecedor" + "heating": "Adicionar aquecedor", + "unknown": "Adicionar ponto de carregamento ou aquecedor" }, "titleEdit": { "charging": "Editar ponto de carregamento", - "heating": "Editar dispositivo de aquecimento", - "unknown": "Editar carregador ou aquecedor" + "heating": "Editar aquecedor", + "unknown": "Editar ponto de carregamento ou aquecedor" }, "titleExample": { "charging": "Garagem, Carport, etc.", @@ -470,7 +535,7 @@ "addAdditional": "Adicionar Contador adicional", "addConsumer": "Adicionar consumidor", "addGrid": "Adicionar Contador de Rede", - "addLoadpoint": "Adicionar carregador ou aquecedor", + "addLoadpoint": "Adicionar ponto de carregamento ou aquecedor", "addPvBattery": "Adicionar solar ou bateria", "addTariffs": "Adicionar tarifário(s)", "addVehicle": "Adicionar veículo", @@ -501,18 +566,18 @@ "titleDefault": "Veículo inativo" }, "connect": { - "messageDefault": "Carro conectado com {pvPower}kW PV", - "title": "Quando um carro se conecta", - "titleDefault": "Carro conectado" + "messageDefault": "Viatura conectada com {pvPower}kW PV", + "title": "Quando uma viatura se conecta", + "titleDefault": "Viatura conectada" }, "disconnect": { - "messageDefault": "Carro desligado após {connectedDuration}", - "title": "Quando um carro se desliga", - "titleDefault": "Carro desligado" + "messageDefault": "Viatura desligada após {connectedDuration}", + "title": "Quando uma viatura se desliga", + "titleDefault": "Viatura desligada" }, "guest": { "messageDefault": "Veículo desconhecido, convidado conectado?", - "title": "Quando um carro desconhecido se conecta", + "title": "Quando uma viatura desconhecida se conecta", "titleDefault": "Veículo desconhecido" }, "planoverrun": { @@ -534,6 +599,11 @@ "messageDefault": "Concluída a carga de {chargedEnergy} kWh em {chargeDuration}.", "title": "Quando o carregamento parar", "titleDefault": "Carregamento concluído" + }, + "suggestion": { + "messageDefault": "Sugestão do otimizador para {suggestionTitle}: se o controlo do otimizador estivesse ativo, aplicaria «{suggestionAction}» neste momento. Trata-se apenas de um aviso; não foi alterado nada. Experimental — agradecemos o vosso feedback em https://github.com/evcc-io/evcc/issues/31903", + "title": "Quando a ação recomendada pelo otimizador se altera", + "titleDefault": "Sugestão do otimizador" } }, "eventMessage": "Mensagem", @@ -589,6 +659,8 @@ "connectionHintTcpip": "O dispositivo pode ser acedido através da rede (LAN/WiFi).", "connectionValueSerial": "RS485", "connectionValueTcpip": "Rede Informática", + "delay": "Atraso", + "delayHint": "Tempo de espera opcional antes de cada acesso ao registo. Necessário apenas para dispositivos lentos.", "device": "Nome do dispositivo", "deviceHint": "Exemplo: /dev/ttyUSB0", "host": "Endereço IP ou hostname", @@ -599,7 +671,9 @@ "protocolHintRtu": "Ligação através de adaptador RS485 / Ethernet sem tradução de protocolo.", "protocolHintTcp": "O dispositivo tem suporte LAN/WIFI nativo ou está ligado a um adaptador RS485 / Ethernet com tradução de protocolo.", "protocolValueRtu": "RTU", - "protocolValueTcp": "TCP" + "protocolValueTcp": "TCP", + "timeout": "Tempo limite", + "timeoutHint": "Tempo máximo opcional de espera pela resposta de um dispositivo." }, "modbusproxy": { "add": "Adicionar uma ligação proxy", @@ -763,7 +837,7 @@ "general": "Geral", "grid": "Rede", "integrations": "Integrações", - "loadpoints": "Carregamento e aquecimento", + "loadpoints": "Pontos de carregamento e aquecedores", "meter": "Solar e Bateria", "services": "Serviços", "system": "Sistema", @@ -878,7 +952,8 @@ "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" + "solar": "Adicionar previsão solar", + "temperature": "Adicionar previsão de temperatura" }, "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.", @@ -891,6 +966,11 @@ "titleAdd": "Adicionar previsão solar", "titleEdit": "Editar previsão solar" }, + "temperature": { + "description": "Previsão da temperatura exterior para a sua localização. Apresentada na interface e utilizada para prever a procura de aquecimento.", + "titleAdd": "Adicionar previsão de temperatura", + "titleEdit": "Editar previsão de temperatura" + }, "template": "Fornecedor", "title": "Tarifas e previsões", "titleChoice": "O que deseja adicionar?", @@ -899,7 +979,8 @@ "feedIn": "Preço de exportação da rede", "grid": "Preço de importação da rede", "planner": "Planeador", - "solar": "Solar" + "solar": "Solar", + "temperature": "Previsão de temperatura" }, "zones": { "add": "Adicionar zona", @@ -1020,6 +1101,8 @@ "version": { "availableLong": "nova versão disponível", "community": "comunidade evcc", + "labelEmail": "E-mail", + "labelPhone": "Telefone", "labelRelease": "Lançamento", "labelVersion": "Versão", "labelWebsite": "Website", @@ -1052,7 +1135,8 @@ "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." + "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.", + "temperature": "Consulte a temperatura exterior prevista para a sua localização. Utilizada para prever a procura de aquecimento." }, "hideLine": "ocultar linha", "modalTitle": "Previsões", @@ -1074,13 +1158,20 @@ "solarAdjust": "Ajustar a previsão solar com base em dados reais de produção{percent}.", "solarAdjustMedium": "ajuste com dados reais", "solarAdjustShort": "ajustar", + "temperature": { + "average": "Média", + "range": "Intervalo" + }, "type": { "co2": "Emissões de CO₂", "price": "Preço da rede", - "solar": "Produção solar" + "solar": "Produção solar", + "temperature": "Temperatura exterior" } }, "general": { + "download": "Descarregar", + "none": "nenhum", "note": "Nota:" }, "header": { @@ -1094,6 +1185,7 @@ "sessions": "Sessões de carregamento" }, "help": { + "contactButton": "Contacte-nos", "discussionsButton": "Discussões no GitHub", "documentationButton": "Documentação", "issueButton": "Reportar um problema", @@ -1133,8 +1225,11 @@ "additionalContext": "Contexto adicional", "additionalContextPlaceholder": "Alguma informação adicional que possa ser útil...\n- Detalhes da configuração\n- O que tentou\n- Detalhes do ambiente", "createButtonDiscussion": "Iniciar discussão no GitHub...", + "createButtonEmail": "Enviar pedido por e-mail", "createButtonIssue": "Criar problema no GitHub...", "description": "A sua instalação não está a funcionar conforme o esperado? Utilize esta página para obter ajuda ou para comunicar problemas. Forneça detalhes suficientes para nos ajudar a compreender e reproduzir o problema, mantendo a sua descrição concisa, clara e fácil de compreender.", + "downloadButton": "Descarregar informações de diagnóstico", + "downloadHint": "Anexe este ficheiro ao seu e-mail, caso sejam necessárias mais informações.", "helpType": { "discussion": "Preciso de ajuda com a minha configuração", "discussionDescription": "As discussões comunitárias fornecem respostas.", @@ -1146,6 +1241,7 @@ "issueTitle": "Título", "stepsToReproduce": "Passos para reproduzir", "subTitleDiscussion": "Descreva o seu problema", + "subTitleEmail": "Descreva o seu problema", "subTitleIssue": "Descreva o problema", "summary": { "confirmationButtonDiscussion": "Iniciar discussão no GitHub", @@ -1198,24 +1294,33 @@ "goal": "Carga pretendida", "modalTitle": "Plano de carga", "none": "nenhum", + "once": "uma vez", "optimization": { "cheapest": "mais barato", + "cheapestDescription": "Seleciona os intervalos mais baratos. Pode resultar em várias partidas e paragens.", + "cleanest": "o mais limpo", + "cleanestDescription": "Seleciona os intervalos mais livres. Pode resultar em várias inicializações e interrupções.", "continuous": "contínuo", + "continuousCo2Description": "Seleciona a série de intervalos mais limpa e não interrompida.", + "continuousPriceDescription": "Seleciona a série de intervalos de tempo não interrompidos mais económica.", "label": "Otimização" }, "planNumber": "Plano {number}", "precondition": { - "description": "Carregar {duration} antes da partida para pré-condicionamento da bateria.", + "description": "Faz parte do processo de carregamento imediatamente antes da partida: bateria aquecida, menos tempo com um nível de carga elevado.", "label": "Carga tardia", "optionAll": "tudo", - "optionNo": "não" + "optionNo": "não", + "summary": "{precondition} antes de" }, "remove": "Apagar", "repeating": "recorrente", "repeatingPlans": "Repetição de planos", "selectAll": "Selecionar tudo", + "strategy": { + "label": "Estratégia" + }, "strategyDisabledDescription": "O carregamento começa o mais tarde possível para terminar a tempo da partida. Com preços dinâmicos da rede ou tarifa de CO₂, há mais opções disponíveis aqui.", - "strategySettings": "Definições da estratégia", "time": "Hora", "title": "Plano", "unsavedChanges": "Há alterações não salvas. Aplicar agora?", @@ -1246,11 +1351,15 @@ "pv": "Sistema fotovoltaico", "pvExport": "Injeção na Rede", "pvProduction": "Produção Solar", - "selfConsumption": "Autoconsumo" + "selfConsumption": "Autoconsumo", + "toggleDetails": "Detalhes do fluxo de energia" }, "heatingStatus": { "charging": "A aquecer…", "connected": "Standby.", + "minTemp": "Aquecimento mínimo até {temp}.", + "suggestionStartTooltip": "O otimizador recomenda que se ligue o aquecimento agora.", + "suggestionStopTooltip": "O otimizador recomenda interromper o aquecimento.", "vehicleLimit": "Limite do aquecedor", "waitForVehicle": "Pronto para aquecer…" }, @@ -1274,6 +1383,7 @@ "returnEnergy": "Energia (inversa)" } }, + "download": "Descarregar", "empty": "Não existem dados históricos disponíveis para este intervalo de tempo.", "group": { "battery": "Bateria", @@ -1295,12 +1405,15 @@ "emission": "Emissões", "fallbackName": "Posto de Carregamento", "finished": "Tempo de finalização", + "last24hEnergy": "Últimas 24 horas", + "last7dEnergy": "Últimos 7 dias", "power": "Potência", "price": "Custo", "remaining": "Tempo restante", "remoteDisabledHard": "{source}: desativado", "remoteDisabledSoft": "{source}: carregamento PV adaptável desativado", - "solar": "Solar" + "solar": "Solar", + "todayEnergy": "Hoje" }, "loadpointSettings": { "batteryBoost": { @@ -1318,12 +1431,17 @@ "batteryUsage": "Bateria de casa", "currents": "Corrente de carga", "default": "predefinido", + "heating": "Aquecimento", "maxCurrent": { "label": "Corrente de carga máxima" }, "minCurrent": { "label": "Corrente de carga mínima" }, + "minTemp": { + "description": "Além disso, a bateria {0} e a energia da rede também são utilizadas no modo solar para manter esta temperatura.", + "label": "Temperatura mínima" + }, "phasesConfigured": { "label": "Fases elétricas", "no1p3pSupport": "Como está ligado o seu carregador?", @@ -1406,6 +1524,20 @@ "unknown": "Veículo Convidado", "vehicleSoc": "Carga" }, + "vehicleSettings": { + "connectedTo": "conectado a {0}", + "description": "Estas configurações predefinidas são aplicadas quando o veículo se liga a um ponto de carregamento.", + "editHint": "Adicionar ou editar viaturas em {0}.", + "keepAsIs": "Manter tal como está", + "learnMore": "Saiba mais.", + "limitSoc": "Limite padrão", + "menu": "Viaturas", + "minSoc": "Carregamento mínimo", + "minSocDescription": "Carregamento rápido até este nível no modo solar.", + "mode": "Modo predefinido", + "notConnected": "não conectado", + "title": "Definições da viatura" + }, "vehicleStatus": { "awaitingAuthorization": "À espera de autorização.", "batteryBoost": "Boost da bateria ativo.", @@ -1432,6 +1564,8 @@ "pvEnable": "Excesso disponível. Carregamento em breve.", "scale1p": "Reduzindo para carregamento monofásico em breve.", "scale3p": "Aumentando para carregamento trifásico em breve.", + "suggestionStartTooltip": "O otimizador recomenda carregar agora.", + "suggestionStopTooltip": "O otimizador recomenda interromper o carregamento.", "targetChargeActive": "Plano de carregamento ativo. Fim estimado em {duration}.", "targetChargePlanned": "O plano de carregamento começa em {duration}.", "targetChargeWaitForVehicle": "Plano de carregamento pronto. A aguardar veículo…", @@ -1471,6 +1605,7 @@ }, "session": { "addOdometer": "Adicionar um valor", + "addedRange": "Autonomia adicional", "cancel": "Cancelar", "co2": "CO₂", "date": "Período", @@ -1481,11 +1616,12 @@ "meterstop": "Contagem final", "odometer": "Quilómetros", "price": "Custo", - "soc": "SoC do veículo", + "soc": "Carga", "started": "Iniciado", "title": "Sessão de carga" }, "sessions": { + "addedRange": "Autonomia", "avgPower": "Potência", "avgPrice": "Preço ⌀", "chargeDuration": "Duração", @@ -1511,6 +1647,7 @@ }, "co2": "CO₂", "csv": { + "addedrange": "Autonomia adicional (km)", "chargedenergy": "Energia (kWh)", "chargeduration": "Duração", "co2perkwh": "CO₂/kWh", @@ -1529,6 +1666,8 @@ "vehicle": "Veículo" }, "date": "Início", + "download": "Descarregar {period}", + "downloadTotal": "Descarregar total", "energy": "Carregado", "filter": { "allLoadpoints": "todos os postos de carregamento", diff --git a/i18n/ro.json b/i18n/ro.json index a6ade3516..ed5a12745 100644 --- a/i18n/ro.json +++ b/i18n/ro.json @@ -1011,7 +1011,6 @@ }, "planNumber": "Planul {number}", "precondition": { - "description": "Încărcați {duration} înainte de plecare pentru precondiționarea bateriei.", "label": "Încărcare întârziată", "optionAll": "totul", "optionNo": "nu" @@ -1021,7 +1020,6 @@ "repeatingPlans": "Planuri repetitive", "selectAll": "Selectați tot", "strategyDisabledDescription": "Încărcarea începe cât mai târziu posibil, pentru a se termina chiar înainte de plecare. Cu prețuri dinamice la rețea sau tariful CO₂, aici sunt disponibile mai multe opțiuni.", - "strategySettings": "Setări strategie", "time": "Timp", "title": "Plan", "unsavedChanges": "Există modificări nesalvate. Doriți să le aplicați acum?", diff --git a/i18n/ru.json b/i18n/ru.json index 848d8cdef..61f02f331 100644 --- a/i18n/ru.json +++ b/i18n/ru.json @@ -8,7 +8,7 @@ "buttonConnect": "Подключиться к {provider}", "buttonDisconnect": "Отключить", "confirmLogout": "Вы уверены, что хотите отключить {title}?", - "connect": "подключать", + "connect": "подключить", "disconnect": "отключить", "loggedOut": "Успешно вышел из системы", "logoutFailed": "Не удалось выйти из системы", @@ -19,42 +19,114 @@ "successCloseTab": "Теперь вы можете закрыть эту вкладку.", "title": "Статус авторизации" }, + "battery": { + "card": { + "charging": "зарядка аккумулятора", + "discharging": "разрядка аккумулятора", + "energy": "Энергия", + "gridCharge": "зарядка от сети", + "hold": "запретить разрядку", + "holdCharge": "запретить зарядку", + "ofTotal": "от {total}", + "power": "Мощность", + "soc": "Уровень заряда аккумулятора" + }, + "combined": "Комбинированный", + "config": { + "buffer": "Энергия домашнего аккумулятора выше {soc} может использоваться для питания зарядных станций и задерживает отключение в солнечном режиме. Автоматически запускать зарядку от солнца при {start}.", + "bufferNone": "Энергия домашнего аккумулятора не используется для зарядных станций. Установите значение ниже {soc} для включения поддержки.", + "bufferStart": { + "above": "как только уровень заряда аккумулятора превысит {soc}", + "full": "как только аккумулятор будет полностью заряжен", + "never": "только при достаточном избытке" + }, + "bufferTitle": "Аккумулятор в качестве буфера для зарядки", + "discharge": "Запретить разрядку домашнего аккумулятора в быстром режиме и во время зарядки по расписанию.", + "gridDischarge": "Разрешить разрядку домашнего аккумулятора в сеть.", + "priority": "Приоритет отдаётся домашнему аккумулятору, пока его заряд ниже {soc}. После этого избыток направляется на зарядные станции.", + "priorityNone": "Солнечный избыток поступает напрямую на зарядные станции. Установите значение выше {soc} для приоритета домашнего аккумулятора.", + "priorityTitle": "Куда в первую очередь поступает избыток?" + }, + "optimizer": { + "action": { + "charge": "Зарядка от сети", + "discharge": "Разрядка в сеть", + "hold": "Запретить разрядку", + "holdcharge": "Запретить зарядку", + "normal": "Нормальный режим" + }, + "empty": "пустой", + "full": "полный", + "highest": "Максимальный ({value})", + "lowest": "Минимальный ({value})", + "suggestion": "Рекомендуемое значение" + } + }, "batterySettings": { - "batteryLevel": "Уровень заряда батареи", + "batteryLevel": "Уровень заряда аккумулятора", "bufferStart": { "above": "когда выше {soc}.", "full": "когда на {soc}.", "never": "только при наличии достаточного излишка." }, "capacity": "{energy} из {total}", - "control": "Управление батареей", + "control": "Управление аккумулятором", "discharge": "Предотвратить разряд в быстром режиме и запланированной зарядке.", "disclaimerHint": "Примечание:", "disclaimerText": "Эти настройки влияют только на режим солнечной батареи. Поведение зарядки изменяется соответствующе.", - "gridChargeTab": "Зарядка от сети", - "legendBottomName": "домашний приоритет", + "gridChargeTab": "Зарядка сети", + "legendBottomName": "Приоритет зарядки домашнего аккумулятора", "legendBottomSubline": "пока не достигнет {soc}.", - "legendMiddleName": "сначала автомобиль", - "legendMiddleSubline": "когда домашняя батарея выше {soc}.", - "legendTopAutostart": "запускается автоматически", - "legendTopName": "зарядка с поддержкой от аккумулятора", - "legendTopSubline": "когда домашняя батарея выше {soc}.", - "modalTitle": "Настройки батареи", - "usageTab": "Использование батареи" + "legendMiddleName": "Приоритет зарядки электромобиля", + "legendMiddleSubline": "когда домашний аккумулятор заряжен выше {soc}.", + "legendTopAutostart": "Автоматический запуск", + "legendTopName": "Зарядка автомобиля с поддержкой от аккумулятора", + "legendTopSubline": "когда домашний аккумулятор заряжен выше {soc}.", + "legendTopSublineAbove": "когда заряд выше {soc}", + "legendTopSublineDisabled": "составляет {soc}.", + "legendTopSublineDisabledState": "неактивно", + "modalTitle": "Домашний Аккумулятор", + "noBattery": "Аккумуляторы не настроены.", + "usageTab": "Использование аккумулятора" + }, + "colors": { + "auto": "Авто", + "hex": "Hex цвет" }, "config": { + "adminPassword": { + "description": "Скрипт-плагин выполняет команду на уровне системы с правами процесса evcc. Для продолжения введите пароль администратора повторно.", + "invalid": "Неверный пароль. Попробуйте снова.", + "title": "Потенциально опасная операция" + }, + "apiKey": { + "description": "Предоставьте скриптам и задачам автоматизации (например, резервному копированию по расписанию) безопасный доступ без необходимости вводить пароль администратора.", + "exampleLabel": "Попробуйте: загрузите резервную копию с помощью curl", + "generate": "Сгенерировать ключ API", + "generateConfirm": "Введите свой пароль администратора, чтобы сгенерировать новый API-ключ.", + "keyLabel": "API Ключ", + "regenerate": "Сгенерировать новый API-ключ", + "regenerateConfirm": "Введите пароль администратора, чтобы сгенерировать новый API-ключ.", + "regenerateLink": "Сгенерировать новый", + "regenerateWarning": "При генерации нового ключа существующий ключ станет недействительным, и любая автоматизация, использующая его, перестанет работать. Продолжить?", + "revealSuccess": "Ваш новый API-ключ готов к использованию.", + "revealTitle": "Создан ключ API", + "shownOnce": "Этот ключ показывается только один раз. Сохраните его в надёжном месте. Восстановить его позже будет невозможно.", + "title": "API Ключ" + }, "aux": { - "description": "Устройство, которое регулирует потребление в зависимости от доступного избытка (например, интеллектуальные водонагреватели). evcc ожидает, что это устройство снизит потребление энергии в случае необходимости.", - "titleAdd": "Добавить саморегулирующийся потребительский", - "titleEdit": "Редактировать Саморегулирующийся потребитель" + "description": "Устройство, которое регулирует своё потребление в зависимости от доступного избытка энергии (например, умные водонагреватели). evcc ожидает, что это устройство при необходимости снижает свою мощность.", + "titleAdd": "Добавить саморегулируемого Потребителя", + "titleEdit": "Редактировать саморегулируемого потребителя" }, "battery": { - "titleAdd": "Добавить батарею", - "titleEdit": "Редактировать батарею" + "title": "Аккумулятор", + "titleAdd": "Добавить аккумулятор", + "titleEdit": "Редактировать аккумулятор" }, "charge": { - "titleAdd": "Добавить счетчик заряда", - "titleEdit": "Редактировать счетчик заряда" + "titleAdd": "Добавить счётчик зарядки", + "titleEdit": "Редактировать счётчик зарядки" }, "charger": { "chargers": "Зарядные устройства для электромобилей", @@ -73,12 +145,12 @@ "switchsockets": "Переключаемые розетки", "template": "Производитель", "titleAdd": { - "charging": "Добавить зарядное устройство", - "heating": "Добавить нагреватель" + "charging": "Добавить Зарядное устройство", + "heating": "Добавить Нагревательное Устройство" }, "titleEdit": { "charging": "Редактировать зарядное устройство", - "heating": "Редактировать нагреватель" + "heating": "Редактировать Нагревательное Устройство" }, "type": { "custom": { @@ -86,8 +158,8 @@ "heating": "Пользовательский нагреватель" }, "heatpump": "Пользовательский тепловой насос", - "sgready": "Пользовательский тепловой насос (поддерживается через плагины)", - "sgready-boost": "Пользовательский тепловой насос (sg-ready-boost, устарело)", + "sgready": "Пользовательский тепловой насос (sg-ready через плагины)", + "sgready-boost": "Пользовательский тепловой насос (sg-ready-boost, устаревший)", "sgready-relay": "Пользовательский тепловой насос (sg-ready через реле)", "switchsocket": "Пользовательский переключатель" } @@ -95,60 +167,90 @@ "circuits": { "description": "Обеспечивает, чтобы сумма всех точек нагрузки, подключенных к цепи, не превышала настроенные пределы мощности и тока. Цепи могут быть вложенными для построения иерархии.", "title": "Управление нагрузкой", - "usableMeters": "Используемые метрические ссылки" + "usableMeters": "Ссылки на доступные счётчики" + }, + "consumer": { + "description": "Отслеживает небольшие неуправляемые приборы (холодильник, стиральная машина и т. д.). Даёт более полное представление о потреблении в доме.", + "titleAdd": "Добавить счётчик потребителя", + "titleEdit": "Редактировать счётчик потребителя" }, "control": { "description": "Обычно значения по умолчанию подходят. Изменяйте их только в том случае, если знаете, что делаете.", "descriptionInterval": "Цикл обновления в секундах. Определяет, как часто evcc считывает данные счетчика и корректирует зарядку. Значение по умолчанию 30 секунд является безопасным выбором. Устройствам, таким как транспортные средства, настенные зарядные устройства и инверторы, обычно требуется несколько секунд для корректировки своего поведения. Если ваши компоненты реагируют быстро, вы можете использовать более низкие значения. Мы настоятельно рекомендуем не опускаться ниже 10 секунд. Если вы наблюдаете нестабильное поведение системы управления или скачки значений мощности, выберите более длительный интервал.", - "descriptionResidualPower": "Сдвигает точку работы контура управления. Если у вас есть домашняя батарея, рекомендуется установить значение 100 Вт. Таким образом, батарея получит небольшой приоритет по сравнению с использованием электросети.", + "descriptionResidualPower": "Смещает рабочую точку контура регулирования. При наличии домашнего аккумулятора рекомендуется устанавливать значение 100 Вт. Это обеспечит небольшой приоритет батареи перед потреблением из сети.", "labelInterval": "Интервал обновления", "labelResidualPower": "Остаточная мощность", - "title": "Контрольное поведение" + "title": "Поведение управления" + }, + "currency": { + "description": "Используется для форматирования цен на электроэнергию, расходов и экономии на основе вашего тарифа.", + "example": "Ваша стоимость зарядки составила {price}. Вы сэкономили {amount}.", + "label": "Валюта", + "title": "Валюта" }, "deviceValue": { + "activeClients": "Активные клиенты", "amount": "Сумма", + "asleep": "спит", "broker": "Брокер", - "bucket": "Bucket", - "capacity": "Вместимость", + "bucket": "Группа", + "capacity": "Ёмкость", "chargeStatus": "Статус", "chargeStatusA": "не подключено", - "chargeStatusB": "связанный", + "chargeStatusB": "подключено", "chargeStatusC": "зарядка", "chargeStatusE": "нет питания", "chargeStatusF": "ошибка", - "chargedEnergy": "Заряженный", - "co2": "Сеть CO₂", - "configured": "Настроенный", - "connections": "Соединения", - "controllable": "Управляемый", + "chargedEnergy": "Заряжено", + "co2": "CO₂ сети", + "configured": "Настроено", + "connected": "Подключено", + "connections": "Подключения", + "controllable": "Управляемое", "currency": "Валюта", "current": "Текущий", "currentRange": "Текущий", + "curtailLimit": "Лимит выдачи в сеть", + "curtailable": "Сокращаемое", + "curtailed": "Ограничена выдача в сеть", "detected": "Обнаружено", - "dimmed": "Приглушенный", + "dimLimit": "Ограничение на потребление", + "dimmed": "Ограничено потребление", "enabled": "Включено", "energy": "Энергия", - "feedinPrice": "Цена отбора электроэнергии", - "gridPrice": "Цена сетки", + "events": "События", + "exportLimit": "Лимит экспорта в сеть", + "feedinPrice": "Цена выдачи в сеть", + "forecast": "Прогноз", + "gridPrice": "Цена из сети", "heaterTempLimit": "Ограничение нагревателя", "identifier": "RFID-идентификатор", + "loginBlocked": "Достигнут лимит входов", + "max": "максимум", + "messengers": "Сервисы", "no": "нет", "odometer": "Одометр", "org": "Организация", + "outdoorTemp": "Наружная температура", "phaseCurrents": "Текущие", "phasePowers": "Мощность", "phaseVoltages": "Напряжение", - "phases1p3p": "Фазовый переключатель", + "phases1p3p": "Переключение фаз", "power": "Мощность", "powerRange": "Мощность", + "price": "Цена", + "productionLimited": "Производство энергии ограничено", "range": "Диапазон", + "remoteEnabled": "Включено", + "returnEnergy": "Энергия (реверсивная)", "singlePhase": "Однофазный", "soc": "Заряд", - "solarForecast": "Солнечный прогноз", + "solarForecast": "Прогноз солнечной генерации", + "switchDevice": "Управление током", "temp": "Температура", - "topic": "Тема", + "topic": "Топик", "url": "URL", - "vehicleLimitSoc": "Лимит заряда", + "vehicleLimitSoc": "Лимит зарядки", "yes": "да" }, "deviceValueChargeStatus": { @@ -158,7 +260,7 @@ }, "devices": { "auxMeter": "Умный потребитель", - "batteryStorage": "Аккумуляторное хранилище", + "batteryStorage": "Аккумуляторный накопитель", "consumer": "Потребитель", "solarSystem": "Солнечная система" }, @@ -166,21 +268,46 @@ "loading": "Загрузка редактора YAML…" }, "eebus": { - "description": "Конфигурация, позволяющая evcc обмениваться данными с другими устройствами EEBus.", - "title": "EEBus" + "certificate": { + "private": "Приватный ключ", + "public": "Публичный сертификат", + "title": "Сертификаты" + }, + "configuredDevices": "Настроенные устройства", + "description": "Конфигурация, позволяющая evcc обмениваться данными с устройствами, совместимыми с EEBus, например с зарядными станциями или управляющим блоком вашего сетевого оператора. Все необходимые этапы инициализации и генерации сертификатов выполняются автоматически при первом запуске.", + "descriptionAdvanced": "Изменения не требуются. Вносите изменения, только если вы точно знаете, что делаете. При изменении SHIP-ID или сертификатов потребуется повторно выполнить сопряжение устройств.", + "interfaces": "Интерфейсы", + "interfacesHelp": "Чтобы избежать проблем со связью, ограничьте сетевые интерфейсы для EEBus. Если поле пустое, будут использоваться все интерфейсы. Каждый интерфейс указывайте с новой строки.", + "noConfiguredDevices": "Нет устройств, настроенных по SKI.", + "noPairings": "Нет сопряжённых устройств.", + "pairings": "Сопряжённые устройства", + "port": "Порт", + "portHelp": "Порт для использования.", + "qr": "Код сопряжения", + "qrExplain": "Чтобы выполнить автоматическое сопряжение без ручного ввода SKI, отсканируйте это устройство с другого устройства EEBus в процессе установки.", + "removeConfirm": "Вся конфигурация EEBus будет удалена. При следующем запуске будут сгенерированы новые сертификаты и идентификаторы. Вы уверены?", + "removePairing": "Удалить сопряжение", + "removePairingConfirm": "Устройство будет отключено, и необходимо будет заново выполнить сопряжение. Вы уверены?", + "shipid": "SHIP-ID", + "shipidExplain": "Постоянный идентификатор устройства для распознавания в сети EEBus.", + "shipidHelp": "Этот SHIP-ID привязан к сертификатам, указанным ниже.", + "ski": "SKI", + "skiExplain": "Уникальный ключ безопасности для сопряжения устройств EEBus.", + "title": "ЕЕBus" }, "experimental": { - "description": "Включить пользовательский интерфейс для функций, которые все еще тестируются и могут измениться в будущих версиях.", + "description": "Предоставляет ранний доступ к функциям, которые ещё проходят тестирование. Они могут быть нестабильными и могут быть изменены или удалены в будущих версиях.", "title": "Экспериментальный" }, "ext": { - "description": "Регистрирует значения энергии неконтролируемых потребителей (например, холодильник, стиральная машина и т. д.) для статистических целей. Эти счетчики также могут использоваться для управления нагрузкой (например, субдистрибуция).", - "titleAdd": "Добавить счетчик потребления", - "titleEdit": "Редактировать счетчик потребления" + "description": "Отслеживает счётчики, которые не используются для регулирования и не отображаются в интерфейсе (субраспределительные щиты, дополнительные сетевые или солнечные счётчики и т. д.). Их данные, как и данные других счётчиков, записываются в журнал и экспортируются.", + "titleAdd": "Добавить дополнительный счётчик", + "titleEdit": "Редактировать дополнительный счётчик" }, "form": { "danger": "Опасность", "deprecated": "устаревший", + "durationUnit": "Единица времени для {label}", "example": "Пример", "optional": "опционально" }, @@ -189,28 +316,38 @@ "authPerform": "Связаться с {provider}", "authPerformHint": "Откроется в новой вкладке. Вернитесь сюда, чтобы продолжить.", "authPrepare": "Подготовьте соединение", - "cancel": "Отменить", + "cancel": "Отмена", + "change": "Изменение", "clear": "Ясно", "close": "Закрыть", + "confirmDisable": "Отключить устройство? После перезапуска оно не будет учитываться.", + "confirmEnable": "Снова включить это устройство?", + "confirmSave": "Имеются несохранённые изменения. Сохранить сейчас?", "copied": "Скопировано!", "copy": "Копировать", "customHelp": "Создайте пользовательское устройство с помощью системы плагинов evcc.", "customOption": "Определяемое пользователем устройство", "delete": "Удалить", + "disable": "Oтключить", + "disabled": "неактивно", + "dismiss": "Отклонить", "docsLink": "См. документацию.", "dragHandle": "Ручка для перетаскивания", "dragItem": "Перетаскиваемый: {title}", "dragList": "Список, который можно переупорядочить", + "enable": "Включить", + "error": "Ошибка", "experimental": "Экспериментальный", "forceSave": "Сохранить в любом случае", "fromYamlHint": "Примечание: Настраивается через evcc.yaml. Удалите запись из файла, чтобы включить редактирование здесь.", "hideAdvancedSettings": "Скрыть расширенные настройки", "invalidFileSelected": "Выбран недействительный файл", + "legacy": "устаревшее", "noFileSelected": "Файл не выбран.", "off": "выкл.", "on": "вкл.", "readFromFile": "Чтение из файла", - "remove": "Удалить", + "remove": "Убрать", "required": "требуемый", "reset": "Сброс", "save": "Сохранить", @@ -226,10 +363,11 @@ }, "grid": { "title": "Сетевой счетчик", - "titleAdd": "Добавить счетчик электроэнергии", - "titleEdit": "Редактировать счетчик электроэнергии" + "titleAdd": "Добавить Счетчик электроэнергии", + "titleEdit": "Редактировать Счетчик Электроэнергии" }, "hems": { + "changeConfirm": "Текущие настройки будут удалены. Продолжить?", "csv": { "created": "Создано", "finished": "Завершено", @@ -237,10 +375,26 @@ "limitpower": "Предел (кВт)", "type": "Тип" }, - "description": "Ограничение мощности внешними системами (например, §14a EnWG, §9 EEG интерфейс или система управления энергопотреблением более высокого уровня). Работает совместно с функцией управления нагрузкой.", + "customOption": "Пользовательская интеграция", + "description": "Ограничение мощности внешними системами (например, §14a EnWG, интерфейс §9 EEG или вышестоящая система управления энергопотреблением). Работает совместно с функцией управления нагрузкой. Для отдельных устройств также поддерживается ограничение выработки солнечной энергии. Фактический предел ограничения должен быть задан в конфигурации каждого инвертора.", + "download": "Загрузить", + "dynamicLimits": "Динамические лимиты", "eventsRecorded": "Зарегистрировано {count} событий ограничения сетки.", + "exportLimit": "Лимит экспорта в сеть", + "exportLimitDescription": "Чтобы не терять солнечную энергию, оптимизатор батареи соблюдает это ограничение. Однако сигнал урезания от динамических лимитов имеет более высокий приоритет.", + "exportLimitHint": "Статический лимит выдачи в сеть в точке присоединения.", + "generic": "Общие интеграции", + "integrations": "Интеграции", "lastEvent": "Последнее {timeAgo}.", - "title": "Внешний предел" + "noEvents": "События пока не записаны.", + "recordedEvents": "Записанные события", + "staticLimits": "Статические лимиты", + "template": "Интеграция", + "title": "Внешний предел", + "type": { + "custom": "Пользовательская интеграция", + "relay": "Пользовательская интеграция (реле)" + } }, "icon": { "change": "изменение", @@ -249,7 +403,7 @@ "influx": { "description": "Записывает данные о зарядке и другие показатели в InfluxDB. Используйте Grafana или другие инструменты для визуализации данных.", "descriptionToken": "Ознакомьтесь с документацией InfluxDB, чтобы узнать, как его создать. https://docs.influxdata.com/influxdb/v2/admin/", - "labelBucket": "Bucket", + "labelBucket": "Группа", "labelCheckInsecure": "Разрешить самоподписанные сертификаты", "labelDatabase": "База данных", "labelInsecure": "Проверка сертификата", @@ -258,24 +412,25 @@ "labelToken": "Токен API", "labelUrl": "URL", "labelUser": "Имя пользователя", - "title": "InfluxDB", + "title": "InfluхDB", "v1Support": "Нужна поддержка для InfluxDB 1.x?", "v2Support": "Вернуться к InfluxDB 2.x" }, "loadpoint": { "addCharger": { "charging": "Добавить зарядное устройство", - "heating": "Добавить нагреватель" + "heating": "Добавить нагревательное устройство" }, "addMeter": "Добавить специальный счетчик энергии", - "cancel": "Отменить", + "advancedConfig": "Расширенные настройки", + "cancel": "Отмена", "chargerError": { "charging": "Требуется настройка зарядного устройства.", - "heating": "Требуется настройка нагревателя." + "heating": "Требуется настройка нагревательного устройства." }, "chargerLabel": { "charging": "Зарядное устройство", - "heating": "Нагреватель" + "heating": "Нагревательное устройство" }, "chargerPower11kw": "11 kW", "chargerPower11kwHelp": "Будет использовать ток в диапазоне от 6 до 16 А.", @@ -289,26 +444,39 @@ "circuitInvalid": "Цепь не существует", "circuitLabel": "Схема", "circuitUnassigned": "неназначенный", + "created": "{name} создан и использует оптимальные настройки по умолчанию.", + "createdTitle": { + "charging": "Зарядное устройство создано", + "heating": "Нагревательное устройство создано" + }, + "creating": { + "charging": "Данные зарядной станции сохранены. Идёт создание зарядной станции …", + "heating": "Данные нагревательного устройства сохранены. Идёт создание нагревателя …" + }, "defaultModeHelp": { "charging": "Режим зарядки при подключении автомобиля.", "heating": "Устанавливается при запуске системы." }, "defaultModeHelpKeep": "Сохраняет последний выбранный режим.", + "defaultModeKeep": "оставить как есть", "defaultModeLabel": "Режим по умолчанию", "delete": "Удалить", "electricalSubtitle": "Если у вас есть сомнения, обратитесь к электрику.", - "electricalTitle": "Электрический", + "electricalTitle": "Электрика", "energyMeterHelp": "Дополнительный счетчик, если зарядное устройство не имеет встроенного.", "energyMeterLabel": "Энергосчетчик", "estimateLabel": "Интерполировать уровень заряда между обновлениями API", "maxCurrentHelp": "Должно быть больше минимального тока.", "maxCurrentLabel": "Максимальный ток", - "minCurrentHelp": "Снижайте нагрузку ниже 6 А только в том случае, если вы знаете, что делаете.", + "maxTempHelp": "Должно быть больше минимальной температуры.", + "maxTempLabel": "Максимальная температура", + "minCurrentHelp": "Уменьшайте ток ниже 6 А только если вы уверены в своих действиях.", "minCurrentLabel": "Минимальный ток", + "minTempLabel": "Минимальная температура", "noVehicles": "Ни один автомобиль не настроен.", "option": { "charging": "Добавить точку зарядки", - "heating": "Добавить нагревательное устройство" + "heating": "Добавить нагреватель" }, "phases1p": "1-фазный", "phases3p": "3-фазный", @@ -323,7 +491,7 @@ "pollModeAlwaysHelp": "Всегда запрашивайте обновления статуса через регулярные промежутки времени.", "pollModeCharging": "зарядка", "pollModeChargingHelp": "Запрашивайте обновления статуса транспортного средства только во время зарядки.", - "pollModeConnected": "связанный", + "pollModeConnected": "подключено", "pollModeConnectedHelp": "Обновлять статус транспортного средства через регулярные промежутки времени при подключении.", "pollModeLabel": "Обновление поведения", "priorityHelp": "Более высокий приоритет дает преимущественный доступ к избытку солнечной энергии.", @@ -331,9 +499,11 @@ "save": "Сохранить", "solarBehaviorCustomHelp": "Определите свои собственные пороговые значения и задержки для включения и отключения.", "solarBehaviorDefaultHelp": "Начать после {enableDelay} достаточного излишка. Остановить, когда излишка не хватает для {disableDelay}.", - "solarBehaviorLabel": "Солнечная энергия", + "solarBehaviorLabel": "Солнечная", "solarModeCustom": "пользовательский", "solarModeMaximum": "максимальная солнечная", + "temperatureRangeHelp": "Диапазон шкалы температуры для отображения. Не влияет на управление.", + "temperatureRangeTitle": "Диапазон температур", "thresholdDisableDelayLabel": "Отключить задержку", "thresholdDisableHelpInvalid": "Пожалуйста, используйте положительное значение.", "thresholdDisableHelpPositive": "Останавливаться, когда из сети потребляется более {power} в течение {delay}.", @@ -346,13 +516,13 @@ "thresholdEnableLabel": "Включить питание от сети", "titleAdd": { "charging": "Добавить зарядную станцию", - "heating": "Добавить нагревательное устройство", - "unknown": "Добавить зарядное устройство или нагреватель" + "heating": "Добавить Нагреватель", + "unknown": "Добавить Зарядное Устройство или Нагреватель" }, "titleEdit": { "charging": "Редактировать точку зарядки", - "heating": "Редактировать нагревательное устройство", - "unknown": "Редактировать зарядное устройство или нагреватель" + "heating": "Редактировать Нагреватель", + "unknown": "Редактировать Зарядное Устройство или Нагреватель" }, "titleExample": { "charging": "Гараж, навес для автомобиля и т. д.", @@ -360,40 +530,115 @@ }, "titleLabel": "Название", "vehicleAutoDetection": "автоматическое обнаружение", - "vehicleHelpAutoDetection": "Автоматически выбирает наиболее вероятный транспортный средство. Возможен ручной переход в ручной режим.", + "vehicleHelpAutoDetection": "Автоматически выбирает наиболее вероятный автомобиль. Возможен переход в ручной режим.", "vehicleHelpDefault": "Всегда предполагайте, что этот автомобиль заряжается здесь. Автоматическое обнаружение отключено. Возможно ручное переключение.", - "vehicleInvalid": "Транспортное средство не существует", + "vehicleInvalid": "Автомобиля не существует", "vehicleLabel": "Стандартный автомобиль", - "vehiclesTitle": "Транспортные средства" + "vehiclesTitle": "Автомобили" }, "main": { "addAdditional": "Добавить дополнительный счетчик", - "addGrid": "Добавить счетчик сетки", + "addConsumer": "Добавить потребителя", + "addGrid": "Добавить счетчик электроэнергии", "addLoadpoint": "Добавить зарядное устройство или нагреватель", "addPvBattery": "Добавить солнечную батарею или аккумулятор", "addTariffs": "Добавить тарифы", - "addVehicle": "Добавить транспортное средство", + "addVehicle": "Добавить автомобиль", "configured": "настроенный", "edit": "редактировать", "loadpointRequired": "Необходимо настроить как минимум одну точку зарядки.", "name": "Имя", "title": "Конфигурация", "unconfigured": "не настроен", - "vehicles": "Мои транспортные средства", + "vehicles": "Мои автомобили", "welcomeBannerText": "Начните с создания хотя бы одного **зарядного устройства**, **нагревателя**, **сети**, **солнечной батареи**, **аккумулятора** или **дополнительного счетчика**. Если вы просто хотите протестировать, выберите **демонстрационное устройство**.", "welcomeBannerTitle": "Давайте настроим вашу систему!" }, + "mcp": { + "description": "Запускает сервер Model Context Protocol, который позволяет AI-ассистентам (например, Claude) считывать состояние системы и управлять зарядкой.", + "exampleLabel": "Пример: Claude CLI", + "restartHint": "Будет доступно после перезапуска.", + "title": "MCP Сервер", + "url": "Адрес MCP-сервера" + }, "messaging": { - "description": "Получайте сообщения о ваших сеансах зарядки.", + "addMessenger": "Добавить сервис", + "description": "Получайте уведомления о ваших сессиях зарядки.", + "event": { + "asleep": { + "messageDefault": "Зарядка освобождена, автомобиль {vehicleName} не заряжается.", + "title": "Ожидание автомобиля", + "titleDefault": "Автомобиль в спящем режиме" + }, + "connect": { + "messageDefault": "Автомобиль подключён, {pvPower} кВт от солнечной энергии", + "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": "Зарядка завершена: {chargedEnergy} кВт·ч за {chargeDuration}.", + "title": "Когда зарядка останавливается", + "titleDefault": "Зарядка завершена" + }, + "suggestion": { + "messageDefault": "Предложение оптимизатора для {suggestionTitle}: если бы управление оптимизатором было активно, сейчас было бы применено действие «{suggestionAction}». Это только рекомендация, никаких изменений не внесено. Экспериментальная функция — отзывы приветствуются по ссылке https://github.com/evcc-io/evcc/issues/31903", + "title": "Когда изменяется рекомендация оптимизатора", + "titleDefault": "Предложение оптимизатора" + } + }, + "eventMessage": "Сообщение", + "eventTitle": "Название", + "events": "События", + "legacyWarning": "Новая настройка уведомлений готова! Чтобы воспользоваться новым мастером настройки, удалите и сохраните существующую конфигурацию.", + "messengers": "Сервисы", + "seePlaceholders": "см. переменные", "title": "Уведомления" }, + "messenger": { + "custom": "Пользовательский сервис", + "generic": "Общий сервис", + "primary": "Специфический сервис", + "template": "Сервис", + "titleAdd": "Добавить Сервис", + "titleEdit": "Редактировать сервис" + }, "meter": { - "cancel": "Отменить", + "cancel": "Отмена", + "convertToConsumer": { + "confirm": "Преобразует этот «дополнительный счётчик» в «потребителя».", + "link": "Преобразовать в потребителя" + }, "delete": "Удалить", "generic": "Общие интеграции", "option": { - "aux": "Добавить саморегулирующийся потребитель", + "aux": "Добавить саморегулируемого потребителя", "battery": "Добавить индикатор заряда батареи", + "consumer": "Добавить обычного потребителя", "pv": "Добавить солнечный счетчик" }, "save": "Сохранить", @@ -402,34 +647,38 @@ "titleChoice": "Что вы хотите добавить?", "titleLabel": "Название", "usage": { - "aux": "Саморегулирующийся потребитель", + "aux": "Саморегулируемый потребитель", "battery": "Аккумулятор", - "charge": "Потребитель / Зарядное устройство", - "grid": "Сетка", + "charge": "Зарядное устройство", + "grid": "Сеть", "label": "Использование", - "pv": "Производство" + "pv": "Генерация" }, "validateSave": "Проверить и сохранить" }, "modbus": { "baudrate": "Скорость передачи данных", - "comset": "ComSet", + "comset": "Настройки связи", "connection": "Подключение Modbus", "connectionHintSerial": "Устройство подключается напрямую через RS485 (или адаптер USB-RS485).", "connectionHintTcpip": "Доступ к устройству осуществляется через сеть (LAN/WiFi).", "connectionValueSerial": "RS485", - "connectionValueTcpip": "Сеть", + "connectionValueTcpip": "Cеть", + "delay": "Задержка", + "delayHint": "Необязательная задержка перед чтением регистра. Требуется только для медленных устройств.", "device": "Название устройства", "deviceHint": "Пример: /dev/ttyUSB0", "host": "IP-адрес или имя хоста", "hostHint": "Пример: 192.0.2.2", "id": "Идентификатор Modbus", - "port": "порт", - "protocol": "протокол Modbus", + "port": "Порт", + "protocol": "Протокол Modbus", "protocolHintRtu": "Подключение через адаптер RS485 к Ethernet без преобразования протокола.", "protocolHintTcp": "Устройство имеет встроенную поддержку LAN/Wifi или подключено через адаптер RS485 к Ethernet с преобразованием протокола.", "protocolValueRtu": "RTU", - "protocolValueTcp": "TCP" + "protocolValueTcp": "TCP", + "timeout": "Время ожидания", + "timeoutHint": "Опциональный лимит времени на ожидание ответа устройства." }, "modbusproxy": { "add": "Добавить прокси-соединение", @@ -458,7 +707,7 @@ "descriptionClientId": "Автор сообщений. Если поле пустое, используется `evcc-[rand]`.", "descriptionTopic": "Оставьте поле пустым, чтобы отключить публикацию.", "labelBroker": "Брокер", - "labelCaCert": "Серверный сертификат (CA)", + "labelCaCert": "Сертификат сервера (CA)", "labelCheckInsecure": "Разрешить самоподписанные сертификаты", "labelClientCert": "Сертификат клиента", "labelClientId": "Идентификатор клиента", @@ -467,7 +716,7 @@ "labelPassword": "Пароль", "labelTopic": "Тема", "labelUser": "Имя пользователя", - "publishing": "Издательское дело", + "publishing": "Публикация", "title": "MQTT" }, "network": { @@ -479,10 +728,16 @@ "labelHost": "Имя хоста mDNS", "labelInternalUrl": "Внутренний URL", "labelPort": "Порт", - "title": "Сеть" + "title": "Cеть", + "warningUrlPath": "Обычно адрес не содержит пути. Вы уверены, что это правильно?" }, "ocpp": { + "forwardingConfigured": "Переадресация настроена", + "forwardingError": "Ошибка переадресации", + "forwardingOff": "Переадресация не сконфигурирована", "noChargers": "Не обнаружено зарядных устройств OCPP.", + "stations": "ID станций", + "stationsHelp": "Настроенные и обнаруженные зарядные станции OCPP. Помимо локального управления, вы можете перенаправлять сообщения на внешний сервер (например, для биллинга).", "status": { "configured": "Не подключено", "connected": "Подключено", @@ -492,14 +747,44 @@ "url": "URL сервера", "urlHelp": "Скопируйте этот URL-адрес в настройки зарядного устройства. Подробности см. в руководстве производителя. Зарядное устройство должно автоматически добавлять свой уникальный идентификатор (ID станции) к URL-адресу. В редких случаях может потребоваться вручную указать идентификатор. Пример: `{url}`" }, + "ocppforwarder": { + "description": "Перенаправлять OCPP-сообщения этой зарядной станции на внешний сервер (биллинговая платформа, системный оператор и т. д.) в дополнение к evcc.", + "editTitle": "Переадресация OCPP", + "labelCaCert": "Сертификат сервера (CA)", + "labelCheckInsecure": "Разрешить самоподписанные сертификаты", + "labelInsecure": "Проверка сертификата", + "password": "Пароль", + "passwordHelp": "Для HTTP Basic-аутентификации на вышестоящем сервере.", + "readOnly": { + "check": "Блокировать команды от вышестоящего сервера", + "help": { + "false": "Вышестоящий сервер может отправлять команды на зарядную станцию через evcc.", + "true": "Вышестоящий сервер получает все данные с зарядной станции, но не имеет права отправлять команды. Управление полностью остаётся за evcc." + }, + "label": "Команды от вышестоящего сервера" + }, + "status": "Статус", + "upstreamStationId": "ID станции", + "upstreamStationIdHelp": "Идентификатор зарядной станции, отправляемый на вышестоящий сервер.", + "upstreamUrl": "Адрес вышестоящего сервера", + "upstreamUrlHelp": "WebSocket-адрес OCPP внешнего сервера.", + "username": "Имя пользователя", + "usernameHelp": "Для HTTP Basic-аутентификации на вышестоящем сервере." + }, + "optimizer": { + "description": "Анализирует прогноз солнечной генерации, цены на электроэнергию и ваше потребление, чтобы оптимизировать стратегию работы батареи и зарядки. Данные отправляются в сервис оптимизации evcc для обработки. На данный момент только рассчитывает и визуализирует. Управление устройствами пока не осуществляется.", + "enable": "Включить оптимизатор", + "info": "Пункт меню оптимизатора может появиться через несколько минут. Для новых установок сбор достаточного объёма данных evcc может занять до 24 часов.", + "title": "Оптимизатор" + }, "options": { "boolean": { "no": "нет", "yes": "да" }, "endianness": { - "big": "big-endian", - "little": "little-endian" + "big": "big-еndian", + "little": "little-еndian" }, "operationMode": { "heating": "Отопление", @@ -519,45 +804,89 @@ "titleAdd": "Добавить солнечный счетчик", "titleEdit": "Редактировать солнечный счетчик" }, + "remote": { + "active": "Активный", + "addClient": "Добавить клиента", + "addClientDescription": "Учётные данные хранятся и проверяются только локально в вашем экземпляре evcc.", + "addClientTitle": "Добавить Удалённого Клиента", + "clientCreated": "Клиент создан", + "clients": "Клиенты", + "confirmDelete": "Удалить клиента?", + "connected": "Подключено", + "createClient": "Создать клиента", + "description": "Получайте доступ к вашей установке evcc из любого места с помощью мобильного приложения evcc. Проброс портов или VPN не требуются.", + "deviceName": "Название устройства", + "disconnected": "Отключено", + "done": "Готово", + "enableLabel": "Включить удаленный доступ", + "expiration": "Срок действия", + "expirationNone": "Никогда", + "expired": "истекший", + "expiresIn": "истекает {time}", + "lastActive": "активен {time}", + "loginBlocked": "Слишком много неудачных попыток входа. Удалённый доступ временно заблокирован на 1 минуту.", + "manualLogin": "Или войдите вручную в браузере по адресу {url}, используя следующие учётные данные:", + "noClients": "Клиентов пока нет. Подключение пока невозможно.", + "password": "Пароль", + "passwordOnce": "Этот пароль отображается только один раз. Сразу отсканируйте QR-код или скопируйте пароль. Позже он не будет доступен.", + "qrInstall": "Установите приложение evcc для {ios} или {android}.", + "qrScan": "Отсканируйте код камерой вашего телефона для подключения. Нажмите на него, если вы уже используете телефон.", + "removeClient": "Удалить клиента", + "title": "Удалённый доступ", + "url": "Публичный адрес", + "username": "Имя пользователя" + }, "section": { "additionalMeter": "Дополнительные счетчики", + "consumers": "Потребители", "general": "Общее", - "grid": "Сетка", + "grid": "Сеть", "integrations": "Интеграции", - "loadpoints": "Зарядка и нагрев", + "loadpoints": "Зарядные устройства и Нагреватели", "meter": "Солнечная энергия и аккумуляторы", + "services": "Сервисы", "system": "Система", - "vehicles": "Транспортные средства" + "vehicles": "Автомобили" + }, + "security": { + "authDisabledHint": "Аутентификация отключена. Управление учётными данными недоступно.", + "description": "Управление аутентификацией и учётными данными для этого экземпляра.", + "passwordDescription": "Обновить пароль для входа в интерфейс конфигурации.", + "passwordTitle": "Пароль aдминистратора", + "title": "Безопасность", + "updatePassword": "Обновить пароль" }, "shm": { - "cardTitle": "Sunny Home Manager", + "cardTitle": "Sunny Нome Manager", "description": "evcc оснащен интеграцией для SMA Sunny Home Manager (SHM) через протокол SEMP. Если он работает в той же сети, после входа в вашу учетную запись Sunny Portal вам должно быть автоматически предложено добавить все зарядные устройства, настроенные в evcc, в качестве вновь обнаруженных потребителей. Все должно быть готово к немедленному использованию, без каких-либо настроек, указанных ниже.", "descriptionDeviceId": "12-значная строка в шестнадцатеричном формате. Префикс для всех устройств (зарядная станция и т. д.).", + "descriptionDeviceSerial": "12-символьная HEX-строка. Базовый серийный номер для всех устройств (зарядной станции и т.д.). По умолчанию evcc получает его из MAC-адреса хоста.", "descriptionIdPattern": "Шаблон идентификатора", "descriptionIds": "В Sunny Portal каждому потребительскому устройству требуется уникальный идентификатор. evcc генерирует уникальный идентификатор на основе вашего оборудования. При переносе evcc на другое оборудование эти идентификаторы могут измениться. Если вы хотите сохранить историю, вы можете переопределить сгенерированные идентификаторы здесь. Откройте URL SEMP (/semp), чтобы проверить ваши текущие идентификаторы.", "descriptionSempUrl": "URL SEMP", "descriptionVendorId": "8-символьная строка HEX. Общий префикс всех сущностей. По умолчанию evcc будет использовать свой собственный внутренний идентификатор поставщика.", "labelDeviceId": "Идентификатор устройства", + "labelDeviceSerial": "Серийный номер устройства", "labelVendorId": "Идентификатор поставщика", - "title": "SMA Sunny Home Manager" + "title": "SMA Sunny Нome Manager" }, "sponsor": { "addToken": "Введите токен", "changeToken": "Изменить токен", "description": "Модель спонсорства помогает нам поддерживать проект и устойчиво создавать новые интересные функции. Как спонсор, вы получаете доступ ко всем реализациям зарядных устройств.", - "descriptionToken": "Спонсоры могут найти свой токен на {url}. Для начала мы предлагаем {trialToken}.", - "enterYourToken": "Введите свой токен", + "descriptionToken": "Спонсор GitHub может найти свой токен на {url}. Для начала мы предлагаем {trialToken}.", + "enterYourToken": "Ваш Спонсорский Токен", "error": "Токен спонсора недействителен.", "invalid": "недействительный", "labelToken": "Токен спонсора", "title": "Спонсорство", "tokenRequired": "Прежде чем создавать это устройство, необходимо настроить токен спонсора.", "tokenRequiredFeature": "Для этой функции требуется токен спонсора.", - "tokenRequiredLearnMore": "Узнайте больше.", + "tokenRequiredLearnMore": "Узнать больше.", "tokenRequiredShort": "Не настроен токен спонсора.", "trialToken": "пробный токен", "viaYaml": "через evcc.yaml", - "yourToken": "Ваш токен" + "yourToken": "Спонсорский Токен" }, "system": { "backupRestore": { @@ -568,7 +897,7 @@ "description": "Сделайте резервную копию данных в файл. Этот файл можно использовать для восстановления данных в случае сбоя системы.", "title": "Резервное копирование" }, - "cancel": "Отменить", + "cancel": "Отмена", "description": "Резервное копирование, восстановление и сброс данных. Полезно, если вы хотите перенести свои данные на другую систему.", "note": "Примечание: Все вышеуказанные действия влияют только на данные вашей базы данных. Конфигурационный файл evcc.yaml остается без изменений.", "reset": { @@ -599,6 +928,87 @@ "restartingDescription": "Пожалуйста, подождите…", "restartingMessage": "Перезапуск evcc." }, + "tariff": { + "addForecast": "Добавить прогноз", + "addTariff": "Добавить тариф", + "co2": { + "description": "Прогноз выбросов CO₂ на единицу электроэнергии из сети. Для зарядки с учётом углеродного следа и оценки экологической экономии.", + "titleAdd": "Добавить Прогноз CO₂", + "titleEdit": "Редактировать прогноз CO₂" + }, + "co2Services": "CO₂ сервисы", + "customForecast": "Пользовательский прогноз", + "customTariff": "Пользовательский тариф", + "description": "Настройте ваши тарифы на электроэнергию и прогнозы. Используйте конфигурацию на основе устройств для динамического управления или YAML для статических настроек.", + "feedIn": { + "description": "Плата за энергию, проданную в сеть. Учитывается при расчёте реальных затрат на зарядку.", + "titleAdd": "Добавить Тариф на Экспорт в Сеть", + "titleEdit": "Редактировать Тариф на Экспорт в Сеть" + }, + "generic": "Общие интеграции", + "grid": { + "description": "Стоимость электроэнергии из сети. Применяется для расчёта реальных затрат на зарядку и оптимизации зарядки автомобилей, нагревателей или домашней батареи с учётом цены.", + "titleAdd": "Добавить Тариф на Импорт из Сети", + "titleEdit": "Редактировать Тариф на Импорт из Сети" + }, + "legacyWarning": "Доступна новая конфигурация тарифов! Удалите и сохраните ваши тарифы здесь, чтобы использовать новый пошаговый процесс.", + "option": { + "co2": "Добавить прогноз CO₂", + "feedIn": "Добавить тариф на экспорт в сеть", + "grid": "Добавить тариф на импорт из сети", + "planner": "Добавить прогноз для планирования", + "solar": "Добавить прогноз солнечной генерации", + "temperature": "Добавить прогноз температуры" + }, + "planner": { + "description": "Расширенная настройка. Обычно не требуется, так как динамические тарифы на электроэнергию или прогнозы CO₂ используются автоматически. Включает дополнительный источник данных, который используется только для планирования зарядки, а не для статистики и расчёта стоимости.", + "titleAdd": "Добавить Прогноз для Планирования", + "titleEdit": "Редактировать Прогноз для Планирования" + }, + "services": "Сервисы", + "solar": { + "description": "Прогноз солнечной генерации для вашей фотоэлектрической системы. Отображается в интерфейсе и в будущем будет использоваться для алгоритмов оптимизации.", + "titleAdd": "Добавить Прогноз Солнечной Генерации", + "titleEdit": "Редактировать Прогноз Солнечной Генерации" + }, + "temperature": { + "description": "Прогноз уличной температуры для вашего местоположения. Отображается в интерфейсе и используется для прогнозирования потребности в отоплении.", + "titleAdd": "Добавить Прогноз Температуры", + "titleEdit": "Редактировать Прогноз Температуры" + }, + "template": "Провайдер", + "title": "Тарифы и Прогнозы", + "titleChoice": "Что вы хотите добавить?", + "type": { + "co2": "Интенсивность CO₂", + "feedIn": "Цена за Экспорт в Сеть", + "grid": "Цена за Импорт из Сети", + "planner": "Планировщик", + "solar": "Солнечная", + "temperature": "Прогноз Температуры" + }, + "zones": { + "add": "Добавить зону", + "allDays": "Все дни", + "allMonths": "Все месяцы", + "allTimes": "Все время", + "cancel": "Отмена", + "charge": "Заряд", + "days": "Дни", + "edit": "Редактировать", + "hours": "Часы", + "months": "Месяцы", + "price": "Цена", + "priceRequired": "Укажите цену", + "remove": "Удалить зону", + "save": "Сохранить", + "selectAll": "Все дни", + "timeFrom": "С", + "timeRangeError": "Время начала должно быть раньше времени окончания. Чтобы охватить период через полночь, создайте две отдельные зоны.", + "timeTo": "По", + "weekdays": "Дни недели" + } + }, "telemetry": { "description": "Настройте обмен данными, чтобы помочь улучшить evcc. Ваша конфиденциальность важна для нас, и участие в этом процессе является полностью добровольным.", "title": "Телеметрия" @@ -611,7 +1021,7 @@ "validation": { "failed": "неудачный", "label": "Статус", - "running": "Проверка…", + "running": "проверка…", "success": "успешный", "unknown": "неизвестный", "validate": "проверять" @@ -622,51 +1032,61 @@ "primary": "Общие интеграции", "scooter": "Скутер", "template": "Производитель", - "titleAdd": "Добавить транспортное средство", - "titleEdit": "Редактировать транспортное средство" + "titleAdd": "Добавить Автомобиль", + "titleEdit": "Редактировать Автомобиль" } }, "footer": { "community": { - "greenEnergy": "Солнечная электроэнергия", + "greenEnergy": "Солнечная энергия", "greenEnergySub1": "заряжено с помощью evcc", "greenEnergySub2": "c октября 2022 г", "greenShare": "Доля солнечной энергии", "greenShareSub1": "энергия предоставляется от", - "greenShareSub2": "солнца или батарей", + "greenShareSub2": "солнца или аккумуляторов", "power": "Мощность зарядки", "powerSub1": "{activeClients} из {totalClients} участников", - "powerSub2": "зарядка…", + "powerSub2": "идёт зарядка.", "tabTitle": "Данные комьюнити" }, "savings": { - "co2Saved": "{value} сохранен", - "co2Title": "Выбросы CO₂", + "co2Saved": "{value} сохранено", + "co2Title": "CO₂ Выбросы", "configurePriceCo2": "Узнайте, как настроить данные о ценах и выбросах CO₂.", "footerLong": "{percent} солнечной энергии", "footerShort": "{percent} солнечной", + "indicator": { + "co2": "выбросы CO₂", + "co2saved": "сокращение CO₂", + "none": "отсутствует", + "price": "цена электроэнергии", + "savings": "экономия", + "solar": "солнечная энергия" + }, + "indicatorLabel": "Информация в заголовке", "modalTitle": "Обзор потребленной электроэнергии", - "moneySaved": "{value} сохранен", + "moneySaved": "{value} сохранено", "percentGrid": "{grid} кВт/ч из сети", "percentSelf": "{self} кВт/ч солнечной", - "percentTitle": "Солнечная энергия", + "percentTitle": "Солнечная Энергия", "period": { "30d": "последние 30 дней", "365d": "последние 365 дней", "thisYear": "в этом году", "total": "все время" }, - "periodLabel": "Период:", - "priceTitle": "Цена электроэнергии", - "referenceGrid": "Сеть", - "referenceLabel": "Справочные данные:", + "periodLabel": "Период", + "priceTitle": "Цена Электроэнергии", + "referenceGrid": "сеть", + "referenceLabel": "Справочные данные", + "sessionInfo": "На основе завершённых сессий зарядки.", "tabTitle": "Мои данные" }, "sponsor": { "becomeSponsor": "Стать спонсором", "becomeSponsorExtended": "Поддержите нас напрямую, чтобы получить наклейки.", "confetti": "Готовы к конфети?", - "confettiPromise": "Ты получишь стикеры или цифровое конфети", + "confettiPromise": "Вы получите стикеры или цифровое конфетти", "sticker": "... или стикеры evcc?", "supportUs": "Наша миссия — сделать солнечную энергию нормой. Помоги evcc, заплатив столько, сколько считаешь нужным.", "thanks": "Спасибо, {sponsor}! Ваша поддержка поможет развивать evcc.", @@ -685,29 +1105,54 @@ }, "version": { "availableLong": "доступна новая версия", + "community": "сообщество evcc", + "labelEmail": "Email", + "labelPhone": "Телефон", + "labelRelease": "Релиз", + "labelVersion": "Версия", + "labelWebsite": "Веб-сайт", + "latestVersion": "последняя версия", + "madeByCommunity": "Создано {0}.", "modalCancel": "Отмена", "modalDownload": "Загрузить", "modalInstalledVersion": "Установленная версия", + "modalLatest": "Вы используете последнюю версию.", + "modalNextRelease": "Что появится в следующей версии", "modalNoReleaseNotes": "Нет доступных примечаний к выпуску. Дополнительная информация о новой версии:", "modalTitle": "Доступна новая версия", "modalUpdate": "Установить", "modalUpdateNow": "Установить сейчас", "modalUpdateStarted": "Запуск новой версии evcc…", - "modalUpdateStatusStart": "Установка начата:" + "modalUpdateStatusStart": "Установка начата:", + "modalViewOnGitHub": "Посмотреть на GitHub", + "openSource": "с открытым исходным кодом", + "poweredByOpenSource": "Работает на {0}." } }, "forecast": { "co2": { "average": "Среднее", + "constant": "интенсивность CO₂", "lowestHour": "Самый чистый час", "range": "Диапазон" }, + "empty": { + "co2": "Смотрите, когда в вашем регионе энергия из сети чистая. Планировщик зарядки учтёт выбросы и покажет экономию CO₂.", + "price": "Настройте ваш динамический тариф на электроэнергию, чтобы автоматически оптимизировать планы зарядки и рассчитывать экономию.", + "setup": "Настроить тарифы и прогнозы", + "solar": "Посмотрите ожидаемую солнечную генерацию на сегодня и ближайшие дни. В будущем будет также использоваться для автоматической оптимизации зарядки.", + "temperature": "Посмотрите прогноз уличной температуры для вашего местоположения. Используется для прогнозирования потребности в отоплении." + }, + "hideLine": "скрыть линию", "modalTitle": "Прогноз", "price": { "average": "Среднее", + "constant": "Цена", "lowestHour": "Самый дешевый час", "range": "Диапазон" }, + "priceZoom": "приблизить", + "showLine": "показать линию", "solar": { "dayAfterTomorrow": "Послезавтра", "partly": "частично", @@ -716,13 +1161,22 @@ "tomorrow": "Завтра" }, "solarAdjust": "Корректировка прогноза солнечной энергии на основе реальных данных о производстве {percent}.", + "solarAdjustMedium": "корректировка по реальным данным", + "solarAdjustShort": "скорректировать", + "temperature": { + "average": "Среднее", + "range": "Диапазон" + }, "type": { - "co2": "CO₂", - "price": "Цена", - "solar": "Солнечный" + "co2": "Выбросы CO₂", + "price": "Цена из Сети", + "solar": "Солнечная Генерация", + "temperature": "Наружная Температура" } }, "general": { + "download": "Загрузить", + "none": "отсутствует", "note": "Примечание:" }, "header": { @@ -736,6 +1190,7 @@ "sessions": "Сеансы зарядки" }, "help": { + "contactButton": "Свяжитесь с нами", "discussionsButton": "Обсуждения на GitHub", "documentationButton": "Документация", "issueButton": "Сообщить о проблеме", @@ -745,7 +1200,7 @@ "modalTitle": "Нужна помощь?", "primaryActions": "Что-то не работает так, как должно? Здесь вы можете получить необходимую помощь.", "restart": { - "cancel": "Отменить", + "cancel": "Отмена", "confirm": "Да, перезапустите!", "description": "В нормальных условиях перезапуск не должен быть необходим. Пожалуйста, рассмотрите возможность подачи заявки об ошибке, если вам необходимо регулярно перезапускать evcc.", "disclaimer": "Примечание: evcc завершит работу и будет полагаться на операционную систему для перезапуска службы.", @@ -775,8 +1230,11 @@ "additionalContext": "Дополнительный контекст", "additionalContextPlaceholder": "Любая дополнительная информация, которая может быть полезна...\n- Детали конфигурации\n- Что вы пробовали\n- Детали среды", "createButtonDiscussion": "Начать обсуждение на GitHub...", + "createButtonEmail": "Отправить запрос по email", "createButtonIssue": "Создать проблему GitHub...", "description": "Ваша установка не работает должным образом? Воспользуйтесь этой страницей, чтобы получить помощь или сообщить о проблемах. Предоставьте достаточно подробную информацию, чтобы мы могли понять и воспроизвести проблему, при этом ваше описание должно быть лаконичным, ясным и понятным.", + "downloadButton": "Скачать отладочную информацию", + "downloadHint": "Прикрепите этот файл к вашему письму, если потребуется дополнительная информация.", "helpType": { "discussion": "Нужна помощь с настройкой", "discussionDescription": "Ответы можно найти в обсуждениях сообщества.", @@ -788,6 +1246,7 @@ "issueTitle": "Название", "stepsToReproduce": "Шаги для воспроизведения", "subTitleDiscussion": "Опишите свою проблему", + "subTitleEmail": "Опишите свою проблему", "subTitleIssue": "Опишите проблему", "summary": { "confirmationButtonDiscussion": "Начать обсуждение на GitHub", @@ -822,7 +1281,7 @@ "update": "Автоматическое обновление" }, "loginModal": { - "cancel": "Отменить", + "cancel": "Отмена", "demoMode": "В демо-режиме вход в систему не поддерживается.", "iframeHint": "Откройте evcc в новой вкладке.", "iframeIssue": "Ваш пароль правильный, но ваш браузер, похоже, удалил файл cookie аутентификации. Это может произойти, если вы запускаете evcc в iframe через HTTP.", @@ -839,39 +1298,55 @@ "day": "День", "goal": "Цель зарядки", "modalTitle": "План тарификации", - "none": "нет", + "none": "отсутствует", + "once": "один раз", "optimization": { "cheapest": "самый дешевый", + "cheapestDescription": "Выбирает наиболее выгодные по цене временные окна. Это может вызвать частые перезапуски зарядки.", + "cleanest": "наиболее чистый", + "cleanestDescription": "Выбирает наиболее экологичные временные окна. Это может вызывать частые остановки и возобновления зарядки.", "continuous": "непрерывный", + "continuousCo2Description": "Выбирает самые экологичные интервалы с наименьшими выбросами.", + "continuousPriceDescription": "Выбирает непрерывные интервалы с наименьшей стоимостью.", "label": "Оптимизация" }, "planNumber": "План {number}", "precondition": { - "description": "Зарядите {duration} перед отправлением для предварительной подготовки аккумулятора.", + "description": "Сдвигает часть процесса зарядки к моменту выезда — это позволяет прогреть батарею и уменьшить время, в течение которого она находится с высоким уровнем заряда.", "label": "Позднее заряжение", "optionAll": "всё", - "optionNo": "нет" + "optionNo": "нет", + "summary": "{precondition} до" }, - "remove": "Удалить", + "remove": "Убрать", "repeating": "повторяющийся", "repeatingPlans": "Повторяющиеся планы", "selectAll": "Выбрать все", + "strategy": { + "label": "Стратегия" + }, "strategyDisabledDescription": "Зарядка начинается как можно позже, чтобы закончиться как раз к моменту отправления. С динамическими ценами на электроэнергию или тарифом на выбросы CO₂ здесь доступно больше вариантов.", - "strategySettings": "Настройки стратегии", "time": "Время", "title": "План", "unsavedChanges": "Есть несохраненные изменения. Применить сейчас?", "update": "Применить", "weekdays": "Дни" }, + "continuousStatus": { + "charging": "Ускоренная зарядка включена.", + "connected": "Нормальный режим.", + "waitForVehicle": "Запрос на ускоренную зарядку отправлен…" + }, "energyflow": { - "battery": "Батарея", - "batteryCharge": "Батарея заряжается", - "batteryDischarge": "Батарея разряжается", - "batteryDischargeHold": "Батарея (заблокирована)", - "batteryGridChargeActive": "сетевая зарядка активна", - "batteryGridChargeLimit": "зарядка сети при", + "battery": "Аккумулятор", + "batteryCharge": "Аккумулятор заряжается", + "batteryChargeHold": "Аккумулятор (зарядка заблокирована)", + "batteryDischarge": "Аккумулятор разряжается", + "batteryDischargeHold": "Аккумулятор (разрядка заблокирована)", + "batteryGridChargeActive": "Зарядка от сети: активна", + "batteryGridChargeLimit": "Зарядка от сети: когда", "batteryTooltip": "{energy} из {total} ({soc})", + "forecast": "Прогноз", "forecastTooltip": "прогноз: оставшееся производство солнечной энергии на сегодня", "gridImport": "Потребление из сети", "homePower": "Потребление", @@ -881,47 +1356,97 @@ "pv": "Солнечная система", "pvExport": "Экспорт в сеть", "pvProduction": "Генерация", - "selfConsumption": "Собственное потребление" + "selfConsumption": "Собственное потребление", + "toggleDetails": "Детализация энергопотоков" }, "heatingStatus": { "charging": "Отопление…", "connected": "Ожидание.", + "minTemp": "Минимальный нагрев до {temp}.", + "suggestionStartTooltip": "Оптимизатор рекомендует включить нагрев сейчас.", + "suggestionStopTooltip": "Оптимизатор рекомендует приостановить нагрев.", "vehicleLimit": "Ограничение нагревателя", - "waitForVehicle": "Готов. Жду нагревателя…" + "waitForVehicle": "Готов к нагреву…" }, "hemsWarning": { - "title": "Внешний предел:" + "consumption": "Потребление", + "feedIn": "Отдача в сеть", + "title": "Внешнее ограничение активно" + }, + "history": { + "direction": { + "battery": { + "energy": "заряжено", + "returnEnergy": "разряжено" + }, + "grid": { + "energy": "получено из сети", + "returnEnergy": "отдано в сеть" + }, + "meter": { + "energy": "Энергия", + "returnEnergy": "Энергия (реверсивная)" + } + }, + "download": "Загрузить", + "empty": "Нет данных истории за выбранный период.", + "group": { + "battery": "Аккумулятор", + "consumer": "Потребление", + "forecast": "Прогноз", + "grid": "Сеть", + "loadpoint": "Зарядки и Нагреватели", + "meter": "Дополнительные счетчики", + "pv": "Генерация" + }, + "otherConsumers": "Другие", + "title": "История" }, "loadpoint": { "avgPrice": "⌀ Цена", "charged": "Заряжено", "co2": "⌀ CO₂", "duration": "Длительность", + "emission": "Выброс", "fallbackName": "Зарядная станция", "finished": "Время окончания", + "last24hEnergy": "Последние 24 часа", + "last7dEnergy": "Последние 7 дней", "power": "Мощность", "price": "Стоимость", "remaining": "Остаток", "remoteDisabledHard": "{source}: выключено", "remoteDisabledSoft": "{source}: отключена адаптивная солнечная зарядка", - "solar": "Солнечная энергия" + "solar": "Солнечная", + "todayEnergy": "Сегодня" }, "loadpointSettings": { "batteryBoost": { "description": "Быстрая зарядка от домашней батареи, пока она не разрядится до {limit}.", - "label": "Усиление батареи", + "descriptionDisabled": "Установите лимит для разрешения быстрой зарядки от домашней батареи.", + "disabled": "Неактивно", + "label": "Ускоренная зарядка от аккумулятора", "mode": "Доступно только в режимах «Солнечный» и «Мини+солнечный».", - "once": "Усиление активно для этой сессии зарядки." + "once": "Ускоренная зарядка активна для этой сессии.", + "stateActive": "Ускоренная зарядка от аккумулятора активна", + "stateBelowLimit": "Заряд аккумулятора недостаточен для ускоренной зарядки", + "stateHold": "Аккумулятор заблокирован", + "stateReady": "Аккумулятор готов к ускоренной зарядки" }, - "batteryUsage": "Домашняя батарея", + "batteryUsage": "Домашний Аккумулятор", "currents": "Ток зарядки", "default": "по умолчанию", + "heating": "Отопление", "maxCurrent": { "label": "Макс. ток" }, "minCurrent": { "label": "Мин. ток" }, + "minTemp": { + "description": "Если температура падает ниже {0}, в солнечном режиме для её поддержания задействуются батарея и сеть.", + "label": "Мин. температура" + }, "phasesConfigured": { "label": "Фазы", "no1p3pSupport": "Как подключено ваше зарядное устройство?", @@ -934,13 +1459,13 @@ "smartCostCheap": "Дешевая зарядка от электросети", "smartCostClean": "Чистая зарядка от сети", "title": "Настройки {0}", - "vehicle": "Транспортное средство" + "vehicle": "Автомобиль" }, "mode": { "minpv": "Мин+солн. эн", "now": "Быстро", "off": "Выкл", - "pv": "Солн. эн", + "pv": "Солнечная", "smart": "Умный" }, "provider": { @@ -950,11 +1475,11 @@ "startConfiguration": "Начнем настройку", "targetCharge": { "activate": "Активировать", - "co2Limit": "Предел CO₂ {co2}", + "co2Limit": "Лимит CO₂ {co2}", "costLimitIgnore": "Настроенное {limit} будет игнорироваться в течение этого периода.", "currentPlan": "Активный план", - "descriptionEnergy": "До какого момента {targetEnergy} следует загружать в транспортное средство?", - "descriptionSoc": "Когда транспортное средство следует зарядить до {targetSoc}?", + "descriptionEnergy": "До какого времени следует заряжать {targetEnergy} автомобиль?", + "descriptionSoc": "Когда автомобиль следует зарядить до {targetSoc}?", "goalReached": "Цель уже достигнута", "inactiveLabel": "Запланированное время", "nextPlan": "Следующий план", @@ -965,11 +1490,11 @@ "planPeriodValue": "{start} до {end}", "planUnknown": "пока неизвестно", "preview": "Предварительный просмотр плана", - "priceLimit": "ценовой предел {price}", - "remove": "Удалить", - "setTargetTime": "ни одного", + "priceLimit": "ценовой лимит {price}", + "remove": "Убрать", + "setTargetTime": "отсутствует", "targetIsAboveLimit": "Настроенный лимит зарядки {limit} будет игнорироваться в течение этого периода.", - "targetIsAboveVehicleLimit": "Предел для транспортных средств ниже цели по сбору средств.", + "targetIsAboveVehicleLimit": "Предел для транспортных средств ниже целевого уровня заряда.", "targetIsInThePast": "Выбери время в будущем.", "targetIsTooFarInTheFuture": "Мы скорректируем план, как только узнаем больше о будущем.", "title": "Запланированное время", @@ -982,31 +1507,50 @@ "targetChargePlan": { "chargeDuration": "Время зарядки", "co2Label": "Выбросы CO₂ ⌀", - "priceLabel": "Цена на энергию", + "priceLabel": "Цена электроэнергии", "timeRange": "{day} {range} ч", "unknownPrice": "по-прежнему неизвестно" }, "targetEnergy": { "label": "Лимит", - "noLimit": "ни одного" + "noLimit": "отсутствует" }, "vehicle": { "addVehicle": "Добавить автомобиль", - "changeVehicle": "Сменить транспортное средство", + "changeVehicle": "Сменить автомобиль", "detectionActive": "Обнаружение транспортного средства…", - "fallbackName": "Транспортное средство", + "fallbackName": "Автомобиль", "moreActions": "Дополнительные действия", - "none": "Нет транспортного средства", + "none": "Нет автомобиля", "notReachable": "Не удалось установить связь с транспортным средством. Попробуйте перезапустить evcc.", "targetSoc": "Лимит", "temp": "Временный.", "tempLimit": "Температурный предел", - "unknown": "Гостевое транспортное средство", + "unknown": "Гостевой автомобиль", "vehicleSoc": "Заряд" }, + "vehicleSettings": { + "connectedTo": "подключён к {0}", + "description": "Эти параметры по умолчанию вступают в силу, когда автомобиль подключается к зарядной станции.", + "editHint": "Добавляйте и редактируйте автомобили в разделе {0}.", + "keepAsIs": "Оставить как есть", + "learnMore": "Узнать больше.", + "limitSoc": "Лимит по умолчанию", + "limitSocDescription": "Применяется, когда автомобиль подключён.", + "menu": "Автомобили", + "minSoc": "Минимальный уровень заряда", + "minSocDescription": "В солнечном режиме выполнять быструю зарядку до этого уровня.", + "mode": "Режим по умолчанию", + "notConnected": "не подключено", + "title": "Настройки автомобиля" + }, "vehicleStatus": { "awaitingAuthorization": "Ожидание авторизации.", - "batteryBoost": "Активирован усилитель батареи.", + "batteryBoost": "Активирована ускоренная зарядка от аккумулятора.", + "batteryBoostBelowLimit": "Уровень заряда аккумулятора слишком низкий для активации ускоренной зарядки.", + "batteryBoostDisabled": "Ускоренная зарядка от аккумулятора отключена.", + "batteryBoostEnabled": "Ускоренная зарядка пока заряд аккумулятора не достигнет {limit}.", + "batteryBoostHold": "Аккумулятор заблокирован. Ускоренная зарядка недоступна.", "charging": "Зарядка…", "cheapEnergyCharging": "Доступная дешевая энергия.", "cheapEnergyNextStart": "Дешевая энергия в {duration}.", @@ -1022,15 +1566,14 @@ "feedinPriorityPausing": "Солнечная зарядка приостановлена для максимального увеличения подачи энергии.", "finished": "Готово.", "minCharge": "Минимальная зарядка до {soc}.", - "pvDisable": "Недостаточно избытка. Пауза через {remaining}...", - "pvEnable": "Доступен избыток. Начинаю через {remaining}...", - "scale1p": "Снижаю до одной фазы через {remaining}...", - "scale3p": "Увеличиваю до трех фаз через {remaining}...", + "suggestionStartTooltip": "Оптимизатор рекомендует начать зарядку сейчас.", + "suggestionStopTooltip": "Оптимизатор рекомендует приостановить зарядку.", "targetChargeActive": "Плановая зарядка активна. Ориентировочное завершение через {duration}.", "targetChargePlanned": "Плановая зарядка начнется в {duration}.", "targetChargeWaitForVehicle": "План зарядки готов. Ожидание автомобиля…", "vehicleLimit": "Ограничение по транспортным средствам", "vehicleLimitReached": "Достигнут предел количества транспортных средств.", + "waitForAuthorization": "Подключено. Ожидание авторизации…", "waitForVehicle": "Готов. Жду транспорт…", "welcome": "Короткая начальная зарядка для подтверждения подключения." }, @@ -1060,26 +1603,31 @@ "noMatch": "Пароли не совпадают", "titleNew": "Установить пароль администратора", "titleUpdate": "Обновление пароля администратора", - "updatePassword": "Обновление пароля" + "updatePassword": "Обновить пароль" }, "session": { - "cancel": "Скасувати", + "addOdometer": "Добавить значение", + "addedRange": "Прирост запаса хода", + "cancel": "Отмена", "co2": "CO₂", "date": "Период", - "delete": "Видалити", + "delete": "Удалить", "finished": "Завершено", - "meter": "счетчик", - "meterstart": "Початок лічильника", - "meterstop": "Зупинка лічильника", - "odometer": "Пробіг", - "price": "Цена", - "started": "Розпочато", + "meter": "Счетчик", + "meterstart": "Начало учёта", + "meterstop": "Остановка учёта", + "odometer": "Пробег", + "price": "Стоимость", + "soc": "Заряд", + "started": "Начато", "title": "Сессия зарядки" }, "sessions": { + "addedRange": "Диапазон", "avgPower": "⌀ Мощность", "avgPrice": "⌀ Цена", "chargeDuration": "Продолжительность", + "chargedSoc": "Заряд", "chartTitle": { "avgCo2ByGroup": "⌀ CO₂ {byGroup}", "avgPriceByGroup": "⌀ Цена {byGroup}", @@ -1092,7 +1640,7 @@ "energySubTotal": "{value} всего", "groupedCo2ByGroup": "Количество CO₂ {byGroup}", "groupedPriceByGroup": "Общая стоимость {byGroup}", - "historyCo2": "Выбросы CO₂", + "historyCo2": "Выбросы-CO₂", "historyCo2Sub": "{value} всего", "historyPrice": "Расходы на зарядку", "historyPriceSub": "{value} всего", @@ -1101,6 +1649,7 @@ }, "co2": "⌀ CO₂", "csv": { + "addedrange": "Прирост запаса хода (км)", "chargedenergy": "Электроэнергия (кВт/ч)", "chargeduration": "Продолжительность", "co2perkwh": "CO₂/кВт·ч", @@ -1111,12 +1660,16 @@ "meterstart": "Начало учета (кВтч)", "meterstop": "Остановка счетчика (кВтч)", "odometer": "Пробег (км)", - "price": "Цена", + "price": "Стоимость", "priceperkwh": "Цена/кВтч", + "socend": "Конечный уровень заряда (%)", + "socstart": "Начальный уровень заряда (%)", "solarpercentage": "Солнечная энергия (%)", - "vehicle": "Транспортное средство" + "vehicle": "Автомобиль" }, "date": "Начало", + "download": "Скачать {period}", + "downloadTotal": "Скачать итог", "energy": "Заряжено", "filter": { "allLoadpoints": "все точки зарядки", @@ -1125,19 +1678,21 @@ }, "group": { "co2": "Выбросы", - "grid": "Сетка", - "price": "Цена", - "self": "Солнечный" + "grid": "Сеть", + "price": "Стоимость", + "self": "Солнечная энергия" }, "groupBy": { "loadpoint": "Зарядная станция", - "none": "Всего", - "vehicle": "Транспортное средство" + "none": "Солнечная энергия", + "vehicle": "Автомобиль" }, "loadpoint": "Зарядная станция", "noData": "В этом месяце сеансы зарядки не проводятся.", + "odometer": "Пробег", "overview": "Обзор", "period": { + "day": "День", "month": "Месяц", "total": "Всего", "year": "Год" @@ -1145,15 +1700,15 @@ "price": "Стоимость", "reallyDelete": "Вы действительно хотите удалить эту сессию?", "showIndividualEntries": "Показать отдельные сессии", - "solar": "Солнечная", + "solar": "Солнечная энергия", "title": "Сессии зарядки", "total": "Всего", "type": { "co2": "CO₂", - "price": "Цена", - "solar": "Солнечная" + "price": "Стоимость", + "solar": "Энергия" }, - "vehicle": "Транспортное средство" + "vehicle": "Автомобиль" }, "settings": { "deviceInfo": "Настройки, которые вы выполняете в этом диалоговом окне, влияют только на это устройство.", @@ -1164,13 +1719,14 @@ }, "hiddenFeatures": { "label": "Экспериментальный", - "value": "Показать экспериментальные функции." + "value": "Включить экспериментальные функции." }, "language": { "auto": "Автоматически", "label": "Язык" }, "loadpoints": { + "disabledDevice": "неактивно", "help": "Изменение порядка и видимости для пользовательского интерфейса.", "hide": "Скрыть {title}", "label": "Зарядные станции", @@ -1211,14 +1767,14 @@ "batteryDescription": "Заряжает домашнюю батарею энергией из сети.", "cheapTitle": "Дешевая зарядка от электросети", "cleanTitle": "Чистая зарядка от сети", - "co2Label": "Выбросы CO₂", - "co2Limit": "предел CO₂", + "co2Label": "выброс CO₂", + "co2Limit": "лимит CO₂", "enable": "Включить ограничение", "loadpointDescription": "Включает временную быструю зарядку в солнечном режиме.", "modalTitle": "Зарядка с помощью интеллектуальной сети", - "none": "нет", - "priceLabel": "Цена на энергию", - "priceLimit": "Ценовой предел", + "none": "отсутствует", + "priceLabel": "Цена электроэнергии", + "priceLimit": "Ценовой лимит", "resetAction": "Снять ограничение", "resetWarning": "Динамическая цена электроэнергии или источник CO₂ не настроены. Однако ограничение {limit} по-прежнему действует. Очистить настройки?", "saved": "Сохранено." @@ -1227,7 +1783,7 @@ "activeHoursLabel": "Приостановленное время", "description": "При высоких ценах приостанавливает зарядку, чтобы приоритезировать прибыльную подачу энергии в сеть.", "priceLabel": "Ставка за поставку электроэнергии", - "priceLimit": "Лимит подачи", + "priceLimit": "Лимит выдачи в сеть", "resetWarning": "Динамический тариф на подачу электроэнергии не настроен. Однако все еще существует ограничение {limit}. Очистить конфигурацию?", "title": "Приоритет подачи" }, @@ -1243,5 +1799,13 @@ "lineErrorLink": "линия {0}", "restartButton": "Рестарт", "title": "Ошибка запуска" + }, + "tabBar": { + "battery": "Аккумулятор", + "charge": "Заряд", + "comingSoon": "Эта страница находится в разработке.", + "forecast": "Прогноз", + "more": "Подробнее", + "sessions": "Сессии" } } diff --git a/i18n/sk.json b/i18n/sk.json index 514512fa9..79480e43a 100644 --- a/i18n/sk.json +++ b/i18n/sk.json @@ -120,8 +120,6 @@ "titleEdit": "Upraviť samoregulačný spotrebič" }, "battery": { - "description": "Experimentálne ovládanie batérie. Tieto funkcie sa testujú a môžu sa zmeniť alebo byť odstránené v budúcich verziách.", - "gridDischarge": "Umožniť batérií vybíjanie do siete", "title": "Batéria", "titleAdd": "Pridať batériu", "titleEdit": "Upraviť batériu" @@ -221,6 +219,7 @@ "enabled": "Povolené", "energy": "Energia", "events": "Udalosti", + "exportLimit": "Limit exportu do siete", "feedinPrice": "Výkupná cena", "forecast": "Predpoveď", "gridPrice": "Cena zo siete", @@ -308,6 +307,7 @@ "form": { "danger": "Nebezpečenstvo", "deprecated": "zastarané", + "durationUnit": "Časová jednotka pre {label}", "example": "Príklad", "optional": "voliteľné" }, @@ -320,17 +320,22 @@ "change": "Zmeniť", "clear": "Vymazať", "close": "Zatvoriť", + "confirmDisable": "Vypnúť toto zariadenie? Bude to ignorované po ďalšom reštarte.", + "confirmEnable": "Opäť povoliť toto zariadenie?", "confirmSave": "Existujú neuložené zmeny. Uložiť teraz?", "copied": "Skopírované!", "copy": "Kopírovať", "customHelp": "Vytvorte používateľom definované zariadenie pomocou systému pluginov evcc.", "customOption": "Užívateľom definované zariadenie", "delete": "Zmazať", + "disable": "Zakázať", + "disabled": "zakázané", "dismiss": "Zamietnuť", "docsLink": "Pozri dokumentáciu.", "dragHandle": "Ovládací prvok na presun", "dragItem": "Presunuteľné: {title}", "dragList": "Zoznam s možnosťou zmeny poradia", + "enable": "Povoliť", "error": "Chyba", "experimental": "Experimentálne", "forceSave": "Napriek tomu uložiť", @@ -373,14 +378,23 @@ "customOption": "Užívateľom-definovaná integrácia", "description": "Obmedzenie výkonu externými systémami (napr. §14a EnWG, §9 rozhranie EEG alebo systém riadenia energie nadradenej úrovne). Funguje spoločne s funkciou riadenia záťaže. Pre vybrané zariadenia je podporované aj obmedzenie výroby solárnej energie. Skutočný limit obmedzenia musí byť nastavený v každej konfigurácii meniča.", "download": "Stiahnuť", + "dynamicLimits": "Dynamické limity", "eventsRecorded": "Zaznamenané {count} sieťové obmedzenia udalostí.", + "exportLimit": "Limit exportu do siete", + "exportLimitDescription": "Optimalizátor batérie rešpektuje tento limit, aby sa predišlo stratám solárnej energie. Prednosť má signál aktívneho obmedzenia z dynamickej integrácie limitov.", + "exportLimitHint": "Statický limit importu v bode pripojenia k sieti.", "generic": "Všeobecné integrácie", "integrations": "Integrácie", "lastEvent": "Najnovšie {timeAgo}.", "noEvents": "Zatiaľ žiadne udalosti zaznamenané.", "recordedEvents": "Nahrané udalosti", + "staticLimits": "Statické limity", "template": "Integrácia", - "title": "Externý limit" + "title": "Externý limit", + "type": { + "custom": "Užívateľom-definovaná integrácia", + "relay": "Užívateľ-definovaná integrácia (relay)" + } }, "icon": { "change": "zmeniť", @@ -465,7 +479,7 @@ "heating": "Pridať ohrievač" }, "phases1p": "1-fáza", - "phases3p": "3-fázový", + "phases3p": "3-fázy", "phasesAutomatic": "Automatické fázy", "phasesAutomaticHelp": "Váš nabíjačka podporuje automatické prepínanie medzi 1- a 3-fázovým nabíjaním. V hlavnej obrazovke môžete upraviť správanie fáz počas nabíjania.", "phasesHelp": "Počet pripojených fáz.", @@ -828,7 +842,7 @@ "general": "Všeobecné", "grid": "Sieť", "integrations": "Integrácie", - "loadpoints": "Nabíjacie body a ohrievače", + "loadpoints": "Nabíjacie body a Ohrievače", "meter": "Solár & Batéria", "services": "Služby", "system": "Systém", @@ -1092,6 +1106,8 @@ "version": { "availableLong": "nová verzia k dispozícii", "community": "evcc komunita", + "labelEmail": "E-mail", + "labelPhone": "Telefón", "labelRelease": "Vydanie", "labelVersion": "Verzia", "labelWebsite": "Webová stránka", @@ -1159,9 +1175,12 @@ } }, "general": { + "back": "Späť", "download": "Stiahnuť", + "error": "Chyba", "none": "žiadny", - "note": "Poznámka:" + "note": "Poznámka:", + "warning": "Upozornenie" }, "header": { "about": "O nás", @@ -1174,6 +1193,7 @@ "sessions": "Nabíjacie Relácie" }, "help": { + "contactButton": "Kontaktujte nás", "discussionsButton": "Diskusie na GitHube", "documentationButton": "Dokumentácia", "issueButton": "Nahlásiť problém", @@ -1213,8 +1233,11 @@ "additionalContext": "Dodatočný kontext", "additionalContextPlaceholder": "Akékoľvek ďalšie informácie, ktoré by mohli byť užitočné...\n- Podrobnosti o konfigurácii\n- Čo ste vyskúšali\n- Podrobnosti o prostredí", "createButtonDiscussion": "Začnite diskusiu na GitHube...", + "createButtonEmail": "Poslať žiadosť e-mailom", "createButtonIssue": "Vytvoriť problém na GitHube...", "description": "Vaša inštalácia nefunguje podľa očakávaní? Použite túto stránku na získanie pomoci alebo nahlásenie problémov. Poskytnite dostatok podrobností, aby sme problém pochopili a zreprodukovali, pričom váš popis musí byť stručný, jasný a ľahko pochopiteľný.", + "downloadButton": "Stiahnuť debug informácie", + "downloadHint": "Ak potrebujete viac informácií, priložte tento súbor k svojmu e-mailu.", "helpType": { "discussion": "Potrebujem pomoc s nastavením", "discussionDescription": "Komunitné diskusie poskytujú odpovede.", @@ -1226,6 +1249,7 @@ "issueTitle": "Názov", "stepsToReproduce": "Kroky k reprodukcii", "subTitleDiscussion": "Opíšte svoj problém", + "subTitleEmail": "Opíšte svoj problém", "subTitleIssue": "Popíšte problém", "summary": { "confirmationButtonDiscussion": "Začnite diskusiu na GitHube", @@ -1278,24 +1302,33 @@ "goal": "Cieľ nabíjania", "modalTitle": "Nabíjací plán", "none": "žiadny", + "once": "raz", "optimization": { "cheapest": "najlacnejší", + "cheapestDescription": "Vyberie najlacnejšie sloty. Môže viesť k viacerým štartom a zastaveniam.", + "cleanest": "najčistejšie", + "cleanestDescription": "Vyberie najčistejšie sloty. Môže viesť k viacerým štartom a zastaveniam.", "continuous": "nepretržitý", + "continuousCo2Description": "Vyberie najčistejšiu neprerušenú sériu slotov.", + "continuousPriceDescription": "Vyberie najlacnejšiu neprerušenú sériu slotov.", "label": "Optimalizácia" }, "planNumber": "Plán {number}", "precondition": { - "description": "Nabíjať batériu {duration} pred odchodom na predkondicionovanie.", + "description": "Presunie časť nabíjania tesne pred odchod: zohriata batéria, menej času pri vysokom stave nabitia.", "label": "Neskoré nabíjanie", "optionAll": "všetko", - "optionNo": "nie" + "optionNo": "nie", + "summary": "{precondition} pred" }, "remove": "Odstrániť", "repeating": "opakujúce sa", "repeatingPlans": "Opakujúce sa plány", "selectAll": "Vybrať všetko", + "strategy": { + "label": "Stratégia" + }, "strategyDisabledDescription": "Nabíjanie začína čo najneskôr, aby sa skončilo presne včas pred odchodom. S dynamickými sieťovými cenami alebo tarifom CO₂ je tu viac možností k dispozícii.", - "strategySettings": "Nastavenie stratégie", "time": "Čas", "title": "Plán", "unsavedChanges": "Existujú neuložené zmeny. Uložiť teraz?", @@ -1506,6 +1539,7 @@ "keepAsIs": "Ponechať ako je", "learnMore": "Viac informácií.", "limitSoc": "Predvolený limit", + "limitSocDescription": "Aplikované, keď je vozidlo pripojené.", "menu": "Vozidlá", "minSoc": "Minimálne nabitie", "minSocDescription": "Rýchle nabíjanie na túto úroveň v solárnom režime.", @@ -1580,6 +1614,7 @@ }, "session": { "addOdometer": "Pridať hodnotu", + "addedRange": "Pridaný rozsah", "cancel": "Zrušiť", "co2": "CO₂", "date": "Obdobie", @@ -1595,6 +1630,7 @@ "title": "Nabíjacia relácia" }, "sessions": { + "addedRange": "Rozsah", "avgPower": "⌀ Výkon", "avgPrice": "⌀ Cena", "chargeDuration": "Trvanie", @@ -1620,6 +1656,7 @@ }, "co2": "⌀ CO2", "csv": { + "addedrange": "Pridaný rozsah (km)", "chargedenergy": "Energia (kWh)", "chargeduration": "Trvanie", "co2perkwh": "CO₂/kWh", @@ -1696,6 +1733,7 @@ "label": "Jazyk" }, "loadpoints": { + "disabledDevice": "zakázané", "help": "Zmeniť poradie a viditeľnosť používateľského rozhrania.", "hide": "Skryť {title}", "label": "Nabíjacie stanice", diff --git a/i18n/sl.json b/i18n/sl.json index e79c5f066..2649e94bb 100644 --- a/i18n/sl.json +++ b/i18n/sl.json @@ -864,7 +864,6 @@ }, "planNumber": "Načrt {number}", "precondition": { - "description": "Pred odhodom napolnite {duration} za predhodno ogrevanje baterije.", "label": "Pozno polnjenje", "optionAll": "vse", "optionNo": "ne" @@ -874,7 +873,6 @@ "repeatingPlans": "Ponavljajoči načrti", "selectAll": "Izberi vse", "strategyDisabledDescription": "Polnjenje se začne čim pozneje, da se konča ravno pravočasno za odhod. Z dinamičnimi cenami omrežja ali tarifo CO₂ je na voljo več možnosti.", - "strategySettings": "Nastavitve strategije", "time": "Čas", "title": "Načrt", "unsavedChanges": "Obstajajo neshranjene spremembe. Želite uporabiti zdaj?", diff --git a/i18n/sv.json b/i18n/sv.json index 330146f9c..d7afd91df 100644 --- a/i18n/sv.json +++ b/i18n/sv.json @@ -42,6 +42,7 @@ }, "bufferTitle": "Batteriet som laddningsbuffert", "discharge": "Förhindra att hemmabatteriet laddas ur i snabbläge och under planerad laddning.", + "gridDischarge": "Tillåt hemmabatteriet att återmata till nätet.", "priority": "Hemmabatteriet har prioritet så länge det är under {soc}. Därefter får billaddarna överskottet.", "priorityNone": "Solelens överskott går direkt till billaddarna. Ställ in ett värde över {soc} för att prioritera hemmabatteriet.", "priorityTitle": "Vart ska överskottet gå i första hand?" @@ -49,9 +50,10 @@ "optimizer": { "action": { "charge": "Laddning från nätet", - "hold": "Urladdning spärrad", - "holdcharge": "Laddning pausad", - "normal": "Vid solelöverskott" + "discharge": "Återmata till nätet", + "hold": "Blockera urladdning", + "holdcharge": "Blockera laddning", + "normal": "Normal drift" }, "empty": "tom", "full": "full", @@ -118,6 +120,7 @@ "titleEdit": "Ändra enhet som själv anpassar förbrukning" }, "battery": { + "title": "Batteri", "titleAdd": "Lägg till batteri", "titleEdit": "Ändra batteri" }, @@ -188,6 +191,7 @@ "deviceValue": { "activeClients": "Aktiva klienter", "amount": "Belopp", + "asleep": "sover", "broker": "Broker", "bucket": "Bucket", "capacity": "Kapacitet", @@ -215,6 +219,7 @@ "enabled": "Aktiverad", "energy": "Energi", "events": "Händelser", + "exportLimit": "Begränsning nätexport", "feedinPrice": "Inmatningspris", "forecast": "Prognos", "gridPrice": "Nätpris", @@ -226,6 +231,7 @@ "no": "nej", "odometer": "Mätarställning", "org": "Organisation", + "outdoorTemp": "Temperatur ute", "phaseCurrents": "Ström", "phasePowers": "Effekt", "phaseVoltages": "Spänning", @@ -235,6 +241,7 @@ "price": "Pris", "productionLimited": "Produktion begränsad", "range": "Räckvidd", + "remoteEnabled": "Aktiverad", "returnEnergy": "Energi (retur)", "singlePhase": "En-fas", "soc": "Laddnivå", @@ -300,6 +307,7 @@ "form": { "danger": "Fara", "deprecated": "utfasad", + "durationUnit": "Tidsenhet för {label}", "example": "Exempel", "optional": "valfritt" }, @@ -312,17 +320,22 @@ "change": "Ändra", "clear": "Rensa", "close": "Stäng", + "confirmDisable": "Avaktivera denna enhet? Kommer ignoreras efter nästa omstart.", + "confirmEnable": "Aktivera denna enhet igen?", "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.", "customOption": "Egen-definierad enhet", "delete": "Radera", + "disable": "Avaktivera", + "disabled": "avaktiverad", "dismiss": "Avvisa", "docsLink": "Se dokumentation.", "dragHandle": "Senarelägga", "dragItem": "Går att senarelägga: {title}", "dragList": "Sorterbar lista", + "enable": "Aktivera", "error": "Fel", "experimental": "Experimentella funktioner", "forceSave": "Spara ändå", @@ -364,12 +377,24 @@ }, "customOption": "Användardefinerad integration", "description": "Effektbegränsning genom externa system (t.ex. §14a EnWG, §9 EEG-gränssnitt eller överordnat energihanteringssystem). Fungerar tillsammans med lasthanteringsfunktionen. Begränsning av solproduktion stöds även för valda enheter. Den faktiska begränsningsnivån måste ställas in i konfigurationen för varje växelriktare.", + "download": "Ladda hem", + "dynamicLimits": "Dynamisk begränsning", "eventsRecorded": "Registrerade {count} händelser med nätbegränsning.", + "exportLimit": "Nätets exportbegränsning", + "exportLimitDescription": "Optimeraren respekterar gränsen för att undvika solförluster. Aktiv effektbegränsning från dynamisk styrning har företräde.", + "exportLimitHint": "Statisk gräns för utmatning till nätet vid anslutningspunkten.", "generic": "Generiska integrationer", "integrations": "Integrationer", "lastEvent": "Senaste {timeAgo}.", + "noEvents": "Inga händelser ännu.", + "recordedEvents": "Sparade händelser", + "staticLimits": "Statisk begränsning", "template": "Integration", - "title": "Extern gräns" + "title": "Extern gräns", + "type": { + "custom": "Användardefenierad integration", + "relay": "Användardefenierad integration (relä)" + } }, "icon": { "change": "ändra", @@ -397,6 +422,7 @@ "heating": "Lägg till värmekälla" }, "addMeter": "Lägg till dedikerad energimätare", + "advancedConfig": "Avancerade inställningar", "cancel": "Avbryt", "chargerError": { "charging": "Konfigurering av laddare krävs.", @@ -418,11 +444,21 @@ "circuitInvalid": "Kretsen saknas", "circuitLabel": "Krets", "circuitUnassigned": "ej vald", + "created": "“{name}” skapades och använder förnuftiga standardvärden.", + "createdTitle": { + "charging": "Laddpunkt skapades", + "heating": "Värmare skapad" + }, + "creating": { + "charging": "Laddare sparad. Skapar laddpunkt …", + "heating": "Värmare sparad. Skapar värmare …" + }, "defaultModeHelp": { "charging": "Laddningsläge vid anslutning till fordon.", "heating": "Ställs in vid systemstart." }, "defaultModeHelpKeep": "Använder senast valda läge.", + "defaultModeKeep": "behåll", "defaultModeLabel": "Standardinställning", "delete": "Radera", "electricalSubtitle": "Om du är osäker, kontakta elektriker.", @@ -440,7 +476,7 @@ "noVehicles": "Inga fordon är konfigurerade.", "option": { "charging": "Lägg till laddplats", - "heating": "Lägg till värmningsenhet" + "heating": "Lägg till värmare" }, "phases1p": "1-fas", "phases3p": "3-fas", @@ -480,13 +516,13 @@ "thresholdEnableLabel": "Anslut elnätet", "titleAdd": { "charging": "Lägg till laddplats", - "heating": "Lägg till värmningsenhet", - "unknown": "Lägg till laddare eller värmekälla" + "heating": "Lägg till värmare", + "unknown": "Lägg till laddare eller värmare" }, "titleEdit": { "charging": "Redigera laddare", - "heating": "Ändra värmekälla", - "unknown": "Ändra laddare eller värmekälla" + "heating": "Ändra värmare", + "unknown": "Ändra laddare eller värmare" }, "titleExample": { "charging": "Garage, Carport, etc.", @@ -504,7 +540,7 @@ "addAdditional": "Lägg till ytterligare mätare", "addConsumer": "Lägg till förbrukare", "addGrid": "Lägg till elmätare", - "addLoadpoint": "Lägg till laddare eller värmekälla", + "addLoadpoint": "Lägg till laddare eller värmare", "addPvBattery": "Lägg till solceller eller batteri", "addTariffs": "Lägg till tariffer", "addVehicle": "Lägg till fordon", @@ -535,14 +571,14 @@ "titleDefault": "Fordonet sover" }, "connect": { - "messageDefault": "Bil ansluten {pvPower}kW", - "title": "När en bil ansluts", - "titleDefault": "Bil ansluten" + "messageDefault": "Fordon ansluten {pvPower}kW", + "title": "När ett fordon ansluts", + "titleDefault": "Fordon anslutet" }, "disconnect": { - "messageDefault": "Bil frånkopplad efter {connectedDuration}", - "title": "När en bil frånkopplas", - "titleDefault": "Bil frånkopplad" + "messageDefault": "Fordon frånkopplad efter {connectedDuration}", + "title": "När ett fordon frånkopplas", + "titleDefault": "Fordon frånkopplad" }, "guest": { "messageDefault": "Okänt fordon, gäst inkopplad?", @@ -568,6 +604,11 @@ "messageDefault": "Laddning klar, {chargedEnergy}kWh på {chargeDuration}.", "title": "När laddning stoppar", "titleDefault": "Laddning klar" + }, + "suggestion": { + "messageDefault": "Optimeringsförslag for {suggestionTitle}: om optimeringsstyrning var aktiv '{suggestionAction}' nu. Detta är bara ett förslag, inget har ändrats. Experimentellt — feedback mottages gärna https://github.com/evcc-io/evcc/issues/31903", + "title": "När optimerarens förslag ändras", + "titleDefault": "Optimerarens förslag" } }, "eventMessage": "Meddelande", @@ -623,6 +664,8 @@ "connectionHintTcpip": "Enheten kan nås via nätverk (LAN/Wifi).", "connectionValueSerial": "RS485", "connectionValueTcpip": "Nätverk", + "delay": "Fördröjning", + "delayHint": "Valfri väntetid före varje registeråtkomst. Behövs endast för långsamma enheter.", "device": "Enhetsnamn", "deviceHint": "Exempel: /dev/ttyUSB0", "host": "IP adress eller värdnamn", @@ -633,7 +676,9 @@ "protocolHintRtu": "Förbindelse via RS485 till Ethernet-adapter utan protokollöversättning.", "protocolHintTcp": "Enheten har inbyggt LAN/Wifi-stöd eller är förbunden via RS485 till Ethernet-adapter med protokollöversättning.", "protocolValueRtu": "RTU", - "protocolValueTcp": "TCP" + "protocolValueTcp": "TCP", + "timeout": "Timeout", + "timeoutHint": "Valfri maximal väntetid för enhetssvar." }, "modbusproxy": { "add": "Lägg till proxyanslutning", @@ -797,7 +842,7 @@ "general": "Allmänna inställningar", "grid": "Elnät", "integrations": "Integrationer", - "loadpoints": "Laddare & värmekällor", + "loadpoints": "Laddare & värmare", "meter": "Sol & batteri", "services": "Tjänster", "system": "System", @@ -912,7 +957,8 @@ "feedIn": "Lägg till tariff för export av el", "grid": "Lägg till tariff för import av el", "planner": "Lägg till planeringsprognos", - "solar": "Lägg till solprognos" + "solar": "Lägg till solprognos", + "temperature": "Lägg till temperaturprognos" }, "planner": { "description": "Avancerad inställning. Behövs vanligtvis inte eftersom dynamiska elpriser eller CO₂-prognoser används automatiskt. Aktiverar en extra datakälla som endast används för laddningsplanering, inte för statistik och priskalkylering.", @@ -925,6 +971,11 @@ "titleAdd": "Lägg till solprognos", "titleEdit": "Ändra solprognos" }, + "temperature": { + "description": "Utomhustemperaturprognos för din plats. Visas i gränssnittet och används för att beräkna värmebehov.", + "titleAdd": "Lägg till temperaturprognos", + "titleEdit": "Ändra temperaturprognos" + }, "template": "Leverantör", "title": "Tariffer & prognoser", "titleChoice": "Vad vill du lägga till?", @@ -933,7 +984,8 @@ "feedIn": "Exportpris", "grid": "Importpris från nätet", "planner": "Planering", - "solar": "Sol" + "solar": "Sol", + "temperature": "Temperaturprognos" }, "zones": { "add": "Lägg till zon", @@ -1054,6 +1106,8 @@ "version": { "availableLong": "ny version tillgänglig", "community": "evcc community", + "labelEmail": "Epost", + "labelPhone": "Telefon", "labelRelease": "Release", "labelVersion": "Version", "labelWebsite": "Hemsida", @@ -1086,7 +1140,8 @@ "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.", "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." + "solar": "Förväntat solproduktion idag och imorgon. Kommer i framtiden även att användas för automatisk laddningsoptimering.", + "temperature": "Se förväntad utomhustemperatur för din plats. Används till att förutse värmebehovet." }, "hideLine": "göm graf", "modalTitle": "Prognos", @@ -1108,14 +1163,24 @@ "solarAdjust": "Justera solprognosen baserat på verkliga produktionsvärden{percent}.", "solarAdjustMedium": "Justera med verkliga data", "solarAdjustShort": "justera", + "temperature": { + "average": "Medel", + "range": "Räckvidd" + }, "type": { "co2": "CO₂-utsläpp", "price": "Nätpris", - "solar": "Solenergi produktion" + "solar": "Solenergi produktion", + "temperature": "Utetemperatur" } }, "general": { - "note": "Anmärkning:" + "back": "Tillbaka", + "download": "Ladda hem", + "error": "Fel", + "none": "inget", + "note": "Anmärkning:", + "warning": "Varning" }, "header": { "about": "Om", @@ -1128,6 +1193,7 @@ "sessions": "Laddningar" }, "help": { + "contactButton": "Kontakta oss", "discussionsButton": "GitHub diskussioner", "documentationButton": "Dokumentation", "issueButton": "Rapportera ett problem", @@ -1167,8 +1233,11 @@ "additionalContext": "Ytterligare upplysningar", "additionalContextPlaceholder": "Eventuellt ytterligare information som kan vara till hjälp...\n- Konfigurationsdetaljer\n- Vad du provat\n- Plattforms och systemupplysningar", "createButtonDiscussion": "Starta en GitHub-diskussion...", + "createButtonEmail": "Sänd förfrågan via epost", "createButtonIssue": "Starta ett GitHub-ärende...", "description": "Fungerar inte din installation som förväntat? Använd denna sida för hjälp eller rapportera problem. Bifoga tillräckligt med upplysningar för att vi ska förstå och kunna reproducera problemet. Gör beskrivningen kort, koncis och enkel att följa.", + "downloadButton": "Ladda ner debuginformation", + "downloadHint": "Bifoga denna fil til ditt mail om mer information efterfrågas.", "helpType": { "discussion": "Behöver du hjälp med inställningarna", "discussionDescription": "Forumdiskussioner ger dig svar.", @@ -1180,6 +1249,7 @@ "issueTitle": "Titel", "stepsToReproduce": "Steg för att återskapa", "subTitleDiscussion": "Beskriv ditt problem", + "subTitleEmail": "Beskriv ditt problem", "subTitleIssue": "Beskriv ditt problem", "summary": { "confirmationButtonDiscussion": "Starta en GitHub-diskussion", @@ -1232,24 +1302,33 @@ "goal": "Laddmål", "modalTitle": "Laddplan", "none": "ingen", + "once": "en gång", "optimization": { "cheapest": "billigaste", + "cheapestDescription": "Väljer de billigaste perioderna. Kan leda till många start/stopp.", + "cleanest": "renaste", + "cleanestDescription": "Väljer de renaste perioderna. Kan leda till många start/stopp.", "continuous": "kontinuerlig", + "continuousCo2Description": "Väljer de renaste sammanhängande perioderna.", + "continuousPriceDescription": "Väljer de billigaste sammanhängande perioderna.", "label": "Optimering" }, "planNumber": "Plan {number}", "precondition": { - "description": "Ladda {duration} före avfärd för batteriuppvärmning.", + "description": "Flyttar delar av laddningen till strax före avfärd: varmt batteri, kortare tid med hög laddningsnivå i batteriet.", "label": "Sen laddning", "optionAll": "allt", - "optionNo": "nej" + "optionNo": "nej", + "summary": "{precondition} före" }, "remove": "Ta bort", "repeating": "återkommande", "repeatingPlans": "Återkommande planer", "selectAll": "Välj alla", + "strategy": { + "label": "Strategi" + }, "strategyDisabledDescription": "Laddningen startar så sent som möjligt för att avslutas precis i tid före avfärd. Med dynamiska elpriser eller CO₂-avgifter finns fler möjligheter här.", - "strategySettings": "Strategiinställningar", "time": "Tid", "title": "Plan", "unsavedChanges": "Det finns ej sparade ändringar. Spara nu?", @@ -1280,11 +1359,15 @@ "pv": "Solanläggning", "pvExport": "Export till elnät", "pvProduction": "Produktion", - "selfConsumption": "Egenförbrukning" + "selfConsumption": "Egenförbrukning", + "toggleDetails": "Detaljer om energiflödet" }, "heatingStatus": { "charging": "Värmer…", "connected": "Standby.", + "minTemp": "Minimum uppvärmning till {temp}.", + "suggestionStartTooltip": "Optimeraren rekommenderar uppvärmning nu.", + "suggestionStopTooltip": "Optimeraren rekommenderar att pausa uppvärmningen.", "vehicleLimit": "Värmarbegränsning", "waitForVehicle": "Redo att värma…" }, @@ -1308,6 +1391,7 @@ "returnEnergy": "Energi (baklänges)" } }, + "download": "Ladda hem", "empty": "Ingen data för aktuell tidsperiod tillgänglig.", "group": { "battery": "Batteri", @@ -1329,12 +1413,15 @@ "emission": "Utsläpp", "fallbackName": "Laddplats", "finished": "Sluttid", + "last24hEnergy": "Senaste 24 timmar", + "last7dEnergy": "Senaste 7 dagar", "power": "Effekt", "price": "Kostnad", "remaining": "Återstående tid", "remoteDisabledHard": "{source}: avstängd", "remoteDisabledSoft": "{source}: Adaptiv solladdning avstängd", - "solar": "Solenergi" + "solar": "Solenergi", + "todayEnergy": "Idag" }, "loadpointSettings": { "batteryBoost": { @@ -1352,12 +1439,17 @@ "batteryUsage": "Hembatteri", "currents": "Laddström", "default": "förvald", + "heating": "Uppvärmning", "maxCurrent": { "label": "Max. ström" }, "minCurrent": { "label": "Min. ström" }, + "minTemp": { + "description": "Under {0} används batteri- och nätström i sol-läge för behålla denna temperatur.", + "label": "Min. temperatur" + }, "phasesConfigured": { "label": "Faser", "no1p3pSupport": "Hur är din laddbox ansluten?", @@ -1440,6 +1532,21 @@ "unknown": "Gästfordon", "vehicleSoc": "Laddnivå" }, + "vehicleSettings": { + "connectedTo": "ansluten till {0}", + "description": "Dessa standardvärden används när fordonet ansluts till en laddpunkt.", + "editHint": "Lägg till eller redigera fordon i {0}.", + "keepAsIs": "Behåll", + "learnMore": "Lär dig mer.", + "limitSoc": "Standardgräns", + "limitSocDescription": "Används när fordonet är anslutet.", + "menu": "Fordon", + "minSoc": "Minimum laddning", + "minSocDescription": "Ladda snabbt till denna nivå i sol-läge.", + "mode": "Standardläge", + "notConnected": "ej ansluten", + "title": "Fordonsinställningar" + }, "vehicleStatus": { "awaitingAuthorization": "Väntar på godkännande.", "batteryBoost": "Batteriboost är aktiv.", @@ -1466,6 +1573,8 @@ "pvEnable": "Överskott tillgängligt. Börjar strax.", "scale1p": "Minskar till 1-fas-laddning strax.", "scale3p": "Ökar till 3-fas-laddning strax.", + "suggestionStartTooltip": "Optimeraren rekommenderar laddning nu.", + "suggestionStopTooltip": "Optimeraren rekommenderar att pausa laddningen.", "targetChargeActive": "Laddplan aktiv. Beräknas vara klar om {duration}.", "targetChargePlanned": "Laddplan startar om {duration}.", "targetChargeWaitForVehicle": "Laddare beredd. Väntar på fordon…", @@ -1505,6 +1614,7 @@ }, "session": { "addOdometer": "Lägg till värde", + "addedRange": "Tillagd räckvidd", "cancel": "Avbryt", "co2": "CO₂", "date": "Period", @@ -1520,6 +1630,7 @@ "title": "Laddning" }, "sessions": { + "addedRange": "Räckvidd", "avgPower": "⌀-effekt", "avgPrice": "⌀-pris", "chargeDuration": "Laddtid", @@ -1545,6 +1656,7 @@ }, "co2": "⌀ CO₂", "csv": { + "addedrange": "Tillagd räckvidd (km)", "chargedenergy": "Energi (kWh)", "chargeduration": "Laddtid", "co2perkwh": "CO₂/kWh", @@ -1563,6 +1675,8 @@ "vehicle": "Fordon" }, "date": "Start", + "download": "Ladda ner {period}", + "downloadTotal": "Ladda ner total", "energy": "Laddat", "filter": { "allLoadpoints": "alla laddplatser", @@ -1619,6 +1733,7 @@ "label": "Språk" }, "loadpoints": { + "disabledDevice": "avaktiverad", "help": "Ändra sortering och synlighet för UI.", "hide": "Göm {title}", "label": "Laddpunkter", diff --git a/i18n/ta.json b/i18n/ta.json index 0df32f9ca..8faf7a747 100644 --- a/i18n/ta.json +++ b/i18n/ta.json @@ -1187,7 +1187,6 @@ }, "planNumber": "திட்டம் {number}", "precondition": { - "description": "பேட்டரி முன்நிபந்தனைக்கு புறப்படுவதற்கு முன் {duration} சார்ஜ் செய்யவும்.", "label": "தாமதமாக சார்சிங்", "optionAll": "எல்லாம்", "optionNo": "இல்லை" @@ -1197,7 +1196,6 @@ "repeatingPlans": "மறுநிகழ்வு திட்டங்கள்", "selectAll": "அனைத்தையும் தேர்ந்தெடு", "strategyDisabledDescription": "புறப்படும் நேரத்தில் முடிவதற்காக சார்சிங் முடிந்தவரை தாமதமாகத் தொடங்குகிறது. மாறும் கிரிட் விலைகள் அல்லது CO₂ கட்டணத்துடன், கூடுதல் விருப்பங்கள் இங்கே கிடைக்கின்றன.", - "strategySettings": "மூலோபாய அமைப்புகள்", "time": "நேரம்", "title": "திட்டம்", "unsavedChanges": "There அரே unsaved changes. இடு now?", diff --git a/i18n/tr.json b/i18n/tr.json index 3c88c41cb..2f44259a5 100644 --- a/i18n/tr.json +++ b/i18n/tr.json @@ -19,6 +19,31 @@ "successCloseTab": "Artık bu sekmeyi kapatabilirsin.", "title": "Yetkilendirme Durumu" }, + "battery": { + "card": { + "charging": "şarj oluyor", + "discharging": "pilden çalışıyor", + "energy": "Enerji", + "gridCharge": "şebekeden şarj", + "hold": "bataryadan kullanım kapalı", + "holdCharge": "şarj olma kilitli", + "power": "Güç", + "soc": "Batarya seviyesi" + }, + "combined": "Birleşik", + "config": { + "gridDischarge": "Ev tipi bataryanın şebekeye deşarj olmasına izin verin.", + "priorityNone": "Güneş enerjisi fazlası doğrudan şarj noktalarına aktarılır. Ev bataryasına öncelik vermek için {soc} değerinin üzerinde bir ayar yapın.", + "priorityTitle": "Fazla akım ilk nereye gitsin?" + }, + "optimizer": { + "action": { + "charge": "Şebekeden şarj", + "discharge": "Şebekeye deşarj", + "hold": "Pil kullanımını önle" + } + } + }, "batterySettings": { "batteryLevel": "Doluluk oranı", "bufferStart": { @@ -1129,7 +1154,6 @@ }, "planNumber": "Plan {number}", "precondition": { - "description": "Batarya ön ısıtması için kalkıştan önce {duration} doldur.", "label": "Geç doldurma", "optionAll": "hepsi", "optionNo": "hayır" @@ -1139,7 +1163,6 @@ "repeatingPlans": "“Tekrarlanan planlar”", "selectAll": "“Tümünü seç”", "strategyDisabledDescription": "Şarj işlemi, kalkış saatine tam zamanında bitmek üzere mümkün olduğunca geç başlar. Dinamik şebeke fiyatları veya CO₂ tarifesi ile burada daha fazla seçenek mevcuttur.", - "strategySettings": "Strateji ayarları", "time": "Zaman", "title": "Plan", "unsavedChanges": "Kaydedilmemiş değişiklikler var. Şimdi uygulansın mı?", diff --git a/i18n/uk.json b/i18n/uk.json index 73011ed90..2eca43d5f 100644 --- a/i18n/uk.json +++ b/i18n/uk.json @@ -849,7 +849,6 @@ }, "planNumber": "План {number}", "precondition": { - "description": "Зарядіть {duration} перед відправленням для попередньої підготовки акумулятора.", "label": "Пізня зарядка", "optionAll": "все", "optionNo": "ні" @@ -859,7 +858,6 @@ "repeatingPlans": "Повторювані плани", "selectAll": "Вибрати все", "strategyDisabledDescription": "Зарядка починається якомога пізніше, щоб завершитися якраз вчасно до відправлення. Завдяки динамічним цінам мережі або тарифу на викиди CO₂ тут доступні додаткові опції.", - "strategySettings": "Налаштування стратегії", "time": "Час", "title": "План", "unsavedChanges": "Є незбережені зміни. Застосувати зараз?", diff --git a/i18n/zh-Hans.json b/i18n/zh-Hans.json index fb0af93cf..b26f24ac3 100644 --- a/i18n/zh-Hans.json +++ b/i18n/zh-Hans.json @@ -609,7 +609,6 @@ "none": "无", "planNumber": "计划 {number}", "precondition": { - "description": "出发前充电 {duration} 以进行电池预处理。", "label": "延迟充电", "optionAll": "全部", "optionNo": "否" @@ -784,7 +783,6 @@ "targetChargePlanned": "充电计划将于 {duration} 后开始。", "targetChargeWaitForVehicle": "充电计划准备就绪。等待车辆连接…", "vehicleLimit": "车辆充电上限", - "vehicleLimitReached": "已达到车辆充电上限 {soc}。", "waitForVehicle": "准备就绪。等待车辆连接…", "welcome": "短暂初始充电以确认连接。" }, From e7ab19d7ca25535087bf73c96e5df9479e6315dc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 18 Aug 2026 11:09:24 +0000 Subject: [PATCH 029/135] SMA EV Charger: add setup instructions (#32862) Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: premultiply <4681172+premultiply@users.noreply.github.com> --- templates/definition/charger/semp-sma.yaml | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/templates/definition/charger/semp-sma.yaml b/templates/definition/charger/semp-sma.yaml index 9aa50305c..0962ff788 100644 --- a/templates/definition/charger/semp-sma.yaml +++ b/templates/definition/charger/semp-sma.yaml @@ -13,11 +13,27 @@ requirements: en: | Configure the SEMP base URL (`http:///SEMP`) and the device ID of the charger. IMPORTANT: The charger must NOT be registered directly as a consumer in the Sunny Home Manager / Sunny Portal at the same time! - The configuration option "Disconnect after full charge" must be disabled. + + Setup on the "SMA EV Charger": + 1. Set the rotary switch on the wallbox to PV/Eco charging. + 2. Deactivate the wallbox in the Sunny Portal (it can be added back via evcc later). + 3. Set "Operation with grid connection point meter" to "Yes" and set up a meter (SMA Sunny Home Manager 2 or Energy Meter) under "Configuration -> Parameters -> Meter configuration". This is required for the SEMP interface to be enabled. + 4. Set "Charging release via app" to "No". + 5. Set "Disconnect after full charge" to "No". + 6. Restart the device and wait for it to come back online. + 7. Add the charger in evcc and check/save the configuration (a vehicle may need to be connected for the SEMP interface to report the correct status). de: | Konfiguriere die SEMP Basis-URL (`http:///SEMP`) und die Geräte-ID des Chargers. WICHTIG: Der Charger darf NICHT gleichzeitig im Sunny Home Manager bzw. im Sunny Portal direkt als Verbraucher registriert sein! - Die Konfigurationsoption "Trennung nach Vollladung" muss deaktiviert sein. + + Einrichtung beim "SMA EV Charger": + 1. Drehschalter an der Wallbox auf PV/Eco-Laden stellen. + 2. Wallbox im Sunny Portal deaktivieren (kann später über evcc wieder hinzugefügt werden). + 3. "Betrieb mit Netzanschlusspunktzähler" auf "Ja" stellen und einen Zähler einrichten über "Konfiguration -> Parameter -> Zählerkonfiguration" (SMA Sunny Home Manager 2 oder Energy Meter). Erst dadurch wird die SEMP-Schnittstelle freigegeben. + 4. "Ladefreigabe über App" auf "Nein" stellen. + 5. "Trennung nach Vollladung" auf "Nein" stellen. + 6. Geräteneustart auslösen und warten, bis das Gerät wieder online ist. + 7. Die Wallbox in evcc anlegen und prüfen/speichern (eventuell muss ein Fahrzeug angeschlossen sein, damit die SEMP-Schnittstelle den richtigen Status zurückgibt). params: - name: uri description: From 0ae85295dcb2025a3dd216eb97d9d237090655fd Mon Sep 17 00:00:00 2001 From: andig Date: Tue, 18 Aug 2026 15:22:48 +0200 Subject: [PATCH 030/135] Templates: add http service for parameter choices (#32951) --- templates/definition/tariff/pvnode-v2.yaml | 11 ++-- util/service/http.go | 75 ++++++++++++++++++++++ 2 files changed, 81 insertions(+), 5 deletions(-) create mode 100644 util/service/http.go diff --git a/templates/definition/tariff/pvnode-v2.yaml b/templates/definition/tariff/pvnode-v2.yaml index 34ea67906..4cf9c78bc 100644 --- a/templates/definition/tariff/pvnode-v2.yaml +++ b/templates/definition/tariff/pvnode-v2.yaml @@ -15,16 +15,17 @@ requirements: group: solar params: - - name: site_id - description: - en: Site ID - de: Standort-ID - required: true - name: apikey description: en: pvnode API key de: pvnode API Key required: true + - name: site_id + description: + en: Site ID + de: Standort-ID + required: true + service: http/get?uri=https://api.pvnode.com/v2/sites/&token={apikey}&jq=[.[].id] - name: forecast_days description: en: Forecast days diff --git a/util/service/http.go b/util/service/http.go new file mode 100644 index 000000000..1bef60448 --- /dev/null +++ b/util/service/http.go @@ -0,0 +1,75 @@ +package service + +import ( + "errors" + "net/http" + + "github.com/evcc-io/evcc/server/service" + "github.com/evcc-io/evcc/util" + "github.com/evcc-io/evcc/util/jq" + "github.com/evcc-io/evcc/util/request" + "github.com/evcc-io/evcc/util/transport" + "github.com/itchyny/gojq" + "github.com/spf13/cast" +) + +func init() { + mux := http.NewServeMux() + mux.HandleFunc("GET /get", httpGet) + + service.Register("http", mux) +} + +// httpGet reads uri and extracts the choices using the jq query, driving +// selection of remote resources in templates. The config UI polls while the +// form is filled, hence a rejected request yields an empty list. +func httpGet(w http.ResponseWriter, req *http.Request) { + q := req.URL.Query() + + uri := q.Get("uri") + if uri == "" { + jsonError(w, http.StatusBadRequest, errors.New("missing uri")) + return + } + + expr := q.Get("jq") + if expr == "" { + expr = "." + } + + query, err := gojq.Parse(expr) + if err != nil { + jsonError(w, http.StatusBadRequest, err) + return + } + + log := util.NewLogger("http") + client := request.NewHelper(log) + + if token := q.Get("token"); token != "" { + log.Redact(token) + client.Transport = transport.BearerAuth(token, client.Transport) + } + + res := []string{} + defer func() { jsonWrite(w, res) }() + + body, err := client.GetBody(uri) + if err != nil { + return + } + + val, err := jq.Query(query, body) + if err != nil { + return + } + + if slice, ok := val.([]any); ok { + for _, v := range slice { + res = append(res, cast.ToString(v)) + } + return + } + + res = append(res, cast.ToString(val)) +} From 0be2e0d803551125f9bd175e9654972e75de47db Mon Sep 17 00:00:00 2001 From: andig Date: Tue, 18 Aug 2026 17:21:32 +0200 Subject: [PATCH 031/135] api: let Identify return multiple identities (#32949) --- api/api.go | 5 ++- api/implement/implementations.go | 6 +-- api/mock.go | 4 +- charger/alpitronic.go | 20 +++++---- charger/alpitronic_test.go | 19 +++++--- charger/amperfied.go | 6 +-- charger/bender.go | 19 +++++--- charger/cfos.go | 6 +-- charger/charger.go | 7 ++- charger/compleo.go | 6 +-- charger/dadapower.go | 6 +-- charger/daheimladen.go | 8 ++-- charger/delta.go | 6 +-- charger/e3dc.go | 7 +-- charger/easee.go | 4 +- charger/eebus-evse.go | 21 ++++++--- charger/enovates.go | 6 +-- charger/evecube.go | 6 +-- charger/evsewifi.go | 6 +-- charger/foxess-evc.go | 8 ++-- charger/fronius-wattpilot.go | 5 ++- charger/ghosteebus.go | 23 +++++++--- charger/ghosteebus_test.go | 20 +++++---- charger/go-e.go | 6 +-- charger/goodwe.go | 6 +-- charger/hardybarth-ecb1.go | 4 +- charger/hardybarth-salia.go | 6 +-- charger/kathrein.go | 12 ++--- charger/keba-modbus.go | 6 +-- charger/keba-udp.go | 4 +- charger/kse.go | 6 +-- charger/ocpp.go | 4 +- charger/ocpp_test.go | 2 +- charger/openwb-2.0.go | 6 +-- charger/openwb-native_linux.go | 4 +- charger/openwb-pro.go | 16 ++++--- charger/phoenix-charx.go | 17 +++++--- charger/phoenix-ev-eth.go | 6 +-- charger/smart-evse.go | 6 +-- charger/vestel.go | 8 ++-- charger/warp-ws.go | 14 ++++-- charger/warp2-mqtt.go | 4 +- charger/webasto-next.go | 6 +-- charger/weidmüller.go | 6 +-- charger/zaptec.go | 8 ++-- cmd/dumper.go | 3 +- cmd/implement/implement.go | 4 ++ core/loadpoint_session.go | 7 ++- core/loadpoint_vehicle.go | 75 ++++++++++++++++++++------------ core/loadpoint_vehicle_test.go | 46 +++++++++++++++----- server/http_config_helper.go | 2 +- 51 files changed, 318 insertions(+), 200 deletions(-) diff --git a/api/api.go b/api/api.go index 3535b9dc7..c5c735462 100644 --- a/api/api.go +++ b/api/api.go @@ -130,9 +130,10 @@ type ChargeRater interface { ChargedEnergy() (float64, error) } -// Identifier identifies a vehicle and is implemented by the charger +// Identifier identifies a vehicle and is implemented by the charger. +// A charger may know more than one identity, e.g. an RFID tag and a vehicle id. type Identifier interface { - Identify() (string, error) + Identify() ([]string, error) } // Authorizer authorizes a charging session by supplying RFID credentials diff --git a/api/implement/implementations.go b/api/implement/implementations.go index 7c3ce1da1..882ecb1ac 100644 --- a/api/implement/implementations.go +++ b/api/implement/implementations.go @@ -228,7 +228,7 @@ func (i *iDimmer) Dimmed() (bool, error) { return i.dimmer1() } -func Identifier(identifier0 func() (string, error)) api.Identifier { +func Identifier(identifier0 func() ([]string, error)) api.Identifier { if identifier0 == nil { return nil } @@ -236,10 +236,10 @@ func Identifier(identifier0 func() (string, error)) api.Identifier { } type iIdentifier struct { - identifier0 func() (string, error) + identifier0 func() ([]string, error) } -func (i *iIdentifier) Identify() (string, error) { +func (i *iIdentifier) Identify() ([]string, error) { return i.identifier0() } diff --git a/api/mock.go b/api/mock.go index 22470c2e3..dd59b7f5f 100644 --- a/api/mock.go +++ b/api/mock.go @@ -396,10 +396,10 @@ func (m *MockIdentifier) EXPECT() *MockIdentifierMockRecorder { } // Identify mocks base method. -func (m *MockIdentifier) Identify() (string, error) { +func (m *MockIdentifier) Identify() ([]string, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Identify") - ret0, _ := ret[0].(string) + ret0, _ := ret[0].([]string) ret1, _ := ret[1].(error) return ret0, ret1 } diff --git a/charger/alpitronic.go b/charger/alpitronic.go index e55a64a86..0fbff62e7 100644 --- a/charger/alpitronic.go +++ b/charger/alpitronic.go @@ -22,6 +22,7 @@ import ( "encoding/hex" "errors" "fmt" + "strings" "time" "github.com/evcc-io/evcc/api" @@ -367,21 +368,24 @@ func (wb *AlpitronicHYC) TotalEnergy() (float64, error) { var _ api.Identifier = (*AlpitronicHYC)(nil) // Identify implements the api.Identifier interface -func (wb *AlpitronicHYC) Identify() (string, error) { +func (wb *AlpitronicHYC) Identify() ([]string, error) { b, err := wb.inputG() if err != nil { - return "", err + return nil, err + } + + var res []string + + // the idTag register holds the rfid uid as null-padded ascii string + if idTag := hycInput(b, hycRegIdTag, 20); !allZero(idTag) { + res = append(res, strings.ToLower(strings.TrimRight(string(idTag), "\x00"))) } if vid := hycInput(b, hycRegVID, 8); !allZero(vid) { - return hex.EncodeToString(vid), nil + res = append(res, hex.EncodeToString(vid)) } - if idTag := hycInput(b, hycRegIdTag, 20); !allZero(idTag) { - return hex.EncodeToString(idTag), nil - } - - return "", nil + return res, nil } var _ api.Battery = (*AlpitronicHYC)(nil) diff --git a/charger/alpitronic_test.go b/charger/alpitronic_test.go index 3f1ce0574..911b660c8 100644 --- a/charger/alpitronic_test.go +++ b/charger/alpitronic_test.go @@ -3,7 +3,6 @@ package charger import ( "context" "net" - "strings" "sync" "testing" "time" @@ -218,7 +217,7 @@ func TestAlpitronicIdentify(t *testing.T) { require.NoError(t, err) assert.Empty(t, id) - // vehicle id takes precedence over the tag + // tag before vehicle id regs := hycRegs(1, hycStateCharging) regs[hycReg(1, hycRegVID)+1] = 0xAABB regs[hycReg(1, hycRegVID)+2] = 0xCCDD @@ -229,9 +228,9 @@ func TestAlpitronicIdentify(t *testing.T) { id, err = wb.Identify() require.NoError(t, err) - assert.Equal(t, "0000aabbccddeeff", id) + assert.Equal(t, []string{"ab", "0000aabbccddeeff"}, id) - // tag as fallback + // tag only regs = hycRegs(1, hycStateCharging) regs[hycReg(1, hycRegIdTag)] = 0x4142 @@ -239,7 +238,17 @@ func TestAlpitronicIdentify(t *testing.T) { id, err = wb.Identify() require.NoError(t, err) - assert.Equal(t, "4142"+strings.Repeat("0", 36), id) + assert.Equal(t, []string{"ab"}, id) + + // vehicle id only + regs = hycRegs(1, hycStateCharging) + regs[hycReg(1, hycRegVID)+3] = 0xEEFF + + wb, _ = hycTestCharger(t, 1, regs) + + id, err = wb.Identify() + require.NoError(t, err) + assert.Equal(t, []string{"000000000000eeff"}, id) } func TestAlpitronicEnable(t *testing.T) { diff --git a/charger/amperfied.go b/charger/amperfied.go index ba77a23e4..ce339818e 100644 --- a/charger/amperfied.go +++ b/charger/amperfied.go @@ -299,13 +299,13 @@ func (wb *Amperfied) Voltages() (float64, float64, float64, error) { var _ api.Identifier = (*Amperfied)(nil) // Identify implements the api.Identifier interface -func (wb *Amperfied) Identify() (string, error) { +func (wb *Amperfied) Identify() ([]string, error) { b, err := wb.conn.ReadInputRegisters(ampRegRfidUID, 6) if err != nil { - return "", err + return nil, err } - return hex.EncodeToString(b), nil + return []string{hex.EncodeToString(b)}, nil } var _ api.Diagnosis = (*Amperfied)(nil) diff --git a/charger/bender.go b/charger/bender.go index 285ba1353..f28554e12 100644 --- a/charger/bender.go +++ b/charger/bender.go @@ -504,24 +504,33 @@ func (wb *BenderCC) getPhases() (int, error) { } // identify implements the api.Identifier interface -func (wb *BenderCC) identify() (string, error) { +func (wb *BenderCC) identify() ([]string, error) { + var ids []string + if !wb.legacy && !wb.mennekes4 { b, err := wb.conn.ReadHoldingRegisters(bendRegSmartVehicleDetected, 1) if err == nil && binary.BigEndian.Uint16(b) != 0 { b, err = wb.conn.ReadHoldingRegisters(bendRegEVCCID, 6) } + if err != nil { + return nil, err + } - if id := bytesAsString(b); id != "" || err != nil { - return id, err + if id := bytesAsString(b); id != "" { + ids = append(ids, id) } } b, err := wb.conn.ReadHoldingRegisters(bendRegUserID, 10) if err != nil { - return "", err + return nil, err } - return bytesAsString(b), nil + if id := bytesAsString(b); id != "" { + ids = append(ids, id) + } + + return ids, nil } // soc implements the api.Battery interface diff --git a/charger/cfos.go b/charger/cfos.go index 06794d2f2..26018ff47 100644 --- a/charger/cfos.go +++ b/charger/cfos.go @@ -242,11 +242,11 @@ func (wb *CfosPowerBrain) WakeUp() error { var _ api.Identifier = (*CfosPowerBrain)(nil) // Identify implements the api.Identifier interface -func (wb *CfosPowerBrain) Identify() (string, error) { +func (wb *CfosPowerBrain) Identify() ([]string, error) { b, err := wb.conn.ReadHoldingRegisters(cfosRegLastRfid, 15) if err != nil { - return "", err + return nil, err } - return bytesAsString(b), nil + return []string{bytesAsString(b)}, nil } diff --git a/charger/charger.go b/charger/charger.go index dbaaec800..41a700254 100644 --- a/charger/charger.go +++ b/charger/charger.go @@ -118,7 +118,12 @@ func NewConfigurableFromConfig(ctx context.Context, other map[string]any) (api.C if err != nil { return nil, fmt.Errorf("identify: %w", err) } - implement.May(c, implement.Identifier(identify)) + if identify != nil { + implement.Has(c, implement.Identifier(func() ([]string, error) { + id, err := identify() + return []string{id}, err + })) + } // decorate wakeup if cc.Wakeup != nil { diff --git a/charger/compleo.go b/charger/compleo.go index 5fb2067a8..595263e39 100644 --- a/charger/compleo.go +++ b/charger/compleo.go @@ -289,13 +289,13 @@ func (wb *Compleo) ChargeDuration() (time.Duration, error) { var _ api.Identifier = (*Compleo)(nil) // Identify implements the api.Identifier interface -func (wb *Compleo) Identify() (string, error) { +func (wb *Compleo) Identify() ([]string, error) { b, err := wb.conn.ReadInputRegisters(compleoRegIdTag+wb.offset, 10) if err != nil { - return "", err + return nil, err } - return bytesAsString(b), nil + return []string{bytesAsString(b)}, nil } var _ loadpoint.Controller = (*Compleo)(nil) diff --git a/charger/dadapower.go b/charger/dadapower.go index b6ec31200..101d54821 100644 --- a/charger/dadapower.go +++ b/charger/dadapower.go @@ -291,13 +291,13 @@ func (wb *Dadapower) Phases1p3p(phases int) error { var _ api.Identifier = (*Dadapower)(nil) // Identify implements the api.Identifier interface -func (wb *Dadapower) Identify() (string, error) { +func (wb *Dadapower) Identify() ([]string, error) { u, err := wb.conn.ReadInputRegisters(dadapowerRegIdentification+wb.regOffset, 20) if err != nil { - return "", err + return nil, err } - return bytesAsString(u), nil + return []string{bytesAsString(u)}, nil } var _ api.Diagnosis = (*Dadapower)(nil) diff --git a/charger/daheimladen.go b/charger/daheimladen.go index f48e92146..9c5cccdb6 100644 --- a/charger/daheimladen.go +++ b/charger/daheimladen.go @@ -318,12 +318,14 @@ func (wb *DaheimLaden) Voltages() (float64, float64, float64, error) { var _ api.Identifier = (*DaheimLaden)(nil) // Identify implements the api.Identifier interface. Only usable with PRO -func (wb *DaheimLaden) Identify() (string, error) { +func (wb *DaheimLaden) Identify() ([]string, error) { b, err := wb.conn.ReadHoldingRegisters(dlRegCardId, 16) if err != nil { - return "", err + return nil, err } - return utf16BEBytesAsString(b) + + id, err := utf16BEBytesAsString(b) + return []string{id}, err } // phases1p3p implements the api.PhaseSwitcher interface diff --git a/charger/delta.go b/charger/delta.go index d266f3afa..776d01a37 100644 --- a/charger/delta.go +++ b/charger/delta.go @@ -354,13 +354,13 @@ func (wb *Delta) CurrentPower() (float64, error) { var _ api.Identifier = (*Delta)(nil) // Identify implements the api.Identifier interface -func (wb *Delta) Identify() (string, error) { +func (wb *Delta) Identify() ([]string, error) { b, err := wb.conn.ReadInputRegisters(wb.base+deltaRegEvseRfidUID, 20) if err != nil { - return "", err + return nil, err } - return bytesAsString(b), nil + return []string{bytesAsString(b)}, nil } var _ api.Diagnosis = (*Delta)(nil) diff --git a/charger/e3dc.go b/charger/e3dc.go index c51bc8996..42314c91d 100644 --- a/charger/e3dc.go +++ b/charger/e3dc.go @@ -748,16 +748,17 @@ var _ api.Identifier = (*E3dc)(nil) // Identify implements the api.Identifier interface // Returns the RFID tag ID from WB_SESSION_AUTH_DATA if a session is active -func (wb *E3dc) Identify() (string, error) { +func (wb *E3dc) Identify() ([]string, error) { wb.mu.Lock() defer wb.mu.Unlock() msg, found, err := wb.sessionMessage(rscp.WB_SESSION_AUTH_DATA) if err != nil || !found { - return "", err + return nil, err } - return rscpString(msg) + id, err := rscpString(msg) + return []string{id}, err } var _ api.PhaseSwitcher = (*E3dc)(nil) diff --git a/charger/easee.go b/charger/easee.go index 49cb282f5..f607df372 100644 --- a/charger/easee.go +++ b/charger/easee.go @@ -850,11 +850,11 @@ func (c *Easee) StatusReason() (api.Reason, error) { var _ api.Identifier = (*Easee)(nil) // Currents implements the api.PhaseCurrents interface -func (c *Easee) Identify() (string, error) { +func (c *Easee) Identify() ([]string, error) { c.mux.Lock() defer c.mux.Unlock() - return c.rfid, nil + return []string{c.rfid}, nil } // Set smart charging status to update the chargers led (smart=blue, fast=white) diff --git a/charger/eebus-evse.go b/charger/eebus-evse.go index 43f608aa3..952365f7e 100644 --- a/charger/eebus-evse.go +++ b/charger/eebus-evse.go @@ -520,19 +520,26 @@ func (c *EEBus) currents() (float64, float64, float64, error) { var _ api.Identifier = (*EEBus)(nil) // Identify implements the api.Identifier interface -func (c *EEBus) Identify() (string, error) { +func (c *EEBus) Identify() ([]string, error) { evEntity, ok := c.isEvConnected() if !ok { - return "", nil + return nil, nil } - if identification, err := c.cem.EvCC.Identifications(evEntity); err == nil && len(identification) > 0 { - // return the first identification for now - // later this could be multiple, e.g. MAC Address and PCID - return identification[0].Value, nil + identification, err := c.cem.EvCC.Identifications(evEntity) + if err != nil { + return nil, nil } - return "", nil + // may be multiple, e.g. MAC address and PCID + var res []string + for _, i := range identification { + if i.Value != "" { + res = append(res, i.Value) + } + } + + return res, nil } var _ api.Battery = (*EEBus)(nil) diff --git a/charger/enovates.go b/charger/enovates.go index faabe683c..9fd487ecc 100644 --- a/charger/enovates.go +++ b/charger/enovates.go @@ -233,13 +233,13 @@ func (wb *Enovates) Voltages() (float64, float64, float64, error) { var _ api.Identifier = (*Enovates)(nil) // Identify implements the api.Identifier interface -func (wb *Enovates) Identify() (string, error) { +func (wb *Enovates) Identify() ([]string, error) { b, err := wb.conn.ReadHoldingRegisters(enovatesRegToken, 16) if err != nil { - return "", err + return nil, err } - return trimModbusString(b), nil + return []string{trimModbusString(b)}, nil } var _ api.Diagnosis = (*Enovates)(nil) diff --git a/charger/evecube.go b/charger/evecube.go index 06582c1f9..6292cff1e 100644 --- a/charger/evecube.go +++ b/charger/evecube.go @@ -378,11 +378,11 @@ func (wb *EVECUBE) phases1p3p(phases int) error { } // identify implements the api.Identifier interface -func (wb *EVECUBE) identify() (string, error) { +func (wb *EVECUBE) identify() ([]string, error) { status, err := wb.getAutomationStatus() if err != nil { - return "", err + return nil, err } - return status.AuthTag.Tag, nil + return []string{status.AuthTag.Tag}, nil } diff --git a/charger/evsewifi.go b/charger/evsewifi.go index 498385146..2861616e6 100644 --- a/charger/evsewifi.go +++ b/charger/evsewifi.go @@ -229,14 +229,14 @@ func (wb *EVSEWifi) voltages() (float64, float64, float64, error) { } // Identify implements the api.Identifier interface -func (wb *EVSEWifi) identify() (string, error) { +func (wb *EVSEWifi) identify() ([]string, error) { params, err := wb.paramG.Get() if err != nil { - return "", err + return nil, err } // we can rely on RFIDUID != nil here since identify() is only exposed if the EVSE API supports that property - return *params.RFIDUID, nil + return []string{*params.RFIDUID}, nil } var _ api.Resurrector = (*EVSEWifi)(nil) diff --git a/charger/foxess-evc.go b/charger/foxess-evc.go index cc34b1208..fcfbe0d23 100644 --- a/charger/foxess-evc.go +++ b/charger/foxess-evc.go @@ -599,17 +599,17 @@ func (wb *FoxESSEVC) Voltages() (float64, float64, float64, error) { var _ api.Identifier = (*FoxESSEVC)(nil) // Identify implements the api.Identifier interface -func (wb *FoxESSEVC) Identify() (string, error) { +func (wb *FoxESSEVC) Identify() ([]string, error) { id, err := wb.readUint32(foxRegRFID) if err != nil { - return "", err + return nil, err } if id == 0 { - return "", nil + return nil, nil } - return fmt.Sprintf("%08X", id), nil + return []string{fmt.Sprintf("%08X", id)}, nil } // phases1p3p implements the api.PhaseSwitcher interface diff --git a/charger/fronius-wattpilot.go b/charger/fronius-wattpilot.go index 181b42315..8830f67d6 100644 --- a/charger/fronius-wattpilot.go +++ b/charger/fronius-wattpilot.go @@ -135,8 +135,9 @@ func (c *Wattpilot) Voltages() (float64, float64, float64, error) { var _ api.Identifier = (*Wattpilot)(nil) // Identify implements the api.Identifier interface -func (c *Wattpilot) Identify() (string, error) { - return c.api.GetRFID() +func (c *Wattpilot) Identify() ([]string, error) { + id, err := c.api.GetRFID() + return []string{id}, err } var _ api.PhaseSwitcher = (*Wattpilot)(nil) diff --git a/charger/ghosteebus.go b/charger/ghosteebus.go index 4fa9fe6d8..b8a959570 100644 --- a/charger/ghosteebus.go +++ b/charger/ghosteebus.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "net/http" + "slices" "github.com/enbility/spine-go/model" "github.com/evcc-io/evcc/api" @@ -112,14 +113,22 @@ func NewGhostEEBus(ctx context.Context, ski, ip, user, password string, hasMeter var _ api.Identifier = (*GhostEEBus)(nil) -// Identify implements api.Identifier, preferring RFID over EEBUS identification -func (wb *GhostEEBus) Identify() (string, error) { +// Identify implements api.Identifier, reporting RFID before EEBUS identification +func (wb *GhostEEBus) Identify() ([]string, error) { + var res []string + if wb.hasRFID { - if id, err := wb.identify(); err == nil && id != "" { - return id, nil + if ids, err := wb.identify(); err == nil { + res = append(res, ids...) } } - return wb.EEBus.Identify() + + ids, err := wb.EEBus.Identify() + if err != nil { + return nil, err + } + + return slices.DeleteFunc(append(res, ids...), func(id string) bool { return id == "" }), nil } // getJSONCtx executes a context-aware GET request and decodes the JSON response. @@ -215,8 +224,8 @@ func (wb *GhostEEBus) getPhases() (int, error) { } // identify implements RFID identification via REST API. -func (wb *GhostEEBus) identify() (string, error) { +func (wb *GhostEEBus) identify() ([]string, error) { var res ghostone.RfidCardLastRead err := wb.GetJSON(wb.uri+"/rfid-cards/last-read", &res) - return res.UUID, err + return []string{res.UUID}, err } diff --git a/charger/ghosteebus_test.go b/charger/ghosteebus_test.go index 3f5f7f4e2..fc9f2ded5 100644 --- a/charger/ghosteebus_test.go +++ b/charger/ghosteebus_test.go @@ -332,17 +332,17 @@ func TestGhostEEBus_Identify(t *testing.T) { tests := []struct { name string uuid string - wantID string + wantID []string }{ { name: "with_uuid", uuid: "ABC123", - wantID: "ABC123", + wantID: []string{"ABC123"}, }, { name: "empty", uuid: "", - wantID: "", + wantID: []string{""}, }, } @@ -378,13 +378,17 @@ func TestGhostEEBus_IdentifyFallback(t *testing.T) { id, err := wb.Identify() require.NoError(t, err) - assert.Equal(t, "MAC-001", id) + assert.Equal(t, []string{"MAC-001"}, id) }) - t.Run("rfid_returns_id", func(t *testing.T) { - wb, _, _ := newTestGhostEEBusWithEEBus(t) + t.Run("rfid_and_eebus", func(t *testing.T) { + wb, evccMock, evEntity := newTestGhostEEBusWithEEBus(t) wb.hasRFID = true + // both identities are reported + evccMock.EXPECT().EVConnected(evEntity).Return(true) + evccMock.EXPECT().Identifications(evEntity).Return([]ucapi.IdentificationItem{{Value: "MAC-001"}}, nil) + httpmock.ActivateNonDefault(wb.Client) defer httpmock.DeactivateAndReset() @@ -397,7 +401,7 @@ func TestGhostEEBus_IdentifyFallback(t *testing.T) { id, err := wb.Identify() require.NoError(t, err) - assert.Equal(t, "RFID-42", id) + assert.Equal(t, []string{"RFID-42", "MAC-001"}, id) }) t.Run("rfid_empty_falls_back_to_eebus", func(t *testing.T) { @@ -421,6 +425,6 @@ func TestGhostEEBus_IdentifyFallback(t *testing.T) { id, err := wb.Identify() require.NoError(t, err) - assert.Equal(t, "MAC-001", id) + assert.Equal(t, []string{"MAC-001"}, id) }) } diff --git a/charger/go-e.go b/charger/go-e.go index 9597e2b36..9564ed87c 100644 --- a/charger/go-e.go +++ b/charger/go-e.go @@ -199,12 +199,12 @@ func (c *GoE) Voltages() (float64, float64, float64, error) { var _ api.Identifier = (*GoE)(nil) // Identify implements the api.Identifier interface -func (c *GoE) Identify() (string, error) { +func (c *GoE) Identify() ([]string, error) { resp, err := c.api.Status() if err != nil { - return "", err + return nil, err } - return resp.Identify(), nil + return []string{resp.Identify()}, nil } var _ api.MeterEnergy = (*GoE)(nil) diff --git a/charger/goodwe.go b/charger/goodwe.go index 919685649..9c3851dc5 100644 --- a/charger/goodwe.go +++ b/charger/goodwe.go @@ -270,12 +270,12 @@ func (wb *GoodWe) phases1p3p(phases int) error { var _ api.Identifier = (*GoodWe)(nil) // Identify implements api.Identifier (RFID UID, 14-byte NUL-padded ASCII). -func (wb *GoodWe) Identify() (string, error) { +func (wb *GoodWe) Identify() ([]string, error) { b, err := wb.conn.ReadHoldingRegisters(goodweRegRfid, 7) if err != nil { - return "", err + return nil, err } - return bytesAsString(bytes.Trim(b, "\x00")), nil + return []string{bytesAsString(bytes.Trim(b, "\x00"))}, nil } var _ loadpoint.Controller = (*GoodWe)(nil) diff --git a/charger/hardybarth-ecb1.go b/charger/hardybarth-ecb1.go index 07db7d7a5..20482e41c 100644 --- a/charger/hardybarth-ecb1.go +++ b/charger/hardybarth-ecb1.go @@ -248,6 +248,6 @@ func (wb *HardyBarth) Voltages() (float64, float64, float64, error) { // var _ api.Identifier = (*HardyBarth)(nil) // // Identify implements the api.Identifier interface -// func (wb *HardyBarth) Identify() (string, error) { -// return "", api.ErrNotAvailable +// func (wb *HardyBarth) Identify() ([]string, error) { +// return nil, api.ErrNotAvailable // } diff --git a/charger/hardybarth-salia.go b/charger/hardybarth-salia.go index f5d5f66a0..d2a8dcedd 100644 --- a/charger/hardybarth-salia.go +++ b/charger/hardybarth-salia.go @@ -279,12 +279,12 @@ func (wb *Salia) currents() (float64, float64, float64, error) { return i.L1.Actual / 1e3, i.L2.Actual / 1e3, i.L3.Actual / 1e3, err } -func (wb *Salia) Identify() (string, error) { +func (wb *Salia) Identify() ([]string, error) { res, err := wb.apiG.Get() if err != nil { - return "", err + return nil, err } - return res.Secc.Port0.RFID.AuthorizationRequest.Key, nil + return []string{res.Secc.Port0.RFID.AuthorizationRequest.Key}, nil } func (wb *Salia) getPhases() (int, error) { diff --git a/charger/kathrein.go b/charger/kathrein.go index b3f951dad..eda7df925 100644 --- a/charger/kathrein.go +++ b/charger/kathrein.go @@ -399,29 +399,29 @@ func (wb *Kathrein) StatusReason() (api.Reason, error) { var _ api.Identifier = (*Kathrein)(nil) // Identify implements the api.Identifier interface -func (wb *Kathrein) Identify() (string, error) { +func (wb *Kathrein) Identify() ([]string, error) { s, err := wb.conn.ReadHoldingRegisters(kathreinRegChargingState, 1) if err != nil { - return "", err + return nil, err } state := binary.BigEndian.Uint16(s) if state < 3 || state > 6 { - return "", nil + return nil, nil } b, err := wb.conn.ReadHoldingRegisters(kathreinRegRfid, 24) if err != nil { - return "", err + return nil, err } rfid := string(bytes.TrimRight(b, "\x00")) if strings.HasPrefix(rfid, "RFID:") { - return rfid[5:], nil + return []string{rfid[5:]}, nil } - return rfid, nil + return []string{rfid}, nil } var _ api.Diagnosis = (*Kathrein)(nil) diff --git a/charger/keba-modbus.go b/charger/keba-modbus.go index 4e3e9ce50..f62de558c 100644 --- a/charger/keba-modbus.go +++ b/charger/keba-modbus.go @@ -358,10 +358,10 @@ func (wb *Keba) currents() (float64, float64, float64, error) { } // identify implements the api.Identifier interface -func (wb *Keba) identify() (string, error) { +func (wb *Keba) identify() ([]string, error) { b, err := wb.conn.ReadHoldingRegisters(kebaRegRfid, 2) if err != nil { - return "", err + return nil, err } id := hex.EncodeToString(b) @@ -369,7 +369,7 @@ func (wb *Keba) identify() (string, error) { id = "" } - return id, nil + return []string{id}, nil } // phases1p3p implements the api.PhaseSwitcher interface diff --git a/charger/keba-udp.go b/charger/keba-udp.go index 9e20b0cef..7ee768ec7 100644 --- a/charger/keba-udp.go +++ b/charger/keba-udp.go @@ -305,10 +305,10 @@ func (c *KebaUdp) currents() (float64, float64, float64, error) { var _ api.Identifier = (*KebaUdp)(nil) // Identify implements the api.Identifier interface -func (c *KebaUdp) Identify() (string, error) { +func (c *KebaUdp) Identify() ([]string, error) { var kr keba.Report100 err := c.roundtrip("report", 100, &kr) - return kr.RFIDTag, err + return []string{kr.RFIDTag}, err } var _ api.Diagnosis = (*KebaUdp)(nil) diff --git a/charger/kse.go b/charger/kse.go index d862aba60..1838803f4 100644 --- a/charger/kse.go +++ b/charger/kse.go @@ -263,13 +263,13 @@ func (wb *KSE) getPhases() (int, error) { } // Identify implements the api.Identifier interface -func (wb *KSE) identify() (string, error) { +func (wb *KSE) identify() ([]string, error) { b, err := wb.conn.ReadHoldingRegisters(kseRegNFCTransactionID, 4) if err != nil { - return "", err + return nil, err } - return bytesAsString(b), nil + return []string{bytesAsString(b)}, nil } var _ api.Diagnosis = (*KSE)(nil) diff --git a/charger/ocpp.go b/charger/ocpp.go index 597b13c06..a55c18b1a 100644 --- a/charger/ocpp.go +++ b/charger/ocpp.go @@ -414,8 +414,8 @@ func (c *OCPP) phases1p3p(phases int) error { var _ api.Identifier = (*OCPP)(nil) // Identify implements the api.Identifier interface -func (c *OCPP) Identify() (string, error) { - return c.conn.IdTag(), nil +func (c *OCPP) Identify() ([]string, error) { + return []string{c.conn.IdTag()}, nil } var _ api.Diagnosis = (*OCPP)(nil) diff --git a/charger/ocpp_test.go b/charger/ocpp_test.go index 5588a5bff..d764471f8 100644 --- a/charger/ocpp_test.go +++ b/charger/ocpp_test.go @@ -322,7 +322,7 @@ func (suite *ocppTestSuite) TestAutoStart() { id, err := c1.Identify() suite.Require().NoError(err) - suite.Require().Equal(expectedIdTag, id) + suite.Require().Equal([]string{expectedIdTag}, id) conn1 := c1.Connector() _, err = conn1.TransactionID() diff --git a/charger/openwb-2.0.go b/charger/openwb-2.0.go index dec6ccdcc..a76e1634d 100644 --- a/charger/openwb-2.0.go +++ b/charger/openwb-2.0.go @@ -227,10 +227,10 @@ func (wb *OpenWB20) WakeUp() error { } // Identify implements the api.Identifier interface -func (wb *OpenWB20) identify() (string, error) { +func (wb *OpenWB20) identify() ([]string, error) { b, err := wb.conn.ReadInputRegisters(wb.base+openwbRegRfid, 10) if err != nil { - return "", err + return nil, err } - return bytesAsString(b), nil + return []string{bytesAsString(b)}, nil } diff --git a/charger/openwb-native_linux.go b/charger/openwb-native_linux.go index f27f1f5e7..8c4c9931d 100644 --- a/charger/openwb-native_linux.go +++ b/charger/openwb-native_linux.go @@ -202,6 +202,6 @@ func (wb *OpenWbNative) gpioSwitchPhases(phases int) error { } // Identify implements the api.Identifier interface -func (wb *OpenWbNative) identify() (string, error) { - return wb.rfId.Get(), nil +func (wb *OpenWbNative) identify() ([]string, error) { + return []string{wb.rfId.Get()}, nil } diff --git a/charger/openwb-pro.go b/charger/openwb-pro.go index 5844064e0..c4c98d6b9 100644 --- a/charger/openwb-pro.go +++ b/charger/openwb-pro.go @@ -232,17 +232,21 @@ func (wb *OpenWBPro) Phases1p3p(phases int) error { var _ api.Identifier = (*OpenWBPro)(nil) // Identify implements the api.Identifier interface -func (wb *OpenWBPro) Identify() (string, error) { +func (wb *OpenWBPro) Identify() ([]string, error) { res, err := wb.statusG.Get() - if err != nil || res.VehicleID == "--" { - return "", err + if err != nil { + return nil, err } - if res.VehicleID != "" { - return res.VehicleID, nil + var ids []string + if res.VehicleID != "" && res.VehicleID != "--" { + ids = append(ids, res.VehicleID) + } + if res.RfidTag != "" { + ids = append(ids, res.RfidTag) } - return res.RfidTag, nil + return ids, nil } func (wb *OpenWBPro) wakeup() error { diff --git a/charger/phoenix-charx.go b/charger/phoenix-charx.go index a661459e3..2a31d3dd6 100644 --- a/charger/phoenix-charx.go +++ b/charger/phoenix-charx.go @@ -267,22 +267,27 @@ func (wb *PhoenixCharx) getPhaseValues(reg uint16) (float64, float64, float64, e var _ api.Identifier = (*PhoenixCharx)(nil) // Identify implements the api.Identifier interface -func (wb *PhoenixCharx) Identify() (string, error) { +func (wb *PhoenixCharx) Identify() ([]string, error) { b, err := wb.conn.ReadHoldingRegisters(wb.register(charxRegEvid), 10) if err != nil { - return "", err + return nil, err } - if res := bytesAsString(b); res != "" { - return res, nil + var ids []string + if evid := bytesAsString(b); evid != "" { + ids = append(ids, evid) } b, err = wb.conn.ReadHoldingRegisters(wb.register(charxRegRfid), 10) if err != nil { - return "", err + return nil, err } - return bytesAsString(b), nil + if rfid := bytesAsString(b); rfid != "" { + ids = append(ids, rfid) + } + + return ids, nil } var _ api.Diagnosis = (*PhoenixCharx)(nil) diff --git a/charger/phoenix-ev-eth.go b/charger/phoenix-ev-eth.go index cdc09a9b7..50d48c877 100644 --- a/charger/phoenix-ev-eth.go +++ b/charger/phoenix-ev-eth.go @@ -240,13 +240,13 @@ func (wb *PhoenixEVEth) getPhaseValues(reg uint16) (float64, float64, float64, e } // identify implements the api.Identifier interface -func (wb *PhoenixEVEth) identify() (string, error) { +func (wb *PhoenixEVEth) identify() ([]string, error) { b, err := wb.conn.ReadHoldingRegisters(phxRegCardUID, 16) if err != nil { - return "", err + return nil, err } - return bytesAsString(b), nil + return []string{bytesAsString(b)}, nil } var _ api.Diagnosis = (*PhoenixEVEth)(nil) diff --git a/charger/smart-evse.go b/charger/smart-evse.go index 3c97d6252..c684869bc 100644 --- a/charger/smart-evse.go +++ b/charger/smart-evse.go @@ -381,13 +381,13 @@ func (wb *SmartEVSE3) statusReason() (api.Reason, error) { } // identify implements the api.Identifier interface -func (wb *SmartEVSE3) identify() (string, error) { +func (wb *SmartEVSE3) identify() ([]string, error) { res, err := wb.apiG.Get() if err != nil { - return "", err + return nil, err } - return res.Evse.RFIDLastRead, nil + return []string{res.Evse.RFIDLastRead}, nil } var _ api.Diagnosis = (*SmartEVSE3)(nil) diff --git a/charger/vestel.go b/charger/vestel.go index 58c07e8f3..f05e6f5d5 100644 --- a/charger/vestel.go +++ b/charger/vestel.go @@ -319,12 +319,14 @@ func (wb *Vestel) getPhases() (int, error) { } // Identify implements the api.Identifier interface -func (wb *Vestel) identify() (string, error) { +func (wb *Vestel) identify() ([]string, error) { b, err := wb.conn.ReadInputRegisters(vestelRegRFID, 15) if err != nil { - return "", err + return nil, err } - return utf16BEBytesAsString(b) + + id, err := utf16BEBytesAsString(b) + return []string{id}, err } var _ api.Diagnosis = (*Vestel)(nil) diff --git a/charger/warp-ws.go b/charger/warp-ws.go index cfcec5bfe..d74e3a396 100644 --- a/charger/warp-ws.go +++ b/charger/warp-ws.go @@ -455,14 +455,20 @@ func (w *WarpWS) voltages() (float64, float64, float64, error) { return w.meter.Voltages[0], w.meter.Voltages[1], w.meter.Voltages[2], nil } -// identify prefers the vehicle mac read via ISO 15118 over the RFID tag -func (w *WarpWS) identify() (string, error) { +// identify reports the vehicle mac read via ISO 15118 before the RFID tag +func (w *WarpWS) identify() ([]string, error) { w.mu.RLock() defer w.mu.RUnlock() + + var ids []string if w.evState != nil && w.evState.Mac != "" { - return w.evState.Mac, nil + ids = append(ids, w.evState.Mac) } - return w.chargeTracker.AuthorizationInfo.TagId, nil + if tag := w.chargeTracker.AuthorizationInfo.TagId; tag != "" { + ids = append(ids, tag) + } + + return ids, nil } // soc implements the api.Battery interface diff --git a/charger/warp2-mqtt.go b/charger/warp2-mqtt.go index 8ed3890a5..e2c9d5957 100644 --- a/charger/warp2-mqtt.go +++ b/charger/warp2-mqtt.go @@ -282,10 +282,10 @@ func (wb *Warp2) voltages() (float64, float64, float64, error) { return res[0], res[1], res[2], nil } -func (wb *Warp2) identify() (string, error) { +func (wb *Warp2) identify() ([]string, error) { var res warp.ChargeTrackerCurrentCharge err := wb.chargeG(&res) - return res.AuthorizationInfo.TagId, err + return []string{res.AuthorizationInfo.TagId}, err } func (wb *Warp2) emState() (warp.PmState, error) { diff --git a/charger/webasto-next.go b/charger/webasto-next.go index 3e151bf3c..66dbcf4b9 100644 --- a/charger/webasto-next.go +++ b/charger/webasto-next.go @@ -242,13 +242,13 @@ func (wb *WebastoNext) Currents() (float64, float64, float64, error) { var _ api.Identifier = (*WebastoNext)(nil) // Identify implements the api.Identifier interface -func (wb *WebastoNext) Identify() (string, error) { +func (wb *WebastoNext) Identify() ([]string, error) { b, err := wb.conn.ReadHoldingRegisters(tqRegUserID, 10) if err != nil { - return "", err + return nil, err } - return bytesAsString(b), nil + return []string{bytesAsString(b)}, nil } var _ api.Diagnosis = (*WebastoNext)(nil) diff --git a/charger/weidmüller.go b/charger/weidmüller.go index d9053b030..e94c44a13 100644 --- a/charger/weidmüller.go +++ b/charger/weidmüller.go @@ -245,12 +245,12 @@ func (wb *Weidmüller) Voltages() (float64, float64, float64, error) { var _ api.Identifier = (*Weidmüller)(nil) // Identify implements the api.Identifier interface -func (wb *Weidmüller) Identify() (string, error) { +func (wb *Weidmüller) Identify() ([]string, error) { b, err := wb.conn.ReadHoldingRegisters(wmRegCardId, 11) if err != nil { - return "", err + return nil, err } - return bytesAsString(b), nil + return []string{bytesAsString(b)}, nil } var _ api.PhaseSwitcher = (*Weidmüller)(nil) diff --git a/charger/zaptec.go b/charger/zaptec.go index 43fad7506..423bb0c22 100644 --- a/charger/zaptec.go +++ b/charger/zaptec.go @@ -444,17 +444,17 @@ func (c *Zaptec) switchPhases(phases int) error { var _ api.Identifier = (*Zaptec)(nil) // Identify implements the api.Identifier interface -func (c *Zaptec) Identify() (string, error) { +func (c *Zaptec) Identify() ([]string, error) { res, err := c.statusG.Get() if err != nil { - return "", err + return nil, err } if id := res.ObservationByID(zaptec.ChargerCurrentUserUuid); id != nil { - return id.ValueAsString, nil + return []string{id.ValueAsString}, nil } - return "", nil + return nil, nil } func (c *Zaptec) installation() (zaptec.Installation, error) { diff --git a/cmd/dumper.go b/cmd/dumper.go index 21e59036d..248cc97ea 100644 --- a/cmd/dumper.go +++ b/cmd/dumper.go @@ -313,7 +313,8 @@ func (d *dumper) Dump(name string, v any) { if v, ok := api.Cap[api.Identifier](v); ok { d.measureTime(w, "Identifier", func() (string, error) { - id, err := v.Identify() + ids, err := v.Identify() + id := strings.Join(ids, ", ") if err == nil && id == "" { id = "" } diff --git a/cmd/implement/implement.go b/cmd/implement/implement.go index 2659f5108..1f5319450 100644 --- a/cmd/implement/implement.go +++ b/cmd/implement/implement.go @@ -38,6 +38,10 @@ type typeStruct struct { func getTypeImport(t reflect.Type) string { n := t.Name() + if n == "" { + // unnamed type, e.g. []string + return t.String() + } if p := t.PkgPath(); p != "" { if s := strings.Split(p, "github.com/evcc-io/evcc/"); len(s) == 2 { return fmt.Sprintf("%s.%s", s[1], n) diff --git a/core/loadpoint_session.go b/core/loadpoint_session.go index d566d9b27..9e0857c2e 100644 --- a/core/loadpoint_session.go +++ b/core/loadpoint_session.go @@ -2,6 +2,7 @@ package core import ( "errors" + "slices" "time" "github.com/evcc-io/evcc/api" @@ -48,8 +49,10 @@ func (lp *Loadpoint) createSession() { } if c, ok := api.Cap[api.Identifier](lp.charger); ok { - if id, err := c.Identify(); err == nil { - lp.session.Identifier = id + if ids, err := c.Identify(); err == nil { + if idx := slices.IndexFunc(ids, func(id string) bool { return id != "" }); idx >= 0 { + lp.session.Identifier = ids[idx] + } } } diff --git a/core/loadpoint_vehicle.go b/core/loadpoint_vehicle.go index 1860df617..c156682cf 100644 --- a/core/loadpoint_vehicle.go +++ b/core/loadpoint_vehicle.go @@ -3,6 +3,7 @@ package core import ( "errors" "regexp" + "slices" "strings" "time" @@ -59,63 +60,79 @@ func (lp *Loadpoint) identifyVehicle() { return } - id, err := identifier.Identify() + ids, err := identifier.Identify() if err != nil { lp.log.ERROR.Println("charger vehicle id:", err) return } - if lp.vehicleIdentifier == id { + ids = slices.DeleteFunc(ids, func(id string) bool { return id == "" }) + + // vehicle removed + if len(ids) == 0 { + lp.setVehicleIdentifier("") return } - // vehicle found or removed + // identity unchanged + if slices.Contains(ids, lp.vehicleIdentifier) { + return + } + + lp.log.DEBUG.Println("charger vehicle id:", strings.Join(ids, ", ")) + + // the charger may report multiple identities, e.g. rfid tag and vehicle id + vehicle, id := lp.selectVehicleByID(ids...) + if id == "" { + id = ids[0] + } + lp.setVehicleIdentifier(id) - if id != "" { - lp.log.DEBUG.Println("charger vehicle id:", id) + if vehicle != nil { + lp.stopVehicleDetection() - if vehicle := lp.selectVehicleByID(id); vehicle != nil { - lp.stopVehicleDetection() - - // already active via a different detection path - avoid reapplying its mode - if lp.GetVehicle() != vehicle { - lp.setActiveVehicle(vehicle) - } + // already active via a different detection path - avoid reapplying its mode + if lp.GetVehicle() != vehicle { + lp.setActiveVehicle(vehicle) } } } -// selectVehicleByID selects the vehicle with the given ID -func (lp *Loadpoint) selectVehicleByID(id string) api.Vehicle { +// selectVehicleByID selects the vehicle matching any of the given IDs and returns the matching ID +func (lp *Loadpoint) selectVehicleByID(ids ...string) (api.Vehicle, string) { vehicles := lp.coordinatedVehicles() // find exact match - for _, vehicle := range vehicles { - for _, vid := range vehicle.Identifiers() { - if strings.EqualFold(id, vid) { - return vehicle + for _, id := range ids { + for _, vehicle := range vehicles { + if slices.ContainsFunc(vehicle.Identifiers(), func(vid string) bool { + return strings.EqualFold(id, vid) + }) { + return vehicle, id } } } // find placeholder match - for _, vehicle := range vehicles { - for _, vid := range vehicle.Identifiers() { - // case insensitive match - re, err := regexp.Compile("(?i)" + strings.ReplaceAll(vid, "*", ".*?")) - if err != nil { - lp.log.ERROR.Printf("vehicle id: %v", err) - continue - } + for _, id := range ids { + for _, vehicle := range vehicles { + if slices.ContainsFunc(vehicle.Identifiers(), func(vid string) bool { + // case insensitive match + re, err := regexp.Compile("(?i)" + strings.ReplaceAll(vid, "*", ".*?")) + if err != nil { + lp.log.ERROR.Printf("vehicle id: %v", err) + return false + } - if re.MatchString(id) { - return vehicle + return re.MatchString(id) + }) { + return vehicle, id } } } - return nil + return nil, "" } // setActiveVehicle assigns currently active vehicle, configures soc estimator diff --git a/core/loadpoint_vehicle_test.go b/core/loadpoint_vehicle_test.go index 028e447c9..7db0ed926 100644 --- a/core/loadpoint_vehicle_test.go +++ b/core/loadpoint_vehicle_test.go @@ -201,42 +201,62 @@ func TestVehicleDetectByID(t *testing.T) { type testcase struct { string - id, i1, i2 string - res api.Vehicle - prepare func(testcase) + ids []string + i1, i2 string + res api.Vehicle + match string + prepare func(testcase) } tc := []testcase{ - {"1/_/_->0", "1", "", "", nil, func(tc testcase) { + {"1/_/_->0", []string{"1"}, "", "", nil, "", func(tc testcase) { v1.EXPECT().Identifiers().Return(nil) v2.EXPECT().Identifiers().Return(nil) v1.EXPECT().Identifiers().Return(nil) v2.EXPECT().Identifiers().Return(nil) }}, - {"1/1/2->1", "1", "1", "2", v1, func(tc testcase) { + {"1/1/2->1", []string{"1"}, "1", "2", v1, "1", func(tc testcase) { v1.EXPECT().Identifiers().Return([]string{tc.i1}) }}, - {"2/1/2->2", "2", "1", "2", v2, func(tc testcase) { + {"2/1/2->2", []string{"2"}, "1", "2", v2, "2", func(tc testcase) { v1.EXPECT().Identifiers().Return([]string{tc.i1}) v2.EXPECT().Identifiers().Return([]string{tc.i2}) }}, - {"11/1*/2->1", "11", "1*", "2", v1, func(tc testcase) { + {"11/1*/2->1", []string{"11"}, "1*", "2", v1, "11", func(tc testcase) { v1.EXPECT().Identifiers().Return([]string{tc.i1}) v2.EXPECT().Identifiers().Return([]string{tc.i2}) v1.EXPECT().Identifiers().Return([]string{tc.i1}) // v2.EXPECT().Identifiers().Return([]string{tc.i2}) }}, - {"22/1*/2*->2", "22", "1*", "2*", v2, func(tc testcase) { + {"22/1*/2*->2", []string{"22"}, "1*", "2*", v2, "22", func(tc testcase) { v1.EXPECT().Identifiers().Return([]string{tc.i1}) v2.EXPECT().Identifiers().Return([]string{tc.i2}) v1.EXPECT().Identifiers().Return([]string{tc.i1}) v2.EXPECT().Identifiers().Return([]string{tc.i2}) }}, - {"2/_/*->2", "2", "", "*", v2, func(tc testcase) { + {"2/_/*->2", []string{"2"}, "", "*", v2, "2", func(tc testcase) { v1.EXPECT().Identifiers().Return(nil) v2.EXPECT().Identifiers().Return([]string{tc.i2}) v1.EXPECT().Identifiers().Return(nil) v2.EXPECT().Identifiers().Return([]string{tc.i2}) }}, + // second identity matches, e.g. rfid tag and vehicle id + {"x,2/1/2->2", []string{"x", "2"}, "1", "2", v2, "2", func(tc testcase) { + v1.EXPECT().Identifiers().Return([]string{tc.i1}) + v2.EXPECT().Identifiers().Return([]string{tc.i2}) + v1.EXPECT().Identifiers().Return([]string{tc.i1}) + v2.EXPECT().Identifiers().Return([]string{tc.i2}) + }}, + // wildcard matches the second identity + {"x,22/1/2*->2", []string{"x", "22"}, "1", "2*", v2, "22", func(tc testcase) { + v1.EXPECT().Identifiers().Return([]string{tc.i1}) + v2.EXPECT().Identifiers().Return([]string{tc.i2}) + v1.EXPECT().Identifiers().Return([]string{tc.i1}) + v2.EXPECT().Identifiers().Return([]string{tc.i2}) + v1.EXPECT().Identifiers().Return([]string{tc.i1}) + v2.EXPECT().Identifiers().Return([]string{tc.i2}) + v1.EXPECT().Identifiers().Return([]string{tc.i1}) + v2.EXPECT().Identifiers().Return([]string{tc.i2}) + }}, } for _, tc := range tc { @@ -252,9 +272,13 @@ func TestVehicleDetectByID(t *testing.T) { tc.prepare(tc) } - if res := lp.selectVehicleByID(tc.id); tc.res != res { + res, match := lp.selectVehicleByID(tc.ids...) + if tc.res != res { t.Errorf("expected %v, got %v", tc.res, res) } + if tc.match != match { + t.Errorf("expected match %q, got %q", tc.match, match) + } } } @@ -332,7 +356,7 @@ func (c *idCharger) Status() (api.ChargeStatus, error) { return api.StatusB, nil func (c *idCharger) Enabled() (bool, error) { return false, nil } func (c *idCharger) Enable(bool) error { return nil } func (c *idCharger) MaxCurrent(int64) error { return nil } -func (c *idCharger) Identify() (string, error) { return c.id, nil } +func (c *idCharger) Identify() ([]string, error) { return []string{c.id}, nil } // TestReidentifyActiveVehicleKeepsMode is a regression test for #31499: // re-identifying the already-active vehicle must not reapply its default mode. diff --git a/server/http_config_helper.go b/server/http_config_helper.go index 9acc17a1d..90c40cd6e 100644 --- a/server/http_config_helper.go +++ b/server/http_config_helper.go @@ -470,7 +470,7 @@ func testInstance(ctx context.Context, instance any) map[string]testResult { wg.Go(func() { if dev, ok := api.Cap[api.Identifier](instance); ok { val, err := dev.Identify() - makeResult("identifier", val, err) + makeResult("identifier", strings.Join(val, ", "), err) } }) From c745672437a928731173bf305e522150a8e6d24d Mon Sep 17 00:00:00 2001 From: andig Date: Wed, 19 Aug 2026 08:34:40 +0200 Subject: [PATCH 032/135] Afore hybrid: add RS485 (#32961) --- templates/definition/meter/afore-hybrid.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/definition/meter/afore-hybrid.yaml b/templates/definition/meter/afore-hybrid.yaml index 275307ee8..717e6ef77 100644 --- a/templates/definition/meter/afore-hybrid.yaml +++ b/templates/definition/meter/afore-hybrid.yaml @@ -15,7 +15,7 @@ params: - name: usage choice: ["grid", "pv", "battery"] - name: modbus - choice: ["tcpip"] + choice: ["tcpip", "rs485"] port: 502 id: 1 - name: maxacpower From 72b7e270570241713dbda2c59421acc5d7dcc750 Mon Sep 17 00:00:00 2001 From: andig Date: Wed, 19 Aug 2026 08:50:24 +0200 Subject: [PATCH 033/135] Loadpoint: sync charger phases on vehicle connect (#32962) --- core/loadpoint.go | 37 ++++++++--------------------------- core/loadpoint_phases.go | 42 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 29 deletions(-) diff --git a/core/loadpoint.go b/core/loadpoint.go index ee0884870..69d21af59 100644 --- a/core/loadpoint.go +++ b/core/loadpoint.go @@ -561,6 +561,11 @@ func (lp *Loadpoint) evVehicleConnectHandler() { // soc update reset lp.socUpdated = time.Time{} + // charger may have reconfigured phases internally while disconnected + if err := lp.syncChargerPhases(); err != nil { + lp.log.ERROR.Println(err) + } + // set default or start detection if !lp.chargerHasFeature(api.IntegratedDevice) { lp.vehicleDefaultOrDetect() @@ -884,35 +889,9 @@ func (lp *Loadpoint) syncCharger() error { } // sync phases - _, isPs := api.Cap[api.PhaseSwitcher](lp.charger) - if phases := lp.GetPhases(); isPs && shouldBeConsistent && phases > 0 { - // fallback to active phases from measured phases - chargerPhases := lp.measuredPhases - if chargerPhases == 2 { - chargerPhases = 3 - } - - pg, isPg := api.Cap[api.PhaseGetter](lp.charger) - if isPg { - if chargerPhases, err = pg.GetPhases(); err == nil { - if chargerPhases > 0 && chargerPhases != phases { - lp.log.WARN.Printf("charger logic error: phases mismatch (got %d, expected %d)", chargerPhases, phases) - lp.SetPhases(chargerPhases) - } - } else { - if errors.Is(err, api.ErrNotAvailable) { - return nil - } - return fmt.Errorf("charger get phases: %w", err) - } - } - - // use measured phase currents for active phases as fallback if charger does not provide phases - if !isPg || errors.Is(err, api.ErrNotAvailable) { - if chargerPhases > phases { - lp.log.WARN.Printf("charger logic error: phases mismatch (got %d measured, expected %d)", chargerPhases, phases) - lp.SetPhases(chargerPhases) - } + if shouldBeConsistent { + if err := lp.syncChargerPhases(); err != nil { + return err } } diff --git a/core/loadpoint_phases.go b/core/loadpoint_phases.go index 207ef9234..50d9e8f2a 100644 --- a/core/loadpoint_phases.go +++ b/core/loadpoint_phases.go @@ -1,6 +1,9 @@ package core import ( + "errors" + "fmt" + "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/core/keys" ) @@ -161,3 +164,42 @@ func (lp *Loadpoint) getChargerPhysicalPhases() int { func (lp *Loadpoint) hasPhaseSwitching() bool { return api.HasCap[api.PhaseSwitcher](lp.charger) } + +// syncChargerPhases synchronizes the assumed phase state with the charger's actual state. +// Chargers may reconfigure phases internally, i.e. when the vehicle is (dis)connected. +func (lp *Loadpoint) syncChargerPhases() error { + phases := lp.GetPhases() + if !lp.hasPhaseSwitching() || phases <= 0 { + return nil + } + + if pg, ok := api.Cap[api.PhaseGetter](lp.charger); ok { + chargerPhases, err := pg.GetPhases() + if err != nil { + if errors.Is(err, api.ErrNotAvailable) { + return nil + } + return fmt.Errorf("charger get phases: %w", err) + } + + if chargerPhases > 0 && chargerPhases != phases { + lp.log.WARN.Printf("charger logic error: phases mismatch (got %d, expected %d)", chargerPhases, phases) + lp.SetPhases(chargerPhases) + } + + return nil + } + + // use measured phase currents for active phases as fallback if charger does not provide phases + chargerPhases := lp.GetMeasuredPhases() + if chargerPhases == 2 { + chargerPhases = 3 + } + + if chargerPhases > phases { + lp.log.WARN.Printf("charger logic error: phases mismatch (got %d measured, expected %d)", chargerPhases, phases) + lp.SetPhases(chargerPhases) + } + + return nil +} From 829f11f23ec7ae7ae77c42a6af8864e23ea80f0f Mon Sep 17 00:00:00 2001 From: andig Date: Wed, 19 Aug 2026 09:22:37 +0200 Subject: [PATCH 034/135] EEBus: don't deny pairing while devices are still being configured (#32966) --- cmd/root.go | 3 +++ server/eebus/eebus.go | 25 +++++++++++++++++++++++++ server/eebus/eebus_test.go | 22 ++++++++++++++++++++++ 3 files changed, 50 insertions(+) diff --git a/cmd/root.go b/cmd/root.go index 90ff5cf9f..670985ad9 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -391,6 +391,9 @@ func runRoot(cmd *cobra.Command, args []string) { } } + // all device skis are registered, unknown ones may now be denied + eebus.ConfigComplete() + // setup MCP if err == nil && isMcp() { router := httpd.Router() diff --git a/server/eebus/eebus.go b/server/eebus/eebus.go index 742671c5e..24f2e9ba7 100644 --- a/server/eebus/eebus.go +++ b/server/eebus/eebus.go @@ -83,6 +83,10 @@ type EEBus struct { mux sync.Mutex log *util.Logger + // configured is set once device configuration has finished. Until then, an + // unknown ski may still belong to a device that is not configured yet. + configured bool + ski string paired []shipapi.ServiceIdentity // devices paired via SHIP Pairing Service @@ -108,6 +112,20 @@ func Instance() (*EEBus, error) { return instance, nil } +// ConfigComplete marks device configuration as finished. Pairing requests from +// unknown skis are left pending until then- the ski of a device that is still +// being configured is not yet registered, and denying it would lock the remote +// service out until the next restart. +func ConfigComplete() { + if instance == nil { + return + } + + instance.mux.Lock() + defer instance.mux.Unlock() + instance.configured = true +} + func GetStatus() any { var ski string if instance != nil { @@ -565,6 +583,13 @@ func (c *EEBus) ServicePairingDetailUpdate(identity shipapi.ServiceIdentity, det defer c.mux.Unlock() if clients, ok := c.clients[identity.SKI]; !ok || len(clients) == 0 { + if !c.configured { + // device configuration is still running- leave the request pending + // instead of denying a ski that is about to be registered + c.log.DEBUG.Printf("pairing request from %s while configuring, left pending", identity.SKI) + return + } + // this is an unknown SKI, so deny pairing c.service.CancelPairing(identity) } diff --git a/server/eebus/eebus_test.go b/server/eebus/eebus_test.go index 25412737c..2e7958ee3 100644 --- a/server/eebus/eebus_test.go +++ b/server/eebus/eebus_test.go @@ -76,3 +76,25 @@ func TestUnregisterDevice_MutexNotHeldDuringShipCall(t *testing.T) { c.UnregisterDevice("aabbcc", dev) } + +// TestPairingDeniedOnlyWhenConfigured guards that an unknown ski is not denied +// while device configuration is still running- its device may register the ski +// moments later, and a denial locks the remote service out until the next restart. +func TestPairingDeniedOnlyWhenConfigured(t *testing.T) { + identity := shipapi.NewServiceIdentity("aabbcc", "", "") + detail := shipapi.NewConnectionStateDetail(shipapi.ConnectionStateReceivedPairingRequest, nil) + + service := eebusmocks.NewServiceInterface(t) + c := &EEBus{ + log: util.NewLogger("test"), + clients: make(map[string][]Device), + service: service, + } + + // still configuring - no CancelPairing expected + c.ServicePairingDetailUpdate(identity, detail) + + service.EXPECT().CancelPairing(identity).Once() + c.configured = true + c.ServicePairingDetailUpdate(identity, detail) +} From 859c9056403dcd2627b3e6e8d35c43612c2e1ce5 Mon Sep 17 00:00:00 2001 From: andig Date: Wed, 19 Aug 2026 09:36:46 +0200 Subject: [PATCH 035/135] Battery control: apply battery mode per battery and only on change (#32967) --- core/site.go | 1 + core/site_battery.go | 30 +++++++++++----- core/site_battery_test.go | 75 +++++++++++++++++++++++++++++++++++++-- 3 files changed, 96 insertions(+), 10 deletions(-) diff --git a/core/site.go b/core/site.go index f1c47d0a0..24e0b2535 100644 --- a/core/site.go +++ b/core/site.go @@ -116,6 +116,7 @@ type Site struct { batteryMode api.BatteryMode // Battery mode (runtime only, not persisted) batteryModeExternal api.BatteryMode // Battery mode (external, runtime only, not persisted) batteryModeExternalTimer time.Time // Battery mode timer for external control + batteryModeApplied map[string]api.BatteryMode // Battery mode last applied per battery meter suggestions map[string]types.Suggestion // Optimizer suggestions by device key suggestionActions map[string]string // last notified actionable optimizer action by device key diff --git a/core/site_battery.go b/core/site_battery.go index d0eb2a691..b2745af69 100644 --- a/core/site_battery.go +++ b/core/site_battery.go @@ -146,10 +146,14 @@ func (site *Site) batteryMaxSocReached(dev config.Device[api.Meter]) (bool, erro // api.BatteryCharge: // // The current soc is validated against max soc. -// In case max soc is reached, hold mode is applied. +// In case max soc is reached, hold mode is applied to that battery only. func (site *Site) applyBatteryMode(mode api.BatteryMode) error { fromToCharge := mode == api.BatteryCharge || mode == api.BatteryUnknown && site.batteryMode == api.BatteryCharge + if site.batteryModeApplied == nil { + site.batteryModeApplied = make(map[string]api.BatteryMode) + } + for _, dev := range site.batteryMeters { meter := dev.Instance() @@ -158,8 +162,11 @@ func (site *Site) applyBatteryMode(mode api.BatteryMode) error { continue } + // mode is per battery, max soc is validated individually + devMode := mode + // validate max soc - if fromToCharge && mode != api.BatteryHold { + if fromToCharge && devMode != api.BatteryHold { ok, err := site.batteryMaxSocReached(dev) if err != nil && !errors.Is(err, api.ErrNotAvailable) { return err @@ -167,18 +174,25 @@ func (site *Site) applyBatteryMode(mode api.BatteryMode) error { // put battery into hold mode when soc limit reached if ok { - // TODO do this only once - mode = api.BatteryHold + devMode = api.BatteryHold } } - if mode != api.BatteryUnknown { - if err := batCtrl.SetBatteryMode(mode); err == nil { - site.log.DEBUG.Printf("set battery %s mode: %s", deviceTitleOrName(dev), mode) - } else if !errors.Is(err, api.ErrNotAvailable) { + // don't re-apply the mode the battery is already in + name := dev.Config().Name + if devMode == api.BatteryUnknown || devMode == site.batteryModeApplied[name] { + continue + } + + if err := batCtrl.SetBatteryMode(devMode); err != nil { + if !errors.Is(err, api.ErrNotAvailable) { return err } + continue } + + site.batteryModeApplied[name] = devMode + site.log.DEBUG.Printf("set battery %s mode: %s", deviceTitleOrName(dev), devMode) } return nil diff --git a/core/site_battery_test.go b/core/site_battery_test.go index fb397a317..1654e7368 100644 --- a/core/site_battery_test.go +++ b/core/site_battery_test.go @@ -85,6 +85,75 @@ func TestApplyBatteryMode(t *testing.T) { } } +// battery meter with soc, controller and soc limits +func batteryControlMock(ctrl *gomock.Controller, soc, maxSoc float64) (api.Meter, *api.MockBatteryController) { + batSoc := api.NewMockBattery(ctrl) + batSoc.EXPECT().Soc().Return(soc, nil).AnyTimes() + + batSocLimit := api.NewMockBatterySocLimiter(ctrl) + batSocLimit.EXPECT().GetSocLimits().Return(0.0, maxSoc).AnyTimes() + + batCon := api.NewMockBatteryController(ctrl) + + return &struct { + api.Meter + api.Battery + api.BatteryController + api.BatterySocLimiter + }{ + Battery: batSoc, + BatteryController: batCon, + BatterySocLimiter: batSocLimit, + }, batCon +} + +// TestBatteryHoldAppliedOnce guards that reaching max soc during grid charge switches +// the battery to hold mode once instead of on every update +func TestBatteryHoldAppliedOnce(t *testing.T) { + ctrl := gomock.NewController(t) + + bat, batCon := batteryControlMock(ctrl, 90, 80) + + site := &Site{ + log: util.NewLogger("foo"), + batteryMeters: []config.Device[api.Meter]{config.NewStaticDevice(config.Named{Name: "bat"}, bat)}, + batteryMode: api.BatteryCharge, + } + + batCon.EXPECT().SetBatteryMode(api.BatteryHold).Times(1) + + for range 3 { + site.updateBatteryMode(true, api.Rate{}) + } + + ctrl.Finish() +} + +// TestBatteryHoldNotShared guards that one battery reaching max soc does not put the +// remaining batteries into hold mode +func TestBatteryHoldNotShared(t *testing.T) { + ctrl := gomock.NewController(t) + + full, fullCon := batteryControlMock(ctrl, 90, 80) + empty, emptyCon := batteryControlMock(ctrl, 50, 80) + + site := &Site{ + log: util.NewLogger("foo"), + batteryMeters: []config.Device[api.Meter]{ + config.NewStaticDevice(config.Named{Name: "full"}, full), + config.NewStaticDevice(config.Named{Name: "empty"}, empty), + }, + batteryMode: api.BatteryCharge, + } + + fullCon.EXPECT().SetBatteryMode(api.BatteryHold).Times(1) + emptyCon.EXPECT().SetBatteryMode(gomock.Any()).Times(0) + + site.updateBatteryMode(true, api.Rate{}) + + ctrl.Finish() +} + func TestRequiredExternalBatteryMode(t *testing.T) { for _, tc := range []struct { internal, external, new api.BatteryMode @@ -184,8 +253,10 @@ func TestExternalBatteryModeChange(t *testing.T) { assert.Equal(t, site.batteryModeExternal, api.BatteryUnknown) assert.False(t, site.batteryModeExternalTimer.IsZero()) - // battery switched back to normal mode - batCon.EXPECT().SetBatteryMode(api.BatteryNormal).Times(1) + // battery switched back to normal mode unless already applied in step 2 + if tc.expected != api.BatteryNormal { + batCon.EXPECT().SetBatteryMode(api.BatteryNormal).Times(1) + } site.updateBatteryMode(false, api.Rate{}) // timer disabled From 45f49aac526acc65ff61456cf8aff85b52b7c96e Mon Sep 17 00:00:00 2001 From: shennings1992 <147423846+shennings1992@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:04:05 +0200 Subject: [PATCH 036/135] Sungrow: add curtailment (#32976) --- .../definition/meter/sungrow-inverter.yaml | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/templates/definition/meter/sungrow-inverter.yaml b/templates/definition/meter/sungrow-inverter.yaml index 32cb98120..413b710d2 100644 --- a/templates/definition/meter/sungrow-inverter.yaml +++ b/templates/definition/meter/sungrow-inverter.yaml @@ -3,6 +3,7 @@ products: - brand: Sungrow description: generic: SG Series Inverter +capabilities: ["curtail"] params: - name: usage choice: ["grid", "pv"] @@ -43,4 +44,65 @@ render: | address: 5003 # Total power yields, 1 kWh type: input decode: uint32s + # EEG §9 curtailment via the power limitation registers + # 5006 (doc 5007) power limitation switch, 0xAA enable / 0x55 disable + # 5007 (doc 5008) power limitation value, 0.1 % of nominal power + curtail: + source: sequence + set: + # Order matters: the switch must be set BEFORE the value. While the + # limitation is disabled the inverter resets register 5007 to its 110 % + # default, so writing the value first would be discarded when coming back + # from an uncurtailed state. + - source: go + script: | + ena := 170 // 0xAA - enable limitation + if curtail == 100 { + ena = 85 // 0x55 - disable limitation + } + ena + out: + - name: ena + type: int + config: + source: modbus + {{- include "modbus" . | indent 12 }} + register: + address: 5006 # Power limitation switch + type: writesingle + decode: uint16 + - source: modbus + {{- include "modbus" . | indent 6 }} + register: + address: 5007 # Power limitation setting, 0.1 % + type: writesingle + decode: uint16 + scale: 10 + curtailed: + source: go + in: + - name: ena + type: int + config: + source: modbus + {{- include "modbus" . | indent 8 }} + register: + address: 5006 + type: holding + decode: uint16 + - name: limit + type: int + config: + source: modbus + {{- include "modbus" . | indent 8 }} + register: + address: 5007 + type: holding + decode: uint16 + script: | + percent := 100 + if ena == 170 { + percent = limit / 10 + } + percent {{- end }} From 80a6231a118c8c7a5cfd8f3a06b8e7d2cee11db0 Mon Sep 17 00:00:00 2001 From: Stef van Hooijdonk Date: Wed, 19 Aug 2026 16:44:58 +0200 Subject: [PATCH 037/135] [BUGFIX] Update url / backup example curl command (#32984) --- assets/js/components/Config/Security/ApiKeyModal.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/js/components/Config/Security/ApiKeyModal.vue b/assets/js/components/Config/Security/ApiKeyModal.vue index 39892ffca..9f7c6c943 100644 --- a/assets/js/components/Config/Security/ApiKeyModal.vue +++ b/assets/js/components/Config/Security/ApiKeyModal.vue @@ -127,9 +127,9 @@ export default defineComponent({ }, curlExample(): string { const key = this.revealedKey ?? ""; - const url = `${window.location.origin}/api/system/backup`; + const url = `${window.location.origin}/api/db/backup`; return [ - `curl -X POST ${url} \\`, + `curl -X GET ${url} \\`, ` -H "Authorization: Bearer ${key}" \\`, ` -o evcc-backup.db`, ].join("\n"); From 41319dfa39b164a11154450e998337dcd32fb3ab Mon Sep 17 00:00:00 2001 From: andig Date: Wed, 19 Aug 2026 18:17:04 +0200 Subject: [PATCH 038/135] SolarMax MAX.STORAGE: drop the whole-kWh energy register (#32986) --- templates/definition/meter/solarmax-maxstorage.yaml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/templates/definition/meter/solarmax-maxstorage.yaml b/templates/definition/meter/solarmax-maxstorage.yaml index 80f80630e..556abf5f7 100644 --- a/templates/definition/meter/solarmax-maxstorage.yaml +++ b/templates/definition/meter/solarmax-maxstorage.yaml @@ -39,13 +39,7 @@ render: | address: 110 # PV-Leistung MAX.STORAGE type: input decode: int32s - energy: - source: modbus - {{- include "modbus" . | indent 2 }} - register: # manual non-sunspec register configuration - address: 150 # Produzierte PV-Energie - type: input - decode: int32s + # energy removed: register 150 (Produzierte PV-Energie) only resolves whole kWh (#32324) maxacpower: {{ .maxacpower }} {{- end }} {{- if eq .usage "battery" }} From 748d36d1306aef31977691207d4120d6cb98400e Mon Sep 17 00:00:00 2001 From: andig Date: Wed, 19 Aug 2026 21:14:57 +0200 Subject: [PATCH 039/135] Phases: disable instead of scaling down to 1p without sufficient surplus (#32991) --- core/loadpoint.go | 9 ++++++++- core/loadpoint_phases_test.go | 12 ++++++++++++ core/loadpoint_test.go | 5 +++-- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/core/loadpoint.go b/core/loadpoint.go index 69d21af59..3223400aa 100644 --- a/core/loadpoint.go +++ b/core/loadpoint.go @@ -1449,8 +1449,15 @@ func (lp *Loadpoint) pvScalePhases(sitePower, minCurrent, maxCurrent float64) in lp.log.DEBUG.Printf("available power %.0fW < %.0fW min %dp threshold", availablePower, float64(activePhases)*Voltage*minCurrent, activePhases) } + // while charging, scaling down only helps if 1p is sustainable, otherwise it + // merely delays the pv disable timer by the phase timer duration + useful := !lp.enabled || !lp.charging() || powerToCurrent(availablePower, 1) >= minCurrent + if insufficient && !useful { + lp.log.DEBUG.Printf("available power %.0fW < %.0fW min 1p threshold, disabling instead of scaling down", availablePower, Voltage*minCurrent) + } + // scaling down also frees load management headroom for min power on activePhases - scalable = insufficient || !lp.circuitAllowsPhases(activePhases, minCurrent) + scalable = insufficient && useful || !lp.circuitAllowsPhases(activePhases, minCurrent) } // scale down phases diff --git a/core/loadpoint_phases_test.go b/core/loadpoint_phases_test.go index da713e4a7..cc723ea14 100644 --- a/core/loadpoint_phases_test.go +++ b/core/loadpoint_phases_test.go @@ -271,6 +271,7 @@ func TestPvScalePhases(t *testing.T) { // scale down min1p := 0.1 lp.phaseTimer = time.Time{} + lp.chargePower = float64(lp.ActivePhases()) * minA * Voltage // charging at min current plainCharger.EXPECT().Enable(false).Return(nil).MaxTimes(1) phaseCharger.EXPECT().Phases1p3p(1).Return(nil).MaxTimes(1) @@ -372,6 +373,17 @@ func TestPvScalePhasesTimer(t *testing.T) { lp.phaseTimer = elapsed }}, + // charging with insufficient power for 1p: disable instead of scaling down + {"3/3->1, insufficient for 1p, charging", 3, 3, 0.1, 3, 0, func(lp *Loadpoint) { + lp.phaseTimer = elapsed + lp.enabled = true + }}, + {"3/3->1, sufficient for 1p, charging", 3, 3, 3 * Voltage * minA / 2, 1, 1, func(lp *Loadpoint) { + lp.phaseTimer = elapsed + lp.enabled = true + lp.chargePower = 3 * Voltage * minA + }}, + // switch down from 3p/0p while not yet charging {"3/0->1, not enough power, not charging", 3, 0, 0, 1, 1, func(lp *Loadpoint) { lp.status = api.StatusB diff --git a/core/loadpoint_test.go b/core/loadpoint_test.go index d427b0f1d..ccb160dcf 100644 --- a/core/loadpoint_test.go +++ b/core/loadpoint_test.go @@ -752,8 +752,9 @@ func TestPVHysteresisAfterPhaseSwitch(t *testing.T) { Disable: loadpoint.ThresholdConfig{ Delay: dt, }, - status: api.StatusC, - enabled: true, + status: api.StatusC, + enabled: true, + chargePower: 3 * Voltage * minA, // charging 3p at min current } start := clock.Now() From e63bae0c8bdc674a397ad9550e254f10789ccced Mon Sep 17 00:00:00 2001 From: andig Date: Thu, 20 Aug 2026 13:36:53 +0200 Subject: [PATCH 040/135] chore: upgrade Go to 1.27 (#30601) --- .github/actions/build-toolchain/action.yml | 2 +- .golangci.yml | 1 + Dockerfile | 2 +- charger/evsewifi_test.go | 8 +++--- charger/ghostone/identity_test.go | 12 ++++---- charger/go-e/api_test.go | 6 ++-- charger/go-e_test.go | 9 ++++-- charger/ocpp/forwarder_test.go | 8 +++--- charger/semp_test.go | 32 +++++++++++----------- charger/trydan_test.go | 13 +++++---- go.mod | 4 +-- hems/shm/shm.go | 8 ++---- meter/dsmr_test.go | 4 +-- meter/eebus.go | 7 ++--- meter/shelly/gen2.go | 14 +++++----- plugin/cached.go | 10 +++---- plugin/cached_test.go | 4 +-- plugin/http_test.go | 7 ++--- plugin/script.go | 8 +++--- plugin/sleep.go | 9 +++--- plugin/socket_test.go | 5 ++-- plugin/valid.go | 10 +++---- plugin/watchdog.go | 9 +++--- plugin/watchdog_test.go | 10 +++---- server/remote/tunnel_test.go | 13 +++++---- util/homeassistant/connection_test.go | 8 +++--- util/urlvalues/url.go | 10 ------- vehicle/bluelink/cci.go | 4 +-- vehicle/bluelink/identity.go | 6 ++-- vehicle/bluelink/identity_test.go | 12 ++++---- vehicle/jlr.go | 4 +-- vehicle/mercedes/helper.go | 3 +- vehicle/mercedes/identity.go | 2 +- vehicle/polestar/grpc.go | 4 +-- vehicle/saic/api.go | 10 +++---- vehicle/tesla/api_test.go | 4 +-- vehicle/vag/vwidentity/endpoint.go | 7 ++--- 37 files changed, 135 insertions(+), 154 deletions(-) diff --git a/.github/actions/build-toolchain/action.yml b/.github/actions/build-toolchain/action.yml index 7d0f17453..89adc042f 100644 --- a/.github/actions/build-toolchain/action.yml +++ b/.github/actions/build-toolchain/action.yml @@ -13,7 +13,7 @@ inputs: runs: using: composite steps: - - uses: actions/setup-go@v6 + - uses: actions/setup-go@v7 with: go-version-file: go.mod cache: false # managed explicitly below to keep the scoped namespaces diff --git a/.golangci.yml b/.golangci.yml index 8c1836a1e..ae69e7c0f 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -23,6 +23,7 @@ linters: settings: modernize: disable: + - embedlit - stringsbuilder staticcheck: checks: diff --git a/Dockerfile b/Dockerfile index 7dafc4a83..4341548b7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,7 +24,7 @@ RUN make ui # STEP 2 build executable binary -FROM --platform=$BUILDPLATFORM golang:1.26.3-alpine AS builder +FROM --platform=$BUILDPLATFORM golang:1.27.0-alpine AS builder # Install git + SSL ca certificates. # Git is required for fetching the dependencies. diff --git a/charger/evsewifi_test.go b/charger/evsewifi_test.go index 03fd1cd2f..020e5b298 100644 --- a/charger/evsewifi_test.go +++ b/charger/evsewifi_test.go @@ -10,10 +10,10 @@ import ( ) func TestEvseWifi(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ts := httptest.NewTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = fmt.Fprintln(w, `{"list":[{"useMeter":true, "alwaysActive":true}]}`) })) - defer ts.Close() + ts.Start() wb, err := NewEVSEWifiFromConfig(map[string]any{ "uri": ts.URL, @@ -46,10 +46,10 @@ func TestEvseWifi(t *testing.T) { } func TestEvseWifiEx(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ts := httptest.NewTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = fmt.Fprintln(w, `{"list":[{"actualCurrentMA":600, "alwaysActive":true}]}`) })) - defer ts.Close() + ts.Start() wb, err := NewEVSEWifiFromConfig(map[string]any{ "uri": ts.URL, diff --git a/charger/ghostone/identity_test.go b/charger/ghostone/identity_test.go index 03f441d2e..50153a2da 100644 --- a/charger/ghostone/identity_test.go +++ b/charger/ghostone/identity_test.go @@ -16,13 +16,11 @@ import ( func TestTokenSource_ContextCancellation(t *testing.T) { // server that blocks -- simulates slow/unreachable wallbox unblock := make(chan struct{}) - srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + srv := httptest.NewTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { <-unblock })) - defer func() { - close(unblock) - srv.Close() - }() + srv.StartTLS() + defer close(unblock) log := util.NewLogger("test") @@ -40,12 +38,12 @@ func TestTokenSource_ContextCancellation(t *testing.T) { func TestTokenSource_Success(t *testing.T) { // server that returns a valid JWT-style token - srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + srv := httptest.NewTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Authorization", fmt.Sprintf("Bearer %s", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiZXhwIjo5OTk5OTk5OTk5fQ.signature")) w.WriteHeader(http.StatusOK) })) - defer srv.Close() + srv.StartTLS() log := util.NewLogger("test") diff --git a/charger/go-e/api_test.go b/charger/go-e/api_test.go index 3db51a3d5..ee674860d 100644 --- a/charger/go-e/api_test.go +++ b/charger/go-e/api_test.go @@ -34,7 +34,8 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { func TestLocalV1(t *testing.T) { h := &handler{} - srv := httptest.NewServer(h) + srv := httptest.NewTestServer(t, h) + srv.Start() // h.expect("/api/status?filter=alw") local := NewLocal(util.NewLogger("foo"), srv.URL, 0) @@ -52,7 +53,8 @@ func TestLocalV1(t *testing.T) { func TestLocalV2(t *testing.T) { h := &handler{} - srv := httptest.NewServer(h) + srv := httptest.NewTestServer(t, h) + srv.Start() h.expect("/api/status?filter=alw") local := NewLocal(util.NewLogger("foo"), srv.URL, 0) diff --git a/charger/go-e_test.go b/charger/go-e_test.go index ed77a96ff..d74bfcf61 100644 --- a/charger/go-e_test.go +++ b/charger/go-e_test.go @@ -33,7 +33,8 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } func TestGoEV1(t *testing.T) { - srv := httptest.NewServer(new(handler)) + srv := httptest.NewTestServer(t, new(handler)) + srv.Start() sponsor.Subject = "foo" @@ -56,8 +57,10 @@ func TestGoEV1(t *testing.T) { } func TestGoEV2(t *testing.T) { - srv := httptest.NewServer(new(handler)) - srv.Config.Handler.(*handler).expect("/api/status?filter=alw") + h := new(handler) + srv := httptest.NewTestServer(t, h) + srv.Start() + h.expect("/api/status?filter=alw") sponsor.Subject = "foo" diff --git a/charger/ocpp/forwarder_test.go b/charger/ocpp/forwarder_test.go index 7eded2504..00f178750 100644 --- a/charger/ocpp/forwarder_test.go +++ b/charger/ocpp/forwarder_test.go @@ -109,7 +109,7 @@ func TestForwarderUpstreamReconnect(t *testing.T) { var conns atomic.Int32 frames := make(chan []byte, 1) - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + srv := httptest.NewTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { c, err := websocket.Accept(w, r, &websocket.AcceptOptions{Subprotocols: []string{"ocpp1.6"}}) if err != nil { return @@ -127,7 +127,7 @@ func TestForwarderUpstreamReconnect(t *testing.T) { } c.CloseNow() })) - defer srv.Close() + srv.Start() cleanup := forwarderTestSetup(t, id) defer cleanup() @@ -144,7 +144,7 @@ func TestForwarderDialRetry(t *testing.T) { var reqs atomic.Int32 frames := make(chan []byte, 1) - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + srv := httptest.NewTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if reqs.Add(1) <= 2 { // reject the websocket upgrade to simulate an unreachable upstream http.Error(w, "unavailable", http.StatusServiceUnavailable) @@ -162,7 +162,7 @@ func TestForwarderDialRetry(t *testing.T) { } c.CloseNow() })) - defer srv.Close() + srv.Start() cleanup := forwarderTestSetup(t, id) defer cleanup() diff --git a/charger/semp_test.go b/charger/semp_test.go index 18eaafe71..0061c5ade 100644 --- a/charger/semp_test.go +++ b/charger/semp_test.go @@ -251,8 +251,8 @@ func TestSEMPCharger(t *testing.T) { planningResponse: mockPlanningRequestResponse, infoResponse: mockDeviceInfoResponse, } - server := httptest.NewServer(handler) - defer server.Close() + server := httptest.NewTestServer(t, handler) + server.Start() ctx := t.Context() @@ -318,8 +318,8 @@ func TestSEMPChargerOff(t *testing.T) { planningResponse: mockEmptyPlanningRequestResponse, infoResponse: mockDeviceInfoResponse, } - server := httptest.NewServer(handler) - defer server.Close() + server := httptest.NewTestServer(t, handler) + server.Start() ctx := t.Context() @@ -353,8 +353,8 @@ func TestSEMPChargerDeviceNotFound(t *testing.T) { planningResponse: mockPlanningRequestResponse, infoResponse: mockDeviceInfoResponse, } - server := httptest.NewServer(handler) - defer server.Close() + server := httptest.NewTestServer(t, handler) + server.Start() // NewSEMP now calls Enabled() which will fail if device is not found _, err := NewSEMP(t.Context(), server.URL+"/semp", "F-12345678-ABCDEF123456-00", time.Second) @@ -368,8 +368,8 @@ func TestSEMPChargerReady(t *testing.T) { planningResponse: mockPlanningRequestResponse, infoResponse: mockDeviceInfoResponse, } - server := httptest.NewServer(handler) - defer server.Close() + server := httptest.NewTestServer(t, handler) + server.Start() ctx := t.Context() @@ -396,8 +396,8 @@ func TestSEMPChargerPhases1p3p(t *testing.T) { planningResponse: mockPlanningRequestResponse, infoResponse: mockDeviceInfoPhases1p3pResponse, } - server := httptest.NewServer(handler) - defer server.Close() + server := httptest.NewTestServer(t, handler) + server.Start() ctx := t.Context() @@ -440,8 +440,8 @@ func TestSEMPChargerChargedEnergy(t *testing.T) { infoResponse: mockDeviceInfoResponse, parametersResponse: mockParametersResponse, } - server := httptest.NewServer(handler) - defer server.Close() + server := httptest.NewTestServer(t, handler) + server.Start() ctx := t.Context() @@ -465,8 +465,8 @@ func TestSEMPChargerChargedEnergy(t *testing.T) { infoResponse: mockDeviceInfoResponse, // parametersResponse left empty } - server2 := httptest.NewServer(handler2) - defer server2.Close() + server2 := httptest.NewTestServer(t, handler2) + server2.Start() wb2, err := NewSEMP(t.Context(), server2.URL+"/semp", "F-12345678-ABCDEF123456-00", time.Second) require.NoError(t, err) @@ -483,8 +483,8 @@ func TestSEMPChargerAutoDetectDeviceID(t *testing.T) { planningResponse: mockPlanningRequestResponse, infoResponse: mockDeviceInfoResponse, } - server := httptest.NewServer(handler) - defer server.Close() + server := httptest.NewTestServer(t, handler) + server.Start() ctx := t.Context() diff --git a/charger/trydan_test.go b/charger/trydan_test.go index 368625006..cba28705f 100644 --- a/charger/trydan_test.go +++ b/charger/trydan_test.go @@ -20,10 +20,13 @@ func withSponsor(t *testing.T) { t.Cleanup(func() { sponsor.Subject = orig }) } -func trydanTestServerWithBody(body string) *httptest.Server { - return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { +func trydanTestServerWithBody(t *testing.T, body string) *httptest.Server { + t.Helper() + srv := httptest.NewTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, body) })) + srv.Start() + return srv } // ChargeState maps directly to api.ChargeStatus, except firmware 2.5.0 keeps it at @@ -47,8 +50,7 @@ func TestTrydanStatus(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - srv := trydanTestServerWithBody(tc.json) - defer srv.Close() + srv := trydanTestServerWithBody(t, tc.json) wb, err := NewTrydan(srv.URL, 0) if err != nil { @@ -107,8 +109,7 @@ func TestTrydanPhaseMeasurementsUnavailable(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - srv := trydanTestServerWithBody(tc.json) - defer srv.Close() + srv := trydanTestServerWithBody(t, tc.json) wb, err := NewTrydan(srv.URL, 0) if err != nil { diff --git a/go.mod b/go.mod index 044665c48..eed038174 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/evcc-io/evcc -go 1.26.3 +go 1.27.0 require ( dario.cat/mergo v1.0.2 @@ -45,7 +45,6 @@ require ( github.com/gokrazy/updater v0.0.0-20260620140544-0a84d8ab3878 github.com/golang-jwt/jwt/v5 v5.3.1 github.com/google/go-github/v32 v32.1.0 - github.com/google/uuid v1.6.0 github.com/gorilla/handlers v1.5.2 github.com/gorilla/mux v1.8.1 github.com/gosimple/slug v1.15.0 @@ -175,6 +174,7 @@ require ( github.com/google/go-querystring v1.1.0 // indirect github.com/google/jsonschema-go v0.4.3 // indirect github.com/google/renameio/v2 v2.0.0 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.5.3 // indirect github.com/gosimple/unidecode v1.0.1 // indirect github.com/govalues/decimal v0.1.36 // indirect diff --git a/hems/shm/shm.go b/hems/shm/shm.go index d731b2ff1..7a4e5968d 100644 --- a/hems/shm/shm.go +++ b/hems/shm/shm.go @@ -8,13 +8,13 @@ import ( "net/http" "strings" "time" + "uuid" "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/core/loadpoint" "github.com/evcc-io/evcc/core/site" "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/machine" - "github.com/google/uuid" "github.com/gorilla/mux" "github.com/koron/go-ssdp" ) @@ -56,10 +56,7 @@ func NewFromConfig(cfg Config, hostUri string, site site.API, addr string, route return fmt.Errorf("invalid vendor id: %v. Must be 8 characters HEX string", vendorId) } - uid, err := uuid.NewUUID() - if err != nil { - return err - } + uid := uuid.New() // Only if DeviceSerial is explicitly configured: validate it and patch the // UUID node (last 6 bytes) to ensure the UDN and DeviceSerial are stable across restarts. @@ -80,6 +77,7 @@ func NewFromConfig(cfg Config, hostUri string, site site.API, addr string, route } var did []byte + var err error if cfg.DeviceId == "" { if did, err = UniqueDeviceID(); err != nil { return fmt.Errorf("creating device id: %w", err) diff --git a/meter/dsmr_test.go b/meter/dsmr_test.go index e856b4c49..4d32f08d7 100644 --- a/meter/dsmr_test.go +++ b/meter/dsmr_test.go @@ -266,7 +266,7 @@ func TestDsmrWebSocket(t *testing.T) { t.Run(tc.name, func(t *testing.T) { payload := dsmrFrame(dsmrTelegram50) - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + srv := httptest.NewTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { conn, err := websocket.Accept(w, r, nil) if err != nil { return @@ -280,7 +280,7 @@ func TestDsmrWebSocket(t *testing.T) { time.Sleep(50 * time.Millisecond) } })) - defer srv.Close() + srv.Start() uri := "ws" + strings.TrimPrefix(srv.URL, "http") diff --git a/meter/eebus.go b/meter/eebus.go index 69262af24..e15dc824a 100644 --- a/meter/eebus.go +++ b/meter/eebus.go @@ -186,7 +186,7 @@ func (c *EEBus) lastCurtailPercent() int { return c.curtailPercent } -func (c *EEBus) readValue(scenario uint, update func(entity spineapi.EntityRemoteInterface) (float64, error)) (float64, error) { +func (c *EEBus) readValue[T any](scenario uint, update func(entity spineapi.EntityRemoteInterface) (T, error)) (T, error) { c.mu.Lock() defer c.mu.Unlock() return eebusReadValue(c.mm, c.maEntity, scenario, update) @@ -205,10 +205,7 @@ func (c *EEBus) TotalEnergy() (float64, error) { } func (c *EEBus) readPhases(scenario uint, update func(entity spineapi.EntityRemoteInterface) ([]float64, error)) (float64, float64, float64, error) { - c.mu.Lock() - defer c.mu.Unlock() - - res, err := eebusReadValue(c.mm, c.maEntity, scenario, update) + res, err := c.readValue(scenario, update) if err != nil { return 0, 0, 0, err } diff --git a/meter/shelly/gen2.go b/meter/shelly/gen2.go index a34cc6822..1d18314b0 100644 --- a/meter/shelly/gen2.go +++ b/meter/shelly/gen2.go @@ -108,7 +108,7 @@ type gen2 struct { emdata func() (Gen2EMData, error) } -func apiCall[T any](c *gen2, id int, method string) func() (T, error) { +func (c *gen2) apiCall[T any](id int, method string) func() (T, error) { return func() (T, error) { var res T if err := c.execCmd(id, method, &res); err != nil { @@ -152,9 +152,9 @@ func newGen2(helper *request.Helper, uri, model string, channel int, user, passw } if c.hasMethod("PM1.GetStatus") { - c.switchstatus = util.ResettableCached(apiCall[Gen2SwitchStatus](c, channel, "PM1.GetStatus"), cache) + c.switchstatus = util.ResettableCached(c.apiCall[Gen2SwitchStatus](channel, "PM1.GetStatus"), cache) } else { - c.switchstatus = util.ResettableCached(apiCall[Gen2SwitchStatus](c, c.switchchannel, "Switch.GetStatus"), cache) + c.switchstatus = util.ResettableCached(c.apiCall[Gen2SwitchStatus](c.switchchannel, "Switch.GetStatus"), cache) } // device-side "Reverse power measurement" setting (requires restart, hence static) var cfgMethod string @@ -177,10 +177,10 @@ func newGen2(helper *request.Helper, uri, model string, channel int, user, passw c.reversed = cfg.Reverse } - c.em1status = util.Cached(apiCall[Gen2EM1Status](c, channel, "EM1.GetStatus"), cache) - c.em1data = util.Cached(apiCall[Gen2EM1Data](c, channel, "EM1Data.GetStatus"), cache) - c.emstatus = util.Cached(apiCall[Gen2EMStatus](c, channel, "EM.GetStatus"), cache) - c.emdata = util.Cached(apiCall[Gen2EMData](c, channel, "EMData.GetStatus"), cache) + c.em1status = util.Cached(c.apiCall[Gen2EM1Status](channel, "EM1.GetStatus"), cache) + c.em1data = util.Cached(c.apiCall[Gen2EM1Data](channel, "EM1Data.GetStatus"), cache) + c.emstatus = util.Cached(c.apiCall[Gen2EMStatus](channel, "EM.GetStatus"), cache) + c.emdata = util.Cached(c.apiCall[Gen2EMData](channel, "EMData.GetStatus"), cache) return c, nil } diff --git a/plugin/cached.go b/plugin/cached.go index 94a37aaad..9d8b068fa 100644 --- a/plugin/cached.go +++ b/plugin/cached.go @@ -46,7 +46,7 @@ func NewCachedFromConfig(ctx context.Context, other map[string]any) (Plugin, err }, nil } -func cachedGetter[T any](o *cachedPlugin, valuer func(ctx context.Context) (func() (T, error), error)) (func() (T, error), error) { +func (o *cachedPlugin) cachedGetter[T any](valuer func(ctx context.Context) (func() (T, error), error)) (func() (T, error), error) { value, err := valuer(o.ctx) if err != nil { return nil, fmt.Errorf("cached: %w", err) @@ -77,23 +77,23 @@ func cachedGetter[T any](o *cachedPlugin, valuer func(ctx context.Context) (func var _ StringGetter = (*cachedPlugin)(nil) func (o *cachedPlugin) StringGetter() (func() (string, error), error) { - return cachedGetter(o, o.value.StringGetter) + return o.cachedGetter(o.value.StringGetter) } var _ FloatGetter = (*cachedPlugin)(nil) func (o *cachedPlugin) FloatGetter() (func() (float64, error), error) { - return cachedGetter(o, o.value.FloatGetter) + return o.cachedGetter(o.value.FloatGetter) } var _ IntGetter = (*cachedPlugin)(nil) func (o *cachedPlugin) IntGetter() (func() (int64, error), error) { - return cachedGetter(o, o.value.IntGetter) + return o.cachedGetter(o.value.IntGetter) } var _ BoolGetter = (*cachedPlugin)(nil) func (o *cachedPlugin) BoolGetter() (func() (bool, error), error) { - return cachedGetter(o, o.value.BoolGetter) + return o.cachedGetter(o.value.BoolGetter) } diff --git a/plugin/cached_test.go b/plugin/cached_test.go index 0e27859c0..9c4c1d2b5 100644 --- a/plugin/cached_test.go +++ b/plugin/cached_test.go @@ -15,7 +15,7 @@ func TestCachedHitAndExpiry(t *testing.T) { o := &cachedPlugin{ctx: context.Background(), clock: c, cache: time.Hour} var calls int - get, err := cachedGetter(o, func(context.Context) (func() (int64, error), error) { + get, err := o.cachedGetter(func(context.Context) (func() (int64, error), error) { return func() (int64, error) { calls++ return int64(calls), nil @@ -47,7 +47,7 @@ func TestCachedDoesNotCacheErrors(t *testing.T) { o := &cachedPlugin{ctx: context.Background(), clock: c, cache: time.Hour} var calls int - get, err := cachedGetter(o, func(context.Context) (func() (int64, error), error) { + get, err := o.cachedGetter(func(context.Context) (func() (int64, error), error) { return func() (int64, error) { calls++ return 0, errors.New("boom") diff --git a/plugin/http_test.go b/plugin/http_test.go index 543a80257..05a59b748 100644 --- a/plugin/http_test.go +++ b/plugin/http_test.go @@ -60,11 +60,8 @@ type httpTestSuite struct { func (suite *httpTestSuite) SetupSuite() { suite.h = new(httpHandler) - suite.srv = httptest.NewServer(suite.h) -} - -func (suite *httpTestSuite) TearDown() { - suite.srv.Close() + suite.srv = httptest.NewTestServer(suite.T(), suite.h) + suite.srv.Start() } func (suite *httpTestSuite) TestGet() { diff --git a/plugin/script.go b/plugin/script.go index 5cb98c423..6b2a2223e 100644 --- a/plugin/script.go +++ b/plugin/script.go @@ -125,7 +125,7 @@ func (p *Script) StringGetter() (func() (string, error), error) { }, nil } -func scriptSetter[T any](p *Script, param string) (func(T) error, error) { +func (p *Script) scriptSetter[T any](param string) (func(T) error, error) { return func(val T) error { cmd, err := util.ReplaceFormatted(p.script, map[string]any{ param: val, @@ -143,19 +143,19 @@ var _ IntSetter = (*Script)(nil) // IntSetter invokes script with parameter replaced by int value func (p *Script) IntSetter(param string) (func(int64) error, error) { - return scriptSetter[int64](p, param) + return p.scriptSetter[int64](param) } var _ BoolSetter = (*Script)(nil) // BoolSetter invokes script with parameter replaced by bool value func (p *Script) BoolSetter(param string) (func(bool) error, error) { - return scriptSetter[bool](p, param) + return p.scriptSetter[bool](param) } var _ StringSetter = (*Script)(nil) // StringSetter returns a function that invokes a script with parameter by a string value func (p *Script) StringSetter(param string) (func(string) error, error) { - return scriptSetter[string](p, param) + return p.scriptSetter[string](param) } diff --git a/plugin/sleep.go b/plugin/sleep.go index dd3c14d3e..27646d0e6 100644 --- a/plugin/sleep.go +++ b/plugin/sleep.go @@ -33,8 +33,7 @@ func NewSleepFromConfig(ctx context.Context, other map[string]any) (Plugin, erro } // sleeper is the generic sleeper function for sleepPlugin -// it is currently not possible to write this as a method -func sleeper[T comparable](o *sleepPlugin) func(T) error { +func (o *sleepPlugin) sleeper[T comparable]() func(T) error { return func(val T) error { <-time.After(o.duration) @@ -45,17 +44,17 @@ func sleeper[T comparable](o *sleepPlugin) func(T) error { var _ IntSetter = (*sleepPlugin)(nil) func (o *sleepPlugin) IntSetter(param string) (func(int64) error, error) { - return sleeper[int64](o), nil + return o.sleeper[int64](), nil } var _ FloatSetter = (*sleepPlugin)(nil) func (o *sleepPlugin) FloatSetter(param string) (func(float64) error, error) { - return sleeper[float64](o), nil + return o.sleeper[float64](), nil } var _ BoolSetter = (*sleepPlugin)(nil) func (o *sleepPlugin) BoolSetter(param string) (func(bool) error, error) { - return sleeper[bool](o), nil + return o.sleeper[bool](), nil } diff --git a/plugin/socket_test.go b/plugin/socket_test.go index 06d3b69b7..06c5cbd7f 100644 --- a/plugin/socket_test.go +++ b/plugin/socket_test.go @@ -16,7 +16,7 @@ func TestSockePlugin(t *testing.T) { ctx, cancel := context.WithTimeout(t.Context(), time.Second*10) defer cancel() - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + srv := httptest.NewTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { c, err := websocket.Accept(w, r, nil) require.NoError(t, err) defer c.Close(websocket.StatusNormalClosure, "") @@ -36,8 +36,7 @@ func TestSockePlugin(t *testing.T) { } } })) - - defer srv.Close() + srv.Start() addr := "ws://" + srv.Listener.Addr().String() p, err := NewSocketPluginFromConfig(map[string]any{ diff --git a/plugin/valid.go b/plugin/valid.go index a739d3de5..1df0400c3 100644 --- a/plugin/valid.go +++ b/plugin/valid.go @@ -48,9 +48,7 @@ func NewValidPlugin(ctx context.Context, valid func() (bool, error), value Confi } } -var _ StringGetter = (*validPlugin)(nil) - -func validGetter[T any](o *validPlugin, valuer func(ctx context.Context) (func() (T, error), error)) (func() (T, error), error) { +func (o *validPlugin) validGetter[T any](valuer func(ctx context.Context) (func() (T, error), error)) (func() (T, error), error) { value, err := valuer(o.ctx) if err != nil { return nil, fmt.Errorf("valid: %w", err) @@ -74,17 +72,17 @@ func validGetter[T any](o *validPlugin, valuer func(ctx context.Context) (func() var _ StringGetter = (*validPlugin)(nil) func (o *validPlugin) StringGetter() (func() (string, error), error) { - return validGetter(o, o.value.StringGetter) + return o.validGetter(o.value.StringGetter) } var _ FloatGetter = (*validPlugin)(nil) func (o *validPlugin) FloatGetter() (func() (float64, error), error) { - return validGetter(o, o.value.FloatGetter) + return o.validGetter(o.value.FloatGetter) } var _ IntGetter = (*validPlugin)(nil) func (o *validPlugin) IntGetter() (func() (int64, error), error) { - return validGetter(o, o.value.IntGetter) + return o.validGetter(o.value.IntGetter) } diff --git a/plugin/watchdog.go b/plugin/watchdog.go index 3d3754e1f..6abbe6341 100644 --- a/plugin/watchdog.go +++ b/plugin/watchdog.go @@ -76,8 +76,7 @@ type deferredState[T comparable] struct { } // setter is the generic setter function for watchdogPlugin -// it is currently not possible to write this as a method -func setter[T comparable](o *watchdogPlugin, set func(T) error, reset []T) func(T) error { +func (o *watchdogPlugin) setter[T comparable](set func(T) error, reset []T) func(T) error { var state *deferredState[T] // seed with now, not zero: otherwise the first write's delay computes to 0 and skips // deferral, which is wrong for an unknown last write @@ -195,7 +194,7 @@ func (o *watchdogPlugin) IntSetter(param string) (func(int64) error, error) { } } - res := setter(o, set, reset) + res := o.setter(set, reset) if o.initial != nil { val, err := strconv.ParseInt(*o.initial, 10, 64) if err != nil { @@ -229,7 +228,7 @@ func (o *watchdogPlugin) FloatSetter(param string) (func(float64) error, error) } } - res := setter(o, set, reset) + res := o.setter(set, reset) if o.initial != nil { val, err := strconv.ParseFloat(*o.initial, 64) if err != nil { @@ -263,7 +262,7 @@ func (o *watchdogPlugin) BoolSetter(param string) (func(bool) error, error) { reset = append(reset, val) } - res := setter(o, set, reset) + res := o.setter(set, reset) if o.initial != nil { val, err := strconv.ParseBool(*o.initial) if err != nil { diff --git a/plugin/watchdog_test.go b/plugin/watchdog_test.go index abae08f3e..cec104158 100644 --- a/plugin/watchdog_test.go +++ b/plugin/watchdog_test.go @@ -22,7 +22,7 @@ func TestWatchdogSetterConcurrency(t *testing.T) { var u atomic.Uint32 - set := setter(p, func(i int) error { + set := p.setter(func(i int) error { if !u.CompareAndSwap(0, 1) { return errors.New("race") } @@ -63,7 +63,7 @@ func TestWatchdogDeferredUpdate(t *testing.T) { } var calls []int - set := setter(p, func(i int) error { + set := p.setter(func(i int) error { calls = append(calls, i) return nil }, []int{1}) // 1 is reset value @@ -108,7 +108,7 @@ func TestWatchdogCancelPendingDeferredUpdate(t *testing.T) { } var calls []int - set := setter(p, func(i int) error { + set := p.setter(func(i int) error { calls = append(calls, i) return nil }, []int{1}) // 1 is reset value @@ -154,7 +154,7 @@ func TestWatchdogDelayBackwardCompatibility(t *testing.T) { } var calls []int - set := setter(p, func(i int) error { + set := p.setter(func(i int) error { calls = append(calls, i) return nil }, []int{1}) // 1 is reset value @@ -185,7 +185,7 @@ func TestWatchdogResetStopsInflightTick(t *testing.T) { var sawReset, stale atomic.Bool - set := setter(p, func(v int) error { + set := p.setter(func(v int) error { if v == 1 { sawReset.Store(true) // hold the lock so an in-flight tick queues behind the reset diff --git a/server/remote/tunnel_test.go b/server/remote/tunnel_test.go index 3bad1eb48..a1088389a 100644 --- a/server/remote/tunnel_test.go +++ b/server/remote/tunnel_test.go @@ -39,9 +39,11 @@ func serveSession(w http.ResponseWriter, r *http.Request, sessions chan<- *yamux // session to the sessions channel. Simulates the cloud proxy. func tunnelTestServer(t *testing.T, sessions chan<- *yamux.Session) *httptest.Server { t.Helper() - return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + srv := httptest.NewTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { serveSession(w, r, sessions) })) + srv.Start() + return srv } // requireReachable opens a stream through the server session and issues an @@ -78,7 +80,6 @@ func TestTunnelReconnect(t *testing.T) { sessions := make(chan *yamux.Session, 4) srv := tunnelTestServer(t, sessions) - defer srv.Close() wsURL := "ws" + strings.TrimPrefix(srv.URL, "http") @@ -104,11 +105,11 @@ func TestTunnelReconnect(t *testing.T) { // the proxy rejects credentials (401/403); a new token requires a restart. func TestTunnelRejectedCredentialsStops(t *testing.T) { var attempts atomic.Int32 - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + srv := httptest.NewTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { attempts.Add(1) w.WriteHeader(http.StatusUnauthorized) })) - defer srv.Close() + srv.Start() wsURL := "ws" + strings.TrimPrefix(srv.URL, "http") @@ -138,7 +139,7 @@ func TestTunnelReconnectsAfterTransientError(t *testing.T) { var attempts atomic.Int32 sessions := make(chan *yamux.Session, 4) - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + srv := httptest.NewTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // first attempt fails transiently, later ones succeed if attempts.Add(1) == 1 { w.WriteHeader(http.StatusServiceUnavailable) @@ -146,7 +147,7 @@ func TestTunnelReconnectsAfterTransientError(t *testing.T) { } serveSession(w, r, sessions) })) - defer srv.Close() + srv.Start() wsURL := "ws" + strings.TrimPrefix(srv.URL, "http") diff --git a/util/homeassistant/connection_test.go b/util/homeassistant/connection_test.go index 21c29b1e8..f504bc7c6 100644 --- a/util/homeassistant/connection_test.go +++ b/util/homeassistant/connection_test.go @@ -23,10 +23,10 @@ func newTestConnection(baseURL string) *Connection { // newStateConnection returns a connection serving state for any entity func newStateConnection(t *testing.T, state string) *Connection { - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + srv := httptest.NewTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, `{"entity_id":"sensor.foo","state":%q}`, state) })) - t.Cleanup(srv.Close) + srv.Start() return newTestConnection(srv.URL) } @@ -116,13 +116,13 @@ func TestCallSwitchService_DomainDispatch(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { var gotPath, gotBody string - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + srv := httptest.NewTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gotPath = r.URL.Path body, _ := io.ReadAll(r.Body) gotBody = string(body) w.WriteHeader(http.StatusOK) })) - defer srv.Close() + srv.Start() err := newTestConnection(srv.URL).CallSwitchService(tc.entity, tc.turnOn) diff --git a/util/urlvalues/url.go b/util/urlvalues/url.go index d96eb4256..048d4dbd1 100644 --- a/util/urlvalues/url.go +++ b/util/urlvalues/url.go @@ -4,19 +4,9 @@ package urlvalues import ( "errors" "net/url" - "slices" "strings" ) -// Copy creates a deep copy of url values -func Copy(q url.Values) url.Values { - res := make(url.Values, len(q)) - for k, v := range q { - res[k] = slices.Clone(v) - } - return res -} - // Require verifies that url contains the required non-nil values func Require(q url.Values, keys ...string) error { for _, k := range keys { diff --git a/vehicle/bluelink/cci.go b/vehicle/bluelink/cci.go index 5d31d631a..4fdc4e016 100644 --- a/vehicle/bluelink/cci.go +++ b/vehicle/bluelink/cci.go @@ -14,10 +14,10 @@ import ( "net/url" "strings" "time" + "uuid" "github.com/evcc-io/evcc/server/db/settings" "github.com/evcc-io/evcc/util/request" - "github.com/google/uuid" "golang.org/x/oauth2" ) @@ -92,7 +92,7 @@ func (v *Identity) loginCCI(password string) (*oauth2.Token, error) { // fetch RSA cert, signin, exchange the auth code for CCI and then CCS tokens func (v *Identity) loginCCIPassword(password string) (*oauth2.Token, error) { c := v.config.CCI - deviceID := uuid.NewString() + deviceID := uuid.New().String() v.log.DEBUG.Println("cci: logging in via OneApp/CCI password login") diff --git a/vehicle/bluelink/identity.go b/vehicle/bluelink/identity.go index af9ef836a..25eb245e4 100644 --- a/vehicle/bluelink/identity.go +++ b/vehicle/bluelink/identity.go @@ -9,12 +9,12 @@ import ( "strconv" "strings" "time" + "uuid" "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/oauth" "github.com/evcc-io/evcc/util/request" - "github.com/google/uuid" "github.com/samber/lo" "golang.org/x/oauth2" ) @@ -79,11 +79,11 @@ func (v *Identity) getDeviceID() (string, error) { return "", err } - uuid := uuid.NewString() + id := uuid.New().String() data := map[string]any{ "pushRegId": lo.RandomString(64, []rune("0123456789ABCDEF")), "pushType": v.config.PushType, - "uuid": uuid, + "uuid": id, } headers := map[string]string{ diff --git a/vehicle/bluelink/identity_test.go b/vehicle/bluelink/identity_test.go index 2c75d76f1..3fc8b98ed 100644 --- a/vehicle/bluelink/identity_test.go +++ b/vehicle/bluelink/identity_test.go @@ -150,8 +150,8 @@ func TestLoginUsesCCIAndWiresRefreshCCI(t *testing.T) { loginSrv := httptest.NewServer(loginMux) defer loginSrv.Close() - var refreshCalls int32 - var exchangeCalls int32 + var refreshCalls atomic.Int32 + var exchangeCalls atomic.Int32 cciMux := http.NewServeMux() cciMux.HandleFunc("/domain/api/v1/auth/token", func(w http.ResponseWriter, _ *http.Request) { _ = json.NewEncoder(w).Encode(map[string]any{ @@ -165,14 +165,14 @@ func TestLoginUsesCCIAndWiresRefreshCCI(t *testing.T) { // the first (login) exchange returns a token expiring inside oauth2's // 10s buffer, so the next Token() call triggers exactly one refresh resp := map[string]any{"accessToken": "ccs-token-2", "expiresTime": time.Now().Add(time.Hour).Unix()} - if atomic.AddInt32(&exchangeCalls, 1) == 1 { + if exchangeCalls.Add(1) == 1 { resp["accessToken"] = "ccs-token-1" resp["expiresTime"] = time.Now().Add(5 * time.Second).Unix() } _ = json.NewEncoder(w).Encode(resp) }) cciMux.HandleFunc("/domain/api/v2/auth/token-refresh", func(w http.ResponseWriter, _ *http.Request) { - atomic.AddInt32(&refreshCalls, 1) + refreshCalls.Add(1) _ = json.NewEncoder(w).Encode(map[string]any{ "accessToken": "cci-access-2", "refreshToken": "cci-refresh-2", }) @@ -192,12 +192,12 @@ func TestLoginUsesCCIAndWiresRefreshCCI(t *testing.T) { token, err := identity.Token() require.NoError(t, err) assert.Equal(t, "ccs-token-2", token.AccessToken) - assert.EqualValues(t, 1, atomic.LoadInt32(&refreshCalls), "expected TokenSource to be wired to refreshCCI") + assert.EqualValues(t, 1, refreshCalls.Load(), "expected TokenSource to be wired to refreshCCI") // the now long-lived token must not trigger a further refresh _, err = identity.Token() require.NoError(t, err) - assert.EqualValues(t, 1, atomic.LoadInt32(&refreshCalls), "unexpected additional refresh") + assert.EqualValues(t, 1, refreshCalls.Load(), "unexpected additional refresh") } // TestLoginPropagatesLegacyError covers the legacy path: a failure there must diff --git a/vehicle/jlr.go b/vehicle/jlr.go index a300f2b79..bad56e758 100644 --- a/vehicle/jlr.go +++ b/vehicle/jlr.go @@ -5,12 +5,12 @@ import ( "net/http" "net/url" "time" + "uuid" "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/request" "github.com/evcc-io/evcc/vehicle/jlr" - "github.com/google/uuid" ) // https://github.com/ardevd/jlrpy @@ -54,7 +54,7 @@ func NewJLRFromConfig(other map[string]any) (api.Vehicle, error) { log := util.NewLogger("jlr").Redact(cc.User, cc.Password, cc.VIN, cc.DeviceID) if cc.DeviceID == "" { - cc.DeviceID = uuid.NewString() + cc.DeviceID = uuid.New().String() log.WARN.Println("new device id generated, add `deviceid` to config:", cc.DeviceID) } diff --git a/vehicle/mercedes/helper.go b/vehicle/mercedes/helper.go index 7bc3ada48..22dc5a3b1 100644 --- a/vehicle/mercedes/helper.go +++ b/vehicle/mercedes/helper.go @@ -4,8 +4,7 @@ import ( "fmt" "net/http" "sync" - - "github.com/google/uuid" + "uuid" ) // Helper provides utility primitives diff --git a/vehicle/mercedes/identity.go b/vehicle/mercedes/identity.go index 1ec34dfda..a2761a911 100644 --- a/vehicle/mercedes/identity.go +++ b/vehicle/mercedes/identity.go @@ -7,13 +7,13 @@ import ( "net/url" "strings" "sync" + "uuid" "github.com/evcc-io/evcc/server/db/settings" "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/oauth" "github.com/evcc-io/evcc/util/request" "github.com/evcc-io/evcc/util/transport" - "github.com/google/uuid" "golang.org/x/oauth2" ) diff --git a/vehicle/polestar/grpc.go b/vehicle/polestar/grpc.go index 70de34f8d..7504d14e6 100644 --- a/vehicle/polestar/grpc.go +++ b/vehicle/polestar/grpc.go @@ -8,12 +8,12 @@ import ( "net" "net/http" "strconv" + "uuid" "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/request" "github.com/evcc-io/evcc/vehicle/polestar/pb" - "github.com/google/uuid" "golang.org/x/oauth2" "google.golang.org/grpc" "google.golang.org/grpc/codes" @@ -119,7 +119,7 @@ func (v *GrpcAPI) Battery(ctx context.Context, vin string) (*pb.Battery, error) ctx = metadata.AppendToOutgoingContext(ctx, "vin", vin) res, err := v.client.GetLatestBattery(ctx, &pb.GetBatteryRequest{ - Id: uuid.NewString(), + Id: uuid.New().String(), Vin: vin, }) if err != nil { diff --git a/vehicle/saic/api.go b/vehicle/saic/api.go index c1a32570d..3e405b3e2 100644 --- a/vehicle/saic/api.go +++ b/vehicle/saic/api.go @@ -95,7 +95,7 @@ func (v *API) doRepeatedRequest(path string, event_id string) error { event_id) var res requests.Answer[requests.ChargeStatus] - if _, err = doRequest(v, req, &res); err == nil { + if _, err = v.doRequest(req, &res); err == nil { v.store(res.Data) } return err @@ -120,7 +120,7 @@ func (v *API) repeatRequest(path string, event_id string) { v.mu.Unlock() } -func doRequest[T any](v *API, req *http.Request, result *requests.Answer[T]) (string, error) { +func (v *API) doRequest[T any](req *http.Request, result *requests.Answer[T]) (string, error) { resp, err := v.Do(req) if err != nil { return "", err @@ -183,7 +183,7 @@ func (v *API) Wakeup(vin string) error { return err } - doRequest[any](v, req, nil) + v.doRequest[any](req, nil) return nil } @@ -214,7 +214,7 @@ func (v *API) Status(vin string) (requests.ChargeStatus, error) { "") var res requests.Answer[requests.ChargeStatus] - event_id, err := doRequest(v, req, &res) + event_id, err := v.doRequest(req, &res) if err != nil { return zero, err } @@ -234,7 +234,7 @@ func (v *API) Status(vin string) (requests.ChargeStatus, error) { event_id) // answer not yet available, keep polling in the background - if _, err = doRequest(v, req, &res); err == api.ErrMustRetry { + if _, err = v.doRequest(req, &res); err == api.ErrMustRetry { v.mu.Lock() v.state = stateRunning v.mu.Unlock() diff --git a/vehicle/tesla/api_test.go b/vehicle/tesla/api_test.go index 93924019e..dd0ce9de6 100644 --- a/vehicle/tesla/api_test.go +++ b/vehicle/tesla/api_test.go @@ -16,7 +16,7 @@ import ( func TestCommandResponse(t *testing.T) { sponsor.Subject = "any" - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + srv := httptest.NewTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Println(r.URL.Path) switch r.URL.Path { @@ -29,7 +29,7 @@ func TestCommandResponse(t *testing.T) { w.Write([]byte(`{"response": null, "error": "vehicle unavailable: vehicle is offline or asleep"}`)) } })) - defer srv.Close() + srv.Start() ts := oauth2.StaticTokenSource(new(oauth2.Token)) client, err := tesla.NewClient(t.Context(), tesla.WithTokenSource(ts)) diff --git a/vehicle/vag/vwidentity/endpoint.go b/vehicle/vag/vwidentity/endpoint.go index 9dc390e7f..c3a7fd245 100644 --- a/vehicle/vag/vwidentity/endpoint.go +++ b/vehicle/vag/vwidentity/endpoint.go @@ -9,15 +9,14 @@ import ( "net/http/cookiejar" "net/url" "strings" + "uuid" "github.com/PuerkitoBio/goquery" "github.com/coreos/go-oidc/v3/oidc" "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/urlvalues" "github.com/evcc-io/evcc/vehicle/vag" - "github.com/google/uuid" "github.com/samber/lo" "golang.org/x/net/publicsuffix" ) @@ -42,7 +41,7 @@ func LoginWithAuthURL(log *util.Logger, uri string, q url.Values, user, password var verify func(url.Values) // add code challenge - q = urlvalues.Copy(q) + q = q.Clone() if rt := q.Get("response_type"); strings.Contains(rt, "code") { verify = vag.ChallengeAndVerifier(q) } @@ -92,7 +91,7 @@ func (v *Service) Login(uri, user, password string) (url.Values, error) { // add nonce and state query := url.Values{ "nonce": {lo.RandomString(43, lo.LettersCharset)}, - "state": {uuid.NewString()}, + "state": {uuid.New().String()}, } uri = uri + "&" + query.Encode() From 6715711d49a290f51e2223afd3bcc262051aa0ce Mon Sep 17 00:00:00 2001 From: "Thijs W." Date: Thu, 20 Aug 2026 13:37:50 +0200 Subject: [PATCH 041/135] Home Assistant: auto-detect Supervisor instance and token (#33000) --- util/homeassistant/instance.go | 47 ++++++++++++-- util/homeassistant/oauth2.go | 31 +-------- util/homeassistant/supervisor.go | 34 ++++++++++ util/homeassistant/supervisor_test.go | 92 +++++++++++++++++++++++++++ 4 files changed, 170 insertions(+), 34 deletions(-) create mode 100644 util/homeassistant/supervisor.go create mode 100644 util/homeassistant/supervisor_test.go diff --git a/util/homeassistant/instance.go b/util/homeassistant/instance.go index c28c0ad87..519c7c6ba 100644 --- a/util/homeassistant/instance.go +++ b/util/homeassistant/instance.go @@ -4,9 +4,43 @@ import ( "fmt" "sync" + "github.com/evcc-io/evcc/plugin/auth" + "github.com/evcc-io/evcc/util" "golang.org/x/oauth2" ) +func init() { + auth.Register("homeassistant", NewHomeAssistantFromConfig) +} + +// NewHomeAssistantFromConfig creates a Home Assistant token source from configuration +func NewHomeAssistantFromConfig(other map[string]any) (oauth2.TokenSource, error) { + var cc struct { + URI string + Home string // TODO remove deprecated + Insecure bool + } + + if err := util.DecodeOther(other, &cc); err != nil { + return nil, err + } + + uri := cc.URI + + if uri == "" && cc.Home != "" { + uri = instanceUriByName(cc.Home) + if uri == "" { + return nil, fmt.Errorf("unknown instance: %s", cc.Home) + } + } + + if ts, ok := supervisorTokenSource(uri); ok { + return ts, nil + } + + return NewOAuth(uri, cc.Insecure) +} + type proxyInstance struct { mu sync.Mutex home, uri string @@ -43,12 +77,15 @@ func (inst *proxyInstance) Token() (*oauth2.Token, error) { defer inst.mu.Unlock() if inst.TokenSource == nil { - ts, err := NewHomeAssistant(uri, inst.insecure) - if err != nil { - return nil, err + if ts, ok := supervisorTokenSource(uri); ok { + inst.TokenSource = ts + } else { + ts, err := NewOAuth(uri, inst.insecure) + if err != nil { + return nil, err + } + inst.TokenSource = ts } - - inst.TokenSource = ts } return inst.TokenSource.Token() diff --git a/util/homeassistant/oauth2.go b/util/homeassistant/oauth2.go index f021eb493..c3bbd0a95 100644 --- a/util/homeassistant/oauth2.go +++ b/util/homeassistant/oauth2.go @@ -2,7 +2,6 @@ package homeassistant import ( "context" - "fmt" "net" "net/http" "net/url" @@ -17,34 +16,8 @@ import ( // https://developers.home-assistant.io/docs/auth_api -func init() { - auth.Register("homeassistant", NewHomeAssistantFromConfig) -} - -func NewHomeAssistantFromConfig(other map[string]any) (oauth2.TokenSource, error) { - var cc struct { - URI string - Home string // TODO remove deprecated - Insecure bool - } - - if err := util.DecodeOther(other, &cc); err != nil { - return nil, err - } - - uri := cc.URI - - if uri == "" && cc.Home != "" { - uri = instanceUriByName(cc.Home) - if uri == "" { - return nil, fmt.Errorf("unknown instance: %s", cc.Home) - } - } - - return NewHomeAssistant(uri, cc.Insecure) -} - -func NewHomeAssistant(uri string, insecure bool) (oauth2.TokenSource, error) { +// NewOAuth creates a Home Assistant OAuth token source +func NewOAuth(uri string, insecure bool) (oauth2.TokenSource, error) { uri = strings.TrimRight(uri, "/") // normalize extUrl := network.Config().ExternalURL() diff --git a/util/homeassistant/supervisor.go b/util/homeassistant/supervisor.go new file mode 100644 index 000000000..a441e9706 --- /dev/null +++ b/util/homeassistant/supervisor.go @@ -0,0 +1,34 @@ +package homeassistant + +import ( + "os" + "strings" + + "golang.org/x/oauth2" +) + +const ( + // SupervisorURI is the Home Assistant Core API endpoint when running as a Home Assistant add-on + SupervisorURI = "http://supervisor/core" + // SupervisorToken is the environment variable name containing the bearer token + SupervisorToken = "SUPERVISOR_TOKEN" + // SupervisorInstance is the discovered instance name for the Supervisor integration + SupervisorInstance = "HomeAssistant Host" +) + +func init() { + if hasSupervisorToken() { + addInstance(SupervisorInstance, SupervisorURI) + } +} + +func hasSupervisorToken() bool { + return os.Getenv(SupervisorToken) != "" +} + +func supervisorTokenSource(uri string) (oauth2.TokenSource, bool) { + if token := os.Getenv(SupervisorToken); token != "" && strings.TrimRight(uri, "/") == SupervisorURI { + return oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token}), true + } + return nil, false +} diff --git a/util/homeassistant/supervisor_test.go b/util/homeassistant/supervisor_test.go new file mode 100644 index 000000000..ffe011396 --- /dev/null +++ b/util/homeassistant/supervisor_test.go @@ -0,0 +1,92 @@ +package homeassistant + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/util" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSupervisorToken(t *testing.T) { + t.Setenv(SupervisorToken, "test_supervisor_token") + + ts, ok := supervisorTokenSource(SupervisorURI) + require.True(t, ok) + + tok, err := ts.Token() + require.NoError(t, err) + assert.Equal(t, "test_supervisor_token", tok.AccessToken) + + // SupervisorURI variant with trailing slash should still match + tsSlash, ok := supervisorTokenSource(SupervisorURI + "/") + require.True(t, ok) + + tokSlash, err := tsSlash.Token() + require.NoError(t, err) + assert.Equal(t, "test_supervisor_token", tokSlash.AccessToken) + + // empty uri does not match supervisor + _, ok = supervisorTokenSource("") + assert.False(t, ok) + + // other uri does not match supervisor + _, ok = supervisorTokenSource("http://homeassistant.local:8123") + assert.False(t, ok) + + // from config with SupervisorURI + ts3, err := NewHomeAssistantFromConfig(map[string]any{"uri": SupervisorURI}) + require.NoError(t, err) + + tok3, err := ts3.Token() + require.NoError(t, err) + assert.Equal(t, "test_supervisor_token", tok3.AccessToken) + + // when SUPERVISOR_TOKEN is unset, NewHomeAssistantFromConfig should fall back to the standard OAuth token source + t.Setenv(SupervisorToken, "") + + ts4, err := NewHomeAssistantFromConfig(map[string]any{"uri": SupervisorURI}) + require.NoError(t, err) + + _, err = ts4.Token() + require.Error(t, err) + var elr *api.ErrLoginRequired + assert.ErrorAs(t, err, &elr) + // connection requires uri + _, err = NewConnection(util.NewLogger("test"), "", "", false) + assert.Error(t, err) + + conn, err := NewConnection(util.NewLogger("test"), SupervisorURI, "", false) + require.NoError(t, err) + assert.Equal(t, SupervisorURI, conn.URI()) + + // test authenticated request using connection + var authHeader string + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + authHeader = r.Header.Get("Authorization") + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`[{"entity_id":"sensor.test","state":"10"}]`)) + })) + defer srv.Close() + + testConn, err := NewConnection(util.NewLogger("test"), srv.URL, "", false) + require.NoError(t, err) + // override instance token source directly or test via proxyInstance + testConn.instance.TokenSource = ts + + states, err := testConn.GetStates() + require.NoError(t, err) + assert.Len(t, states, 1) + assert.Equal(t, "Bearer test_supervisor_token", authHeader) +} + +func TestSupervisorDiscovery(t *testing.T) { + t.Setenv(SupervisorToken, "test_supervisor_token") + + addInstance(SupervisorInstance, SupervisorURI) + assert.Equal(t, SupervisorURI, instanceUriByName(SupervisorInstance)) + assert.Equal(t, SupervisorInstance, instanceNameByUri(SupervisorURI)) +} From 9a87bd105e1b2db5712a508b02e825378754b6b3 Mon Sep 17 00:00:00 2001 From: Andreas Ertel Date: Thu, 20 Aug 2026 13:51:19 +0200 Subject: [PATCH 042/135] Add StecaCharger (#32990) Co-authored-by: andreas.ertel --- templates/definition/charger/ghost.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/templates/definition/charger/ghost.yaml b/templates/definition/charger/ghost.yaml index 62627f187..cdbfa7c6f 100644 --- a/templates/definition/charger/ghost.yaml +++ b/templates/definition/charger/ghost.yaml @@ -6,6 +6,9 @@ products: - brand: Kontron Solar description: generic: Charger + - brand: Steca + description: + generic: StecaCharger capabilities: ["iso151182", "mA", "rfid", "1p3p", "meter", "dim"] requirements: description: From f6e6db9eae5f04fed4d1f1c42a060e5a7db59c37 Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Thu, 20 Aug 2026 15:51:52 +0200 Subject: [PATCH 043/135] Issue: remove obsolete number input spinner styles (#33006) --- assets/js/views/Issue.vue | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/assets/js/views/Issue.vue b/assets/js/views/Issue.vue index a144dfb43..3458e578c 100644 --- a/assets/js/views/Issue.vue +++ b/assets/js/views/Issue.vue @@ -262,7 +262,7 @@ @@ -687,11 +687,6 @@ export default defineComponent({ diff --git a/assets/js/directives/tooltip.ts b/assets/js/directives/tooltip.ts new file mode 100644 index 000000000..7e362153b --- /dev/null +++ b/assets/js/directives/tooltip.ts @@ -0,0 +1,25 @@ +import Tooltip from "bootstrap/js/dist/tooltip"; +import type { Directive, DirectiveBinding } from "vue"; + +function update(el: HTMLElement, { value, oldValue }: DirectiveBinding) { + if (value === oldValue) return; + const instance = Tooltip.getInstance(el); + if (!value) { + instance?.dispose(); + } else if (instance) { + instance.setContent({ ".tooltip-inner": value }); + } else { + new Tooltip(el, { title: value }); + } +} + +// v-tooltip="text": bootstrap tooltip bound to text, removed when text is empty +const tooltip: Directive = { + mounted: update, + updated: update, + beforeUnmount(el) { + Tooltip.getInstance(el)?.dispose(); + }, +}; + +export default tooltip; diff --git a/i18n/de.json b/i18n/de.json index 48b308e1b..e0ac3da1e 100644 --- a/i18n/de.json +++ b/i18n/de.json @@ -1432,6 +1432,7 @@ "finished": "Ladeende", "last24hEnergy": "Letzte 24h", "last7dEnergy": "Letzte 7 Tage", + "phasesMismatch": "Mehr Phasen aktiv als konfiguriert. Phasenkonfiguration prüfen.", "power": "Leistung", "price": "Kosten", "remaining": "Restzeit", diff --git a/i18n/en.json b/i18n/en.json index e57b0edf4..71781f7b1 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1432,6 +1432,7 @@ "finished": "Finish time", "last24hEnergy": "Last 24h", "last7dEnergy": "Last 7 days", + "phasesMismatch": "More phases active than configured. Check phase configuration.", "power": "Power", "price": "Cost", "remaining": "Remaining", From ea8e2a72c0522f51471891e40ff9f916902e5ada Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 28 Aug 2026 09:05:47 +0200 Subject: [PATCH 125/135] Site: add returnEnergy to aggregated battery state (#33241) Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Michael Geers --- assets/js/types/evcc.ts | 4 +++- core/site.go | 6 ++++++ core/types/types.go | 13 +++++++------ server/mcp/openapi.json | 6 +++++- server/mqtt_test.go | 4 ++-- server/openapi.state.yaml | 5 ++++- 6 files changed, 27 insertions(+), 11 deletions(-) diff --git a/assets/js/types/evcc.ts b/assets/js/types/evcc.ts index d478f5e27..22f697402 100644 --- a/assets/js/types/evcc.ts +++ b/assets/js/types/evcc.ts @@ -1131,8 +1131,10 @@ export interface Battery { capacity: number; /** Charge level in %. Weighted by capacity across all batteries. */ soc: number; - /** Total battery energy in kWh. */ + /** Total discharged energy in kWh. */ energy?: number; + /** Total charged energy in kWh. */ + returnEnergy?: number; /** Measurement data per battery meter. */ devices?: BatteryMeter[]; /** Projected charge levels based on the solar and price forecast. */ diff --git a/core/site.go b/core/site.go index e345fb85e..0f42d6016 100644 --- a/core/site.go +++ b/core/site.go @@ -867,6 +867,12 @@ func (site *Site) updateBatteryMeters() { } return *m.Energy }) + site.battery.ReturnEnergy = lo.SumBy(mm, func(m types.Measurement) float64 { + if m.ReturnEnergy == nil { + return 0 + } + return *m.ReturnEnergy + }) site.battery.Devices = mm battery := site.battery diff --git a/core/types/types.go b/core/types/types.go index d060d1767..f3b254861 100644 --- a/core/types/types.go +++ b/core/types/types.go @@ -56,10 +56,11 @@ func (m Measurement) GetTitle() string { } type BatteryState struct { - Power float64 `json:"power"` - Energy float64 `json:"energy,omitempty"` - Capacity float64 `json:"capacity,omitempty"` - Soc float64 `json:"soc"` - Devices []Measurement `json:"devices,omitempty" influxdb:"battery"` - Forecast *BatteryForecast `json:"forecast,omitempty"` + Power float64 `json:"power"` + Energy float64 `json:"energy,omitempty"` + ReturnEnergy float64 `json:"returnEnergy,omitempty"` + Capacity float64 `json:"capacity,omitempty"` + Soc float64 `json:"soc"` + Devices []Measurement `json:"devices,omitempty" influxdb:"battery"` + Forecast *BatteryForecast `json:"forecast,omitempty"` } diff --git a/server/mcp/openapi.json b/server/mcp/openapi.json index 4a1d4537f..537003276 100644 --- a/server/mcp/openapi.json +++ b/server/mcp/openapi.json @@ -3843,7 +3843,11 @@ "type": "number" }, "energy": { - "description": "Total battery energy in kWh.", + "description": "Total discharged energy in kWh.", + "type": "number" + }, + "returnEnergy": { + "description": "Total charged energy in kWh.", "type": "number" }, "devices": { diff --git a/server/mqtt_test.go b/server/mqtt_test.go index 115800433..d16e6a36d 100644 --- a/server/mqtt_test.go +++ b/server/mqtt_test.go @@ -152,7 +152,7 @@ func (suite *mqttSuite) TestMeasurement() { func (suite *mqttSuite) TestBatteryState() { topics := lo.Map([]string{ - "power", "energy", "capacity", "soc", + "power", "energy", "returnEnergy", "capacity", "soc", "devices", "devices/1/name", "devices/1/title", "devices/1/icon", "devices/1/power", "devices/1/energy", "devices/1/returnEnergy", "devices/1/powers", "devices/1/currents", "devices/1/excessDCPower", "devices/1/capacity", "devices/1/soc", "devices/1/controllable", "devices/1/suggestion", "forecast", }, func(s string, _ int) string { @@ -169,5 +169,5 @@ func (suite *mqttSuite) TestBatteryState() { }) suite.Equal(topics, suite.topics, "topics") - suite.Equal([]string{"2", "", "", "20", "1", "", "", "", "1", "", "", "", "", "", "", "10", "", "", ""}, suite.payloads, "payloads") + suite.Equal([]string{"2", "", "", "", "20", "1", "", "", "", "1", "", "", "", "", "", "", "10", "", "", ""}, suite.payloads, "payloads") } diff --git a/server/openapi.state.yaml b/server/openapi.state.yaml index 62442c35c..664858fc7 100644 --- a/server/openapi.state.yaml +++ b/server/openapi.state.yaml @@ -302,7 +302,10 @@ components: description: Charge level in %. Weighted by capacity across all batteries. type: number energy: - description: Total battery energy in kWh. + description: Total discharged energy in kWh. + type: number + returnEnergy: + description: Total charged energy in kWh. type: number devices: description: Measurement data per battery meter. From 19048976ab7380fc2b791979eabededec3317e36 Mon Sep 17 00:00:00 2001 From: Gabik21 Date: Fri, 28 Aug 2026 17:31:07 +0200 Subject: [PATCH 126/135] chore: add curtailment to meter template (#33237) --- assets/js/components/Config/defaultYaml/meter.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/assets/js/components/Config/defaultYaml/meter.yaml b/assets/js/components/Config/defaultYaml/meter.yaml index 544def442..764f95f1b 100644 --- a/assets/js/components/Config/defaultYaml/meter.yaml +++ b/assets/js/components/Config/defaultYaml/meter.yaml @@ -16,6 +16,9 @@ power: # current power #maxpower: # maximum AC power in W (for hybrid pv) # source: const # value: 5000 +#curtailed: # current feed-in limit in % (0..100, 100 = uncurtailed) +# source: const +# value: 100 #soc: # state of charge in % (for battery) # source: const # value: 75 @@ -50,3 +53,6 @@ power: # current power #batterymode: # set charging mode directly (1: normal, 2: hold, 3: charge) (for battery) # source: js # script: console.log(batterymode) +#curtail: # set feed-in limit in % (0..100, 100 = uncurtailed) +# source: js +# script: console.log(curtail) From e8b547737389eda23666e31ac77b6390ee168452 Mon Sep 17 00:00:00 2001 From: Gabik21 Date: Sat, 29 Aug 2026 11:01:53 +0200 Subject: [PATCH 127/135] Cardata: fix retrying failed container setup (#33243) --- vehicle/bmw/cardata/provider.go | 18 +++++++++--------- vehicle/bmw/cardata/provider_test.go | 8 ++++---- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/vehicle/bmw/cardata/provider.go b/vehicle/bmw/cardata/provider.go index dd47620f1..595ebf397 100644 --- a/vehicle/bmw/cardata/provider.go +++ b/vehicle/bmw/cardata/provider.go @@ -132,17 +132,17 @@ func (v *Provider) any(key string) (any, error) { _, tokenErr := v.ts.Token() - switch { - case tokenErr == nil && v.updated.IsZero(): - // this will only happen once - if err := v.setupContainer(); err != nil { - v.log.WARN.Println(err) + if tokenErr == nil && (v.updated.IsZero() || time.Since(v.updated) > v.cache) { + if v.container == "" { + if err := v.setupContainer(); err != nil { + v.log.WARN.Println(err) + } } - fallthrough - case tokenErr == nil && time.Since(v.updated) > v.cache && v.container != "": - if err := v.updateContainerData(); err != nil { - v.log.WARN.Println(err) + if v.container != "" { + if err := v.updateContainerData(); err != nil { + v.log.WARN.Println(err) + } } v.updated = time.Now() } diff --git a/vehicle/bmw/cardata/provider_test.go b/vehicle/bmw/cardata/provider_test.go index 05bb65196..cbaad346a 100644 --- a/vehicle/bmw/cardata/provider_test.go +++ b/vehicle/bmw/cardata/provider_test.go @@ -15,7 +15,7 @@ func TestCardataStreaming(t *testing.T) { p := NewProvider(ctx, util.NewLogger("foo"), nil, oauth2.StaticTokenSource(&oauth2.Token{ AccessToken: "at", - }), "client", "vin", 0) + }), "client", "vin", time.Hour) // prevent container panic p.updated = time.Now() @@ -51,7 +51,7 @@ func TestSocFallback(t *testing.T) { p := NewProvider(ctx, util.NewLogger("foo"), nil, oauth2.StaticTokenSource(&oauth2.Token{ AccessToken: "at", - }), "client", "vin", 0) + }), "client", "vin", time.Hour) // prevent container panic p.updated = time.Now() @@ -108,7 +108,7 @@ func TestRangeFallback(t *testing.T) { p := NewProvider(ctx, util.NewLogger("foo"), nil, oauth2.StaticTokenSource(&oauth2.Token{ AccessToken: "at", - }), "client", "vin", 0) + }), "client", "vin", time.Hour) // prevent container panic p.updated = time.Now() @@ -165,7 +165,7 @@ func TestStatusFallback(t *testing.T) { p := NewProvider(ctx, util.NewLogger("foo"), nil, oauth2.StaticTokenSource(&oauth2.Token{ AccessToken: "at", - }), "client", "vin", 0) + }), "client", "vin", time.Hour) // prevent container panic p.updated = time.Now() From 7dd4007cfa5d4496861a728a95aaaf0c0eb3e8e9 Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Sat, 29 Aug 2026 13:10:43 +0200 Subject: [PATCH 128/135] Add BYN, UAH, RUB and KZT to currency list (#33259) --- assets/js/mixins/formatter.ts | 5 +++++ assets/js/types/evcc.ts | 4 ++++ server/mcp/openapi.json | 6 +++++- server/openapi.state.yaml | 4 ++++ 4 files changed, 18 insertions(+), 1 deletion(-) diff --git a/assets/js/mixins/formatter.ts b/assets/js/mixins/formatter.ts index 47c05d1b5..4b0b3a9d8 100644 --- a/assets/js/mixins/formatter.ts +++ b/assets/js/mixins/formatter.ts @@ -75,6 +75,10 @@ const CURRENCY_SYMBOLS: Record = { TRY: "₺", MYR: "RM", THB: "฿", + BYN: "Br", + UAH: "₴", + RUB: "₽", + KZT: "₸", }; // list of currencies where energy price should be displayed in subunits (factor 100) @@ -94,6 +98,7 @@ const ENERGY_PRICE_IN_SUBUNIT: Partial> = { SEK: "öre", // Swedish öre ZAR: "c", // South African cent TRY: "krş", // Türkiye kuruş + BYN: "к.", // Belarusian kapeyka }; export enum POWER_UNIT { diff --git a/assets/js/types/evcc.ts b/assets/js/types/evcc.ts index 22f697402..659042f95 100644 --- a/assets/js/types/evcc.ts +++ b/assets/js/types/evcc.ts @@ -816,6 +816,10 @@ export enum CURRENCY { TRY = "TRY", MYR = "MYR", THB = "THB", + BYN = "BYN", + UAH = "UAH", + RUB = "RUB", + KZT = "KZT", } export enum ICON_SIZE { diff --git a/server/mcp/openapi.json b/server/mcp/openapi.json index 537003276..02cf646e6 100644 --- a/server/mcp/openapi.json +++ b/server/mcp/openapi.json @@ -3648,7 +3648,11 @@ "ZAR", "TRY", "MYR", - "THB" + "THB", + "BYN", + "UAH", + "RUB", + "KZT" ] }, "FatalError": { diff --git a/server/openapi.state.yaml b/server/openapi.state.yaml index 664858fc7..ada39bc1c 100644 --- a/server/openapi.state.yaml +++ b/server/openapi.state.yaml @@ -510,6 +510,10 @@ components: - TRY - MYR - THB + - BYN + - UAH + - RUB + - KZT DeviceColorEntry: description: Color assigned to a device for consistent UI display. type: object From 8abae0fd6888b3cc8e26a9f9d4426800ad492d43 Mon Sep 17 00:00:00 2001 From: andig Date: Sat, 29 Aug 2026 13:13:32 +0200 Subject: [PATCH 129/135] OpenDTU: tolerate missing radio_stats in energy filter (#33279) --- templates/definition/meter/hoymiles-opendtu.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/templates/definition/meter/hoymiles-opendtu.yaml b/templates/definition/meter/hoymiles-opendtu.yaml index 1b222c7ba..740572a91 100644 --- a/templates/definition/meter/hoymiles-opendtu.yaml +++ b/templates/definition/meter/hoymiles-opendtu.yaml @@ -16,4 +16,5 @@ render: | energy: source: http uri: http://{{ .host }}/api/livedata/status - jq: 'if all(.inverters[]; .radio_stats.rssi > -127) then .total.YieldTotal.v else empty end' + # skip partial sums until all inverters are contacted; radio_stats needs firmware 2024.10.22+ + jq: 'if all(.inverters[]; .radio_stats.rssi == null or .radio_stats.rssi > -127) then .total.YieldTotal.v else empty end' From f193eb199af4840a369d95564c8fab95e1fa8003 Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Sat, 29 Aug 2026 13:34:42 +0200 Subject: [PATCH 130/135] Home Assistant: link charge status docs (#33260) --- .../definition/charger/homeassistant.yaml | 12 +++++------ .../definition/vehicle/homeassistant.yaml | 20 +++++++++---------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/templates/definition/charger/homeassistant.yaml b/templates/definition/charger/homeassistant.yaml index 41ca1c0fe..6ac6b3d4a 100644 --- a/templates/definition/charger/homeassistant.yaml +++ b/templates/definition/charger/homeassistant.yaml @@ -41,8 +41,8 @@ params: example: not_plugged, disconnected advanced: true help: - en: Comma-separated list of additional states meaning ready - de: Komma-getrennte Liste zusätzlicher Zustände für bereit + en: Comma-separated list of additional states meaning ready, see [docs.evcc.io](https://docs.evcc.io/en/smarthome/home-assistant#charge-status) + de: Komma-getrennte Liste zusätzlicher Zustände für bereit, siehe [docs.evcc.io](https://docs.evcc.io/de/smarthome/home-assistant#charge-status) - name: statusB description: de: Zustände für Status B @@ -50,8 +50,8 @@ params: example: charging_stopped, charging_completed advanced: true help: - en: Comma-separated list of additional states meaning connected - de: Komma-getrennte Liste zusätzlicher Zustände für verbunden + en: Comma-separated list of additional states meaning connected, see [docs.evcc.io](https://docs.evcc.io/en/smarthome/home-assistant#charge-status) + de: Komma-getrennte Liste zusätzlicher Zustände für verbunden, siehe [docs.evcc.io](https://docs.evcc.io/de/smarthome/home-assistant#charge-status) - name: statusC description: de: Zustände für Status C @@ -59,8 +59,8 @@ params: example: instant_charging, fast_charging advanced: true help: - en: Comma-separated list of additional states meaning charging - de: Komma-getrennte Liste zusätzlicher Zustände für laden + en: Comma-separated list of additional states meaning charging, see [docs.evcc.io](https://docs.evcc.io/en/smarthome/home-assistant#charge-status) + de: Komma-getrennte Liste zusätzlicher Zustände für laden, siehe [docs.evcc.io](https://docs.evcc.io/de/smarthome/home-assistant#charge-status) - name: enabled description: de: Aktivierungsstatus-Sensor diff --git a/templates/definition/vehicle/homeassistant.yaml b/templates/definition/vehicle/homeassistant.yaml index 191c079da..7b737d8c8 100644 --- a/templates/definition/vehicle/homeassistant.yaml +++ b/templates/definition/vehicle/homeassistant.yaml @@ -61,8 +61,8 @@ params: example: not_plugged, disconnected advanced: true help: - en: Comma-separated list of additional states meaning disconnected - de: Komma-getrennte Liste zusätzlicher Zustände für getrennt + en: Comma-separated list of additional states meaning disconnected, see [docs.evcc.io](https://docs.evcc.io/en/smarthome/home-assistant#charge-status) + de: Komma-getrennte Liste zusätzlicher Zustände für getrennt, siehe [docs.evcc.io](https://docs.evcc.io/de/smarthome/home-assistant#charge-status) - name: statusB description: de: Zustände für Status B @@ -70,8 +70,8 @@ params: example: charging_stopped, charging_completed advanced: true help: - en: Comma-separated list of additional states meaning connected - de: Komma-getrennte Liste zusätzlicher Zustände für verbunden + en: Comma-separated list of additional states meaning connected, see [docs.evcc.io](https://docs.evcc.io/en/smarthome/home-assistant#charge-status) + de: Komma-getrennte Liste zusätzlicher Zustände für verbunden, siehe [docs.evcc.io](https://docs.evcc.io/de/smarthome/home-assistant#charge-status) - name: statusC description: de: Zustände für Status C @@ -79,8 +79,8 @@ params: example: instant_charging, fast_charging advanced: true help: - en: Comma-separated list of additional states meaning charging - de: Komma-getrennte Liste zusätzlicher Zustände für laden + en: Comma-separated list of additional states meaning charging, see [docs.evcc.io](https://docs.evcc.io/en/smarthome/home-assistant#charge-status) + de: Komma-getrennte Liste zusätzlicher Zustände für laden, siehe [docs.evcc.io](https://docs.evcc.io/de/smarthome/home-assistant#charge-status) - name: limitSoc description: de: Ziel-Ladezustand [%] @@ -124,8 +124,8 @@ params: service: homeassistant/entities?uri={uri}&insecure={insecure}&domain=script,switch example: "script.vehicle_start_charge; switch.vehicle_charger" help: - en: Entity ID for a script or a switch that starts charging the vehicle. Only useful with the [docs.evcc.io](https://docs.evcc.io/en/docs/devices/chargers#vehicle-api-only-charger). If a switch is provided, Service to stop charging must be left empty. - de: Entitäts-ID für ein Skript oder einen Schalter zum Starten des Ladevorgangs. Nur sinnvoll mit der [docs.evcc.io](https://docs.evcc.io/docs/devices/chargers#vehicle-api-only-charger). Wenn ein Schalter angegeben wird, muss der Service zum Stoppen des Ladens leer bleiben. + en: Entity ID for a script or a switch that starts charging the vehicle. Only useful with the [Vehicle API-only charger](https://docs.evcc.io/en/chargers/vehicle-api-only-charger). If a switch is provided, Service to stop charging must be left empty. + de: Entitäts-ID für ein Skript oder einen Schalter zum Starten des Ladevorgangs. Nur sinnvoll mit der [Fahrzeug-API Ladesteuerung](https://docs.evcc.io/de/chargers/vehicle-api-only-charger). Wenn ein Schalter angegeben wird, muss der Service zum Stoppen des Ladens leer bleiben. - name: stop_charging description: de: Service zum Laden stoppen @@ -133,8 +133,8 @@ params: service: homeassistant/entities?uri={uri}&insecure={insecure}&domain=script example: "script.vehicle_stop_charge" help: - en: Entity ID for a script that stops charging the vehicle. Only useful with the [docs.evcc.io](https://docs.evcc.io/en/docs/devices/chargers#vehicle-api-only-charger). - de: Entitäts-ID für ein Skript zum Stoppen des Ladevorgangs. Nur sinnvoll mit der [docs.evcc.io](https://docs.evcc.io/docs/devices/chargers#vehicle-api-only-charger). + en: Entity ID for a script that stops charging the vehicle. Only useful with the [Vehicle API-only charger](https://docs.evcc.io/en/chargers/vehicle-api-only-charger). + de: Entitäts-ID für ein Skript zum Stoppen des Ladevorgangs. Nur sinnvoll mit der [Fahrzeug-API Ladesteuerung](https://docs.evcc.io/de/chargers/vehicle-api-only-charger). - name: wakeup description: de: Service zum Aufwecken From 490f209e57287bb64b87f5f6a6687baac1fd3c5c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 30 Aug 2026 10:15:51 +0200 Subject: [PATCH 131/135] EEBus: apply the configured failsafe limits consistently (#33288) --- hems/eebus/eebus.go | 35 +++++++++++++++++++++---------- hems/eebus/eebus_test.go | 45 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 11 deletions(-) diff --git a/hems/eebus/eebus.go b/hems/eebus/eebus.go index 4c3f1bdc3..ec94decfc 100644 --- a/hems/eebus/eebus.go +++ b/hems/eebus/eebus.go @@ -232,6 +232,9 @@ func (c *EEBus) run() error { if c.failsafeProductionLimit != nil { // production limit is negative, failsafe limits are always positive c.setProductionLimit(-*c.failsafeProductionLimit, true) + } else { + // no failsafe limit configured, release any stale Energy Guard limit + c.setProductionLimit(0, false) } return nil @@ -352,6 +355,24 @@ func (c *EEBus) setProductionLimit(limit float64, active bool) { } } +// effectiveConsumptionLimit returns the LPC limit in force: the configured failsafe +// limit while in failsafe, else the Energy Guard's. Caller holds the mutex. +func (c *EEBus) effectiveConsumptionLimit() float64 { + if c.status == StatusFailsafe { + return c.failsafeConsumptionLimit + } + return c.consumptionLimit.Value +} + +// effectiveProductionLimit returns the LPP limit in force as positive watts, see +// effectiveConsumptionLimit. LPP states its limits as negative watts. +func (c *EEBus) effectiveProductionLimit() float64 { + if c.status == StatusFailsafe && c.failsafeProductionLimit != nil { + return *c.failsafeProductionLimit + } + return -c.productionLimit.Value +} + var _ api.HEMS = (*EEBus)(nil) // CurtailedPercent implements api.HEMS, converting the active LPP production @@ -372,8 +393,7 @@ func (c *EEBus) CurtailedPercent() *int { percent := 100 if limitActive(c.productionLimitActivated) { - // production limits are negative watts - percent = int(-c.productionLimit.Value / c.productionNominalMax * 100) + percent = int(c.effectiveProductionLimit() / c.productionNominalMax * 100) } return &percent @@ -390,10 +410,7 @@ func (c *EEBus) MaxConsumptionPower() *float64 { if !limitActive(c.consumptionLimitActivated) { return new(0.0) } - if c.status == StatusFailsafe { - return new(c.failsafeConsumptionLimit) - } - return new(c.consumptionLimit.Value) + return new(c.effectiveConsumptionLimit()) } // MaxProductionPower implements api.HEMS: nil until first connected, @@ -407,9 +424,5 @@ func (c *EEBus) MaxProductionPower() *float64 { if !limitActive(c.productionLimitActivated) { return new(0.0) } - if c.status == StatusFailsafe { - return c.failsafeProductionLimit - } - // production limits are negative watts, the api.HEMS cap is positive - return new(-c.productionLimit.Value) + return new(c.effectiveProductionLimit()) } diff --git a/hems/eebus/eebus_test.go b/hems/eebus/eebus_test.go index 070b1dbd7..7d8274b50 100644 --- a/hems/eebus/eebus_test.go +++ b/hems/eebus/eebus_test.go @@ -280,6 +280,51 @@ func TestRun_LimitWithoutDuration(t *testing.T) { assertProductionLimit(t, c, true) } +// TestProductionLimit_Failsafe verifies both api.HEMS production getters report the +// configured failsafe limit while in failsafe, not the stale EG-supplied limit +// (#33286): the EG limit had already been released (Value=0) before the heartbeat +// was lost, which must not curtail feed-in to 0%. +func TestProductionLimit_Failsafe(t *testing.T) { + c := newTestEEBus(t) + // EG limit already released before the heartbeat is lost. + c.productionLimit = ucapi.LoadLimit{Value: 0, IsActive: false} + + require.NoError(t, c.run()) + require.Equal(t, StatusFailsafe, c.status) + + percent := c.CurtailedPercent() + require.NotNil(t, percent) + assert.Equal(t, int(testFailsafeProduction/testProductionNominal*100), *percent) + + power := c.MaxProductionPower() + require.NotNil(t, power) + assert.Equal(t, testFailsafeProduction, *power) +} + +// TestProductionLimit_FailsafeUnconfigured verifies that entering failsafe without a +// configured failsafe production limit releases a stale EG limit. run() returns early +// while in failsafe, so a limit left applied here would stay applied indefinitely. +func TestProductionLimit_FailsafeUnconfigured(t *testing.T) { + c := newTestEEBus(t) + c.failsafeProductionLimit = nil + c.heartbeat.Set(struct{}{}) + + c.productionLimit = ucapi.LoadLimit{Value: -500, IsActive: true} + require.NoError(t, c.run()) + assertProductionLimit(t, c, true) + + // heartbeat lost + c.heartbeat = util.NewValue[struct{}](time.Hour) + + require.NoError(t, c.run()) + require.Equal(t, StatusFailsafe, c.status) + assertProductionLimit(t, c, false) + + power := c.MaxProductionPower() + require.NotNil(t, power) + assert.Equal(t, 0.0, *power) +} + // TestMaxProductionPower verifies the api.HEMS export cap is a positive wattage, // while LPP states its limits as negative watts. func TestMaxProductionPower(t *testing.T) { From dbe100b24bceff14b2aaa5c0b05d5a846185747b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 30 Aug 2026 08:15:52 +0000 Subject: [PATCH 132/135] HomeWizard: support 1p kWh meters (#33293) --- meter/homewizard/connection.go | 23 +++++++++++++++---- meter/homewizard/types.go | 38 +++++++++++++++++--------------- meter/homewizard/types_test.go | 40 ++++++++++++++++++++++++---------- 3 files changed, 67 insertions(+), 34 deletions(-) diff --git a/meter/homewizard/connection.go b/meter/homewizard/connection.go index a79f5d4a1..4025f68c0 100644 --- a/meter/homewizard/connection.go +++ b/meter/homewizard/connection.go @@ -10,6 +10,7 @@ import ( "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/request" "github.com/evcc-io/evcc/util/transport" + "github.com/samber/lo" ) // Connection is the homewizard connection @@ -119,10 +120,17 @@ func (c *Connection) TotalEnergy() (float64, error) { // Currents implements the api.PhaseCurrents interface func (c *Connection) Currents() (float64, float64, float64, error) { res, err := c.dataG.Get() - if c.usage == "pv" { - return -res.ActiveCurrentL1A, -res.ActiveCurrentL2A, -res.ActiveCurrentL3A, err + + // 1p meters report the total instead of per-phase values + l1, l2, l3 := res.ActiveCurrentA, 0.0, 0.0 + if res.ActiveCurrentL1A != nil { + l1, l2, l3 = lo.FromPtr(res.ActiveCurrentL1A), lo.FromPtr(res.ActiveCurrentL2A), lo.FromPtr(res.ActiveCurrentL3A) } - return res.ActiveCurrentL1A, res.ActiveCurrentL2A, res.ActiveCurrentL3A, err + + if c.usage == "pv" { + return -l1, -l2, -l3, err + } + return l1, l2, l3, err } // Powers implements the api.PhasePowers interface @@ -137,5 +145,12 @@ func (c *Connection) Powers() (float64, float64, float64, error) { // Voltages implements the api.PhaseVoltages interface func (c *Connection) Voltages() (float64, float64, float64, error) { res, err := c.dataG.Get() - return res.ActiveVoltageL1V, res.ActiveVoltageL2V, res.ActiveVoltageL3V, err + + // 1p meters report the total instead of per-phase values + l1, l2, l3 := res.ActiveVoltageV, 0.0, 0.0 + if res.ActiveCurrentL1A != nil { + l1, l2, l3 = lo.FromPtr(res.ActiveVoltageL1V), lo.FromPtr(res.ActiveVoltageL2V), lo.FromPtr(res.ActiveVoltageL3V) + } + + return l1, l2, l3, err } diff --git a/meter/homewizard/types.go b/meter/homewizard/types.go index 20c3e4e86..f4926ec4e 100644 --- a/meter/homewizard/types.go +++ b/meter/homewizard/types.go @@ -16,22 +16,24 @@ type StateResponse struct { // DataResponse returns the most recent measurements from the HomeWizard device // https://homewizard-energy-api.readthedocs.io/endpoints.html#state-api-v1-state type DataResponse struct { - ActivePowerW float64 `json:"active_power_w"` - ActivePowerL1W float64 `json:"active_power_l1_w"` - ActivePowerL2W float64 `json:"active_power_l2_w"` - ActivePowerL3W float64 `json:"active_power_l3_w"` - TotalPowerImportT1kWh float64 `json:"total_power_import_t1_kwh"` - TotalPowerImportT2kWh float64 `json:"total_power_import_t2_kwh"` - TotalPowerImportT3kWh float64 `json:"total_power_import_t3_kwh"` - TotalPowerImportT4kWh float64 `json:"total_power_import_t4_kwh"` - TotalPowerExportT1kWh float64 `json:"total_power_export_t1_kwh"` - TotalPowerExportT2kWh float64 `json:"total_power_export_t2_kwh"` - TotalPowerExportT3kWh float64 `json:"total_power_export_t3_kwh"` - TotalPowerExportT4kWh float64 `json:"total_power_export_t4_kwh"` - ActiveCurrentL1A float64 `json:"active_current_l1_a"` - ActiveCurrentL2A float64 `json:"active_current_l2_a"` - ActiveCurrentL3A float64 `json:"active_current_l3_a"` - ActiveVoltageL1V float64 `json:"active_voltage_l1_v"` - ActiveVoltageL2V float64 `json:"active_voltage_l2_v"` - ActiveVoltageL3V float64 `json:"active_voltage_l3_v"` + ActivePowerW float64 `json:"active_power_w"` + ActivePowerL1W float64 `json:"active_power_l1_w"` + ActivePowerL2W float64 `json:"active_power_l2_w"` + ActivePowerL3W float64 `json:"active_power_l3_w"` + TotalPowerImportT1kWh float64 `json:"total_power_import_t1_kwh"` + TotalPowerImportT2kWh float64 `json:"total_power_import_t2_kwh"` + TotalPowerImportT3kWh float64 `json:"total_power_import_t3_kwh"` + TotalPowerImportT4kWh float64 `json:"total_power_import_t4_kwh"` + TotalPowerExportT1kWh float64 `json:"total_power_export_t1_kwh"` + TotalPowerExportT2kWh float64 `json:"total_power_export_t2_kwh"` + TotalPowerExportT3kWh float64 `json:"total_power_export_t3_kwh"` + TotalPowerExportT4kWh float64 `json:"total_power_export_t4_kwh"` + ActiveCurrentA float64 `json:"active_current_a"` + ActiveVoltageV float64 `json:"active_voltage_v"` + ActiveCurrentL1A *float64 `json:"active_current_l1_a"` + ActiveCurrentL2A *float64 `json:"active_current_l2_a"` + ActiveCurrentL3A *float64 `json:"active_current_l3_a"` + ActiveVoltageL1V *float64 `json:"active_voltage_l1_v"` + ActiveVoltageL2V *float64 `json:"active_voltage_l2_v"` + ActiveVoltageL3V *float64 `json:"active_voltage_l3_v"` } diff --git a/meter/homewizard/types_test.go b/meter/homewizard/types_test.go index 0a5ee4555..b13e74fde 100644 --- a/meter/homewizard/types_test.go +++ b/meter/homewizard/types_test.go @@ -47,12 +47,28 @@ func TestUnmarshalKwhDataResponse(t *testing.T) { assert.Equal(t, float64(28), res.ActivePowerL1W) assert.Equal(t, float64(0), res.ActivePowerL2W) assert.Equal(t, float64(-181), res.ActivePowerL3W) - assert.Equal(t, float64(235.4), res.ActiveVoltageL1V) - assert.Equal(t, float64(235.8), res.ActiveVoltageL2V) - assert.Equal(t, float64(236.1), res.ActiveVoltageL3V) - assert.Equal(t, float64(1.19), res.ActiveCurrentL1A) - assert.Equal(t, float64(0.37), res.ActiveCurrentL2A) - assert.Equal(t, float64(-0.93), res.ActiveCurrentL3A) + assert.Equal(t, float64(235.4), *res.ActiveVoltageL1V) + assert.Equal(t, float64(235.8), *res.ActiveVoltageL2V) + assert.Equal(t, float64(236.1), *res.ActiveVoltageL3V) + assert.Equal(t, float64(1.19), *res.ActiveCurrentL1A) + assert.Equal(t, float64(0.37), *res.ActiveCurrentL2A) + assert.Equal(t, float64(-0.93), *res.ActiveCurrentL3A) + } +} + +// Test homewizard kWh Meter 1-Phase response without per-phase current/voltage +func TestUnmarshalKwh1PhaseDataResponse(t *testing.T) { + { + var res DataResponse + // https://api-documentation.homewizard.com/docs/v1/measurement#kwh-meters-1-and-3-phase + jsonstr := `{"total_power_import_kwh":1.097,"total_power_import_t1_kwh":1.097,"total_power_export_kwh":0,"total_power_export_t1_kwh":0,"active_power_w":1381.396,"active_power_l1_w":1381.396,"active_voltage_v":228.641,"active_current_a":6.051,"active_apparent_current_a":6.068,"active_reactive_current_a":0.461,"active_apparent_power_va":1385.536,"active_reactive_power_var":-105.28,"active_power_factor":0.997,"active_frequency_hz":50}` + require.NoError(t, json.Unmarshal([]byte(jsonstr), &res)) + + assert.Equal(t, float64(1381.396), res.ActivePowerW) + assert.Equal(t, float64(1381.396), res.ActivePowerL1W) + assert.Equal(t, float64(228.641), res.ActiveVoltageV) + assert.Equal(t, float64(6.051), res.ActiveCurrentA) + assert.Nil(t, res.ActiveCurrentL1A) } } @@ -70,11 +86,11 @@ func TestUnmarshalP1DataResponse(t *testing.T) { assert.Equal(t, float64(-21), res.ActivePowerL1W) assert.Equal(t, float64(57), res.ActivePowerL2W) assert.Equal(t, float64(168), res.ActivePowerL3W) - assert.Equal(t, float64(228), res.ActiveVoltageL1V) - assert.Equal(t, float64(226), res.ActiveVoltageL2V) - assert.Equal(t, float64(225), res.ActiveVoltageL3V) - assert.Equal(t, float64(-0.092), res.ActiveCurrentL1A) - assert.Equal(t, float64(0.252), res.ActiveCurrentL2A) - assert.Equal(t, float64(0.747), res.ActiveCurrentL3A) + assert.Equal(t, float64(228), *res.ActiveVoltageL1V) + assert.Equal(t, float64(226), *res.ActiveVoltageL2V) + assert.Equal(t, float64(225), *res.ActiveVoltageL3V) + assert.Equal(t, float64(-0.092), *res.ActiveCurrentL1A) + assert.Equal(t, float64(0.252), *res.ActiveCurrentL2A) + assert.Equal(t, float64(0.747), *res.ActiveCurrentL3A) } } From d379b7802e54acb269a1898b040d97b691b70f44 Mon Sep 17 00:00:00 2001 From: andig Date: Sun, 30 Aug 2026 10:31:40 +0200 Subject: [PATCH 133/135] EEBus: clamp the curtailment percent to the documented maximum (#33300) --- hems/eebus/eebus.go | 3 ++- hems/eebus/eebus_test.go | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/hems/eebus/eebus.go b/hems/eebus/eebus.go index ec94decfc..bb0f5a79c 100644 --- a/hems/eebus/eebus.go +++ b/hems/eebus/eebus.go @@ -393,7 +393,8 @@ func (c *EEBus) CurtailedPercent() *int { percent := 100 if limitActive(c.productionLimitActivated) { - percent = int(c.effectiveProductionLimit() / c.productionNominalMax * 100) + // the EG may state a limit above the nominal production power + percent = min(int(c.effectiveProductionLimit()/c.productionNominalMax*100), 100) } return &percent diff --git a/hems/eebus/eebus_test.go b/hems/eebus/eebus_test.go index 7d8274b50..2ca8a5689 100644 --- a/hems/eebus/eebus_test.go +++ b/hems/eebus/eebus_test.go @@ -325,6 +325,21 @@ func TestProductionLimit_FailsafeUnconfigured(t *testing.T) { assert.Equal(t, 0.0, *power) } +// TestCurtailedPercent_AboveNominal verifies a limit above the nominal production +// power reports 100%, not more - api.HEMS documents the percent as 0..100 and it +// reaches the inverter unchecked. +func TestCurtailedPercent_AboveNominal(t *testing.T) { + c := newTestEEBus(t) + c.heartbeat.Set(struct{}{}) + c.productionLimit = ucapi.LoadLimit{Value: -2 * testProductionNominal, IsActive: true} + + require.NoError(t, c.run()) + + percent := c.CurtailedPercent() + require.NotNil(t, percent) + assert.Equal(t, 100, *percent) +} + // TestMaxProductionPower verifies the api.HEMS export cap is a positive wattage, // while LPP states its limits as negative watts. func TestMaxProductionPower(t *testing.T) { From 13215c74c248e5d3a44b9f482cdc96cf63f57145 Mon Sep 17 00:00:00 2001 From: Stefan Date: Sun, 30 Aug 2026 10:41:18 +0200 Subject: [PATCH 134/135] Remove sponsor token gating Unlock all locally gated features without a sponsor token. The change sits in util/sponsor/auth.go instead of the ~68 device constructors: - Subject defaults to a non-empty value, so RedactedStatus reports an active sponsorship and the frontend unlocks isSponsor - IsAuthorized always returns true, opening every caller including the modbus proxy and the optimizer gate - ConfigureSponsorship still validates a configured token but never fails, so an expired token no longer aborts startup Drops TestAlpitronicSponsorGate and TestSigenergyEVDCSponsorGate, which asserted exactly the gate that is removed here. Cloud-backed services (optimizer API, remote access, cloud vehicles, telemetry) still require a real token - those are checked server side. See FORK.md. --- FORK.md | 54 ++++++++++++++++++++++++++++++++++ charger/alpitronic_test.go | 12 -------- charger/sigenergy-evdc_test.go | 12 -------- util/sponsor/auth.go | 39 +++++++++++++++++++----- 4 files changed, 86 insertions(+), 31 deletions(-) create mode 100644 FORK.md diff --git a/FORK.md b/FORK.md new file mode 100644 index 000000000..4508b30fd --- /dev/null +++ b/FORK.md @@ -0,0 +1,54 @@ +# Fork-Hinweise + +Fork von [evcc-io/evcc](https://github.com/evcc-io/evcc). + +## Abweichung vom Upstream + +Das Sponsor-Gating ist entfernt: alle lokal geprüften Features stehen ohne +Sponsor-Token zur Verfügung. Der Eingriff sitzt an einer einzigen Stelle in +`util/sponsor/auth.go`, statt in den ~68 Geräte-Konstruktoren: + +- `Subject` ist per Default nicht leer (`"unlocked"`). Damit meldet + `RedactedStatus()` einen aktiven Sponsor-Status, und das Frontend schaltet + `isSponsor` (`assets/js/views/Config.vue`) frei — Geräte-Dialoge zeigen kein + „Sponsor-Token erforderlich" mehr. +- `IsAuthorized()` liefert immer `true`. Das öffnet alle Aufrufer, u. a. die + Charger-/Vehicle-Konstruktoren, `server/modbus/proxy.go` und den + Optimizer-Gate in `core/site_optimizer.go`. +- `ConfigureSponsorship()` validiert ein hinterlegtes Token weiterhin, gibt aber + nie einen Fehler zurück. Ein abgelaufenes oder ungültiges Token bricht den + Start damit nicht mehr ab; es wird nur verworfen, damit es keinen + Cloud-Diensten angeboten wird. + +Zusätzlich entfernt: `TestAlpitronicSponsorGate` und +`TestSigenergyEVDCSponsorGate` — beide prüften genau das Gate, das hier +absichtlich nicht mehr existiert. + +## Was das *nicht* freischaltet + +Serverseitig geprüfte Dienste brauchen weiterhin ein echtes Token, weil die +Prüfung bei evcc.io stattfindet und nicht im Client: + +- Optimizer-API (`core/site_optimizer.go` sendet `Bearer `) +- Remote Access / Tunnel (`server/remote/`) +- Cloud-Fahrzeuge und Tronity (`vehicle/cloud.go`, `vehicle/tronity.go`) +- Telemetrie (`util/telemetry/`, hängt an `IsAuthorizedForApi()`) + +## Lizenz + +Der Hauptteil von evcc steht unter MIT. `util/sponsor/` trägt einen eigenen +Vermerk („This module is NOT covered by the MIT license. All rights reserved."). +Diese Änderung betrifft genau dieses Modul. + +evcc finanziert sich über Sponsoring: + +## Upstream-Sync + +``` +git remote add upstream https://github.com/evcc-io/evcc.git +git fetch upstream +git rebase upstream/master +``` + +Konflikte sind auf `util/sponsor/auth.go` beschränkt; `FORK.md` liegt bewusst +außerhalb der Upstream-Dateien. diff --git a/charger/alpitronic_test.go b/charger/alpitronic_test.go index 911b660c8..d6b01dd3e 100644 --- a/charger/alpitronic_test.go +++ b/charger/alpitronic_test.go @@ -10,7 +10,6 @@ import ( "github.com/andig/mbserver" "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/util/modbus" - "github.com/evcc-io/evcc/util/sponsor" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -362,14 +361,3 @@ func TestAlpitronicReadFailure(t *testing.T) { _, err = wb.CurrentPower() assert.Error(t, err) } - -func TestAlpitronicSponsorGate(t *testing.T) { - // go-e tests set the global sponsor.Subject and never reset it - old := sponsor.Subject - sponsor.Subject = "" - t.Cleanup(func() { sponsor.Subject = old }) - - // tests run without sponsorship: the public constructor must refuse - _, err := NewAlpitronicHYC(t.Context(), modbus.TcpSettings{URI: "localhost:0", ID: 1}, 1) - assert.ErrorIs(t, err, api.ErrSponsorRequired) -} diff --git a/charger/sigenergy-evdc_test.go b/charger/sigenergy-evdc_test.go index 36f78b18d..32a197d87 100644 --- a/charger/sigenergy-evdc_test.go +++ b/charger/sigenergy-evdc_test.go @@ -10,7 +10,6 @@ import ( "github.com/andig/mbserver" "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/util/modbus" - "github.com/evcc-io/evcc/util/sponsor" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -290,17 +289,6 @@ func TestSigenergyEVDCMinMaxCurrent(t *testing.T) { assert.InDelta(t, 36.23, maxA, 0.01) // 25000 W / 690 } -func TestSigenergyEVDCSponsorGate(t *testing.T) { - // go-e tests set the global sponsor.Subject and never reset it - old := sponsor.Subject - sponsor.Subject = "" - t.Cleanup(func() { sponsor.Subject = old }) - - // tests run without sponsorship: the public constructor must refuse - _, err := NewSigenergyEVDC(t.Context(), "localhost:0", 1) - assert.ErrorIs(t, err, api.ErrSponsorRequired) -} - func TestSigenergyEVDCReadFailure(t *testing.T) { // missing register in the bulk-read block -> IllegalDataAddress propagates regs := evdcRegs(evdcStateCharging) diff --git a/util/sponsor/auth.go b/util/sponsor/auth.go index ed225fccf..9b707a889 100644 --- a/util/sponsor/auth.go +++ b/util/sponsor/auth.go @@ -35,10 +35,15 @@ import ( "google.golang.org/grpc/status" ) +// unlocked is the sponsorship subject used by this fork. Sponsor gating is +// removed here, so Subject is never empty and IsAuthorized always holds. +const unlocked = "unlocked" + var ( - mu sync.RWMutex - Subject, Token string - ExpiresAt time.Time + mu sync.RWMutex + Subject = unlocked + Token string + ExpiresAt time.Time ) func machineID() string { @@ -51,9 +56,8 @@ const unavailable = "sponsorship unavailable" const startupTimeout = 30 * time.Second func IsAuthorized() bool { - mu.RLock() - defer mu.RUnlock() - return len(Subject) > 0 + // sponsorship gating removed in this fork + return true } func IsAuthorizedForApi() bool { @@ -62,8 +66,29 @@ func IsAuthorizedForApi() bool { return IsAuthorized() && Subject != unavailable && Token != "" } -// check and set sponsorship token +// ConfigureSponsorship validates a sponsor token when one is configured, but +// never fails: sponsorship is not required in this fork. A valid token is +// still picked up so cloud-backed services keep working for real sponsors. func ConfigureSponsorship(token string) error { + err := configureSponsorship(token) + + mu.Lock() + defer mu.Unlock() + + if err != nil { + // stay unlocked, but do not offer a rejected token to cloud services + Token = "" + } + + if Subject == "" { + Subject = unlocked + } + + return nil +} + +// check and set sponsorship token +func configureSponsorship(token string) error { mu.Lock() defer mu.Unlock() From 19348d3a37eba160e804e648495f0f228f79c788 Mon Sep 17 00:00:00 2001 From: Stefan Date: Sun, 30 Aug 2026 10:52:56 +0200 Subject: [PATCH 135/135] Add Forgejo CI workflow Verify job compiles and tests the Go side with a stubbed dist/, which main.go embeds and the frontend build produces. Binary job builds the UI and the real binary. Living in .forgejo/workflows means Forgejo uses this instead of the 23 upstream .github workflows, which need GitHub-only runner labels. --- .forgejo/workflows/build.yml | 73 ++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 .forgejo/workflows/build.yml diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml new file mode 100644 index 000000000..b01847c3f --- /dev/null +++ b/.forgejo/workflows/build.yml @@ -0,0 +1,73 @@ +name: Build + +# Forgejo bevorzugt .forgejo/workflows gegenüber .github/workflows. Die 23 +# Upstream-Workflows laufen hier also nicht mit — die verlangen ohnehin +# GitHub-spezifische Runner-Label (depot-ubuntu-24.04-arm) und Secrets. + +on: + push: + branches: [master] + workflow_dispatch: + +jobs: + verify: + name: Go build & test + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + + # main.go bindet dist/ per //go:embed ein. Das Verzeichnis wird vom + # Frontend-Build erzeugt und ist gitignored — für den reinen Compile-Check + # reicht ein Platzhalter, go:embed braucht nur mindestens eine Datei. + - name: Stub embedded assets + run: mkdir -p dist && echo stub > dist/index.html + + - name: Build + run: go build ./... + + - name: Vet + run: go vet ./util/sponsor/... ./charger/... ./core/... ./server/... + + - name: Test + run: CGO_ENABLED=0 go test ./... + + binary: + name: evcc binary + runs-on: ubuntu-latest + needs: verify + + steps: + # fetch-depth 0: das Makefile leitet VERSION aus git describe --tags ab + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + + - uses: actions/setup-node@v4 + with: + node-version: 26 + cache: npm + + - name: Install UI deps + run: npm ci + + - name: Build UI + run: make ui + + - name: Build binary + run: make build + + - uses: actions/upload-artifact@v3 + with: + name: evcc + path: evcc