From f3c87f00b492f54eff15795a3775870e2029f829 Mon Sep 17 00:00:00 2001 From: Ingo <71161062+iseeberg79@users.noreply.github.com> Date: Sun, 25 Jan 2026 12:01:02 +0100 Subject: [PATCH 0001/1747] Plugins: make watchdog deferable (#26790) --- plugin/watchdog.go | 117 +++++++++++++++++++++++++++++++------- plugin/watchdog_test.go | 121 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 217 insertions(+), 21 deletions(-) diff --git a/plugin/watchdog.go b/plugin/watchdog.go index 8c4a579c8..a192d819d 100644 --- a/plugin/watchdog.go +++ b/plugin/watchdog.go @@ -8,18 +8,21 @@ import ( "sync" "time" + "github.com/benbjohnson/clock" "github.com/evcc-io/evcc/util" ) type watchdogPlugin struct { - mu sync.Mutex - ctx context.Context - log *util.Logger - reset []string - initial *string - set Config - timeout time.Duration - cancel func() + mu sync.Mutex + ctx context.Context + log *util.Logger + reset []string + initial *string + set Config + timeout time.Duration + deferred bool + cancel func() + clock clock.Clock } func init() { @@ -33,6 +36,7 @@ func NewWatchDogFromConfig(ctx context.Context, other map[string]any) (Plugin, e Initial *string Set Config Timeout time.Duration + Defer bool `mapstructure:"defer"` } if err := util.DecodeOther(other, &cc); err != nil { @@ -40,12 +44,14 @@ func NewWatchDogFromConfig(ctx context.Context, other map[string]any) (Plugin, e } o := &watchdogPlugin{ - ctx: ctx, - log: util.ContextLoggerWithDefault(ctx, util.NewLogger("watchdog")), - reset: cc.Reset, - initial: cc.Initial, - set: cc.Set, - timeout: cc.Timeout, + ctx: ctx, + log: util.ContextLoggerWithDefault(ctx, util.NewLogger("watchdog")), + reset: cc.Reset, + initial: cc.Initial, + set: cc.Set, + timeout: cc.Timeout, + deferred: cc.Defer, + clock: clock.New(), } return o, nil @@ -64,20 +70,35 @@ func (o *watchdogPlugin) wdt(ctx context.Context, set func() error) { } } +type deferredState[T comparable] struct { + val T + timer *clock.Timer +} + // 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 { - return func(val T) error { - o.mu.Lock() - defer o.mu.Unlock() + var state *deferredState[T] + var lastUpdated time.Time + var last *T - // stop wdt on new write + // stop running wdt + stopWdt := func() { if o.cancel != nil { o.cancel() o.cancel = nil } + } - // start wdt on non-reset value + // set value and start wdt + setAndStartWdt := func(val T) error { + if err := set(val); err != nil { + return err + } + lastUpdated = o.clock.Now() + last = &val + + // start wdt for non-reset value if !slices.Contains(reset, val) { var ctx context.Context ctx, o.cancel = context.WithCancel(context.Background()) @@ -86,11 +107,65 @@ func setter[T comparable](o *watchdogPlugin, set func(T) error, reset []T) func( o.mu.Lock() defer o.mu.Unlock() - return set(val) + if err := set(val); err != nil { + return err + } + lastUpdated = o.clock.Now() + + return nil }) } - return set(val) + return nil + } + + return func(val T) error { + o.mu.Lock() + defer o.mu.Unlock() + + // cancel deferred update + if state != nil { + state.timer.Stop() + state = nil + } + + // if value unchanged, let wdt continue running + // TODO refactor use of last value once batterymode is set only once, currently required to avoid defer loops + if last != nil && *last == val && o.cancel != nil { + return nil + } + + // calculate remaining deferred delay + delay := max(0, o.timeout+5*time.Second-o.clock.Since(lastUpdated)) + + // defer update to non-reset value + if o.deferred && delay > 0 && !lastUpdated.IsZero() && !slices.Contains(reset, val) { + stopWdt() + + // store deferred value + state = &deferredState[T]{ + val: val, + timer: o.clock.AfterFunc(delay, func() { + o.mu.Lock() + defer o.mu.Unlock() + + state = nil + + o.log.TRACE.Printf("deferred update executing: to=%v", val) + if err := setAndStartWdt(val); err != nil { + o.log.ERROR.Printf("deferred update failed: %v", err) + return + } + o.log.TRACE.Printf("deferred update completed: value=%v", val) + }), + } + + return nil + } + + // immediate update + stopWdt() + return setAndStartWdt(val) } } diff --git a/plugin/watchdog_test.go b/plugin/watchdog_test.go index 52d0274e9..be9dc4151 100644 --- a/plugin/watchdog_test.go +++ b/plugin/watchdog_test.go @@ -7,6 +7,7 @@ import ( "testing" "time" + "github.com/benbjohnson/clock" "github.com/evcc-io/evcc/util" "github.com/stretchr/testify/require" "golang.org/x/sync/errgroup" @@ -16,6 +17,7 @@ func TestWatchdogSetterConcurrency(t *testing.T) { p := &watchdogPlugin{ log: util.NewLogger("foo"), timeout: 10 * time.Nanosecond, + clock: clock.New(), } var u atomic.Uint32 @@ -44,3 +46,122 @@ func TestWatchdogSetterConcurrency(t *testing.T) { require.NoError(t, eg.Wait()) } + +func TestWatchdogDeferredUpdate(t *testing.T) { + // Test: Value 1 → 3 → 2 with delay + // 1 → 3: delayed (target 3 is non-reset) + // 3 → 2: delayed (target 2 is non-reset) + // Expected: [1, , 3, , 2] + + timeout := 60 * time.Second + c := clock.NewMock() + p := &watchdogPlugin{ + log: util.NewLogger("test"), + timeout: timeout, + deferred: true, + clock: c, + } + + var calls []int + set := setter(p, func(i int) error { + calls = append(calls, i) + return nil + }, []int{1}) // 1 is reset value + + // Value 1 (reset) → should set immediately + require.NoError(t, set(1)) + require.Equal(t, []int{1}, calls) + + // Value 3 (target is non-reset) → should be delayed + require.NoError(t, set(3)) + require.Equal(t, []int{1}, calls, "Value 3 should not be set yet") + + // Wait for delay + expectedDelay := p.timeout + 5*time.Second + c.Add(expectedDelay) + + // Now value 3 should be set + require.Equal(t, []int{1, 3}, calls) + + // Value 2 (non-reset to non-reset) → should delay + require.NoError(t, set(2)) + require.Equal(t, []int{1, 3}, calls, "Value 2 should not be set yet") + + // Wait for delay + c.Add(expectedDelay) + + // Now value 2 should be set (exactly once) + require.Equal(t, []int{1, 3, 2}, calls) +} + +func TestWatchdogCancelPendingDeferredUpdate(t *testing.T) { + // Test: Value 3 → 2 started, then set Value 1 during delay + // Expected: Deferred update cancelled, Value 1 set immediately + + timeout := 60 * time.Second + c := clock.NewMock() + p := &watchdogPlugin{ + log: util.NewLogger("test"), + timeout: timeout, + deferred: true, + clock: c, + } + + var calls []int + set := setter(p, func(i int) error { + calls = append(calls, i) + return nil + }, []int{1}) // 1 is reset value + + // Value 3 (non-reset) + require.NoError(t, set(3)) + require.Equal(t, []int{3}, calls) + + // Value 2 (deferred update) + require.NoError(t, set(2)) + require.Equal(t, []int{3}, calls, "Value 2 should not be set yet") + + // Wait a bit but not the full delay + c.Add(30 * time.Second) + + // Value 1 (reset) → should cancel pending deferred update and set immediately + require.NoError(t, set(1)) + require.Equal(t, []int{3, 1}, calls, "Value 1 should be set, Value 2 should be cancelled") + + // Wait for what would have been the original delay + c.Add(timeout + 5*time.Second) + + // Value 2 should still not have been set + require.Equal(t, []int{3, 1}, calls, "Value 2 should remain cancelled") +} + +func TestWatchdogDelayBackwardCompatibility(t *testing.T) { + // Test: deferred=false behaves like old implementation + // Expected: All updates immediate + + p := &watchdogPlugin{ + log: util.NewLogger("test"), + timeout: 60 * time.Second, + deferred: false, // explicitly false + clock: clock.New(), + } + + var calls []int + set := setter(p, func(i int) error { + calls = append(calls, i) + return nil + }, []int{1}) // 1 is reset value + + // All updates should be immediate + require.NoError(t, set(1)) + require.Equal(t, []int{1}, calls) + + require.NoError(t, set(3)) + require.Equal(t, []int{1, 3}, calls) + + require.NoError(t, set(2)) + require.Equal(t, []int{1, 3, 2}, calls, "Value 2 should be set immediately (no delay)") + + require.NoError(t, set(4)) + require.Equal(t, []int{1, 3, 2, 4}, calls, "Value 4 should be set immediately (no delay)") +} From 4c6ec1709ab40d18901ae18ab51174c48cea90e5 Mon Sep 17 00:00:00 2001 From: Ingo <71161062+iseeberg79@users.noreply.github.com> Date: Sun, 25 Jan 2026 12:08:55 +0100 Subject: [PATCH 0002/1747] Kostal Plenticore: fix battery mode changes (#26972) --- templates/definition/meter/kostal-plenticore-gen2.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/templates/definition/meter/kostal-plenticore-gen2.yaml b/templates/definition/meter/kostal-plenticore-gen2.yaml index 22f83d764..905214684 100644 --- a/templates/definition/meter/kostal-plenticore-gen2.yaml +++ b/templates/definition/meter/kostal-plenticore-gen2.yaml @@ -85,6 +85,7 @@ render: | batterymode: source: watchdog timeout: {{ .watchdog }} # re-write at timeout/2 + defer: true # make sure timeouts are followed on update reset: 1 # reset watchdog on normal set: source: switch From 766b314efeeb76819c598ee10b8e95ee1a24b5ca Mon Sep 17 00:00:00 2001 From: andig Date: Sun, 25 Jan 2026 12:20:11 +0100 Subject: [PATCH 0003/1747] chore: fix repo access --- .github/workflows/triage-agent.lock.yml | 60 +++++++++++++++++-------- .github/workflows/triage-agent.md | 5 +++ 2 files changed, 46 insertions(+), 19 deletions(-) diff --git a/.github/workflows/triage-agent.lock.yml b/.github/workflows/triage-agent.lock.yml index 4af93d2b0..f945bb84a 100644 --- a/.github/workflows/triage-agent.lock.yml +++ b/.github/workflows/triage-agent.lock.yml @@ -13,7 +13,7 @@ # \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ # \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ # -# This file was automatically generated by gh-aw (v0.37.13). DO NOT EDIT. +# This file was automatically generated by gh-aw (v0.37.19). DO NOT EDIT. # # To update this file, edit githubnext/gh-aw/.github/workflows/issue-triage-agent.md@87fe98fa15e2bb50f41225a356bbc07318b54fcf and run: # gh aw compile @@ -56,7 +56,7 @@ jobs: comment_repo: "" steps: - name: Setup Scripts - uses: githubnext/gh-aw/actions/setup@87fe98fa15e2bb50f41225a356bbc07318b54fcf # v0.37.13 + uses: githubnext/gh-aw/actions/setup@959bc34031479db1c6d2726d509c4696cdbed47f # v0.37.19 with: destination: /opt/gh-aw/actions - name: Check workflow file timestamps @@ -74,6 +74,7 @@ jobs: needs: activation runs-on: ubuntu-latest permissions: + contents: read issues: read pull-requests: read env: @@ -93,9 +94,13 @@ jobs: secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} steps: - name: Setup Scripts - uses: githubnext/gh-aw/actions/setup@87fe98fa15e2bb50f41225a356bbc07318b54fcf # v0.37.13 + uses: githubnext/gh-aw/actions/setup@959bc34031479db1c6d2726d509c4696cdbed47f # v0.37.19 with: destination: /opt/gh-aw/actions + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - name: Create gh-aw temp directory run: bash /opt/gh-aw/actions/create_gh_aw_tmp_dir.sh - name: Configure Git credentials @@ -109,6 +114,19 @@ jobs: SERVER_URL_STRIPPED="${SERVER_URL#https://}" git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" echo "Git configured with standard GitHub Actions identity" + - name: Checkout PR branch + if: | + github.event.pull_request + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/checkout_pr_branch.cjs'); + await main(); - name: Validate COPILOT_GITHUB_TOKEN secret id: validate-secret run: /opt/gh-aw/actions/validate_multi_secret.sh COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://githubnext.github.io/gh-aw/reference/engines/#github-copilot-default @@ -129,7 +147,7 @@ jobs: const determineAutomaticLockdown = require('/opt/gh-aw/actions/determine_automatic_lockdown.cjs'); await determineAutomaticLockdown(github, context, core); - name: Download container images - run: bash /opt/gh-aw/actions/download_docker_images.sh ghcr.io/github/github-mcp-server:v0.29.0 ghcr.io/githubnext/gh-aw-mcpg:v0.0.76 node:lts-alpine + run: bash /opt/gh-aw/actions/download_docker_images.sh ghcr.io/github/github-mcp-server:v0.29.0 ghcr.io/githubnext/gh-aw-mcpg:v0.0.78 node:lts-alpine - name: Write Safe Outputs Config run: | mkdir -p /opt/gh-aw/safeoutputs @@ -378,7 +396,7 @@ jobs: # Register API key as secret to mask it from logs echo "::add-mask::${MCP_GATEWAY_API_KEY}" export GH_AW_ENGINE="copilot" - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e DEBUG="*" -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_LOCKDOWN -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/githubnext/gh-aw-mcpg:v0.0.76' + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e DEBUG="*" -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_LOCKDOWN -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/githubnext/gh-aw-mcpg:v0.0.78' mkdir -p /home/runner/.copilot cat << MCPCONFIG_EOF | bash /opt/gh-aw/actions/start_mcp_gateway.sh @@ -422,7 +440,7 @@ jobs: model: process.env.GH_AW_MODEL_AGENT_COPILOT || "", version: "", agent_version: "0.0.389", - cli_version: "v0.37.13", + cli_version: "v0.37.19", workflow_name: "Triage Agent", experimental: false, supports_tools_allowlist: true, @@ -436,11 +454,10 @@ jobs: actor: context.actor, event_name: context.eventName, staged: false, - network_mode: "defaults", - allowed_domains: [], + allowed_domains: ["defaults"], firewall_enabled: true, awf_version: "v0.10.0", - awmg_version: "v0.0.76", + awmg_version: "v0.0.78", steps: { firewall: "squid" }, @@ -529,6 +546,10 @@ jobs: cat << 'PROMPT_EOF' >> "$GH_AW_PROMPT" # Triage Agent + ## Context + + - **Repository**: __GH_AW_GITHUB_REPOSITORY__ + ## Label the Issue/Pull Request Look at the issue/pull request. Analyze title and body, then add one of the allowed labels: `bug`, `enhancement`, `documentation`, `question`, `device`, `tariff`, `vehicle`, `heating`. @@ -583,6 +604,7 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} with: script: | const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); @@ -603,7 +625,7 @@ jobs: timeout-minutes: 5 run: | set -o pipefail - sudo -E awf --env-all --container-workdir "${GITHUB_WORKSPACE}" --mount /tmp:/tmp:rw --mount "${GITHUB_WORKSPACE}:${GITHUB_WORKSPACE}:rw" --mount /usr/bin/date:/usr/bin/date:ro --mount /usr/bin/gh:/usr/bin/gh:ro --mount /usr/bin/yq:/usr/bin/yq:ro --mount /usr/local/bin/copilot:/usr/local/bin/copilot:ro --mount /home/runner/.copilot:/home/runner/.copilot:rw --mount /opt/gh-aw:/opt/gh-aw:ro --allow-domains api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,github.com,host.docker.internal,raw.githubusercontent.com,registry.npmjs.org --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --enable-host-access --image-tag 0.10.0 \ + sudo -E awf --env-all --container-workdir "${GITHUB_WORKSPACE}" --mount /tmp:/tmp:rw --mount "${GITHUB_WORKSPACE}:${GITHUB_WORKSPACE}:rw" --mount /usr/bin/date:/usr/bin/date:ro --mount /usr/bin/gh:/usr/bin/gh:ro --mount /usr/bin/yq:/usr/bin/yq:ro --mount /usr/local/bin/copilot:/usr/local/bin/copilot:ro --mount /home/runner/.copilot:/home/runner/.copilot:rw --mount /opt/gh-aw:/opt/gh-aw:ro --allow-domains api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --enable-host-access --image-tag 0.10.0 \ -- /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --add-dir "${GITHUB_WORKSPACE}" --disable-builtin-mcps --allow-all-tools --allow-all-paths --share /tmp/gh-aw/sandbox/agent/logs/conversation.md --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"${GH_AW_MODEL_AGENT_COPILOT:+ --model "$GH_AW_MODEL_AGENT_COPILOT"} \ 2>&1 | tee /tmp/gh-aw/agent-stdio.log env: @@ -671,7 +693,7 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} - GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,github.com,host.docker.internal,raw.githubusercontent.com,registry.npmjs.org" + GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} with: @@ -758,7 +780,7 @@ jobs: total_count: ${{ steps.missing_tool.outputs.total_count }} steps: - name: Setup Scripts - uses: githubnext/gh-aw/actions/setup@87fe98fa15e2bb50f41225a356bbc07318b54fcf # v0.37.13 + uses: githubnext/gh-aw/actions/setup@959bc34031479db1c6d2726d509c4696cdbed47f # v0.37.19 with: destination: /opt/gh-aw/actions - name: Debug job inputs @@ -774,7 +796,7 @@ jobs: echo "Agent Conclusion: $AGENT_CONCLUSION" - name: Download agent output artifact continue-on-error: true - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: name: agent-output path: /tmp/gh-aw/safeoutputs/ @@ -861,18 +883,18 @@ jobs: success: ${{ steps.parse_results.outputs.success }} steps: - name: Setup Scripts - uses: githubnext/gh-aw/actions/setup@87fe98fa15e2bb50f41225a356bbc07318b54fcf # v0.37.13 + uses: githubnext/gh-aw/actions/setup@959bc34031479db1c6d2726d509c4696cdbed47f # v0.37.19 with: destination: /opt/gh-aw/actions - name: Download agent artifacts continue-on-error: true - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: name: agent-artifacts path: /tmp/gh-aw/threat-detection/ - name: Download agent output artifact continue-on-error: true - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: name: agent-output path: /tmp/gh-aw/threat-detection/ @@ -999,7 +1021,7 @@ jobs: activated: ${{ steps.check_membership.outputs.is_team_member == 'true' }} steps: - name: Setup Scripts - uses: githubnext/gh-aw/actions/setup@87fe98fa15e2bb50f41225a356bbc07318b54fcf # v0.37.13 + uses: githubnext/gh-aw/actions/setup@959bc34031479db1c6d2726d509c4696cdbed47f # v0.37.19 with: destination: /opt/gh-aw/actions - name: Check team membership for workflow @@ -1038,12 +1060,12 @@ jobs: process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} steps: - name: Setup Scripts - uses: githubnext/gh-aw/actions/setup@87fe98fa15e2bb50f41225a356bbc07318b54fcf # v0.37.13 + uses: githubnext/gh-aw/actions/setup@959bc34031479db1c6d2726d509c4696cdbed47f # v0.37.19 with: destination: /opt/gh-aw/actions - name: Download agent output artifact continue-on-error: true - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: name: agent-output path: /tmp/gh-aw/safeoutputs/ diff --git a/.github/workflows/triage-agent.md b/.github/workflows/triage-agent.md index 27639c63e..99d974e0b 100644 --- a/.github/workflows/triage-agent.md +++ b/.github/workflows/triage-agent.md @@ -7,6 +7,7 @@ on: pull_request: types: [opened, edited, reopened] permissions: + contents: read issues: read pull-requests: read tools: @@ -21,6 +22,10 @@ source: githubnext/gh-aw/.github/workflows/issue-triage-agent.md@87fe98fa15e2bb5 # Triage Agent +## Context + +- **Repository**: ${{ github.repository }} + ## Label the Issue/Pull Request Look at the issue/pull request. Analyze title and body, then add one of the allowed labels: `bug`, `enhancement`, `documentation`, `question`, `device`, `tariff`, `vehicle`, `heating`. From ae9a64984f17b339cfedc97b6f66a8990d58208b Mon Sep 17 00:00:00 2001 From: andig Date: Sun, 25 Jan 2026 13:09:44 +0100 Subject: [PATCH 0004/1747] Loadpoint: simplify estimator (#26956) --- core/loadpoint.go | 7 +--- core/soc/README.md | 6 +++ core/soc/estimator.go | 85 ++++++++++++-------------------------- core/soc/estimator_test.go | 21 +++------- 4 files changed, 39 insertions(+), 80 deletions(-) create mode 100644 core/soc/README.md diff --git a/core/loadpoint.go b/core/loadpoint.go index 555670f5a..6efca15e4 100644 --- a/core/loadpoint.go +++ b/core/loadpoint.go @@ -506,11 +506,6 @@ func (lp *Loadpoint) evVehicleConnectHandler() { // soc update reset lp.socUpdated = time.Time{} - // soc update reset on car change - if lp.socEstimator != nil { - lp.socEstimator.Reset() - } - // set default or start detection if !lp.chargerHasFeature(api.IntegratedDevice) { lp.vehicleDefaultOrDetect() @@ -1780,7 +1775,7 @@ func (lp *Loadpoint) publishSocAndRange() { if socEstimator == nil { lp.vehicleSoc = *soc } else { - lp.vehicleSoc, _ = socEstimator.Soc(soc, lp.GetChargedEnergy()) + lp.vehicleSoc = socEstimator.Soc(soc, lp.GetChargedEnergy()) lp.log.DEBUG.Printf("vehicle soc (estimator): %.0f%%", lp.vehicleSoc) } } diff --git a/core/soc/README.md b/core/soc/README.md new file mode 100644 index 000000000..be6fb732e --- /dev/null +++ b/core/soc/README.md @@ -0,0 +1,6 @@ +| fetchedSoc | chargedEnergy | result | +| ---------- | ------------- | --------------------------------------------------- | +| nil | <=0 | 0 | +| nil | value | prevsoc + delta | +| value | <=0 | initialsoc setzen | +| value | value | initialsoc/initialenergy setzen falls nicht gesetzt | diff --git a/core/soc/estimator.go b/core/soc/estimator.go index e6b1e0fe8..d296b655e 100644 --- a/core/soc/estimator.go +++ b/core/soc/estimator.go @@ -25,7 +25,6 @@ type Estimator struct { charger api.Charger vehicle api.Vehicle - capacity float64 // vehicle capacity in Wh cached to simplify testing virtualCapacity float64 // estimated virtual vehicle capacity in Wh vehicleSoc float64 // estimated vehicle Soc initialSoc float64 // first received valid vehicle Soc @@ -43,21 +42,12 @@ func NewEstimator(log *util.Logger, charger api.Charger, vehicle api.Vehicle) *E vehicle: vehicle, } - s.Reset() + s.virtualCapacity = s.vehicle.Capacity() * 1e3 / ChargeEfficiency // initial capacity taking efficiency into account + s.energyPerSocStep = s.virtualCapacity / 100 return s } -// Reset resets the estimation process to default values -func (s *Estimator) Reset() { - s.prevSoc = 0 - s.prevChargedEnergy = 0 - s.initialSoc = 0 - s.capacity = s.vehicle.Capacity() * 1e3 // cache to simplify debugging - s.virtualCapacity = s.capacity / ChargeEfficiency // initial capacity taking efficiency into account - s.energyPerSocStep = s.virtualCapacity / 100 -} - // RemainingChargeDuration returns the estimated remaining duration func (s *Estimator) RemainingChargeDuration(targetSoc, chargePower float64) time.Duration { return remainingChargeDuration(targetSoc, chargePower, s.vehicleSoc, s.virtualCapacity) @@ -99,60 +89,39 @@ func (s *Estimator) RemainingChargeEnergy(targetSoc int) float64 { } // Soc replaces the api.Vehicle.Soc interface to take charged energy into account -func (s *Estimator) Soc(fetchedSoc *float64, chargedEnergy float64) (float64, error) { +func (s *Estimator) Soc(fetchedSoc *float64, chargedEnergy float64) float64 { if fetchedSoc != nil { s.vehicleSoc = *fetchedSoc } else { s.log.WARN.Printf("missing vehicle soc- ignored by estimator") } - if s.virtualCapacity > 0 { - socDelta := s.vehicleSoc - s.prevSoc - energyDelta := max(chargedEnergy, 0) - s.prevChargedEnergy + socDelta := s.vehicleSoc - s.prevSoc + energyDelta := max(chargedEnergy, 0) - s.prevChargedEnergy - if socDelta != 0 || energyDelta < 0 { // soc value change or unexpected energy reset - // compare ChargeState of vehicle and charger - var invalid bool - - if vs, ok := s.vehicle.(api.ChargeState); ok { - ccs, err := s.charger.Status() - if err != nil { - return 0, err - } - vcs, err := vs.Status() - if err != nil { - vcs = ccs // sanitize vehicle errors - } else { - s.log.DEBUG.Printf("vehicle status: %s", vcs) - } - invalid = vcs != ccs - } - - if !invalid { - if s.initialSoc == 0 { - s.initialSoc = s.vehicleSoc - s.initialEnergy = chargedEnergy - } - - socDiff := s.vehicleSoc - s.initialSoc - energyDiff := chargedEnergy - s.initialEnergy - - // recalculate gradient, wh per soc % - if socDiff > 10 && energyDiff > 0 { - s.energyPerSocStep = energyDiff / socDiff - s.virtualCapacity = s.energyPerSocStep * 100 - s.log.DEBUG.Printf("soc gradient updated: soc: %.1f%%, socDiff: %.1f%%, energyDiff: %.0fWh, energyPerSocStep: %.1fWh, virtualCapacity: %.0fWh", s.vehicleSoc, socDiff, energyDiff, s.energyPerSocStep, s.virtualCapacity) - } - } - - // sample charged energy at soc change, reset energy delta - s.prevChargedEnergy = max(chargedEnergy, 0) - s.prevSoc = s.vehicleSoc - } else { - s.vehicleSoc = min(*fetchedSoc+energyDelta/s.energyPerSocStep, 100) - s.log.DEBUG.Printf("soc estimated: %.2f%% (vehicle: %.2f%%)", s.vehicleSoc, *fetchedSoc) + if socDelta != 0 || energyDelta < 0 { // soc value change or unexpected energy reset + if s.initialSoc == 0 { + s.initialSoc = s.vehicleSoc + s.initialEnergy = chargedEnergy } + + socDiff := s.vehicleSoc - s.initialSoc + energyDiff := chargedEnergy - s.initialEnergy + + // recalculate gradient, wh per soc % + if socDiff > 10 && energyDiff > 0 { + s.energyPerSocStep = energyDiff / socDiff + s.virtualCapacity = s.energyPerSocStep * 100 + s.log.DEBUG.Printf("soc gradient updated: soc: %.1f%%, socDiff: %.1f%%, energyDiff: %.0fWh, energyPerSocStep: %.1fWh, virtualCapacity: %.0fWh", s.vehicleSoc, socDiff, energyDiff, s.energyPerSocStep, s.virtualCapacity) + } + + // sample charged energy at soc change, reset energy delta + s.prevChargedEnergy = max(chargedEnergy, 0) + s.prevSoc = s.vehicleSoc + } else { + s.vehicleSoc = min(*fetchedSoc+energyDelta/s.energyPerSocStep, 100) + s.log.DEBUG.Printf("soc estimated: %.2f%% (vehicle: %.2f%%)", s.vehicleSoc, *fetchedSoc) } - return s.vehicleSoc, nil + return s.vehicleSoc } diff --git a/core/soc/estimator_test.go b/core/soc/estimator_test.go index ad6b73d5b..84f3052bf 100644 --- a/core/soc/estimator_test.go +++ b/core/soc/estimator_test.go @@ -71,20 +71,11 @@ func TestSocEstimation(t *testing.T) { for _, tc := range tc { t.Logf("%+v", tc) - soc, err := ce.Soc(&tc.vehicleSoc, tc.chargedEnergy) - if err != nil { - t.Error(err) - } + soc := ce.Soc(&tc.vehicleSoc, tc.chargedEnergy) - // validate soc estimate - if tc.estimatedSoc != soc { - t.Errorf("expected estimated soc: %g, got: %g", tc.estimatedSoc, soc) - } - - // validate capacity estimate - if tc.virtualCapacity != ce.virtualCapacity { - t.Errorf("expected virtual capacity: %v, got: %v", tc.virtualCapacity, ce.virtualCapacity) - } + // validate soc/capacity estimate + assert.Equal(t, tc.estimatedSoc, soc, "estimated soc") + assert.Equal(t, tc.virtualCapacity, ce.virtualCapacity, "virtual capacity") // validate duration estimate chargePower := 1e3 @@ -92,9 +83,7 @@ func TestSocEstimation(t *testing.T) { remainingHours := (float64(targetSoc) - soc) / 100 * tc.virtualCapacity / chargePower remainingDuration := time.Duration(float64(time.Hour) * remainingHours).Round(time.Second) - if rm := ce.RemainingChargeDuration(targetSoc, chargePower); rm != remainingDuration { - t.Errorf("expected estimated duration: %v, got: %v", remainingDuration, rm) - } + assert.Equal(t, remainingDuration, ce.RemainingChargeDuration(targetSoc, chargePower), "remaining duration") } } From 618385701b6b1be087088a2017c1e4fe116af513 Mon Sep 17 00:00:00 2001 From: Maschga <88616799+Maschga@users.noreply.github.com> Date: Sun, 25 Jan 2026 13:12:10 +0100 Subject: [PATCH 0005/1747] RCT: reset soc strategy (#26945) --- meter/rct.go | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/meter/rct.go b/meter/rct.go index 9ae5947b5..30f20fa9b 100644 --- a/meter/rct.go +++ b/meter/rct.go @@ -22,6 +22,7 @@ type RCT struct { conn *rct.Connection // connection with the RCT device usage string // grid, pv, battery externalPower bool // whether to query external power + rSocStrategy *uint8 // remembers overwritten soc strategy value } var ( @@ -166,6 +167,15 @@ func NewRCT(ctx context.Context, uri, usage string, batterySocLimits batterySocL if batStatus != 0 && batStatus != 1032 && batStatus != 2048 { return fmt.Errorf("invalid battery operating mode: %d", batStatus) } + + // read soc strategy to reset afterwards + if m.rSocStrategy == nil { + strategy, err := m.queryUint8(rct.PowerMngSocStrategy) + if err != nil { + return err + } + m.rSocStrategy = &strategy + } } var eg errgroup.Group @@ -173,7 +183,9 @@ func NewRCT(ctx context.Context, uri, usage string, batterySocLimits batterySocL switch mode { case api.BatteryNormal: eg.Go(func() error { - return m.conn.Write(rct.PowerMngSocStrategy, []byte{rct.SOCTargetInternal}) + err := m.conn.Write(rct.PowerMngSocStrategy, []byte{*m.rSocStrategy}) + m.rSocStrategy = nil + return err }) eg.Go(func() error { @@ -335,3 +347,8 @@ func (m *RCT) queryFloat(id rct.Identifier) (float64, error) { func (m *RCT) queryInt32(id rct.Identifier) (int32, error) { return queryRCT(id, m.conn.QueryInt32) } + +// queryUint8 adds retry logic of recoverable errors to QueryUint8 +func (m *RCT) queryUint8(id rct.Identifier) (uint8, error) { + return queryRCT(id, m.conn.QueryUint8) +} From 2b13eb266879212e8a5ea02ec7f1904f9ea4e3a3 Mon Sep 17 00:00:00 2001 From: andig Date: Sun, 25 Jan 2026 13:42:54 +0100 Subject: [PATCH 0006/1747] Loadpoint: clamp estimator virtual capacity (#26978) --- core/soc/estimator.go | 2 +- core/soc/estimator_test.go | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/core/soc/estimator.go b/core/soc/estimator.go index d296b655e..fa5d0591e 100644 --- a/core/soc/estimator.go +++ b/core/soc/estimator.go @@ -111,7 +111,7 @@ func (s *Estimator) Soc(fetchedSoc *float64, chargedEnergy float64) float64 { // recalculate gradient, wh per soc % if socDiff > 10 && energyDiff > 0 { s.energyPerSocStep = energyDiff / socDiff - s.virtualCapacity = s.energyPerSocStep * 100 + s.virtualCapacity = max(s.vehicle.Capacity()*1e3, s.energyPerSocStep*100) s.log.DEBUG.Printf("soc gradient updated: soc: %.1f%%, socDiff: %.1f%%, energyDiff: %.0fWh, energyPerSocStep: %.1fWh, virtualCapacity: %.0fWh", s.vehicleSoc, socDiff, energyDiff, s.energyPerSocStep, s.virtualCapacity) } diff --git a/core/soc/estimator_test.go b/core/soc/estimator_test.go index 84f3052bf..5edd30350 100644 --- a/core/soc/estimator_test.go +++ b/core/soc/estimator_test.go @@ -39,7 +39,7 @@ func TestSocEstimation(t *testing.T) { charger := &chargerStruct{api.NewMockCharger(ctrl), api.NewMockBattery(ctrl)} // 8.5 kWh user battery capacity is converted to initial value of 10 kWh virtual capacity (at 85% efficiency) - vehicle.EXPECT().Capacity().Return(8.5) + vehicle.EXPECT().Capacity().Return(8.5).AnyTimes() ce := NewEstimator(util.NewLogger("foo"), charger, vehicle) @@ -66,6 +66,7 @@ func TestSocEstimation(t *testing.T) { {5001, 50.0, 100.0, 10000}, {0, 20.0, 20.0, 10000}, {1000, 30.0, 30.0, 10000}, + {1000, 50.0, 50.0, 8500}, // cap virtual capacity minimum to physical capacity } for _, tc := range tc { From 2691bbda1b0150f11c960f72a993a1f78e553b16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20He=C3=9F?= Date: Sun, 25 Jan 2026 14:15:54 +0100 Subject: [PATCH 0007/1747] chore: reuse user token source (#26144) --- charger/easee/identity.go | 25 +++++++++----- charger/zaptec.go | 2 +- charger/zaptec/auth.go | 73 ++++++++++++++++----------------------- util/cache/cache.go | 37 ++++++++++++++++++++ 4 files changed, 85 insertions(+), 52 deletions(-) create mode 100644 util/cache/cache.go diff --git a/charger/easee/identity.go b/charger/easee/identity.go index 164d64b40..e4dedf409 100644 --- a/charger/easee/identity.go +++ b/charger/easee/identity.go @@ -6,6 +6,7 @@ import ( "time" "github.com/evcc-io/evcc/util" + "github.com/evcc-io/evcc/util/cache" "github.com/evcc-io/evcc/util/oauth" "github.com/evcc-io/evcc/util/request" "golang.org/x/oauth2" @@ -38,17 +39,25 @@ type tokenSource struct { user, password string } -// TokenSource creates an Easee token source +// tokenSourceCache stores per-user token sources +var tokenSourceCache = cache.New[oauth2.TokenSource]() + +// TokenSource returns a shared oauth2.TokenSource for the given user. func TokenSource(log *util.Logger, user, password string) (oauth2.TokenSource, error) { - c := &tokenSource{ - Helper: request.NewHelper(log), - user: user, - password: password, - } + return tokenSourceCache.GetOrCreate(user, func() (oauth2.TokenSource, error) { + c := &tokenSource{ + Helper: request.NewHelper(log), + user: user, + password: password, + } - token, err := c.authenticate() + token, err := c.authenticate() + if err != nil { + return nil, err + } - return oauth.RefreshTokenSource(token.AsOAuth2Token(), c.refreshToken), err + return oauth.RefreshTokenSource(token.AsOAuth2Token(), c.refreshToken), nil + }) } func (c *tokenSource) authenticate() (*Token, error) { diff --git a/charger/zaptec.go b/charger/zaptec.go index 5f5002992..70757197a 100644 --- a/charger/zaptec.go +++ b/charger/zaptec.go @@ -121,7 +121,7 @@ func NewZaptec(ctx context.Context, user, password, id string, priority bool, pa }) // Get shared token source for this user (per-user uniqueness) - ts, err := zaptec.GetTokenSource(tsCtx, user, password) + ts, err := zaptec.TokenSource(tsCtx, user, password) if err != nil { return nil, err } diff --git a/charger/zaptec/auth.go b/charger/zaptec/auth.go index 201fb0f69..e1c509917 100644 --- a/charger/zaptec/auth.go +++ b/charger/zaptec/auth.go @@ -6,6 +6,7 @@ import ( "sync" "github.com/coreos/go-oidc/v3/oidc" + "github.com/evcc-io/evcc/util/cache" "golang.org/x/oauth2" ) @@ -23,10 +24,9 @@ func (p *passwordTokenSource) Token() (*oauth2.Token, error) { return p.config.PasswordCredentialsToken(p.ctx, p.user, p.pass) } -// tokenSourceCache stores per-user token sources var ( - tokenSourceMu sync.Mutex - tokenSourceCache = make(map[string]oauth2.TokenSource) + // TokenSourceCache stores per-user token sources + tokenSourceCache = cache.New[oauth2.TokenSource]() oidcProvider *oidc.Provider oidcProviderOnce sync.Once @@ -41,48 +41,35 @@ func getOIDCProvider(ctx context.Context) (*oidc.Provider, error) { return oidcProvider, oidcProviderErr } -// GetTokenSource returns a shared oauth2.TokenSource for the given user credentials. -// Multiple chargers using the same user credentials will share the same TokenSource, -// ensuring tokens are reused and authentication is deduplicated. -func GetTokenSource(ctx context.Context, user, pass string) (oauth2.TokenSource, error) { - tokenSourceMu.Lock() - defer tokenSourceMu.Unlock() +// TokenSource returns a shared oauth2.TokenSource for the given user. +func TokenSource(ctx context.Context, user, pass string) (oauth2.TokenSource, error) { + return tokenSourceCache.GetOrCreate(user, func() (oauth2.TokenSource, error) { + provider, err := getOIDCProvider(ctx) + if err != nil { + return nil, fmt.Errorf("failed to initialize OIDC provider: %w", err) + } - // Use username as the cache key (assuming username is unique) - if ts, exists := tokenSourceCache[user]; exists { - return ts, nil - } + oc := &oauth2.Config{ + Endpoint: provider.Endpoint(), + Scopes: []string{ + oidc.ScopeOpenID, + }, + } - // Get the cached OIDC provider (initialized once) - provider, err := getOIDCProvider(ctx) - if err != nil { - return nil, fmt.Errorf("failed to initialize OIDC provider: %w", err) - } + // Create the password token source + pts := &passwordTokenSource{ + ctx: ctx, + config: oc, + user: user, + pass: pass, + } - oc := &oauth2.Config{ - Endpoint: provider.Endpoint(), - Scopes: []string{ - oidc.ScopeOpenID, - }, - } + // Get initial token + token, err := pts.Token() + if err != nil { + return nil, err + } - // Create the password token source - pts := &passwordTokenSource{ - ctx: ctx, - config: oc, - user: user, - pass: pass, - } - - // Get initial token - token, err := pts.Token() - if err != nil { - return nil, err - } - - // Wrap with ReuseTokenSource to cache tokens - ts := oauth2.ReuseTokenSource(token, pts) - tokenSourceCache[user] = ts - - return ts, nil + return oauth2.ReuseTokenSource(token, pts), nil + }) } diff --git a/util/cache/cache.go b/util/cache/cache.go new file mode 100644 index 000000000..7c7b6024f --- /dev/null +++ b/util/cache/cache.go @@ -0,0 +1,37 @@ +package cache + +import ( + "sync" +) + +// Cache provides thread-safe caching keyed by username +type Cache[T any] struct { + mu sync.Mutex + cache map[string]T +} + +// New creates a new Cache instance +func New[T any]() *Cache[T] { + return &Cache[T]{ + cache: make(map[string]T), + } +} + +// GetOrCreate atomically gets or creates a cached object +func (c *Cache[T]) GetOrCreate(key string, createFn func() (T, error)) (T, error) { + c.mu.Lock() + defer c.mu.Unlock() + + if res, ok := c.cache[key]; ok { + return res, nil + } + + res, err := createFn() + if err != nil { + var zero T + return zero, err + } + + c.cache[key] = res + return res, nil +} From 6527d9e62e95b0bfd8f130d80f14eb383ce65b64 Mon Sep 17 00:00:00 2001 From: andig Date: Sun, 25 Jan 2026 14:21:06 +0100 Subject: [PATCH 0008/1747] chore: upgrade modules --- go.mod | 24 ++++++++++++------------ go.sum | 45 ++++++++++++++++++++++----------------------- 2 files changed, 34 insertions(+), 35 deletions(-) diff --git a/go.mod b/go.mod index d5d0a0093..db22c06bb 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/Masterminds/sprig/v3 v3.3.0 github.com/PuerkitoBio/goquery v1.11.0 github.com/WulfgarW/sensonet v0.0.7 - github.com/andig/evopt v0.0.0-20251030151600-82023795e6f5 + github.com/andig/evopt v0.0.0-20260119220201-238830f9254a github.com/andig/go-powerwall v0.2.1-0.20230808194509-dd70cdb6e140 github.com/andig/gosunspec v0.0.0-20240918203654-860ce51d602b github.com/andig/mbserver v0.0.0-20230310211055-1d29cbb5820e @@ -16,7 +16,7 @@ require ( github.com/aws/aws-sdk-go-v2 v1.41.1 github.com/aws/aws-sdk-go-v2/config v1.32.7 github.com/aws/aws-sdk-go-v2/credentials v1.19.7 - github.com/aws/aws-sdk-go-v2/service/cognitoidentity v1.33.17 + github.com/aws/aws-sdk-go-v2/service/cognitoidentity v1.33.18 github.com/basgys/goxml2json v1.1.0 github.com/basvdlei/gotsmart v0.0.3 github.com/benbjohnson/clock v1.3.5 @@ -41,7 +41,7 @@ require ( github.com/glebarez/sqlite v1.11.0 github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1 github.com/go-playground/validator/v10 v10.30.1 - github.com/go-telegram/bot v1.17.0 + github.com/go-telegram/bot v1.18.0 github.com/go-viper/mapstructure/v2 v2.5.0 github.com/godbus/dbus/v5 v5.2.2 github.com/gokrazy/updater v0.0.0-20250705135802-db129c40879c @@ -52,7 +52,7 @@ require ( github.com/gorilla/mux v1.8.1 github.com/gosimple/slug v1.15.0 github.com/gregdel/pushover v1.4.0 - github.com/grid-x/modbus v0.0.0-20251121133955-8a6c959be366 + github.com/grid-x/modbus v0.0.0-20260122103929-e192bc287e6e github.com/hashicorp/go-version v1.8.0 github.com/hasura/go-graphql-client v0.15.1 github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83 @@ -77,7 +77,7 @@ require ( github.com/muka/go-bluetooth v0.0.0-20240701044517-04c4f09c514e github.com/mxschmitt/golang-combinations v1.2.0 github.com/nicksnyder/go-i18n/v2 v2.6.1 - github.com/olekukonko/tablewriter v1.1.2 + github.com/olekukonko/tablewriter v1.1.3 github.com/philippseith/signalr v0.8.0 github.com/prometheus-community/pro-bing v0.7.0 github.com/prometheus/client_golang v1.23.2 @@ -85,7 +85,7 @@ require ( github.com/robertkrimen/otto v0.5.1 github.com/samber/lo v1.52.0 github.com/sandrolain/httpcache v1.4.0 - github.com/sirupsen/logrus v1.9.3 + github.com/sirupsen/logrus v1.9.4 github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 github.com/smallnest/chanx v1.2.0 github.com/spali/go-rscp v0.2.2 @@ -103,7 +103,7 @@ require ( gitlab.com/bboehmke/sunny v0.16.0 go.bug.st/serial v1.6.4 go.uber.org/mock v0.6.0 - go.yaml.in/yaml/v4 v4.0.0-rc.3 + go.yaml.in/yaml/v4 v4.0.0-rc.4 golang.org/x/crypto v0.47.0 golang.org/x/crypto/x509roots/fallback v0.0.0-20260113154411-7d0074ccc6f1 golang.org/x/exp v0.0.0-20260112195511-716be5621a96 @@ -141,7 +141,7 @@ require ( github.com/breml/rootcerts v0.2.21 // indirect github.com/cenkalti/backoff/v5 v5.0.2 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/clipperhouse/displaywidth v0.6.0 // indirect + github.com/clipperhouse/displaywidth v0.6.2 // indirect github.com/clipperhouse/stringish v0.1.1 // indirect github.com/clipperhouse/uax29/v2 v2.3.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect @@ -154,7 +154,7 @@ require ( github.com/dustin/go-humanize v1.0.1 // indirect github.com/enbility/go-avahi v0.0.0-20240909195612-d5de6b280d7a // indirect github.com/enbility/zeroconf/v2 v2.0.0-20240920094356-be1cae74fda6 // indirect - github.com/fatih/color v1.17.0 // indirect + github.com/fatih/color v1.18.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/gabriel-vasile/mimetype v1.4.12 // indirect @@ -206,7 +206,7 @@ require ( github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect github.com/olekukonko/errors v1.1.0 // indirect - github.com/olekukonko/ll v0.1.3 // indirect + github.com/olekukonko/ll v0.1.4-0.20260115111900-9e59c2286df0 // indirect github.com/onsi/ginkgo/v2 v2.13.0 // indirect github.com/pascaldekloe/name v1.0.1 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect @@ -245,10 +245,10 @@ require ( gopkg.in/go-playground/validator.v9 v9.31.0 // indirect gopkg.in/sourcemap.v1 v1.0.5 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - modernc.org/libc v1.67.4 // indirect + modernc.org/libc v1.67.6 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect - modernc.org/sqlite v1.44.0 // indirect + modernc.org/sqlite v1.44.3 // indirect ) tool ( diff --git a/go.sum b/go.sum index 8cbccecb0..060f0d2ef 100644 --- a/go.sum +++ b/go.sum @@ -33,8 +33,8 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/andig/evopt v0.0.0-20251030151600-82023795e6f5 h1:jeveaNoQ2MLsP55bg6y6i5oTBz/2X1pSS7YBZ3EPTEc= -github.com/andig/evopt v0.0.0-20251030151600-82023795e6f5/go.mod h1:C05cTe1ffvaip2xNNHMJSy/OtGT/NH1sXQgmV/7be8k= +github.com/andig/evopt v0.0.0-20260119220201-238830f9254a h1:p5/javLeDBFTxKonJe4/2fpuYUoWymp+C3zfAtphO7Q= +github.com/andig/evopt v0.0.0-20260119220201-238830f9254a/go.mod h1:C05cTe1ffvaip2xNNHMJSy/OtGT/NH1sXQgmV/7be8k= github.com/andig/go-powerwall v0.2.1-0.20230808194509-dd70cdb6e140 h1:C93T8vg7CN4Q4BBDbBMOvBoTYBA+CaBEvlY2SZF8aa0= github.com/andig/go-powerwall v0.2.1-0.20230808194509-dd70cdb6e140/go.mod h1:Xk09mD+7RTCuuHMX5RxlqgEUeh9oZdIX0rL0qiY6l/4= github.com/andig/gosunspec v0.0.0-20240918203654-860ce51d602b h1:81UMfM949I7StrRay7YDUZazY8M1u/JHkzwcFEjiilQ= @@ -72,8 +72,8 @@ github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 h1:WWLqlh79iO48yLkj1v github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17/go.mod h1:EhG22vHRrvF8oXSTYStZhJc1aUgKtnJe+aOiFEV90cM= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= -github.com/aws/aws-sdk-go-v2/service/cognitoidentity v1.33.17 h1:JRYcZM6pm0SvtYJhpDm35S3KfdSVpcJFoEakr105vlo= -github.com/aws/aws-sdk-go-v2/service/cognitoidentity v1.33.17/go.mod h1:D8mmxQnWQZL6pskpLJmq/91xku1o8PfWTUCQHxe0cTs= +github.com/aws/aws-sdk-go-v2/service/cognitoidentity v1.33.18 h1:JyhYT6WNHQHgX9lIKh6FSsh/KNJGNk73wlTwAL3CZVU= +github.com/aws/aws-sdk-go-v2/service/cognitoidentity v1.33.18/go.mod h1:D8mmxQnWQZL6pskpLJmq/91xku1o8PfWTUCQHxe0cTs= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 h1:RuNSMoozM8oXlgLG/n6WLaFGoea7/CddrCfIiSA+xdY= @@ -124,8 +124,8 @@ github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4 github.com/cli/browser v1.3.0 h1:LejqCrpWr+1pRqmEPDGnTZOjsMe7sehifLynZJuqJpo= github.com/cli/browser v1.3.0/go.mod h1:HH8s+fOAxjhQoBUAsKuPCbqUuxZDhQ2/aD+SzsEfBTk= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/clipperhouse/displaywidth v0.6.0 h1:k32vueaksef9WIKCNcoqRNyKbyvkvkysNYnAWz2fN4s= -github.com/clipperhouse/displaywidth v0.6.0/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o= +github.com/clipperhouse/displaywidth v0.6.2 h1:ZDpTkFfpHOKte4RG5O/BOyf3ysnvFswpyYrV7z2uAKo= +github.com/clipperhouse/displaywidth v0.6.2/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o= github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4= @@ -206,8 +206,8 @@ github.com/evcc-io/tesla-proxy-client v0.0.0-20240221194046-4168b3759701 h1:3Jpl github.com/evcc-io/tesla-proxy-client v0.0.0-20240221194046-4168b3759701/go.mod h1:zWtAweBqXJTk3HSrPSecz3Q3a2hAUQ4vOE6paJfn03I= github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= -github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= @@ -268,8 +268,8 @@ github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= -github.com/go-telegram/bot v1.17.0 h1:Hs0kGxSj97QFqOQP0zxduY/4tSx8QDzvNI9uVRS+zmY= -github.com/go-telegram/bot v1.17.0/go.mod h1:i2TRs7fXWIeaceF3z7KzsMt/he0TwkVC680mvdTFYeM= +github.com/go-telegram/bot v1.18.0 h1:yQzv437DY42SYTPBY48RinAvwbmf1ox5QICskIYWCD8= +github.com/go-telegram/bot v1.18.0/go.mod h1:i2TRs7fXWIeaceF3z7KzsMt/he0TwkVC680mvdTFYeM= github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= @@ -565,11 +565,11 @@ github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0= github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM= github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= -github.com/olekukonko/ll v0.1.3 h1:sV2jrhQGq5B3W0nENUISCR6azIPf7UBUpVq0x/y70Fg= -github.com/olekukonko/ll v0.1.3/go.mod h1:b52bVQRRPObe+yyBl0TxNfhesL0nedD4Cht0/zx55Ew= +github.com/olekukonko/ll v0.1.4-0.20260115111900-9e59c2286df0 h1:jrYnow5+hy3WRDCBypUFvVKNSPPCdqgSXIE9eJDD8LM= +github.com/olekukonko/ll v0.1.4-0.20260115111900-9e59c2286df0/go.mod h1:b52bVQRRPObe+yyBl0TxNfhesL0nedD4Cht0/zx55Ew= github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= -github.com/olekukonko/tablewriter v1.1.2 h1:L2kI1Y5tZBct/O/TyZK1zIE9GlBj/TVs+AY5tZDCDSc= -github.com/olekukonko/tablewriter v1.1.2/go.mod h1:z7SYPugVqGVavWoA2sGsFIoOVNmEHxUAAMrhXONtfkg= +github.com/olekukonko/tablewriter v1.1.3 h1:VSHhghXxrP0JHl+0NnKid7WoEmd9/urKRJLysb70nnA= +github.com/olekukonko/tablewriter v1.1.3/go.mod h1:9VU0knjhmMkXjnMKrZ3+L2JhhtsQ/L38BbL3CRNE8tM= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= @@ -688,8 +688,8 @@ github.com/simonvetter/modbus v1.6.0/go.mod h1:hh90ZaTaPLcK2REj6/fpTbiV0J6S7GWmd github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= github.com/smallnest/chanx v1.2.0 h1:RLyldZBbQZ4O0dSvdkTMHo4+mDw20Bc1jXXTHf+ymZo= @@ -813,8 +813,8 @@ go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -go.yaml.in/yaml/v4 v4.0.0-rc.3 h1:3h1fjsh1CTAPjW7q/EMe+C8shx5d8ctzZTrLcs/j8Go= -go.yaml.in/yaml/v4 v4.0.0-rc.3/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= +go.yaml.in/yaml/v4 v4.0.0-rc.4 h1:UP4+v6fFrBIb1l934bDl//mmnoIZEDK0idg1+AIvX5U= +go.yaml.in/yaml/v4 v4.0.0-rc.4/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -929,7 +929,6 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210426080607-c94f62235c83/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220804214406-8e32c043e418/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1081,8 +1080,8 @@ modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE= modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= -modernc.org/libc v1.67.4 h1:zZGmCMUVPORtKv95c2ReQN5VDjvkoRm9GWPTEPuvlWg= -modernc.org/libc v1.67.4/go.mod h1:QvvnnJ5P7aitu0ReNpVIEyesuhmDLQ8kaEoyMjIFZJA= +modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI= +modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= @@ -1091,8 +1090,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= -modernc.org/sqlite v1.44.0 h1:YjCKJnzZde2mLVy0cMKTSL4PxCmbIguOq9lGp8ZvGOc= -modernc.org/sqlite v1.44.0/go.mod h1:2Dq41ir5/qri7QJJJKNZcP4UF7TsX/KNeykYgPDtGhE= +modernc.org/sqlite v1.44.3 h1:+39JvV/HWMcYslAwRxHb8067w+2zowvFOUrOWIy9PjY= +modernc.org/sqlite v1.44.3/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= From 5d67c0b7ac518fb1faa3456ea1723c2c36fba9b4 Mon Sep 17 00:00:00 2001 From: andig Date: Sun, 25 Jan 2026 14:27:55 +0100 Subject: [PATCH 0009/1747] Revert "chore: upgrade modules" This reverts commit 6527d9e62e95b0bfd8f130d80f14eb383ce65b64. --- go.mod | 24 ++++++++++++------------ go.sum | 45 +++++++++++++++++++++++---------------------- 2 files changed, 35 insertions(+), 34 deletions(-) diff --git a/go.mod b/go.mod index db22c06bb..d5d0a0093 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/Masterminds/sprig/v3 v3.3.0 github.com/PuerkitoBio/goquery v1.11.0 github.com/WulfgarW/sensonet v0.0.7 - github.com/andig/evopt v0.0.0-20260119220201-238830f9254a + github.com/andig/evopt v0.0.0-20251030151600-82023795e6f5 github.com/andig/go-powerwall v0.2.1-0.20230808194509-dd70cdb6e140 github.com/andig/gosunspec v0.0.0-20240918203654-860ce51d602b github.com/andig/mbserver v0.0.0-20230310211055-1d29cbb5820e @@ -16,7 +16,7 @@ require ( github.com/aws/aws-sdk-go-v2 v1.41.1 github.com/aws/aws-sdk-go-v2/config v1.32.7 github.com/aws/aws-sdk-go-v2/credentials v1.19.7 - github.com/aws/aws-sdk-go-v2/service/cognitoidentity v1.33.18 + github.com/aws/aws-sdk-go-v2/service/cognitoidentity v1.33.17 github.com/basgys/goxml2json v1.1.0 github.com/basvdlei/gotsmart v0.0.3 github.com/benbjohnson/clock v1.3.5 @@ -41,7 +41,7 @@ require ( github.com/glebarez/sqlite v1.11.0 github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1 github.com/go-playground/validator/v10 v10.30.1 - github.com/go-telegram/bot v1.18.0 + github.com/go-telegram/bot v1.17.0 github.com/go-viper/mapstructure/v2 v2.5.0 github.com/godbus/dbus/v5 v5.2.2 github.com/gokrazy/updater v0.0.0-20250705135802-db129c40879c @@ -52,7 +52,7 @@ require ( github.com/gorilla/mux v1.8.1 github.com/gosimple/slug v1.15.0 github.com/gregdel/pushover v1.4.0 - github.com/grid-x/modbus v0.0.0-20260122103929-e192bc287e6e + github.com/grid-x/modbus v0.0.0-20251121133955-8a6c959be366 github.com/hashicorp/go-version v1.8.0 github.com/hasura/go-graphql-client v0.15.1 github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83 @@ -77,7 +77,7 @@ require ( github.com/muka/go-bluetooth v0.0.0-20240701044517-04c4f09c514e github.com/mxschmitt/golang-combinations v1.2.0 github.com/nicksnyder/go-i18n/v2 v2.6.1 - github.com/olekukonko/tablewriter v1.1.3 + github.com/olekukonko/tablewriter v1.1.2 github.com/philippseith/signalr v0.8.0 github.com/prometheus-community/pro-bing v0.7.0 github.com/prometheus/client_golang v1.23.2 @@ -85,7 +85,7 @@ require ( github.com/robertkrimen/otto v0.5.1 github.com/samber/lo v1.52.0 github.com/sandrolain/httpcache v1.4.0 - github.com/sirupsen/logrus v1.9.4 + github.com/sirupsen/logrus v1.9.3 github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 github.com/smallnest/chanx v1.2.0 github.com/spali/go-rscp v0.2.2 @@ -103,7 +103,7 @@ require ( gitlab.com/bboehmke/sunny v0.16.0 go.bug.st/serial v1.6.4 go.uber.org/mock v0.6.0 - go.yaml.in/yaml/v4 v4.0.0-rc.4 + go.yaml.in/yaml/v4 v4.0.0-rc.3 golang.org/x/crypto v0.47.0 golang.org/x/crypto/x509roots/fallback v0.0.0-20260113154411-7d0074ccc6f1 golang.org/x/exp v0.0.0-20260112195511-716be5621a96 @@ -141,7 +141,7 @@ require ( github.com/breml/rootcerts v0.2.21 // indirect github.com/cenkalti/backoff/v5 v5.0.2 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/clipperhouse/displaywidth v0.6.2 // indirect + github.com/clipperhouse/displaywidth v0.6.0 // indirect github.com/clipperhouse/stringish v0.1.1 // indirect github.com/clipperhouse/uax29/v2 v2.3.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect @@ -154,7 +154,7 @@ require ( github.com/dustin/go-humanize v1.0.1 // indirect github.com/enbility/go-avahi v0.0.0-20240909195612-d5de6b280d7a // indirect github.com/enbility/zeroconf/v2 v2.0.0-20240920094356-be1cae74fda6 // indirect - github.com/fatih/color v1.18.0 // indirect + github.com/fatih/color v1.17.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/gabriel-vasile/mimetype v1.4.12 // indirect @@ -206,7 +206,7 @@ require ( github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect github.com/olekukonko/errors v1.1.0 // indirect - github.com/olekukonko/ll v0.1.4-0.20260115111900-9e59c2286df0 // indirect + github.com/olekukonko/ll v0.1.3 // indirect github.com/onsi/ginkgo/v2 v2.13.0 // indirect github.com/pascaldekloe/name v1.0.1 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect @@ -245,10 +245,10 @@ require ( gopkg.in/go-playground/validator.v9 v9.31.0 // indirect gopkg.in/sourcemap.v1 v1.0.5 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - modernc.org/libc v1.67.6 // indirect + modernc.org/libc v1.67.4 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect - modernc.org/sqlite v1.44.3 // indirect + modernc.org/sqlite v1.44.0 // indirect ) tool ( diff --git a/go.sum b/go.sum index 060f0d2ef..8cbccecb0 100644 --- a/go.sum +++ b/go.sum @@ -33,8 +33,8 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/andig/evopt v0.0.0-20260119220201-238830f9254a h1:p5/javLeDBFTxKonJe4/2fpuYUoWymp+C3zfAtphO7Q= -github.com/andig/evopt v0.0.0-20260119220201-238830f9254a/go.mod h1:C05cTe1ffvaip2xNNHMJSy/OtGT/NH1sXQgmV/7be8k= +github.com/andig/evopt v0.0.0-20251030151600-82023795e6f5 h1:jeveaNoQ2MLsP55bg6y6i5oTBz/2X1pSS7YBZ3EPTEc= +github.com/andig/evopt v0.0.0-20251030151600-82023795e6f5/go.mod h1:C05cTe1ffvaip2xNNHMJSy/OtGT/NH1sXQgmV/7be8k= github.com/andig/go-powerwall v0.2.1-0.20230808194509-dd70cdb6e140 h1:C93T8vg7CN4Q4BBDbBMOvBoTYBA+CaBEvlY2SZF8aa0= github.com/andig/go-powerwall v0.2.1-0.20230808194509-dd70cdb6e140/go.mod h1:Xk09mD+7RTCuuHMX5RxlqgEUeh9oZdIX0rL0qiY6l/4= github.com/andig/gosunspec v0.0.0-20240918203654-860ce51d602b h1:81UMfM949I7StrRay7YDUZazY8M1u/JHkzwcFEjiilQ= @@ -72,8 +72,8 @@ github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 h1:WWLqlh79iO48yLkj1v github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17/go.mod h1:EhG22vHRrvF8oXSTYStZhJc1aUgKtnJe+aOiFEV90cM= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= -github.com/aws/aws-sdk-go-v2/service/cognitoidentity v1.33.18 h1:JyhYT6WNHQHgX9lIKh6FSsh/KNJGNk73wlTwAL3CZVU= -github.com/aws/aws-sdk-go-v2/service/cognitoidentity v1.33.18/go.mod h1:D8mmxQnWQZL6pskpLJmq/91xku1o8PfWTUCQHxe0cTs= +github.com/aws/aws-sdk-go-v2/service/cognitoidentity v1.33.17 h1:JRYcZM6pm0SvtYJhpDm35S3KfdSVpcJFoEakr105vlo= +github.com/aws/aws-sdk-go-v2/service/cognitoidentity v1.33.17/go.mod h1:D8mmxQnWQZL6pskpLJmq/91xku1o8PfWTUCQHxe0cTs= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 h1:RuNSMoozM8oXlgLG/n6WLaFGoea7/CddrCfIiSA+xdY= @@ -124,8 +124,8 @@ github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4 github.com/cli/browser v1.3.0 h1:LejqCrpWr+1pRqmEPDGnTZOjsMe7sehifLynZJuqJpo= github.com/cli/browser v1.3.0/go.mod h1:HH8s+fOAxjhQoBUAsKuPCbqUuxZDhQ2/aD+SzsEfBTk= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/clipperhouse/displaywidth v0.6.2 h1:ZDpTkFfpHOKte4RG5O/BOyf3ysnvFswpyYrV7z2uAKo= -github.com/clipperhouse/displaywidth v0.6.2/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o= +github.com/clipperhouse/displaywidth v0.6.0 h1:k32vueaksef9WIKCNcoqRNyKbyvkvkysNYnAWz2fN4s= +github.com/clipperhouse/displaywidth v0.6.0/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o= github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4= @@ -206,8 +206,8 @@ github.com/evcc-io/tesla-proxy-client v0.0.0-20240221194046-4168b3759701 h1:3Jpl github.com/evcc-io/tesla-proxy-client v0.0.0-20240221194046-4168b3759701/go.mod h1:zWtAweBqXJTk3HSrPSecz3Q3a2hAUQ4vOE6paJfn03I= github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= -github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= +github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= @@ -268,8 +268,8 @@ github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= -github.com/go-telegram/bot v1.18.0 h1:yQzv437DY42SYTPBY48RinAvwbmf1ox5QICskIYWCD8= -github.com/go-telegram/bot v1.18.0/go.mod h1:i2TRs7fXWIeaceF3z7KzsMt/he0TwkVC680mvdTFYeM= +github.com/go-telegram/bot v1.17.0 h1:Hs0kGxSj97QFqOQP0zxduY/4tSx8QDzvNI9uVRS+zmY= +github.com/go-telegram/bot v1.17.0/go.mod h1:i2TRs7fXWIeaceF3z7KzsMt/he0TwkVC680mvdTFYeM= github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= @@ -565,11 +565,11 @@ github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0= github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM= github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= -github.com/olekukonko/ll v0.1.4-0.20260115111900-9e59c2286df0 h1:jrYnow5+hy3WRDCBypUFvVKNSPPCdqgSXIE9eJDD8LM= -github.com/olekukonko/ll v0.1.4-0.20260115111900-9e59c2286df0/go.mod h1:b52bVQRRPObe+yyBl0TxNfhesL0nedD4Cht0/zx55Ew= +github.com/olekukonko/ll v0.1.3 h1:sV2jrhQGq5B3W0nENUISCR6azIPf7UBUpVq0x/y70Fg= +github.com/olekukonko/ll v0.1.3/go.mod h1:b52bVQRRPObe+yyBl0TxNfhesL0nedD4Cht0/zx55Ew= github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= -github.com/olekukonko/tablewriter v1.1.3 h1:VSHhghXxrP0JHl+0NnKid7WoEmd9/urKRJLysb70nnA= -github.com/olekukonko/tablewriter v1.1.3/go.mod h1:9VU0knjhmMkXjnMKrZ3+L2JhhtsQ/L38BbL3CRNE8tM= +github.com/olekukonko/tablewriter v1.1.2 h1:L2kI1Y5tZBct/O/TyZK1zIE9GlBj/TVs+AY5tZDCDSc= +github.com/olekukonko/tablewriter v1.1.2/go.mod h1:z7SYPugVqGVavWoA2sGsFIoOVNmEHxUAAMrhXONtfkg= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= @@ -688,8 +688,8 @@ github.com/simonvetter/modbus v1.6.0/go.mod h1:hh90ZaTaPLcK2REj6/fpTbiV0J6S7GWmd github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= -github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= -github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= github.com/smallnest/chanx v1.2.0 h1:RLyldZBbQZ4O0dSvdkTMHo4+mDw20Bc1jXXTHf+ymZo= @@ -813,8 +813,8 @@ go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -go.yaml.in/yaml/v4 v4.0.0-rc.4 h1:UP4+v6fFrBIb1l934bDl//mmnoIZEDK0idg1+AIvX5U= -go.yaml.in/yaml/v4 v4.0.0-rc.4/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= +go.yaml.in/yaml/v4 v4.0.0-rc.3 h1:3h1fjsh1CTAPjW7q/EMe+C8shx5d8ctzZTrLcs/j8Go= +go.yaml.in/yaml/v4 v4.0.0-rc.3/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -929,6 +929,7 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210426080607-c94f62235c83/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220804214406-8e32c043e418/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1080,8 +1081,8 @@ modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE= modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= -modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI= -modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE= +modernc.org/libc v1.67.4 h1:zZGmCMUVPORtKv95c2ReQN5VDjvkoRm9GWPTEPuvlWg= +modernc.org/libc v1.67.4/go.mod h1:QvvnnJ5P7aitu0ReNpVIEyesuhmDLQ8kaEoyMjIFZJA= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= @@ -1090,8 +1091,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= -modernc.org/sqlite v1.44.3 h1:+39JvV/HWMcYslAwRxHb8067w+2zowvFOUrOWIy9PjY= -modernc.org/sqlite v1.44.3/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA= +modernc.org/sqlite v1.44.0 h1:YjCKJnzZde2mLVy0cMKTSL4PxCmbIguOq9lGp8ZvGOc= +modernc.org/sqlite v1.44.0/go.mod h1:2Dq41ir5/qri7QJJJKNZcP4UF7TsX/KNeykYgPDtGhE= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= From 62be5ca7b92fd58040301ca001cb80e1c8303ea7 Mon Sep 17 00:00:00 2001 From: StefanSchoof <4662023+StefanSchoof@users.noreply.github.com> Date: Sun, 25 Jan 2026 17:20:15 +0100 Subject: [PATCH 0010/1747] chore: energyforecast add countries based on new domain choice (#26987) --- templates/definition/tariff/energyforecast.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/definition/tariff/energyforecast.yaml b/templates/definition/tariff/energyforecast.yaml index 7a7240ca8..2e8bb183d 100644 --- a/templates/definition/tariff/energyforecast.yaml +++ b/templates/definition/tariff/energyforecast.yaml @@ -4,7 +4,7 @@ products: requirements: evcc: ["skiptest"] group: price -countries: ["DE"] +countries: ["DE", "LU", "AT", "FR", "NL", "BE", "PL", "DK"] params: - name: token required: true From d1c62a3b49be17ab0d4846309114ac524cbe7464 Mon Sep 17 00:00:00 2001 From: andig Date: Sun, 25 Jan 2026 18:54:37 +0100 Subject: [PATCH 0011/1747] chore: deduplicate mask (#26996) --- templates/definition/charger/evecube.yaml | 1 - templates/definition/charger/fronius-wattpilot.yaml | 1 - templates/definition/charger/homematic.yaml | 1 - templates/definition/charger/openevse.yaml | 1 - templates/definition/charger/shelly-topac.yaml | 1 - templates/definition/charger/shelly.yaml | 1 - templates/definition/charger/tasmota.yaml | 1 - templates/definition/meter/homematic.yaml | 1 - templates/definition/meter/openems.yaml | 1 - templates/definition/meter/sma-inverter-speedwire.yaml | 1 - templates/definition/meter/tasmota-3p.yaml | 1 - templates/definition/meter/tasmota-sml.yaml | 1 - templates/definition/meter/tasmota.yaml | 1 - 13 files changed, 13 deletions(-) diff --git a/templates/definition/charger/evecube.yaml b/templates/definition/charger/evecube.yaml index c8cb4b81d..2a9c1944e 100644 --- a/templates/definition/charger/evecube.yaml +++ b/templates/definition/charger/evecube.yaml @@ -21,7 +21,6 @@ params: de: Benutzername für Admin-API example: admin - name: password - mask: true help: en: Password for admin API de: Passwort für Admin-API diff --git a/templates/definition/charger/fronius-wattpilot.yaml b/templates/definition/charger/fronius-wattpilot.yaml index 15d3d8efd..f3a15e610 100644 --- a/templates/definition/charger/fronius-wattpilot.yaml +++ b/templates/definition/charger/fronius-wattpilot.yaml @@ -14,7 +14,6 @@ requirements: params: - name: host - name: password - mask: true render: | type: wattpilot uri: {{ .host }} diff --git a/templates/definition/charger/homematic.yaml b/templates/definition/charger/homematic.yaml index 3c98b9473..13223c8f8 100644 --- a/templates/definition/charger/homematic.yaml +++ b/templates/definition/charger/homematic.yaml @@ -22,7 +22,6 @@ params: de: Homematic Geräte Id, wie im CCU Webfrontend angezeigt. - name: user - name: password - mask: true - name: meterchannel default: 6 type: int diff --git a/templates/definition/charger/openevse.yaml b/templates/definition/charger/openevse.yaml index 737887fdb..98715709e 100644 --- a/templates/definition/charger/openevse.yaml +++ b/templates/definition/charger/openevse.yaml @@ -9,7 +9,6 @@ params: - name: host - name: user - name: password - mask: true render: | type: openevse uri: http://{{ .host }} diff --git a/templates/definition/charger/shelly-topac.yaml b/templates/definition/charger/shelly-topac.yaml index cb13f48d5..c8468ba1d 100644 --- a/templates/definition/charger/shelly-topac.yaml +++ b/templates/definition/charger/shelly-topac.yaml @@ -14,7 +14,6 @@ params: required: true - name: user - name: password - mask: true render: | type: shelly-topac uri: http://{{ .host }} diff --git a/templates/definition/charger/shelly.yaml b/templates/definition/charger/shelly.yaml index 0763544a4..847b02e71 100644 --- a/templates/definition/charger/shelly.yaml +++ b/templates/definition/charger/shelly.yaml @@ -9,7 +9,6 @@ params: - name: host - name: user - name: password - mask: true - name: channel default: 0 - preset: switchsocket diff --git a/templates/definition/charger/tasmota.yaml b/templates/definition/charger/tasmota.yaml index d0e8fd882..dafa2ed13 100644 --- a/templates/definition/charger/tasmota.yaml +++ b/templates/definition/charger/tasmota.yaml @@ -12,7 +12,6 @@ params: de: Standard-User ist admin en: admin is default - name: password - mask: true - name: channel default: 1 required: true diff --git a/templates/definition/meter/homematic.yaml b/templates/definition/meter/homematic.yaml index 4b22cef2d..80d0dd9e6 100644 --- a/templates/definition/meter/homematic.yaml +++ b/templates/definition/meter/homematic.yaml @@ -18,7 +18,6 @@ params: de: Homematic Geräte Id, wie im CCU Webfrontend angezeigt. - name: user - name: password - mask: true - name: meterchannel default: 6 type: int diff --git a/templates/definition/meter/openems.yaml b/templates/definition/meter/openems.yaml index 94032b731..1bda8d6f1 100644 --- a/templates/definition/meter/openems.yaml +++ b/templates/definition/meter/openems.yaml @@ -32,7 +32,6 @@ params: choice: ["grid", "pv", "battery"] - name: host - name: password - mask: true default: user advanced: true - name: maxacpower diff --git a/templates/definition/meter/sma-inverter-speedwire.yaml b/templates/definition/meter/sma-inverter-speedwire.yaml index 4c142b16a..4deb9daeb 100644 --- a/templates/definition/meter/sma-inverter-speedwire.yaml +++ b/templates/definition/meter/sma-inverter-speedwire.yaml @@ -12,7 +12,6 @@ params: choice: ["pv", "battery"] - name: host - name: password - mask: true help: en: Password for user group Standard de: Passwort für Benutzergruppe Benutzer diff --git a/templates/definition/meter/tasmota-3p.yaml b/templates/definition/meter/tasmota-3p.yaml index 3c1d95fa1..68ac9108b 100644 --- a/templates/definition/meter/tasmota-3p.yaml +++ b/templates/definition/meter/tasmota-3p.yaml @@ -18,7 +18,6 @@ params: de: Standard-User ist admin en: admin is default - name: password - mask: true render: | type: tasmota uri: http://{{ .host }} diff --git a/templates/definition/meter/tasmota-sml.yaml b/templates/definition/meter/tasmota-sml.yaml index 7d8d3a2d9..91ec6dd46 100644 --- a/templates/definition/meter/tasmota-sml.yaml +++ b/templates/definition/meter/tasmota-sml.yaml @@ -63,7 +63,6 @@ params: de: Standard-User ist admin en: admin is default - name: password - mask: true render: | type: tasmota uri: http://{{ .host }} diff --git a/templates/definition/meter/tasmota.yaml b/templates/definition/meter/tasmota.yaml index b9df9d9d4..28de494d2 100644 --- a/templates/definition/meter/tasmota.yaml +++ b/templates/definition/meter/tasmota.yaml @@ -13,7 +13,6 @@ params: de: Standard-User ist admin en: admin is default - name: password - mask: true - name: channel default: 1 required: true From ac720648976f31fb10959a5870323798bb01140a Mon Sep 17 00:00:00 2001 From: andig Date: Sun, 25 Jan 2026 18:55:18 +0100 Subject: [PATCH 0012/1747] chore: make channel int (#26995) --- templates/definition/charger/tasmota.yaml | 6 ++---- templates/definition/meter/tasmota.yaml | 8 +++----- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/templates/definition/charger/tasmota.yaml b/templates/definition/charger/tasmota.yaml index dafa2ed13..afaf7d52e 100644 --- a/templates/definition/charger/tasmota.yaml +++ b/templates/definition/charger/tasmota.yaml @@ -13,14 +13,12 @@ params: en: admin is default - name: password - name: channel + type: int default: 1 required: true description: - de: Schaltkanal Nummer - en: Relaychannel number - help: de: Schaltkanal (1-8) - en: Relaychannel number (1-8) + en: Relay channel (1-8) - preset: switchsocket render: | type: tasmota diff --git a/templates/definition/meter/tasmota.yaml b/templates/definition/meter/tasmota.yaml index 28de494d2..247fa31d8 100644 --- a/templates/definition/meter/tasmota.yaml +++ b/templates/definition/meter/tasmota.yaml @@ -14,14 +14,12 @@ params: en: admin is default - name: password - name: channel + type: int default: 1 required: true description: - de: Kanal Nummer - en: Channel number - help: - de: Meterkanal (1-8) - en: Meterchannel number (1-8) + de: Messkanal (1-8) + en: Meter channel (1-8) render: | type: tasmota uri: http://{{ .host }} From bfdf803c6a28918929583ab514b113d4ef1d54e2 Mon Sep 17 00:00:00 2001 From: andig Date: Sun, 25 Jan 2026 18:55:58 +0100 Subject: [PATCH 0013/1747] Templates: fix azimuth 0 not allowed (#26997) --- templates/definition/tariff/forecast-solar.yaml | 3 ++- util/templates/types.go | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/templates/definition/tariff/forecast-solar.yaml b/templates/definition/tariff/forecast-solar.yaml index 778e48f12..3c9c2aabc 100644 --- a/templates/definition/tariff/forecast-solar.yaml +++ b/templates/definition/tariff/forecast-solar.yaml @@ -16,7 +16,8 @@ params: help: en: -180 = north, -90 = east, 0 = south, 90 = west, 180 = north de: -180 = Norden, -90 = Osten, 0 = Süden, 90 = Westen, 180 = Norden - example: 180 + type: int + example: 0 required: true - name: horizon description: diff --git a/util/templates/types.go b/util/templates/types.go index eff41e91e..3523d4156 100644 --- a/util/templates/types.go +++ b/util/templates/types.go @@ -273,9 +273,9 @@ func (p *Param) IsDeprecated() bool { func (p *Param) IsZero(s string) bool { switch p.Type { case TypeInt: - return cast.ToInt64(s) == 0 + return cast.ToInt64(s) == 0 && p.Name != "az" case TypeFloat: - return cast.ToFloat64(s) == 0 + return cast.ToFloat64(s) == 0 && p.Name != "az" case TypeDuration: return cast.ToDuration(s) == 0 default: From 7d7268cad68f359f91ae1d1393e18cb8dfcab411 Mon Sep 17 00:00:00 2001 From: andig Date: Sun, 25 Jan 2026 20:21:59 +0100 Subject: [PATCH 0014/1747] chore: align implementations (#26999) --- .../definition/charger/tinkerforge-warp.yaml | 2 +- .../charger/tinkerforge-warp3-smart.yaml | 2 +- .../definition/charger/tinkerforge-warp3.yaml | 2 +- templates/definition/vehicle/mg2mqtt.yaml | 15 ++++++++------- templates/definition/vehicle/teslamate.yaml | 15 +++++++++------ vehicle/template_test.go | 1 + 6 files changed, 21 insertions(+), 16 deletions(-) diff --git a/templates/definition/charger/tinkerforge-warp.yaml b/templates/definition/charger/tinkerforge-warp.yaml index 4bfb4310a..61d74a1f9 100644 --- a/templates/definition/charger/tinkerforge-warp.yaml +++ b/templates/definition/charger/tinkerforge-warp.yaml @@ -27,7 +27,7 @@ params: en: WEM Firmware v2 required. render: | type: warp2 - {{ include "mqtt" . }} + {{- include "mqtt" . }} topic: {{ .topic }} {{- if .energymanager }} energymanager: {{ .energymanager }} diff --git a/templates/definition/charger/tinkerforge-warp3-smart.yaml b/templates/definition/charger/tinkerforge-warp3-smart.yaml index 2f114c0f0..e28f2387f 100644 --- a/templates/definition/charger/tinkerforge-warp3-smart.yaml +++ b/templates/definition/charger/tinkerforge-warp3-smart.yaml @@ -12,6 +12,6 @@ params: default: warp render: | type: warp2 - {{ include "mqtt" . }} + {{- include "mqtt" . }} topic: {{ .topic }} energymanager: {{ .topic }} diff --git a/templates/definition/charger/tinkerforge-warp3.yaml b/templates/definition/charger/tinkerforge-warp3.yaml index b1abe3fe2..656dd292f 100644 --- a/templates/definition/charger/tinkerforge-warp3.yaml +++ b/templates/definition/charger/tinkerforge-warp3.yaml @@ -15,6 +15,6 @@ params: default: warp render: | type: warp2 - {{ include "mqtt" . }} + {{- include "mqtt" . }} topic: {{ .topic }} energymanager: {{ .topic }} diff --git a/templates/definition/vehicle/mg2mqtt.yaml b/templates/definition/vehicle/mg2mqtt.yaml index 0acacc80d..90373afbb 100644 --- a/templates/definition/vehicle/mg2mqtt.yaml +++ b/templates/definition/vehicle/mg2mqtt.yaml @@ -8,6 +8,7 @@ requirements: en: Required MQTT broker configuration and a SAIC/MQTT Gateway (https://github.com/SAIC-iSmart-API/saic-python-mqtt-gateway or https://github.com/SAIC-iSmart-API/saic-java-client) de: Voraussetzung ist ein konfigurierter MQTT Broker und ein SAIC/MQTT Gateway (https://github.com/SAIC-iSmart-API/saic-python-mqtt-gateway oder https://github.com/SAIC-iSmart-API/saic-java-client) params: + - preset: mqtt - name: user required: true - name: vin @@ -20,22 +21,22 @@ render: | {{- include "vehicle-common" . }} soc: # battery soc (%) source: mqtt + {{- include "mqtt" . | indent 2 }} topic: saic/{{ .user }}/vehicles/{{ .vin }}/drivetrain/soc - timeout: {{ .timeout }} status: source: combined plugged: source: mqtt + {{- include "mqtt" . | indent 4 }} topic: saic/{{ .user }}/vehicles/{{ .vin }}/drivetrain/chargerConnected - timeout: {{ .timeout }} charging: source: mqtt + {{- include "mqtt" . | indent 4 }} topic: saic/{{ .user }}/vehicles/{{ .vin }}/drivetrain/charging - timeout: {{ .timeout }} range: source: mqtt + {{- include "mqtt" . | indent 2 }} topic: saic/{{ .user }}/vehicles/{{ .vin }}/drivetrain/range - timeout: {{ .timeout }} climater: source: go script: | @@ -45,12 +46,12 @@ render: | type: string config: source: mqtt + {{- include "mqtt" . | indent 8 }} topic: saic/{{ .user }}/vehicles/{{ .vin }}/climate/remoteClimateState - timeout: {{ .timeout }} odometer: source: mqtt + {{- include "mqtt" . | indent 2 }} topic: saic/{{ .user }}/vehicles/{{ .vin }}/drivetrain/mileage - timeout: {{ .timeout }} wakeup: source: go script: | @@ -60,5 +61,5 @@ render: | type: string config: source: mqtt + {{- include "mqtt" . | indent 8 }} topic: saic/{{ .user }}/vehicles/{{ .vin }}/refresh/mode/set - timeout: {{ .timeout }} diff --git a/templates/definition/vehicle/teslamate.yaml b/templates/definition/vehicle/teslamate.yaml index f0225b144..c9d9f570b 100644 --- a/templates/definition/vehicle/teslamate.yaml +++ b/templates/definition/vehicle/teslamate.yaml @@ -8,40 +8,43 @@ requirements: en: Open source Tesla data logger [github.com/adriankumpf/teslamate](https://github.com/adriankumpf/teslamate). MQTT broker required. de: Open Source Tesla Datenlogger [github.com/adriankumpf/teslamate](https://github.com/adriankumpf/teslamate). Voraussetzung ist konfigurierter MQTT Broker. params: + - preset: mqtt - preset: vehicle-common - name: id description: de: Fahrzeug-ID en: Vehicle ID default: 1 + - name: timeout + default: 720h # 30d render: | type: custom {{- include "vehicle-common" . }} soc: source: mqtt + {{- include "mqtt" . | indent 2 }} topic: teslamate/cars/{{ .id }}/battery_level - timeout: 720h # 30d status: source: combined plugged: source: mqtt + {{- include "mqtt" . | indent 4 }} topic: teslamate/cars/{{ .id }}/plugged_in - timeout: 720h # 30d charging: source: mqtt + {{- include "mqtt" . | indent 4 }} topic: teslamate/cars/{{ .id }}/charger_actual_current - timeout: 720h # 30d jq: . > 0 range: source: mqtt + {{- include "mqtt" . | indent 2 }} topic: teslamate/cars/{{ .id }}/rated_battery_range_km - timeout: 720h # 30d odometer: source: mqtt + {{- include "mqtt" . | indent 2 }} topic: teslamate/cars/{{ .id }}/odometer - timeout: 720h # 30d limitsoc: source: mqtt + {{- include "mqtt" . | indent 2 }} topic: teslamate/cars/{{ .id }}/charge_limit_soc - timeout: 720h features: ["coarsecurrent"] diff --git a/vehicle/template_test.go b/vehicle/template_test.go index 1364ecfb8..9f49f3627 100644 --- a/vehicle/template_test.go +++ b/vehicle/template_test.go @@ -26,6 +26,7 @@ var acceptable = []string{ "discussions/17501", // Tesla "login failed: code not found", // Polestar "empty instance type- check for missing usage", // Mercedes + "connect: connection refused", // MQTT } func TestTemplates(t *testing.T) { From 339ad9002d602cb771e81d129fcc745cf0820d44 Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Sun, 25 Jan 2026 21:52:04 +0100 Subject: [PATCH 0015/1747] Translations update from Hosted Weblate (#26679) --- i18n/bs.json | 421 +++++++++++++++++++++++++++++++++++++++++++++++++++ i18n/fi.json | 12 +- i18n/sv.json | 2 +- 3 files changed, 428 insertions(+), 7 deletions(-) diff --git a/i18n/bs.json b/i18n/bs.json index 93e961c1d..60ef79d36 100644 --- a/i18n/bs.json +++ b/i18n/bs.json @@ -5,6 +5,11 @@ "authorizationFailed": "Autorizacija nije uspjela", "authorizationRequired": "Potrebna autorizacija", "authorizationSuccessful": "Autorizacija uspješna", + "buttonConnect": "Poveži se na {provider}", + "buttonDisconnect": "odspojiti", + "confirmLogout": "Jeste li sigurni da želite prekinuti {title}?", + "connect": "povezati", + "disconnect": "odspojiti", "loggedOut": "Uspješno odjavljeno", "logoutFailed": "Nije uspjelo odjavljivanje", "modalDescriptionLogin": "Završite proces autorizacije kako biste uspostavili vezu sa {provider}.", @@ -20,6 +25,422 @@ "above": "kad je iznad {soc}.", "full": "kad na {soc}.", "never": "samo uz dovoljno viška." + }, + "capacity": "{energy} od {total}", + "control": "Kontrola baterije", + "discharge": "Spriječite pražnjenje u brzom načinu rada i planirano punjenje.", + "disclaimerHint": "napomena:", + "disclaimerText": "Ova podešavanja utiču samo na solarni način rada. Ponašanje punjenja se u skladu s tim prilagođava.", + "gridChargeTab": "Mrežno punjenje", + "legendBottomName": "Prioritetizirajte punjenje kućne baterije", + "legendBottomSubline": "dok ne dostigne {soc}.", + "legendMiddleName": "Prioritetizirajte punjenje vozila", + "legendMiddleSubline": "kada je kućna baterija iznad {soc}.", + "legendTopAutostart": "Pokreni automatski", + "legendTopName": "Punjenje vozila podržano baterijama", + "legendTopSubline": "kada je kućna baterija iznad {soc}.", + "modalTitle": "Kućna baterija", + "usageTab": "Upotreba baterije" + }, + "config": { + "aux": { + "description": "Uređaj koji prilagođava svoju potrošnju na osnovu raspoloživog viška (kao pametni bojleri). EVCC očekuje da ovaj uređaj smanji svoju potrošnju energije ako je potrebno.", + "titleAdd": "Dodaj samoregulirajućeg potrošača", + "titleEdit": "Uredi samoregulirajući potrošač" + }, + "battery": { + "titleAdd": "Dodaj bateriju", + "titleEdit": "Uredi bateriju" + }, + "charge": { + "titleAdd": "Dodaj mjerač naplate", + "titleEdit": "Uredi mjerač troškova" + }, + "charger": { + "chargers": "EV punjači", + "generic": "Opće integracije", + "heatingdevices": "Uređaji za grijanje", + "ocppConfirmContinue": "Vaš punjač se još nije povezao na EVCC. Jeste li sigurni da želite nastaviti?", + "ocppConnected": "Povezano!", + "ocppDescription": "evcc ima ugrađeni OCPP server. Slijedite ove korake:", + "ocppHelp": "Kopirajte ovu URL adresu u konfiguraciju svog punjača. Provjerite priručnik proizvođača za detalje. Očekuje se da će punjač automatski dodati svoj jedinstveni identifikator (ID stanice) na URL. U rijetkim slučajevima možda ćete morati ručno navesti identifikator. Primjer: `{url}`", + "ocppLabel": "URL OCPP servera", + "ocppNextStep": "Sljedeći korak", + "ocppStep1": "Konfigurirajte svoj punjač da koristi evcc kao OCPP server.", + "ocppStep2": "Sačekajte da se vaš punjač poveže na EVCC.", + "ocppStep3": "Nastavite i završite konfiguraciju.", + "ocppWaiting": "Čekanje na povezivanje", + "switchsockets": "Prekidivačke utičnice", + "template": "Proizvođač", + "titleAdd": { + "charging": "Dodaj punjač", + "heating": "Dodaj grijač" + }, + "titleEdit": { + "charging": "Uredi punjač", + "heating": "Uredi grijač" + }, + "type": { + "custom": { + "charging": "Korisnički definisani punjač", + "heating": "Korisnički definisani grijač" + }, + "heatpump": "Korisnički definirana toplotna pumpa", + "sgready": "Korisnički definirana toplotna pumpa (pripremljena za SG putem dodataka)", + "sgready-boost": "Korisnički definirana toplotna pumpa (sg-ready-boost, zastarjelo)", + "sgready-relay": "Korisnički definirana toplotna pumpa (spremna za rad putem releja)", + "switchsocket": "Prilagođena utičnica za prekidač" + } + }, + "circuits": { + "description": "Osigurava da zbir svih tačaka opterećenja priključenih na krug ne prelazi konfigurisane granice snage i struje. Krugovi se mogu ugniježđivati kako bi se stvorila hijerarhija.", + "title": "Upravljanje opterećenjem", + "usableMeters": "Referentne vrijednosti mjerila" + }, + "control": { + "description": "Obično su zadane vrijednosti u redu. Mijenjajte ih samo ako znate šta radite.", + "descriptionInterval": "Ažuriranje ciklusa u sekundama. Definira koliko često evcc čita podatke s brojila i prilagođava punjenje. Zadana vrijednost od 30 sekundi je siguran izbor. Uređaji poput vozila, zidnih punjača i invertera obično trebaju nekoliko sekundi da prilagode svoje ponašanje. Ako vaše komponente brzo reaguju, možete koristiti niže vrijednosti. Toplo preporučujemo da ne idete ispod 10 sekundi. Ako primijetite nepravilno ponašanje kontrole ili skakanje vrijednosti snage, odaberite duži interval.", + "descriptionResidualPower": "Pomjera radnu tačku kontrolne petlje. Ako imate kućnu bateriju, preporučuje se postaviti vrijednost od 100 W. Na taj način bateriji će biti dati blagi prioritet u odnosu na korištenje mreže.", + "labelInterval": "Interval ažuriranja", + "labelResidualPower": "Preostala snaga", + "title": "Kontrola ponašanja" + }, + "deviceValue": { + "amount": "Iznos", + "broker": "posrednik", + "bucket": "Bucket", + "capacity": "Kapacitet", + "chargeStatus": "status", + "chargeStatusA": "nije povezano", + "chargeStatusB": "povezan", + "chargeStatusC": "naplaćivanje", + "chargeStatusE": "nema struje", + "chargeStatusF": "greška", + "chargedEnergy": "Naplaćeno", + "co2": "CO₂ mreža", + "configured": "Konfigurisano", + "connections": "Povezanosti", + "controllable": "Kontrolabilan", + "currency": "Valuta", + "current": "Trenutni", + "currentRange": "Trenutni", + "detected": "Detektovano", + "dimmed": "Prigušeno", + "enabled": "Omogućeno", + "energy": "Energia", + "feedinPrice": "Tarifa za prodaju električne energije iz obnovljivih izvora", + "gridPrice": "Mrežna cijena", + "heaterTempLimit": "Ograničenje grijača", + "hemsActiveLimit": "Aktivni limit", + "hemsType": "Komunikacija", + "identifier": "RFID identifikator", + "no": "ne", + "odometer": "brojač kilometara", + "org": "Organizacija", + "phaseCurrents": "Trenutni", + "phasePowers": "moć", + "phaseVoltages": "napon", + "phases1p3p": "Fazni prekidač", + "power": "moć", + "powerRange": "moć", + "range": "domet", + "singlePhase": "Jednofazni", + "soc": "serija", + "solarForecast": "Solarna prognoza", + "temp": "Temperatura", + "url": "URL", + "vehicleLimitSoc": "Ograničenje naplate", + "yes": "da" + }, + "deviceValueChargeStatus": { + "A": "A (nije povezano)", + "B": "B (povezano)", + "C": "C (punjenje)" + }, + "deviceValueHemsType": { + "eebus": "putem EEBus-a", + "relay": "putem štafete" + }, + "devices": { + "auxMeter": "Pametni potrošač", + "batteryStorage": "Skladištenje baterija", + "consumer": "Potrošač", + "solarSystem": "Solarni sistem" + }, + "editor": { + "loading": "Učitavanje YAML uređivača…" + }, + "general": { + "authPerform": "Poveži se sa {provider}", + "authPrepare": "Pripremite vezu", + "cancel": "Otkaži", + "clear": "Jasno", + "close": "Zatvori", + "copied": "Kopirano!", + "copy": "Kopiraj", + "customHelp": "Kreirajte korisnički definisani uređaj koristeći evcc-ov sistem dodataka.", + "customOption": "Korisnički definisani uređaj", + "delete": "Obriši", + "docsLink": "Pogledajte dokumentaciju.", + "dragHandle": "Držač za vuču", + "dragItem": "Povlačivo: {title}", + "dragList": "Ponovo naručiva lista", + "experimental": "Eksperimentalni", + "forceSave": "Sačuvaj svejedno", + "hideAdvancedSettings": "Sakrij napredne postavke", + "invalidFileSelected": "Odabrana neispravna datoteka", + "noFileSelected": "Nije odabrana nijedna datoteka.", + "off": "isključen", + "on": "na", + "password": "Lozinka", + "readFromFile": "Čitaj iz datoteke", + "remove": "Ukloni", + "required": "potrebno", + "reset": "Ponovo pokreni", + "save": "Sačuvaj", + "saved": "Sačuvano.", + "saving": "Spašavanje…", + "selectFile": "Pregledaj", + "showAdvancedSettings": "Prikaži napredne postavke", + "telemetry": "Telemetrija", + "templateLoading": "Učitavanje...", + "title": "Naslov", + "typeDeprecated": "Tip '{type}' je zastario i bit će uklonjen u budućoj verziji. Molimo provjerite zapisnik promjena i ponovo kreirajte ovaj uređaj.", + "validateSave": "Potvrdi i sačuvaj" + }, + "grid": { + "title": "Mrežni brojilac", + "titleAdd": "Dodaj mrežni mjerač", + "titleEdit": "Uredi mrežni mjerač" + }, + "hems": { + "csv": { + "created": "Kreirano", + "finished": "Završeno", + "gridpower": "Mrežna snaga (kW)", + "limitpower": "Ograničenje (kW)", + "type": "Tip" + }, + "description": "Ograničenje snage putem vanjskih sistema (npr. §14a EnWG, §9 EEG interfejs ili viši sistem za upravljanje energijom). Radi zajedno s funkcijom upravljanja opterećenjem.", + "downloadCsv": "Preuzmi CSV", + "eventsRecorded": "Zabilježeno je {count} događaja ograničenja mreže.", + "lastEvent": "Najnoviji prije {timeAgo}.", + "title": "Vanjski limit", + "yamlConfigured": "Napomena: Konfigurisano putem evcc.yaml. Uklonite unos iz datoteke da biste ovdje omogućili uređivanje." + }, + "icon": { + "change": "promjena", + "label": "Ikona" + }, + "influx": { + "description": "Bilježi podatke o naplati i druge metrike u InfluxDB. Koristi Grafanu ili druge alate za vizualizaciju podataka.", + "descriptionToken": "Provjerite dokumentaciju InfluxDB-a da biste saznali kako ga kreirati. https://docs.influxdata.com/influxdb/v2/admin/", + "labelBucket": "Bucket", + "labelCheckInsecure": "Omogući samopotpisane certifikate", + "labelDatabase": "Baza podataka", + "labelInsecure": "Validacija certifikata", + "labelOrg": "Organizacija", + "labelPassword": "Lozenka", + "labelToken": "API token", + "labelUrl": "URL", + "labelUser": "Korisničko ime", + "title": "InfluxDB", + "v1Support": "Trebate podršku za InfluxDB 1.x?", + "v2Support": "Povratak na InfluxDB 2.x" + }, + "loadpoint": { + "addCharger": { + "charging": "Dodaj punjač", + "heating": "Dodaj grijač" + }, + "addMeter": "Dodajte namjenski energetski brojilac", + "cancel": "Otkaži", + "chargerError": { + "charging": "Konfigurisanje punjača je potrebno.", + "heating": "Konfigurisanje grijača je potrebno." + }, + "chargerLabel": { + "charging": "Učitaj", + "heating": "Grijač" + }, + "chargerPower11kw": "11 kW", + "chargerPower11kwHelp": "Koristit će trenutni raspon od 6 do 16 A.", + "chargerPower22kw": "22 kW", + "chargerPower22kwHelp": "Koristit će trenutni raspon od 6 do 32 A.", + "chargerPowerCustom": "drugo", + "chargerPowerCustomHelp": "Definirajte prilagođeni raspon struje.", + "chargerTypeLabel": "Tip opterećenja", + "chargingTitle": "Ponašanje", + "circuitHelp": "Dodjela zadatka upravljanja opterećenjem kako bi se osiguralo da se ne prekorače ograničenja snage i struje.", + "circuitInvalid": "Kružni tok ne postoji", + "circuitLabel": "Kružni tok", + "circuitUnassigned": "nepridruženo", + "defaultModeHelp": { + "charging": "Način punjenja pri povezivanju vozila.", + "heating": "Postavlja se pri pokretanju sistema." + }, + "defaultModeHelpKeep": "Čuva posljednji odabrani način rada.", + "defaultModeLabel": "Zadani način rada", + "delete": "Obriši", + "electricalSubtitle": "Kad ste u nedoumici, pitajte svog električara.", + "electricalTitle": "Električno", + "energyMeterHelp": "Dodatni mjerač ako punjač nema integrisani.", + "energyMeterLabel": "Mjerač energije", + "estimateLabel": "Interpolirajte nivo naboja između API ažuriranja", + "maxCurrentHelp": "Mora biti veće od minimalne struje.", + "maxCurrentLabel": "Maksimalna struja", + "minCurrentHelp": "Smanjujte ispod 6 A samo ako znate šta radite.", + "minCurrentLabel": "Minimalna struja", + "noVehicles": "Nijedno vozilo nije konfigurirano.", + "option": { + "charging": "Dodaj punjač", + "heating": "Dodaj uređaj za grijanje" + }, + "phases1p": "Jednofazni", + "phases3p": "3-fazni", + "phasesAutomatic": "Automatske faze", + "phasesAutomaticHelp": "Vaš punjač podržava automatsko prebacivanje između jednosmjernog i trosmjernog punjenja. Na glavnom ekranu možete prilagoditi ponašanje faza tokom punjenja.", + "phasesHelp": "Broj povezanih faza.", + "phasesLabel": "Faze", + "pollIntervalDanger": "Redovno upitivanje vozila može isprazniti akumulator vozila. Neki proizvođači vozila mogu u ovom slučaju aktivno spriječiti punjenje. Ne preporučuje se! Koristite ovo samo ako ste svjesni rizika.", + "pollIntervalHelp": "Vrijeme između ažuriranja API-ja vozila. Kratki intervali mogu isprazniti bateriju vozila.", + "pollIntervalLabel": "Interval ažuriranja", + "pollModeAlways": "uvijek", + "pollModeAlwaysHelp": "Uvijek tražite ažuriranja statusa u redovnim intervalima.", + "pollModeCharging": "naplaćivanje", + "pollModeChargingHelp": "Tražite ažuriranja statusa vozila samo tokom punjenja.", + "pollModeConnected": "povezan", + "pollModeConnectedHelp": "Ažurirajte status vozila u redovnim intervalima kada je povezano.", + "pollModeLabel": "Ažurirajte ponašanje", + "priorityHelp": "Oni s višim prioritetom dobivaju prednost pri pristupu višku solarne energije.", + "priorityLabel": "Prioritet", + "solarBehaviorDefaultHelp": "Počnite nakon {enableDelay} kada je višak dovoljan. Zaustavite kada viška nije dovoljno za {disableDelay}.", + "thresholdDisableHelpPositive": "Zaustavi kada se iz mreže potroši više od {power} za {delay}.", + "thresholdDisableHelpZero": "Zaustavite kada se za {delay} ne može zadovoljiti minimalna potrebna snaga.", + "thresholdEnableHelpZero": "­ Počnite kada se minimalna potrebna snaga može zadovoljiti za {delay}." + }, + "modbusproxy": { + "connection": "Povezanje #{number}" + }, + "ocpp": { + "urlHelp": "Kopirajte ovu URL adresu u konfiguraciju svog punjača. Provjerite priručnik proizvođača za detalje. Očekuje se da će punjač automatski dodati svoj jedinstveni identifikator (ID stanice) na URL. U rijetkim slučajevima možda ćete morati ručno navesti identifikator. Primjer: `{url}`" + }, + "sponsor": { + "descriptionToken": "Sponzori mogu pronaći svoj token na {url}. Za početak nudimo {trialToken}." } + }, + "footer": { + "community": { + "powerSub1": "{activeClients} od {totalClients} učesnika" + }, + "savings": { + "co2Saved": "{value} sačuvano", + "footerLong": "{percent} solarne energije", + "footerShort": "{percent} solarno", + "moneySaved": "Sačuvano {value}", + "percentGrid": "{grid} kWh mreža", + "percentSelf": "{self} kWh solarno" + }, + "sponsor": { + "thanks": "Hvala vam, {sponsor}! Vaš doprinos pomaže daljem razvoju evcc-a." + }, + "telemetry": { + "optInMoreDetails": "Dalje detalje {0}." + } + }, + "forecast": { + "solarAdjust": "Prilagodite solarnu prognozu na osnovu stvarnih podataka o proizvodnji {percent}." + }, + "log": { + "nAreas": "{count} područja" + }, + "main": { + "chargingPlan": { + "planNumber": "Plan {number}", + "precondition": { + "description": "Punite {duration} prije polaska kako biste pripremili bateriju." + } + }, + "energyflow": { + "batteryTooltip": "{energy} od {total} ({soc})", + "loadpoints": "Punjač | Punjač | {count} punjača", + "loadpointsLimit": "{limit} ograničenje" + }, + "hemsWarning": { + "description": "Smanjiti punjenje kako ne bi prelazilo {limit}." + }, + "loadpoint": { + "remoteDisabledHard": "{source}: isključen", + "remoteDisabledSoft": "{source}: isključeno prilagodljivo solarno punjenje" + }, + "loadpointSettings": { + "minSoc": { + "description": "Vozilo se brzo puni do {0} u solarnom načinu rada. Zatim nastavlja punjenjem viškom solarne energije. Korisno za osiguranje minimalnog dometa čak i za tmurnije dane." + }, + "phasesConfigured": { + "phases_1_hint": "(od {min} do {max})", + "phases_3_hint": "(od {min} do {max})" + }, + "title": "Postavke {0}" + }, + "targetCharge": { + "co2Limit": "Ograničenje CO₂ od {co2}", + "costLimitIgnore": "Konfigurisani {limit} će biti zanemaren tokom ovog perioda.", + "descriptionEnergy": "Do kada bi {targetEnergy} trebalo biti napunjeno u vozilo?", + "descriptionSoc": "Kada bi vozilo trebalo biti napunjeno na {targetSoc}?", + "notReachableInTime": "Cilj će biti ostvaren {overrun} kasnije.", + "planPeriodValue": "{start} do {end}", + "priceLimit": "cjenovni limit od {price}", + "targetIsAboveLimit": "Konfigurisani limit punjenja od {limit} će biti zanemaren tokom ovog perioda." + }, + "targetChargePlan": { + "timeRange": "{day} {range} sati" + }, + "vehicleStatus": { + "cheapEnergyNextStart": "Jeftina energija za {duration}.", + "cleanEnergyNextStart": "Čista energija u {duration}.", + "feedinPriorityNextStart": "Visoke stope ubacivanja počinju za {duration}.", + "minCharge": "Minimalno punjenje do {soc}.", + "targetChargeActive": "Plan naplate je aktivan. Procijenjeno završetanje za {duration}.", + "targetChargePlanned": "Plan naplate počinje za {duration}." + } + }, + "sessions": { + "chartTitle": { + "avgCo2ByGroup": "⌀ CO₂ {byGroup}", + "avgPriceByGroup": "⌀ Cijena {byGroup}", + "energyGroupedByGroup": "Energia {byGroup}", + "energySubSolar": "{value} solarno", + "energySubTotal": "{value} ukupno", + "groupedCo2ByGroup": "Količina CO₂ {byGroup}", + "groupedPriceByGroup": "Ukupni trošak {byGroup}", + "historyCo2Sub": "{value} ukupno", + "historyPriceSub": "{value} ukupno", + "solarByGroup": "Solarni udio {byGroup}" + }, + "csvPeriod": "Preuzmi {period} CSV" + }, + "settings": { + "loadpoints": { + "hide": "Skrij {title}", + "show": "Prikaži {title}" + }, + "sponsorToken": { + "expires": "Vaš sponzorski token ističe za {inXDays} dana.", + "expiresUpdateUi": "{getNewToken} i ažurirajte ga ovdje.", + "expiresUpdateYaml": "{getNewToken} i ažurirajte ga u vašem evcc.yaml." + } + }, + "smartCost": { + "activeHours": "{active} od {total}", + "resetWarning": "Nije konfigurirana dinamička cijena mreže niti izvor CO₂. Međutim, i dalje postoji ograničenje od {limit}. Pročistite svoju konfiguraciju?" + }, + "smartFeedInPriority": { + "resetWarning": "Nije konfigurirana dinamička tarifa za priključenje. Međutim, i dalje postoji ograničenje od {limit}. Očistite svoju konfiguraciju?" + }, + "startupError": { + "description": "Molimo provjerite datoteku konfiguracije. Ako poruka o grešci ne pomogne, pogledajte {0}.", + "lineError": "Greška u {0}.", + "lineErrorLink": "linija {0}" } } diff --git a/i18n/fi.json b/i18n/fi.json index 783d99fbd..290e4b0a9 100644 --- a/i18n/fi.json +++ b/i18n/fi.json @@ -99,7 +99,7 @@ }, "control": { "description": "Yleensä oletusarvot ovat sopivat. Muuta vain jos tiedät mitä olet tekemässä.", - "descriptionInterval": "Päivitysjakso sekunneissa. Määrittää, kuinka usein evcc lukee mittaritietoja ja säätää lataustehoa. Oletus 30 s on turvallinen valinta. Ajoneuvot, latauslaitteet ja inverterit tyypillisesti tarvitsevat useita sekunteja toimintansa säätämiseen. Jos laitteesi reagoivat nopeasti, voit määrittää lyhyemmän aikajakson. Suosittelemme painokkaasti, että alle 10 s jaksoa ei määritettäisi. Jos kohtaat virheellistä toimitaa tai hyppiviä tehoarvoja suosittelemme pidemtämään aikaväliä.", + "descriptionInterval": "Päivitysjakso sekunneissa. Määrittää, kuinka usein evcc lukee mittaritietoja ja säätää lataustehoa. Oletus 30 s on turvallinen asetus. Ajoneuvot, latauslaitteet ja invertterit tyypillisesti tarvitsevat useita sekunteja toimintansa säätämiseen. Jos laitteesi reagoivat nopeasti, voit määrittää lyhyemmän aikajakson. Suosittelemme voimakkaasti, että alle 10 s jaksoa ei määritettäisi. Jos kohtaat virheellistä toimitaa tai hyppiviä tehoarvoja, suosittelemme pidentämään aikaväliä.", "descriptionResidualPower": "Siirtää ohjaussilmukan toimintapistettä. Mikäli sinulla on kotiakku on suositeltavaa asettaa ohjausarvo 100 W:iin. Tämä antaa akulle pienen etusijan suhteessa sähköverkon käyttöön.", "labelInterval": "Päivitystiheys", "labelResidualPower": "Jäännösteho", @@ -243,7 +243,7 @@ "limitpower": "Raja (kW)", "type": "Tyyppi" }, - "description": "Ulkoisten järjestelmien (esim. §14a EnWG, §9 EEG-rajapinta tai ylemmän tason energianhallintajärjestelmä) suorittama tehonrajoitus. Toimii yhdessä kuormituksenhallintaominaisuuden kanssa.", + "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.", "downloadCsv": "Lataa CSV-taulukko", "eventsRecorded": "Tallennettu {count} verkon rajoitustapahtumaa.", "lastEvent": "Viimeisin {timeAgo}.", @@ -389,7 +389,7 @@ "title": "Määritykset", "unconfigured": "ei määritetty", "vehicles": "Minun ajoneuvot", - "welcomeBannerText": "Aloita luomalla vähintään yksi **laturi**, **lämmitin**, **verkko**, **aurinkopaneeli**, **akku** tai **lisämittari**. Jos haluat vain testata, valitse **esittelylaite**.", + "welcomeBannerText": "Aloita luomalla vähintään yksi **laturi**, **lämmitin**, **sähköverkko**, **aurinkopaneeli**, **akku** tai **lisämittari**. Jos haluat vain testata, valitse **esittelylaite**.", "welcomeBannerTitle": "Määritetään järjestelmäsi!", "yaml": "evcc.yaml-asetuksissa määritettyjä laitteita ei voi muokata." }, @@ -894,7 +894,7 @@ "none": "ei mitään", "optimization": { "cheapest": "halvin", - "continuous": "jatkuva", + "continuous": "yhtenäinen", "label": "Optimointi" }, "planNumber": "Suunnitelma {number}", @@ -908,8 +908,8 @@ "repeating": "toistetaan", "repeatingPlans": "Toistuvat suunnitelmat", "selectAll": "Valitse kaikki", - "strategyDisabledDescription": "Lataus aloitetaan mahdollisimman myöhään, jotta se valmistuu juuri ennen lähtöä. Dynaamisten verkkohintojen tai CO₂-tariffin ansiosta tässä on enemmän vaihtoehtoja.", - "strategySettings": "Strategia-asetukset", + "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", "titleMinSoc": "Minimi lataus", diff --git a/i18n/sv.json b/i18n/sv.json index 9aeb9fe61..ecb7366fb 100644 --- a/i18n/sv.json +++ b/i18n/sv.json @@ -243,7 +243,7 @@ "limitpower": "Begränsning (kW)", "type": "Typ" }, - "description": "Effektbegränsning genom externa system (t.ex. §14a EnWG, §9 EEG-gränssnitt eller överordnat energihanteringssystem). Fungerar tillsammans med belastningshanteringsfunktionen.", + "description": "Effektbegränsning genom externa system (t.ex. §14a EnWG, §9 (EEG-gränssnitt eller överordnat energihanteringssystem). Fungerar tillsammans med lasthanteringsfunktionen.", "downloadCsv": "Hämta CSV", "eventsRecorded": "Registrerade {count} händelser med nätbegränsning.", "lastEvent": "Senaste {timeAgo}.", From fbaebef0e807614a8238a03d40ac0fc4c25e5f98 Mon Sep 17 00:00:00 2001 From: andig Date: Mon, 26 Jan 2026 09:49:16 +0100 Subject: [PATCH 0016/1747] MQTT: allow tls prefix (#26998) --- util/templates/defaults.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/util/templates/defaults.yaml b/util/templates/defaults.yaml index e5354eaef..7672b4c2d 100644 --- a/util/templates/defaults.yaml +++ b/util/templates/defaults.yaml @@ -687,6 +687,10 @@ presets: help: de: IP Adresse oder der Hostname des MQTT Brokers en: IP address or hostname of the MQTT broker + pattern: + regex: "^(tls://)?[^\\/\\s]+(:[0-9]{1,5})?$" # optional tls:// prefix, any char except slash/space, optional :port + examples: + ["192.168.1.100", "example.com", "server.local:8080", "tls://mqtt.example.com:8883"] - name: port default: 1883 help: From f268d46192052c793a9d089b1d77edae3c349468 Mon Sep 17 00:00:00 2001 From: andig Date: Mon, 26 Jan 2026 09:58:04 +0100 Subject: [PATCH 0017/1747] chore: fix zero validation --- util/templates/types.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/util/templates/types.go b/util/templates/types.go index 3523d4156..b811c2fd3 100644 --- a/util/templates/types.go +++ b/util/templates/types.go @@ -271,11 +271,18 @@ func (p *Param) IsDeprecated() bool { } func (p *Param) IsZero(s string) bool { + isAzDec := p.Name == "az" || p.Name == "dec" switch p.Type { case TypeInt: - return cast.ToInt64(s) == 0 && p.Name != "az" + if isAzDec { + return len(s) == 0 + } + return cast.ToInt64(s) == 0 case TypeFloat: - return cast.ToFloat64(s) == 0 && p.Name != "az" + if isAzDec { + return len(s) == 0 + } + return cast.ToFloat64(s) == 0 case TypeDuration: return cast.ToDuration(s) == 0 default: From 54fb5586f90ce1cc589c6604e54f629fc9fb782b Mon Sep 17 00:00:00 2001 From: andig Date: Mon, 26 Jan 2026 10:14:56 +0100 Subject: [PATCH 0018/1747] chore: standardize az (#27013) --- templates/definition/tariff/api-akkudoktor-de.yaml | 10 ---------- templates/definition/tariff/forecast-solar.yaml | 10 ---------- templates/definition/tariff/open-meteo.yaml | 10 ---------- util/templates/defaults.yaml | 10 ++++++++++ 4 files changed, 10 insertions(+), 30 deletions(-) diff --git a/templates/definition/tariff/api-akkudoktor-de.yaml b/templates/definition/tariff/api-akkudoktor-de.yaml index 2024ccead..61bf1dcdb 100644 --- a/templates/definition/tariff/api-akkudoktor-de.yaml +++ b/templates/definition/tariff/api-akkudoktor-de.yaml @@ -10,16 +10,6 @@ requirements: group: solar params: - preset: forecast-base - - name: az - description: - en: Azimuth - de: Azimut - help: - en: Orientation of PV modules in azimuth in degree. -180 = north, -90 = east, 0 = south, 90 = west, 180 = north - de: Ausrichtung der PV-Module im Azimut in Grad. -180 = Norden, -90 = Osten, 0 = Süden, 90 = Westen, 180 = Norden - type: int - example: 0 - required: true - name: ac description: en: AC Power [kW] diff --git a/templates/definition/tariff/forecast-solar.yaml b/templates/definition/tariff/forecast-solar.yaml index 3c9c2aabc..37dd88a96 100644 --- a/templates/definition/tariff/forecast-solar.yaml +++ b/templates/definition/tariff/forecast-solar.yaml @@ -9,16 +9,6 @@ requirements: group: solar params: - preset: forecast-base - - name: az - description: - en: Azimuth - de: Azimut - help: - en: -180 = north, -90 = east, 0 = south, 90 = west, 180 = north - de: -180 = Norden, -90 = Osten, 0 = Süden, 90 = Westen, 180 = Norden - type: int - example: 0 - required: true - name: horizon description: en: Horizon diff --git a/templates/definition/tariff/open-meteo.yaml b/templates/definition/tariff/open-meteo.yaml index 3d1fa79a1..c6ee92ba9 100644 --- a/templates/definition/tariff/open-meteo.yaml +++ b/templates/definition/tariff/open-meteo.yaml @@ -9,16 +9,6 @@ requirements: group: solar params: - preset: forecast-base - - name: az - description: - en: Azimuth - de: Azimut - help: - en: -180 = north, -90 = east, 0 = south, 90 = west, 180 = north - de: -180 = Norden, -90 = Osten, 0 = Süden, 90 = Westen, 180 = Norden - type: int - example: 0 - required: true - name: ac description: en: AC Power [kW] diff --git a/util/templates/defaults.yaml b/util/templates/defaults.yaml index 7672b4c2d..9ac5061f2 100644 --- a/util/templates/defaults.yaml +++ b/util/templates/defaults.yaml @@ -564,6 +564,16 @@ presets: required: true - name: lon required: true + - name: az + description: + en: Azimuth + de: Azimut + help: + en: Orientation of PV modules in degree. -180 = north, -90 = east, 0 = south, 90 = west, 180 = north + de: Ausrichtung der PV-Module in Grad. -180 = Norden, -90 = Osten, 0 = Süden, 90 = Westen, 180 = Norden + type: int + example: 0 + required: true - name: dec description: en: Decline From a6e5f934301256f7427e0d09cfd9701f952866c4 Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Mon, 26 Jan 2026 10:21:16 +0100 Subject: [PATCH 0019/1747] fix CNY currency format (#27014) --- assets/js/mixins/formatter.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/assets/js/mixins/formatter.ts b/assets/js/mixins/formatter.ts index 0ff3224ba..1d85f9cb6 100644 --- a/assets/js/mixins/formatter.ts +++ b/assets/js/mixins/formatter.ts @@ -20,13 +20,12 @@ const CURRENCY_SYMBOLS: Record = { }; // list of currencies where energy price should be displayed in subunits (factor 100) -const ENERGY_PRICE_IN_SUBUNIT: Record = { +const ENERGY_PRICE_IN_SUBUNIT: Partial> = { AUD: "c", // Australian cent BGN: "st", // Bulgarian stotinka BRL: "¢", // Brazilian centavo CAD: "¢", // Canadian cent CHF: "rp", // Swiss Rappen - CNY: "f", // Chinese fen EUR: "ct", // Euro cent GBP: "p", // GB pence ILS: "ag", // Israeli agora @@ -325,7 +324,7 @@ export default defineComponent({ return Intl?.DateTimeFormat?.().resolvedOptions?.().timeZone || "UTC"; }, pricePerKWhUnit(currency = CURRENCY.EUR, short = false) { - const unit = ENERGY_PRICE_IN_SUBUNIT[currency] || currency; + const unit = ENERGY_PRICE_IN_SUBUNIT[currency] || CURRENCY_SYMBOLS[currency] || currency; return `${unit}${short ? "" : "/kWh"}`; }, fmtTimeAgo(elapsed: number) { From b5b6cb414f9c685af6f9b7bdd1693429d92b673a Mon Sep 17 00:00:00 2001 From: VolkerK62 <99199865+VolkerK62@users.noreply.github.com> Date: Mon, 26 Jan 2026 13:43:42 +0100 Subject: [PATCH 0020/1747] Add Pro 4PM to Shelly device definitions (#27015) Co-authored-by: Michael Geers --- templates/definition/meter/shelly-1pm.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/templates/definition/meter/shelly-1pm.yaml b/templates/definition/meter/shelly-1pm.yaml index 956203484..0ad3448e4 100644 --- a/templates/definition/meter/shelly-1pm.yaml +++ b/templates/definition/meter/shelly-1pm.yaml @@ -7,6 +7,7 @@ products: - { brand: Shelly, description: { generic: EM } } - { brand: Shelly, description: { generic: Pro EM } } - { brand: Shelly, description: { generic: Pro 3EM (monophase device profile) } } + - { brand: Shelly, description: { generic: Pro 4PM } } - { brand: Shelly, description: { generic: Plug S } } group: switchsockets params: From 033e5031ec615677eeccf8a0b45fba3aaf243023 Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Mon, 26 Jan 2026 17:38:08 +0100 Subject: [PATCH 0021/1747] chore: temp disable triage agent --- .github/workflows/triage-agent.lock.yml | 27 +++++++++++++------------ .github/workflows/triage-agent.md | 1 + 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/.github/workflows/triage-agent.lock.yml b/.github/workflows/triage-agent.lock.yml index f945bb84a..474cb8762 100644 --- a/.github/workflows/triage-agent.lock.yml +++ b/.github/workflows/triage-agent.lock.yml @@ -13,7 +13,7 @@ # \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ # \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ # -# This file was automatically generated by gh-aw (v0.37.19). DO NOT EDIT. +# This file was automatically generated by gh-aw (v0.37.22). DO NOT EDIT. # # To update this file, edit githubnext/gh-aw/.github/workflows/issue-triage-agent.md@87fe98fa15e2bb50f41225a356bbc07318b54fcf and run: # gh aw compile @@ -47,7 +47,8 @@ jobs: activation: needs: pre_activation if: > - (needs.pre_activation.outputs.activated == 'true') && ((github.event_name != 'pull_request') || (github.event.pull_request.head.repo.id == github.repository_id)) + (needs.pre_activation.outputs.activated == 'true') && ((false) && ((github.event_name != 'pull_request') || + (github.event.pull_request.head.repo.id == github.repository_id))) runs-on: ubuntu-slim permissions: contents: read @@ -56,7 +57,7 @@ jobs: comment_repo: "" steps: - name: Setup Scripts - uses: githubnext/gh-aw/actions/setup@959bc34031479db1c6d2726d509c4696cdbed47f # v0.37.19 + uses: githubnext/gh-aw/actions/setup@afac8d00988d0af8af1eefb0d71cd98122b325b2 # v0.37.22 with: destination: /opt/gh-aw/actions - name: Check workflow file timestamps @@ -94,7 +95,7 @@ jobs: secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} steps: - name: Setup Scripts - uses: githubnext/gh-aw/actions/setup@959bc34031479db1c6d2726d509c4696cdbed47f # v0.37.19 + uses: githubnext/gh-aw/actions/setup@afac8d00988d0af8af1eefb0d71cd98122b325b2 # v0.37.22 with: destination: /opt/gh-aw/actions - name: Checkout repository @@ -133,7 +134,7 @@ jobs: env: COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - name: Install GitHub Copilot CLI - run: /opt/gh-aw/actions/install_copilot_cli.sh 0.0.389 + run: /opt/gh-aw/actions/install_copilot_cli.sh 0.0.394 - name: Install awf binary run: bash /opt/gh-aw/actions/install_awf_binary.sh v0.10.0 - name: Determine automatic lockdown mode for GitHub MCP server @@ -439,8 +440,8 @@ jobs: engine_name: "GitHub Copilot CLI", model: process.env.GH_AW_MODEL_AGENT_COPILOT || "", version: "", - agent_version: "0.0.389", - cli_version: "v0.37.19", + agent_version: "0.0.394", + cli_version: "v0.37.22", workflow_name: "Triage Agent", experimental: false, supports_tools_allowlist: true, @@ -780,7 +781,7 @@ jobs: total_count: ${{ steps.missing_tool.outputs.total_count }} steps: - name: Setup Scripts - uses: githubnext/gh-aw/actions/setup@959bc34031479db1c6d2726d509c4696cdbed47f # v0.37.19 + uses: githubnext/gh-aw/actions/setup@afac8d00988d0af8af1eefb0d71cd98122b325b2 # v0.37.22 with: destination: /opt/gh-aw/actions - name: Debug job inputs @@ -883,7 +884,7 @@ jobs: success: ${{ steps.parse_results.outputs.success }} steps: - name: Setup Scripts - uses: githubnext/gh-aw/actions/setup@959bc34031479db1c6d2726d509c4696cdbed47f # v0.37.19 + uses: githubnext/gh-aw/actions/setup@afac8d00988d0af8af1eefb0d71cd98122b325b2 # v0.37.22 with: destination: /opt/gh-aw/actions - name: Download agent artifacts @@ -967,7 +968,7 @@ jobs: env: COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - name: Install GitHub Copilot CLI - run: /opt/gh-aw/actions/install_copilot_cli.sh 0.0.389 + run: /opt/gh-aw/actions/install_copilot_cli.sh 0.0.394 - name: Execute GitHub Copilot CLI id: agentic_execution # Copilot CLI tool arguments (sorted): @@ -1015,13 +1016,13 @@ jobs: if-no-files-found: ignore pre_activation: - if: (github.event_name != 'pull_request') || (github.event.pull_request.head.repo.id == github.repository_id) + if: (false) && ((github.event_name != 'pull_request') || (github.event.pull_request.head.repo.id == github.repository_id)) runs-on: ubuntu-slim outputs: activated: ${{ steps.check_membership.outputs.is_team_member == 'true' }} steps: - name: Setup Scripts - uses: githubnext/gh-aw/actions/setup@959bc34031479db1c6d2726d509c4696cdbed47f # v0.37.19 + uses: githubnext/gh-aw/actions/setup@afac8d00988d0af8af1eefb0d71cd98122b325b2 # v0.37.22 with: destination: /opt/gh-aw/actions - name: Check team membership for workflow @@ -1060,7 +1061,7 @@ jobs: process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} steps: - name: Setup Scripts - uses: githubnext/gh-aw/actions/setup@959bc34031479db1c6d2726d509c4696cdbed47f # v0.37.19 + uses: githubnext/gh-aw/actions/setup@afac8d00988d0af8af1eefb0d71cd98122b325b2 # v0.37.22 with: destination: /opt/gh-aw/actions - name: Download agent output artifact diff --git a/.github/workflows/triage-agent.md b/.github/workflows/triage-agent.md index 99d974e0b..62dc50476 100644 --- a/.github/workflows/triage-agent.md +++ b/.github/workflows/triage-agent.md @@ -6,6 +6,7 @@ on: types: [opened, edited, reopened] pull_request: types: [opened, edited, reopened] +if: "false" permissions: contents: read issues: read From f65e370585c5065e2b2520886e754d536dfea4d8 Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Mon, 26 Jan 2026 19:29:54 +0100 Subject: [PATCH 0022/1747] fix: show battery locked only for discharge (#27025) --- .../js/components/Energyflow/Energyflow.vue | 4 +-- tests/battery-settings.spec.ts | 33 ++++++++++++++++--- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/assets/js/components/Energyflow/Energyflow.vue b/assets/js/components/Energyflow/Energyflow.vue index 7f3c445ca..d28c4a430 100644 --- a/assets/js/components/Energyflow/Energyflow.vue +++ b/assets/js/components/Energyflow/Energyflow.vue @@ -239,7 +239,6 @@ :power="batteryCharge" :powerUnit="powerUnit" :iconProps="{ - hold: batteryHold, soc: batterySoc, gridCharge: batteryGridChargeActive, }" @@ -247,6 +246,7 @@ :detailsFmt="batteryFmt" :expanded="batteryExpanded" detailsClickable + data-testid="energyflow-entry-batterycharge" @details-clicked="openBatterySettingsModal" @toggle="toggleBattery" > @@ -395,7 +395,7 @@ export default defineComponent({ return this.chargePower(this.batteryPower); }, batteryChargeLabel() { - return this.$t(`main.energyflow.battery${this.batteryHold ? "Hold" : "Charge"}`); + return this.$t("main.energyflow.batteryCharge"); }, batteryDischargeLabel() { return this.$t(`main.energyflow.battery${this.batteryHold ? "Hold" : "Discharge"}`); diff --git a/tests/battery-settings.spec.ts b/tests/battery-settings.spec.ts index dcb708ca4..be54c6582 100644 --- a/tests/battery-settings.spec.ts +++ b/tests/battery-settings.spec.ts @@ -1,12 +1,14 @@ import { test, expect } from "@playwright/test"; import { start, stop, baseUrl } from "./evcc"; import { expectModalVisible, expectModalHidden, openTopNavigation } from "./utils"; -test.use({ baseURL: baseUrl() }); -test.beforeAll(async () => { +test.use({ baseURL: baseUrl() }); +test.describe.configure({ mode: "parallel" }); + +test.beforeEach(async () => { await start("battery-settings.evcc.yaml"); }); -test.afterAll(async () => { +test.afterEach(async () => { await stop(); }); @@ -19,7 +21,7 @@ test.describe("battery settings", async () => { const modal = page.getByTestId("battery-settings-modal"); await expectModalVisible(modal); await expect(modal.getByRole("heading", { name: "Home Battery" })).toBeVisible(); - await expect(modal.getByRole("link", { name: "Grid charging 🧪" })).not.toBeVisible(); + await expect(modal.getByRole("link", { name: "Grid charging" })).toBeVisible(); await expect(modal).toContainText("Battery level: 50%"); await expect(modal).toContainText("10.0 kWh of 20.0 kWh"); }); @@ -63,4 +65,27 @@ test.describe("battery settings", async () => { await expect(modal).not.toBeVisible(); await expect(page.getByRole("button", { name: "grid charging when ≤ -10.0 ct" })).toBeVisible(); }); + + test("hold mode display", async ({ page }) => { + await page.goto("/"); + await page.getByTestId("energyflow").click(); + + const discharge = page.getByTestId("energyflow-entry-batterydischarge"); + const charge = page.getByTestId("energyflow-entry-batterycharge"); + + await expect(discharge).toContainText("Battery discharging"); + await expect(charge).toContainText("Battery charging"); + + // enable discharge lock + await openTopNavigation(page); + await page.getByTestId("topnavigation-battery").click(); + const modal = page.getByTestId("battery-settings-modal"); + await expectModalVisible(modal); + await modal.getByLabel("Prevent discharge in fast mode and planned charging.").check(); + await page.getByRole("button", { name: "Close" }).click(); + await expectModalHidden(modal); + + await expect(discharge).toContainText("Battery (locked)"); + await expect(charge).toContainText("Battery charging"); + }); }); From 6757a98bffb31467ee154e474a37cfe78070bb2e Mon Sep 17 00:00:00 2001 From: premultiply <4681172+premultiply@users.noreply.github.com> Date: Mon, 26 Jan 2026 22:46:27 +0100 Subject: [PATCH 0023/1747] Storaxe: fix register offset (#27033) --- templates/definition/meter/storaxe.yaml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/templates/definition/meter/storaxe.yaml b/templates/definition/meter/storaxe.yaml index c69cbcd47..50a84d613 100644 --- a/templates/definition/meter/storaxe.yaml +++ b/templates/definition/meter/storaxe.yaml @@ -16,7 +16,7 @@ render: | source: modbus {{- include "modbus" . | indent 2 }} register: - address: 2 # RealPower + address: 1 # RealPower type: input decode: int16 scale: 100 @@ -24,28 +24,28 @@ render: | source: modbus {{- include "modbus" . | indent 2 }} register: - address: 115 # EnergyExportedAC + address: 114 # EnergyExportedAC type: input decode: uint32 voltages: - source: modbus {{- include "modbus" . | indent 4 }} register: - address: 7 # ACVoltageL1 + address: 6 # ACVoltageL1 type: input decode: int16 scale: 0.1 - source: modbus {{- include "modbus" . | indent 4 }} register: - address: 8 # ACVoltageL2 + address: 7 # ACVoltageL2 type: input decode: int16 scale: 0.1 - source: modbus {{- include "modbus" . | indent 4 }} register: - address: 9 # ACVoltageL3 + address: 8 # ACVoltageL3 type: input decode: int16 scale: 0.1 @@ -53,21 +53,21 @@ render: | - source: modbus {{- include "modbus" . | indent 4 }} register: - address: 10 # ACCurrentL1 + address: 9 # ACCurrentL1 type: input decode: int16 scale: 0.1 - source: modbus {{- include "modbus" . | indent 4 }} register: - address: 11 # ACCurrentL2 + address: 10 # ACCurrentL2 type: input decode: int16 scale: 0.1 - source: modbus {{- include "modbus" . | indent 4 }} register: - address: 12 # ACCurrentL3 + address: 11 # ACCurrentL3 type: input decode: int16 scale: 0.1 @@ -75,7 +75,7 @@ render: | source: modbus {{- include "modbus" . | indent 2 }} register: - address: 119 # SoCestim + address: 125 # SXSSOC type: input decode: int16 {{- include "battery-params" . }} From 143e23778e6bebd1e0678b0ef8d1149f76c3e33f Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Tue, 27 Jan 2026 08:48:13 +0100 Subject: [PATCH 0024/1747] Translations update from Hosted Weblate (#27023) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Kevin Robatel Co-authored-by: André Westphal Co-authored-by: Ikko Eltociear Ashimine Co-authored-by: Michael Geers --- assets/js/i18n.ts | 2 ++ i18n/bs.json | 28 ++++++++++++++++++++++++++++ i18n/fr.json | 4 ++-- i18n/ja.json | 1 + 4 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 i18n/ja.json diff --git a/assets/js/i18n.ts b/assets/js/i18n.ts index 635ba23a3..7a9d20c66 100644 --- a/assets/js/i18n.ts +++ b/assets/js/i18n.ts @@ -8,6 +8,7 @@ import settings from "./settings"; export const LOCALES = { ar: ["Arabic", "العربية"], bg: ["Bulgarian", "Български"], + bs: ["Bosnian", "Bosanski"], ca: ["Catalan", "Català"], cs: ["Czech", "Česky"], da: ["Danish", "Dansk"], @@ -21,6 +22,7 @@ export const LOCALES = { hr: ["Croatian", "Hrvatski"], hu: ["Hungarian", "Magyar"], it: ["Italian", "Italiano"], + ja: ["Japanese", "日本語"], lb: ["Luxembourgish", "Lëtzebuergesch"], lt: ["Lithuanian", "Lietuvių"], nl: ["Dutch", "Nederlands"], diff --git a/i18n/bs.json b/i18n/bs.json index 60ef79d36..2f99fc17d 100644 --- a/i18n/bs.json +++ b/i18n/bs.json @@ -148,6 +148,7 @@ "soc": "serija", "solarForecast": "Solarna prognoza", "temp": "Temperatura", + "topic": "Tema", "url": "URL", "vehicleLimitSoc": "Ograničenje naplate", "yes": "da" @@ -170,8 +171,29 @@ "editor": { "loading": "Učitavanje YAML uređivača…" }, + "eebus": { + "description": "Konfiguracija koja omogućava EVCC-u da komunicira s drugim EEBus uređajima.", + "title": "EEBus" + }, + "experimental": { + "description": "Omogućite korisnički interfejs za funkcije koje se još uvijek testiraju i mogu se promijeniti u budućim verzijama.", + "title": "Eksperimentalni" + }, + "ext": { + "description": "Bilježi energetske vrijednosti nekontroliranih potrošača (npr. hladnjak, perilica rublja itd.) u statističke svrhe. Ovi brojila se također mogu koristiti za upravljanje opterećenjem (npr. podrazdioba).", + "titleAdd": "Dodaj potrošački brojilac", + "titleEdit": "Uredi potrošački brojilac" + }, + "form": { + "danger": "Opasnost", + "deprecated": "zastarjelo", + "example": "Primjer", + "optional": "neobavezno" + }, "general": { + "applyAndClose": "Primijeni i zatvori", "authPerform": "Poveži se sa {provider}", + "authPerformHint": "Otvorit će se u novoj kartici. Vratite se ovdje da nastavite.", "authPrepare": "Pripremite vezu", "cancel": "Otkaži", "clear": "Jasno", @@ -314,11 +336,17 @@ "pollModeLabel": "Ažurirajte ponašanje", "priorityHelp": "Oni s višim prioritetom dobivaju prednost pri pristupu višku solarne energije.", "priorityLabel": "Prioritet", + "save": "Sačuvaj", + "showAllSettings": "Prikaži sve postavke", + "solarBehaviorCustomHelp": "Definirajte vlastite pragove i odgode za omogućavanje i onemogućavanje.", "solarBehaviorDefaultHelp": "Počnite nakon {enableDelay} kada je višak dovoljan. Zaustavite kada viška nije dovoljno za {disableDelay}.", "thresholdDisableHelpPositive": "Zaustavi kada se iz mreže potroši više od {power} za {delay}.", "thresholdDisableHelpZero": "Zaustavite kada se za {delay} ne može zadovoljiti minimalna potrebna snaga.", "thresholdEnableHelpZero": "­ Počnite kada se minimalna potrebna snaga može zadovoljiti za {delay}." }, + "meter": { + "save": "Sačuvaj" + }, "modbusproxy": { "connection": "Povezanje #{number}" }, diff --git a/i18n/fr.json b/i18n/fr.json index e5c4df7ad..1e52d016b 100644 --- a/i18n/fr.json +++ b/i18n/fr.json @@ -525,8 +525,8 @@ "standby": "Standby" }, "schema": { - "http": "HTTP (non crypté)", - "https": "HTTP (crypté)" + "http": "HTTP (non chiffré)", + "https": "HTTPS (chiffré)" }, "status": { "A": "A (non connecté)", diff --git a/i18n/ja.json b/i18n/ja.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/i18n/ja.json @@ -0,0 +1 @@ +{} From fed9915981137b6805102ff7caf4fe5a67292515 Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Tue, 27 Jan 2026 17:27:05 +0100 Subject: [PATCH 0025/1747] UI: improve CHF price format (#27054) --- assets/js/mixins/formatter.test.ts | 20 +++++++++++++++++--- assets/js/mixins/formatter.ts | 11 ++++++++--- tests/config-tariffs.spec.ts | 2 +- tests/statistics.spec.ts | 6 +++--- 4 files changed, 29 insertions(+), 10 deletions(-) diff --git a/assets/js/mixins/formatter.test.ts b/assets/js/mixins/formatter.test.ts index 37e9c39a1..107293805 100644 --- a/assets/js/mixins/formatter.test.ts +++ b/assets/js/mixins/formatter.test.ts @@ -7,7 +7,7 @@ import { CURRENCY } from "@/types/evcc"; const is12hSpy = vi.spyOn(units, "is12hFormat").mockReturnValue(false); -config.global.mocks["$i18n"] = { locale: "de-DE" }; +config.global.mocks["$i18n"] = { locale: "de" }; config.global.mocks["$t"] = (a: string) => a; const fmt = mount( @@ -100,7 +100,7 @@ describe("fmtPricePerKWh", () => { expect(fmt.fmtPricePerKWh(0.234, CURRENCY.USD)).eq("23,4 ¢/kWh"); expect(fmt.fmtPricePerKWh(1234, CURRENCY.SEK)).eq("123.400,0 öre/kWh"); expect(fmt.fmtPricePerKWh(0.2, CURRENCY.EUR, false, false)).eq("20,0"); - expect(fmt.fmtPricePerKWh(0.123, CURRENCY.CHF)).eq("12,3 rp/kWh"); + expect(fmt.fmtPricePerKWh(0.123, CURRENCY.CHF)).eq("12,3 Rp./kWh"); }); }); @@ -111,7 +111,21 @@ describe("pricePerKWhUnit", () => { expect(fmt.pricePerKWhUnit(CURRENCY.USD)).eq("¢/kWh"); expect(fmt.pricePerKWhUnit(CURRENCY.SEK)).eq("öre/kWh"); expect(fmt.pricePerKWhUnit(CURRENCY.SEK, true)).eq("öre"); - expect(fmt.pricePerKWhUnit(CURRENCY.CHF)).eq("rp/kWh"); + expect(fmt.pricePerKWhUnit(CURRENCY.CHF)).eq("Rp./kWh"); + }); +}); + +describe("energyPriceSubunit", () => { + test("should return subunit or undefined", () => { + expect(fmt.energyPriceSubunit(CURRENCY.EUR)).eq("ct"); + expect(fmt.energyPriceSubunit(CURRENCY.CNY)).eq(undefined); + }); + test("should handle CHF locale-specific logic", () => { + config.global.mocks["$i18n"].locale = "de"; + expect(fmt.energyPriceSubunit(CURRENCY.CHF)).eq("Rp."); + config.global.mocks["$i18n"].locale = "fr"; + expect(fmt.energyPriceSubunit(CURRENCY.CHF)).eq("ct."); + config.global.mocks["$i18n"].locale = "de"; }); }); diff --git a/assets/js/mixins/formatter.ts b/assets/js/mixins/formatter.ts index 1d85f9cb6..ff1933df1 100644 --- a/assets/js/mixins/formatter.ts +++ b/assets/js/mixins/formatter.ts @@ -25,7 +25,6 @@ const ENERGY_PRICE_IN_SUBUNIT: Partial> = { BGN: "st", // Bulgarian stotinka BRL: "¢", // Brazilian centavo CAD: "¢", // Canadian cent - CHF: "rp", // Swiss Rappen EUR: "ct", // Euro cent GBP: "p", // GB pence ILS: "ag", // Israeli agora @@ -52,6 +51,12 @@ export default defineComponent({ }; }, methods: { + energyPriceSubunit(currency: CURRENCY): string | undefined { + if (currency === CURRENCY.CHF) { + return this.$i18n?.locale === "de" ? "Rp." : "ct."; + } + return ENERGY_PRICE_IN_SUBUNIT[currency]; + }, round(num: number, precision: number) { const base = 10 ** precision; return (Math.round(num * base) / base).toFixed(precision); @@ -305,7 +310,7 @@ export default defineComponent({ let value = amout; let minimumFractionDigits = 1; let maximumFractionDigits = 3; - if (ENERGY_PRICE_IN_SUBUNIT[currency]) { + if (this.energyPriceSubunit(currency)) { value *= 100; minimumFractionDigits = 1; maximumFractionDigits = 1; @@ -324,7 +329,7 @@ export default defineComponent({ return Intl?.DateTimeFormat?.().resolvedOptions?.().timeZone || "UTC"; }, pricePerKWhUnit(currency = CURRENCY.EUR, short = false) { - const unit = ENERGY_PRICE_IN_SUBUNIT[currency] || CURRENCY_SYMBOLS[currency] || currency; + const unit = this.energyPriceSubunit(currency) || CURRENCY_SYMBOLS[currency] || currency; return `${unit}${short ? "" : "/kWh"}`; }, fmtTimeAgo(elapsed: number) { diff --git a/tests/config-tariffs.spec.ts b/tests/config-tariffs.spec.ts index d87d44113..245f5a541 100644 --- a/tests/config-tariffs.spec.ts +++ b/tests/config-tariffs.spec.ts @@ -68,7 +68,7 @@ grid: await expect(restartButton).not.toBeVisible(); await expect(page.getByTestId("tariffs")).toContainText( - ["Currency", "CHF", "Grid price", "12.3 rp"].join("") + ["Currency", "CHF", "Grid price", "12.3 ct."].join("") ); }); diff --git a/tests/statistics.spec.ts b/tests/statistics.spec.ts index 115bf078a..51ae23b08 100644 --- a/tests/statistics.spec.ts +++ b/tests/statistics.spec.ts @@ -54,7 +54,7 @@ test.describe("statistics values", async () => { await expect(page.getByTestId("savings-tile-solar")).toContainText("30 kWh solar"); await expect(page.getByTestId("savings-tile-solar")).toContainText("20 kWh grid"); - await expect(page.getByTestId("savings-tile-price")).toContainText("18.0rp/kWh"); + await expect(page.getByTestId("savings-tile-price")).toContainText("18.0ct./kWh"); await expect(page.getByTestId("savings-tile-price")).toContainText("6 Fr. saved"); await expect(page.getByTestId("savings-tile-co2")).toContainText("8g/kWh"); @@ -74,7 +74,7 @@ test.describe("statistics values", async () => { await expect(page.getByTestId("savings-tile-solar")).toContainText("30 kWh solar"); await expect(page.getByTestId("savings-tile-solar")).toContainText("70 kWh grid"); - await expect(page.getByTestId("savings-tile-price")).toContainText("24.0rp/kWh"); + await expect(page.getByTestId("savings-tile-price")).toContainText("24.0ct./kWh"); await expect(page.getByTestId("savings-tile-price")).toContainText("6 Fr. saved"); await expect(page.getByTestId("savings-tile-co2")).toContainText("14g/kWh"); @@ -87,7 +87,7 @@ test.describe("statistics values", async () => { await expect(page.getByTestId("savings-modal")).toBeVisible(); await expect(page.getByTestId("savings-reference")).toContainText("Reference data:"); - await expect(page.getByTestId("savings-reference")).toContainText("30.0 rp/kWh (grid)"); + await expect(page.getByTestId("savings-reference")).toContainText("30.0 ct./kWh (grid)"); await expect(page.getByTestId("savings-reference")).toContainText("⌀ 344 g/kWh"); await expect(page.getByTestId("savings-reference")).toContainText("Germany"); From f5d7453ffdc9c6e16f6b3eaa2cdda944f22fe707 Mon Sep 17 00:00:00 2001 From: Urs Wolfer Date: Tue, 27 Jan 2026 21:43:48 +0100 Subject: [PATCH 0026/1747] EKZ: add VAT (#27058) --- templates/definition/tariff/ekz.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/templates/definition/tariff/ekz.yaml b/templates/definition/tariff/ekz.yaml index e4ba55be4..a65ba35a5 100644 --- a/templates/definition/tariff/ekz.yaml +++ b/templates/definition/tariff/ekz.yaml @@ -13,6 +13,7 @@ requirements: group: price countries: ["CH"] params: + - preset: tariff-base - name: tariff_name description: generic: Desired tariff (400D for dynamic). @@ -21,6 +22,8 @@ params: choice: ["400D", "400F", "400ST", "400WP", "400L", "400LS", "16L", "16LS"] required: true default: 400D + - name: tax + default: 0.081 - name: interval default: 1h advanced: true From 0137f2b78daa0be7d80da1ed2bdebc8bc69dbbf8 Mon Sep 17 00:00:00 2001 From: andig Date: Tue, 27 Jan 2026 21:48:25 +0100 Subject: [PATCH 0027/1747] chore: minor --- templates/definition/tariff/enever.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/definition/tariff/enever.yaml b/templates/definition/tariff/enever.yaml index 3133ba6d3..880900eb8 100644 --- a/templates/definition/tariff/enever.yaml +++ b/templates/definition/tariff/enever.yaml @@ -76,7 +76,7 @@ render: | uri: https://enever.nl/apiv3/stroomprijs_morgen.php?token={{ .token }} jq: | [ .data.[] | - { + { "start": .datum, "end": ( (.datum[0:19]) as $dt | From 8fc2b592cbc0d9664e5566e1956cf22d08471bf5 Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Wed, 28 Jan 2026 00:12:41 +0100 Subject: [PATCH 0028/1747] Translations update from Hosted Weblate (#27037) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Kevin Robatel Co-authored-by: André Westphal Co-authored-by: Ikko Eltociear Ashimine --- i18n/ja.json | 253 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 252 insertions(+), 1 deletion(-) diff --git a/i18n/ja.json b/i18n/ja.json index 0967ef424..dd60c2139 100644 --- a/i18n/ja.json +++ b/i18n/ja.json @@ -1 +1,252 @@ -{} +{ + "authProviders": { + "authCode": "認証コード", + "authCodeHelp": "このコードをコピーして次のステップで使用してください。有効期間は{duration}です。", + "authorizationFailed": "認証失敗", + "authorizationRequired": "認証が必要", + "authorizationSuccessful": "認証成功", + "buttonConnect": "{provider}に接続", + "buttonDisconnect": "切断", + "confirmLogout": "{title}を切断してもよろしいですか?", + "connect": "接続", + "disconnect": "切断", + "loggedOut": "ログアウト成功", + "logoutFailed": "ログアウト失敗", + "successCloseModal": "このダイアログを閉じることができます。", + "successCloseTab": "このタブを閉じることができます。", + "title": "認証ステータス" + }, + "batterySettings": { + "batteryLevel": "バッテリー残量", + "bufferStart": { + "never": "十分な余剰がある場合のみ。" + }, + "control": "バッテリーコントロール", + "disclaimerHint": "注意:", + "gridChargeTab": "グリッド充電", + "legendTopAutostart": "自動的に開始", + "modalTitle": "ホームバッテリー", + "usageTab": "バッテリー使用量" + }, + "config": { + "battery": { + "titleAdd": "バッテリーを追加", + "titleEdit": "バッテリーを編集" + }, + "charger": { + "chargers": "EV充電器", + "ocppConnected": "接続しました!", + "ocppLabel": "OCPPサーバーURL", + "ocppNextStep": "次のステップ", + "titleAdd": { + "charging": "充電器を追加", + "heating": "ヒーターを追加" + }, + "titleEdit": { + "charging": "充電器を編集", + "heating": "ヒーターを編集" + } + }, + "deviceValue": { + "chargeStatus": "ステータス", + "chargeStatusA": "未接続", + "chargeStatusB": "接続", + "chargeStatusF": "エラー", + "detected": "検出", + "gridPrice": "グリッド価格", + "url": "URL", + "yes": "はい" + }, + "deviceValueChargeStatus": { + "A": "A(未接続)", + "B": "B(接続済み)", + "C": "C(充電中)" + }, + "devices": { + "solarSystem": "ソーラーシステム" + }, + "experimental": { + "title": "実験的" + }, + "form": { + "danger": "危険", + "deprecated": "非推奨", + "example": "例" + }, + "general": { + "cancel": "キャンセル", + "clear": "クリア", + "close": "閉じる", + "copied": "コピーしました!", + "copy": "コピー", + "delete": "削除", + "hideAdvancedSettings": "詳細設定を隠す", + "noFileSelected": "ファイルが選択されていません。", + "off": "オフ", + "on": "オン", + "password": "パスワード", + "reset": "リセット", + "save": "保存", + "saved": "保存しました。", + "saving": "保存中…", + "selectFile": "ブラウズ", + "showAdvancedSettings": "詳細設定を見る", + "telemetry": "テレメトリ", + "templateLoading": "ロード中...", + "title": "タイトル" + }, + "grid": { + "title": "グリッドメーター", + "titleAdd": "グリッドメーターを追加", + "titleEdit": "グリッドメーターを編集" + }, + "hems": { + "csv": { + "created": "作成済み", + "type": "タイプ" + }, + "downloadCsv": "CSVをダウンロード" + }, + "icon": { + "label": "アイコン" + }, + "influx": { + "labelDatabase": "データベース", + "labelPassword": "パスワード", + "labelToken": "APIトークン", + "labelUrl": "URL", + "labelUser": "ユーザー名", + "title": "InfluxDB" + }, + "loadpoint": { + "cancel": "キャンセル", + "chargerLabel": { + "charging": "充電器", + "heating": "ヒーター" + }, + "defaultModeLabel": "デフォルトモード", + "delete": "削除", + "pollModeConnected": "接続済み", + "priorityLabel": "優先度", + "save": "保存", + "showAllSettings": "すべての設定を見る", + "solarBehaviorLabel": "ソーラー", + "solarModeCustom": "カスタム", + "thresholdEnableLabel": "グリッドパワーを有効化する", + "titleAdd": { + "charging": "充電ポイントを追加" + }, + "titleLabel": "タイトル", + "vehiclesTitle": "車両" + }, + "main": { + "addVehicle": "車両を追加", + "edit": "編集", + "name": "名前" + }, + "messaging": { + "title": "通知" + }, + "meter": { + "cancel": "キャンセル", + "delete": "削除", + "option": { + "pv": "ソーラーメーターを追加" + }, + "save": "保存", + "titleLabel": "タイトル", + "usage": { + "battery": "バッテリー", + "grid": "グリッド", + "label": "使用量" + } + }, + "modbus": { + "connectionValueTcpip": "ネットワーク", + "device": "デバイス名", + "deviceHint": "例: /dev/ttyUSB0", + "host": "IPアドレスもしくはホスト名", + "hostHint": "例: 192.0.2.2", + "port": "ポート" + }, + "modbusproxy": { + "add": "プロキシー接続を追加", + "device": "デバイス", + "option": { + "deny": "エラー", + "false": "いいえ" + }, + "readonly": { + "label": "読み取り専用" + } + }, + "mqtt": { + "labelClientId": "クライアントID", + "labelClientKey": "クライアントキー", + "labelPassword": "パスワード", + "labelTopic": "トピック", + "labelUser": "ユーザー名" + }, + "network": { + "labelExternalUrl": "外部URL", + "labelHost": "mDNSホスト名", + "labelInternalUrl": "内部URL", + "labelPort": "ポート", + "title": "ネットワーク" + }, + "ocpp": { + "status": { + "configured": "未接続", + "connected": "接続済み", + "unknown": "不明" + }, + "title": "OCPPサーバー", + "url": "サーバーURL" + }, + "options": { + "boolean": { + "no": "いいえ", + "yes": "はい" + } + }, + "pv": { + "titleAdd": "ソーラーメーターを追加", + "titleEdit": "ソーラーメーターを編集" + }, + "section": { + "grid": "グリッド", + "system": "システム", + "vehicles": "車両" + }, + "shm": { + "labelDeviceId": "デバイスID", + "labelVendorId": "ベンダーID" + }, + "system": { + "backupRestore": { + "backup": { + "title": "バックアップ" + }, + "cancel": "キャンセル", + "reset": { + "title": "リセット" + }, + "restore": { + "title": "復元" + } + }, + "logs": "ログ", + "restart": "再起動", + "restartingDescription": "お待ちください…" + }, + "title": { + "label": "タイトル", + "title": "タイトルを編集" + }, + "validation": { + "failed": "失敗", + "label": "ステータス", + "success": "成功" + } + } +} From 27e31808e3911eb41e92bd5a4076474466dcbdeb Mon Sep 17 00:00:00 2001 From: andig Date: Wed, 28 Jan 2026 17:55:22 +0100 Subject: [PATCH 0029/1747] Add delta plugin (#26897) --- plugin/delta.go | 102 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 plugin/delta.go diff --git a/plugin/delta.go b/plugin/delta.go new file mode 100644 index 000000000..d1612eb17 --- /dev/null +++ b/plugin/delta.go @@ -0,0 +1,102 @@ +package plugin + +import ( + "context" + + "github.com/evcc-io/evcc/plugin/pipeline" + "github.com/evcc-io/evcc/util" +) + +type deltaPlugin struct { + ctx context.Context + total float64 + set Config + get *Config +} + +func init() { + registry.AddCtx("delta", NewDeltaFromConfig) +} + +// NewDeltaFromConfig creates delta provider +func NewDeltaFromConfig(ctx context.Context, other map[string]any) (Plugin, error) { + var cc struct { + pipeline.Settings `mapstructure:",squash"` + Set Config + Get *Config + } + + if err := util.DecodeOther(other, &cc); err != nil { + return nil, err + } + + p := &deltaPlugin{ + ctx: ctx, + set: cc.Set, + get: cc.Get, + } + + return p, nil +} + +var _ IntSetter = (*deltaPlugin)(nil) + +func (p *deltaPlugin) IntSetter(param string) (func(int64) error, error) { + set, err := p.set.IntSetter(p.ctx, param) + if err != nil { + return nil, err + } + + get, err := p.get.IntGetter(p.ctx) + if err != nil { + return nil, err + } + + return func(val int64) error { + if get != nil { + total, err := get() + if err != nil { + return err + } + p.total = float64(total) + } + + delta := float64(val) - p.total + err := set(int64(delta)) + if err == nil { + p.total = float64(val) + } + return err + }, err +} + +var _ FloatSetter = (*deltaPlugin)(nil) + +func (p *deltaPlugin) FloatSetter(param string) (func(float64) error, error) { + set, err := p.set.FloatSetter(p.ctx, param) + if err != nil { + return nil, err + } + + get, err := p.get.FloatGetter(p.ctx) + if err != nil { + return nil, err + } + + return func(val float64) error { + if get != nil { + total, err := get() + if err != nil { + return err + } + p.total = total + } + + delta := val - p.total + err := set(delta) + if err == nil { + p.total = val + } + return err + }, err +} From f8d09800a5eb91b6621471c73f29bb8a921df33a Mon Sep 17 00:00:00 2001 From: dreadnought Date: Wed, 28 Jan 2026 18:09:03 +0100 Subject: [PATCH 0030/1747] myStrom: add token support (#27047) --- charger/mystrom.go | 3 ++- meter/mystrom.go | 5 +++-- meter/mystrom/mystrom.go | 19 +++++++++++++------ templates/definition/charger/mystrom.yaml | 5 +++++ templates/definition/meter/mystrom.yaml | 5 +++++ 5 files changed, 28 insertions(+), 9 deletions(-) diff --git a/charger/mystrom.go b/charger/mystrom.go index e21d8d04b..760a160b4 100644 --- a/charger/mystrom.go +++ b/charger/mystrom.go @@ -28,6 +28,7 @@ func NewMyStromFromConfig(other map[string]any) (api.Charger, error) { cc := struct { embed `mapstructure:",squash"` URI string + Token string StandbyPower float64 Cache time.Duration }{ @@ -39,7 +40,7 @@ func NewMyStromFromConfig(other map[string]any) (api.Charger, error) { } c := &MyStrom{ - conn: mystrom.NewConnection(cc.URI), + conn: mystrom.NewConnection(cc.URI, cc.Token), } c.switchSocket = NewSwitchSocket(&cc.embed, c.Enabled, c.conn.CurrentPower, cc.StandbyPower) diff --git a/meter/mystrom.go b/meter/mystrom.go index 293ff3808..2823aea03 100644 --- a/meter/mystrom.go +++ b/meter/mystrom.go @@ -16,12 +16,13 @@ func init() { // NewMyStromFromConfig creates a myStrom meter from generic config func NewMyStromFromConfig(other map[string]any) (api.Meter, error) { var cc struct { - URI string + URI string + Token string } if err := util.DecodeOther(other, &cc); err != nil { return nil, err } - return mystrom.NewConnection(cc.URI), nil + return mystrom.NewConnection(cc.URI, cc.Token), nil } diff --git a/meter/mystrom/mystrom.go b/meter/mystrom/mystrom.go index aa8d86d66..ac57f8cb1 100644 --- a/meter/mystrom/mystrom.go +++ b/meter/mystrom/mystrom.go @@ -17,29 +17,36 @@ type Report struct { type Connection struct { *request.Helper - uri string + uri string + token string } -func NewConnection(uri string) *Connection { +func NewConnection(uri, token string) *Connection { return &Connection{ uri: util.DefaultScheme(strings.TrimSuffix(uri, "/"), "http"), + token: token, Helper: request.NewHelper(util.NewLogger("mystrom")), } } func (c *Connection) Request(path string) error { uri := fmt.Sprintf("%s/%s", c.uri, path) - req, err := request.New(http.MethodGet, uri, nil, nil) - if err == nil { - _, err = c.DoBody(req) + req, _ := request.New(http.MethodGet, uri, nil, nil) + if c.token != "" { + req.Header.Set("Token", c.token) } + _, err := c.DoBody(req) return err } func (c *Connection) Report() (Report, error) { var res Report uri := fmt.Sprintf("%s/report", c.uri) - err := c.GetJSON(uri, &res) + req, _ := request.New(http.MethodGet, uri, nil, request.AcceptJSON) + if c.token != "" { + req.Header.Set("Token", c.token) + } + err := c.DoJSON(req, &res) return res, err } diff --git a/templates/definition/charger/mystrom.yaml b/templates/definition/charger/mystrom.yaml index 946f425e7..ba4f9fa7c 100644 --- a/templates/definition/charger/mystrom.yaml +++ b/templates/definition/charger/mystrom.yaml @@ -6,8 +6,13 @@ products: group: switchsockets params: - name: host + - name: token + help: + en: API token, only needed if token is set on device (Advanced -> Enable REST API -> Token) + de: API-Token, nur erforderlich wenn ein Token auf Gerät dem gesetzt ist (Experte -> REST API aktivieren -> Token) - preset: switchsocket render: | type: mystrom uri: http://{{ .host }} + token: {{ .token }} {{ include "switchsocket" . }} diff --git a/templates/definition/meter/mystrom.yaml b/templates/definition/meter/mystrom.yaml index 3b3dc1369..279252cdd 100644 --- a/templates/definition/meter/mystrom.yaml +++ b/templates/definition/meter/mystrom.yaml @@ -8,6 +8,11 @@ params: - name: usage choice: ["pv", "charge"] - name: host + - name: token + help: + en: API token, only needed if token is set on device (Advanced -> Enable REST API -> Token) + de: API-Token, nur erforderlich wenn ein Token auf Gerät dem gesetzt ist (Experte -> REST API aktivieren -> Token) render: | type: mystrom uri: http://{{ .host }} + token: {{ .token }} From a7b965f9d460106597f87507cad21488aad23a09 Mon Sep 17 00:00:00 2001 From: andig Date: Wed, 28 Jan 2026 18:22:30 +0100 Subject: [PATCH 0031/1747] chore: fix mqtt losing significant digits --- server/mqtt.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/mqtt.go b/server/mqtt.go index b33bcb43a..d383d3ebb 100644 --- a/server/mqtt.go +++ b/server/mqtt.go @@ -62,7 +62,7 @@ func (m *MQTT) encode(v any) string { case string: return val case float64: - return fmt.Sprintf("%.5g", val) + return fmt.Sprintf("%.3f", val) case time.Time: if val.IsZero() { return "" From 5ad9c7ea60b051bf07bbd627779f35d9a96ccefb Mon Sep 17 00:00:00 2001 From: andig Date: Thu, 29 Jan 2026 09:13:38 +0100 Subject: [PATCH 0032/1747] chore: fix test --- server/mqtt_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/server/mqtt_test.go b/server/mqtt_test.go index 4e1822cbe..9f60f99fb 100644 --- a/server/mqtt_test.go +++ b/server/mqtt_test.go @@ -109,17 +109,17 @@ func (suite *mqttSuite) TestGrid() { suite.publish("test", false, measurement{}) suite.Equal(topics, suite.topics, "topics") - suite.Equal([]string{"0", "", "", "", ""}, suite.payloads, "payloads") + suite.Equal([]string{"0.000", "", "", "", ""}, suite.payloads, "payloads") suite.publish("test", false, measurement{Energy: 1}) suite.Equal(topics, suite.topics, "topics") - suite.Equal([]string{"0", "1", "", "", ""}, suite.payloads, "payloads") + suite.Equal([]string{"0.000", "1.000", "", "", ""}, suite.payloads, "payloads") suite.publish("test", false, measurement{Controllable: lo.ToPtr(false)}) suite.Equal(topics, suite.topics, "topics") - suite.Equal([]string{"0", "", "", "", "false"}, suite.payloads, "payloads") + suite.Equal([]string{"0.000", "", "", "", "false"}, suite.payloads, "payloads") suite.publish("test", false, measurement{Currents: []float64{1, 2, 3}}) suite.Equal(append(topics, "test/currents/1", "test/currents/2", "test/currents/3"), suite.topics, "topics") - suite.Equal([]string{"0", "", "3", "", "", "1", "2", "3"}, suite.payloads, "payloads") + suite.Equal([]string{"0.000", "", "3", "", "", "1.000", "2.000", "3.000"}, suite.payloads, "payloads") } From 136682cdd220c60a65f20d0a7f6bdc9afaa0ab32 Mon Sep 17 00:00:00 2001 From: Robert <159109590+ganzton@users.noreply.github.com> Date: Thu, 29 Jan 2026 09:30:32 +0100 Subject: [PATCH 0033/1747] Ochsner: fix setting power (#27082) --- .../definition/charger/ochsner-bwwp.yaml | 60 +++++++++---------- 1 file changed, 27 insertions(+), 33 deletions(-) diff --git a/templates/definition/charger/ochsner-bwwp.yaml b/templates/definition/charger/ochsner-bwwp.yaml index c85c2b011..074e70680 100644 --- a/templates/definition/charger/ochsner-bwwp.yaml +++ b/templates/definition/charger/ochsner-bwwp.yaml @@ -10,50 +10,44 @@ params: default: 502 - name: id default: 1 - # - name: tempsource - # type: choice - # choice: ["warmwater", "buffer"] - # description: - # de: Temperaturquelle (Warmwasser oder Pufferspeicher) - # en: Temperature source (warm water or buffer) render: | type: heatpump setmaxpower: - source: const - value: 0 - set: - source: modbus - uri: {{ .host }}:{{ .port }} - id: {{ .id }} - register: - address: 2201 # SUR Überschussleistung Auflösung 1 W - type: writesingle - decode: int16 - power: + source: watchdog + timeout: 600s # da WP nach 10 min ohne Meldung abschaltet + set: + source: delta # nur Differenz zu letztem Wert + get: + source: modbus + uri: {{ .host }}:{{ .port }} + id: {{ .id }} + register: + address: 2012 # totale von der WP berechnete Überschussleistung Auflösung 1 W + type: input + decode: uint16 + set: + source: modbus + uri: {{ .host }}:{{ .port }} + id: {{ .id }} + register: + address: 2201 # SUR Überschussleistung Auflösung 1 W + type: writeholding + decode: uint16 + temp: source: modbus uri: {{ .host }}:{{ .port }} id: {{ .id }} register: - address: 2010 # aktuelle Leistungsaufnahme - type: holding - decode: int16 - # {{- if .tempsource }} - # temp: - # source: modbus - # uri: {{ .host }}:{{ .port }} - # id: {{ .id }} - # register: - # address: 2200 # C1 Solltemperatur Auflösung 0,1°C - # type: holding - # decode: int16 - # scale: 0.1 + address: 2000 # angezeigte Temperatur Auflösung 0,1°C + type: input + decode: uint16 + scale: 0.1 limittemp: source: modbus uri: {{ .host }}:{{ .port }} id: {{ .id }} register: - address: 2200 # C1 Solltemperatur Auflösung 0,1°C + address: 2203 # Legionellen/Überschuss Solltemperatur Auflösung 0,1°C type: holding - decode: int16 + decode: uint16 scale: 0.1 - # {{- end }} From 5a609bb9e38ccbacee4d1eeec6066938ad2695a0 Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Thu, 29 Jan 2026 14:11:03 +0100 Subject: [PATCH 0034/1747] chore: fix heater limit tooltip (#27088) --- assets/js/components/Vehicles/Status.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/js/components/Vehicles/Status.vue b/assets/js/components/Vehicles/Status.vue index 04fa1d1c4..960cd0561 100644 --- a/assets/js/components/Vehicles/Status.vue +++ b/assets/js/components/Vehicles/Status.vue @@ -240,7 +240,7 @@ export default defineComponent({ id: "tempLimit", visible: this.heating && this.vehicleLimitSoc > 0, content: this.fmtTemperature(this.vehicleLimitSoc), - tooltipContent: t("vehicleLimit"), + tooltipContent: this.$t("main.heatingStatus.vehicleLimit"), iconComponent: TempLimitIcon, testId: "vehicle-status-limit", }, From 38d96745cc8dccace064cf9f89297088cd47827f Mon Sep 17 00:00:00 2001 From: Robert <159109590+ganzton@users.noreply.github.com> Date: Thu, 29 Jan 2026 14:11:49 +0100 Subject: [PATCH 0035/1747] chore: use int values (#27086) --- templates/definition/charger/ochsner-bwwp.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/templates/definition/charger/ochsner-bwwp.yaml b/templates/definition/charger/ochsner-bwwp.yaml index 074e70680..65dd0e766 100644 --- a/templates/definition/charger/ochsner-bwwp.yaml +++ b/templates/definition/charger/ochsner-bwwp.yaml @@ -24,7 +24,7 @@ render: | register: address: 2012 # totale von der WP berechnete Überschussleistung Auflösung 1 W type: input - decode: uint16 + decode: int16 set: source: modbus uri: {{ .host }}:{{ .port }} @@ -32,7 +32,7 @@ render: | register: address: 2201 # SUR Überschussleistung Auflösung 1 W type: writeholding - decode: uint16 + decode: int16 temp: source: modbus uri: {{ .host }}:{{ .port }} @@ -40,7 +40,7 @@ render: | register: address: 2000 # angezeigte Temperatur Auflösung 0,1°C type: input - decode: uint16 + decode: int16 scale: 0.1 limittemp: source: modbus @@ -49,5 +49,5 @@ render: | register: address: 2203 # Legionellen/Überschuss Solltemperatur Auflösung 0,1°C type: holding - decode: uint16 + decode: int16 scale: 0.1 From 4891f3a91963fb856714654bf7f8075aef0f3612 Mon Sep 17 00:00:00 2001 From: andig Date: Thu, 29 Jan 2026 14:25:13 +0100 Subject: [PATCH 0036/1747] chore: upgrade modules --- go.mod | 28 +++++++++++++++------------- go.sum | 45 ++++++++++++++++++++++----------------------- 2 files changed, 37 insertions(+), 36 deletions(-) diff --git a/go.mod b/go.mod index d5d0a0093..0359c0065 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/Masterminds/sprig/v3 v3.3.0 github.com/PuerkitoBio/goquery v1.11.0 github.com/WulfgarW/sensonet v0.0.7 - github.com/andig/evopt v0.0.0-20251030151600-82023795e6f5 + github.com/andig/evopt v0.0.0-20260119220201-238830f9254a github.com/andig/go-powerwall v0.2.1-0.20230808194509-dd70cdb6e140 github.com/andig/gosunspec v0.0.0-20240918203654-860ce51d602b github.com/andig/mbserver v0.0.0-20230310211055-1d29cbb5820e @@ -16,7 +16,7 @@ require ( github.com/aws/aws-sdk-go-v2 v1.41.1 github.com/aws/aws-sdk-go-v2/config v1.32.7 github.com/aws/aws-sdk-go-v2/credentials v1.19.7 - github.com/aws/aws-sdk-go-v2/service/cognitoidentity v1.33.17 + github.com/aws/aws-sdk-go-v2/service/cognitoidentity v1.33.18 github.com/basgys/goxml2json v1.1.0 github.com/basvdlei/gotsmart v0.0.3 github.com/benbjohnson/clock v1.3.5 @@ -41,18 +41,18 @@ require ( github.com/glebarez/sqlite v1.11.0 github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1 github.com/go-playground/validator/v10 v10.30.1 - github.com/go-telegram/bot v1.17.0 + github.com/go-telegram/bot v1.18.0 github.com/go-viper/mapstructure/v2 v2.5.0 github.com/godbus/dbus/v5 v5.2.2 github.com/gokrazy/updater v0.0.0-20250705135802-db129c40879c - github.com/golang-jwt/jwt/v5 v5.3.0 + 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 github.com/gregdel/pushover v1.4.0 - github.com/grid-x/modbus v0.0.0-20251121133955-8a6c959be366 + github.com/grid-x/modbus v0.0.0-20260122103929-e192bc287e6e github.com/hashicorp/go-version v1.8.0 github.com/hasura/go-graphql-client v0.15.1 github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83 @@ -77,7 +77,7 @@ require ( github.com/muka/go-bluetooth v0.0.0-20240701044517-04c4f09c514e github.com/mxschmitt/golang-combinations v1.2.0 github.com/nicksnyder/go-i18n/v2 v2.6.1 - github.com/olekukonko/tablewriter v1.1.2 + github.com/olekukonko/tablewriter v1.1.3 github.com/philippseith/signalr v0.8.0 github.com/prometheus-community/pro-bing v0.7.0 github.com/prometheus/client_golang v1.23.2 @@ -85,7 +85,7 @@ require ( github.com/robertkrimen/otto v0.5.1 github.com/samber/lo v1.52.0 github.com/sandrolain/httpcache v1.4.0 - github.com/sirupsen/logrus v1.9.3 + github.com/sirupsen/logrus v1.9.4 github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 github.com/smallnest/chanx v1.2.0 github.com/spali/go-rscp v0.2.2 @@ -103,7 +103,7 @@ require ( gitlab.com/bboehmke/sunny v0.16.0 go.bug.st/serial v1.6.4 go.uber.org/mock v0.6.0 - go.yaml.in/yaml/v4 v4.0.0-rc.3 + go.yaml.in/yaml/v4 v4.0.0-rc.4 golang.org/x/crypto v0.47.0 golang.org/x/crypto/x509roots/fallback v0.0.0-20260113154411-7d0074ccc6f1 golang.org/x/exp v0.0.0-20260112195511-716be5621a96 @@ -141,7 +141,7 @@ require ( github.com/breml/rootcerts v0.2.21 // indirect github.com/cenkalti/backoff/v5 v5.0.2 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/clipperhouse/displaywidth v0.6.0 // indirect + github.com/clipperhouse/displaywidth v0.6.2 // indirect github.com/clipperhouse/stringish v0.1.1 // indirect github.com/clipperhouse/uax29/v2 v2.3.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect @@ -154,7 +154,7 @@ require ( github.com/dustin/go-humanize v1.0.1 // indirect github.com/enbility/go-avahi v0.0.0-20240909195612-d5de6b280d7a // indirect github.com/enbility/zeroconf/v2 v2.0.0-20240920094356-be1cae74fda6 // indirect - github.com/fatih/color v1.17.0 // indirect + github.com/fatih/color v1.18.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/gabriel-vasile/mimetype v1.4.12 // indirect @@ -206,7 +206,7 @@ require ( github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect github.com/olekukonko/errors v1.1.0 // indirect - github.com/olekukonko/ll v0.1.3 // indirect + github.com/olekukonko/ll v0.1.4-0.20260115111900-9e59c2286df0 // indirect github.com/onsi/ginkgo/v2 v2.13.0 // indirect github.com/pascaldekloe/name v1.0.1 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect @@ -245,10 +245,10 @@ require ( gopkg.in/go-playground/validator.v9 v9.31.0 // indirect gopkg.in/sourcemap.v1 v1.0.5 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - modernc.org/libc v1.67.4 // indirect + modernc.org/libc v1.67.6 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect - modernc.org/sqlite v1.44.0 // indirect + modernc.org/sqlite v1.44.3 // indirect ) tool ( @@ -264,3 +264,5 @@ tool ( replace github.com/grid-x/modbus => github.com/evcc-io/modbus v0.0.0-20250501165638-8b6f1fbdb7ea replace github.com/lorenzodonini/ocpp-go => github.com/evcc-io/ocpp-go v0.0.0-20251212212612-b7f92ee0443b + +replace go.yaml.in/yaml/v4 => go.yaml.in/yaml/v4 v4.0.0-rc.3 diff --git a/go.sum b/go.sum index 8cbccecb0..750eff512 100644 --- a/go.sum +++ b/go.sum @@ -33,8 +33,8 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/andig/evopt v0.0.0-20251030151600-82023795e6f5 h1:jeveaNoQ2MLsP55bg6y6i5oTBz/2X1pSS7YBZ3EPTEc= -github.com/andig/evopt v0.0.0-20251030151600-82023795e6f5/go.mod h1:C05cTe1ffvaip2xNNHMJSy/OtGT/NH1sXQgmV/7be8k= +github.com/andig/evopt v0.0.0-20260119220201-238830f9254a h1:p5/javLeDBFTxKonJe4/2fpuYUoWymp+C3zfAtphO7Q= +github.com/andig/evopt v0.0.0-20260119220201-238830f9254a/go.mod h1:C05cTe1ffvaip2xNNHMJSy/OtGT/NH1sXQgmV/7be8k= github.com/andig/go-powerwall v0.2.1-0.20230808194509-dd70cdb6e140 h1:C93T8vg7CN4Q4BBDbBMOvBoTYBA+CaBEvlY2SZF8aa0= github.com/andig/go-powerwall v0.2.1-0.20230808194509-dd70cdb6e140/go.mod h1:Xk09mD+7RTCuuHMX5RxlqgEUeh9oZdIX0rL0qiY6l/4= github.com/andig/gosunspec v0.0.0-20240918203654-860ce51d602b h1:81UMfM949I7StrRay7YDUZazY8M1u/JHkzwcFEjiilQ= @@ -72,8 +72,8 @@ github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 h1:WWLqlh79iO48yLkj1v github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17/go.mod h1:EhG22vHRrvF8oXSTYStZhJc1aUgKtnJe+aOiFEV90cM= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= -github.com/aws/aws-sdk-go-v2/service/cognitoidentity v1.33.17 h1:JRYcZM6pm0SvtYJhpDm35S3KfdSVpcJFoEakr105vlo= -github.com/aws/aws-sdk-go-v2/service/cognitoidentity v1.33.17/go.mod h1:D8mmxQnWQZL6pskpLJmq/91xku1o8PfWTUCQHxe0cTs= +github.com/aws/aws-sdk-go-v2/service/cognitoidentity v1.33.18 h1:JyhYT6WNHQHgX9lIKh6FSsh/KNJGNk73wlTwAL3CZVU= +github.com/aws/aws-sdk-go-v2/service/cognitoidentity v1.33.18/go.mod h1:D8mmxQnWQZL6pskpLJmq/91xku1o8PfWTUCQHxe0cTs= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 h1:RuNSMoozM8oXlgLG/n6WLaFGoea7/CddrCfIiSA+xdY= @@ -124,8 +124,8 @@ github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4 github.com/cli/browser v1.3.0 h1:LejqCrpWr+1pRqmEPDGnTZOjsMe7sehifLynZJuqJpo= github.com/cli/browser v1.3.0/go.mod h1:HH8s+fOAxjhQoBUAsKuPCbqUuxZDhQ2/aD+SzsEfBTk= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/clipperhouse/displaywidth v0.6.0 h1:k32vueaksef9WIKCNcoqRNyKbyvkvkysNYnAWz2fN4s= -github.com/clipperhouse/displaywidth v0.6.0/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o= +github.com/clipperhouse/displaywidth v0.6.2 h1:ZDpTkFfpHOKte4RG5O/BOyf3ysnvFswpyYrV7z2uAKo= +github.com/clipperhouse/displaywidth v0.6.2/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o= github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4= @@ -206,8 +206,8 @@ github.com/evcc-io/tesla-proxy-client v0.0.0-20240221194046-4168b3759701 h1:3Jpl github.com/evcc-io/tesla-proxy-client v0.0.0-20240221194046-4168b3759701/go.mod h1:zWtAweBqXJTk3HSrPSecz3Q3a2hAUQ4vOE6paJfn03I= github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= -github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= @@ -268,8 +268,8 @@ github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= -github.com/go-telegram/bot v1.17.0 h1:Hs0kGxSj97QFqOQP0zxduY/4tSx8QDzvNI9uVRS+zmY= -github.com/go-telegram/bot v1.17.0/go.mod h1:i2TRs7fXWIeaceF3z7KzsMt/he0TwkVC680mvdTFYeM= +github.com/go-telegram/bot v1.18.0 h1:yQzv437DY42SYTPBY48RinAvwbmf1ox5QICskIYWCD8= +github.com/go-telegram/bot v1.18.0/go.mod h1:i2TRs7fXWIeaceF3z7KzsMt/he0TwkVC680mvdTFYeM= github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= @@ -291,8 +291,8 @@ github.com/gokrazy/tools v0.0.0-20250601065736-ab76ef531d30 h1:R5QurTUBl0Uss60WF github.com/gokrazy/tools v0.0.0-20250601065736-ab76ef531d30/go.mod h1:PyRdkXcyS4Gkh7zS9oPCUVCEbdBSsqSxptBcLjLAO/g= github.com/gokrazy/updater v0.0.0-20250705135802-db129c40879c h1:j4/v9FR/cOy6nog5rmXUtauBsOU3mm+rTPn5IENUbmg= github.com/gokrazy/updater v0.0.0-20250705135802-db129c40879c/go.mod h1:EtAn+BPibqnAHnYGj3FW5e284xNsiOOMOL2dJiwu7H4= -github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= -github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -565,11 +565,11 @@ github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0= github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM= github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= -github.com/olekukonko/ll v0.1.3 h1:sV2jrhQGq5B3W0nENUISCR6azIPf7UBUpVq0x/y70Fg= -github.com/olekukonko/ll v0.1.3/go.mod h1:b52bVQRRPObe+yyBl0TxNfhesL0nedD4Cht0/zx55Ew= +github.com/olekukonko/ll v0.1.4-0.20260115111900-9e59c2286df0 h1:jrYnow5+hy3WRDCBypUFvVKNSPPCdqgSXIE9eJDD8LM= +github.com/olekukonko/ll v0.1.4-0.20260115111900-9e59c2286df0/go.mod h1:b52bVQRRPObe+yyBl0TxNfhesL0nedD4Cht0/zx55Ew= github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= -github.com/olekukonko/tablewriter v1.1.2 h1:L2kI1Y5tZBct/O/TyZK1zIE9GlBj/TVs+AY5tZDCDSc= -github.com/olekukonko/tablewriter v1.1.2/go.mod h1:z7SYPugVqGVavWoA2sGsFIoOVNmEHxUAAMrhXONtfkg= +github.com/olekukonko/tablewriter v1.1.3 h1:VSHhghXxrP0JHl+0NnKid7WoEmd9/urKRJLysb70nnA= +github.com/olekukonko/tablewriter v1.1.3/go.mod h1:9VU0knjhmMkXjnMKrZ3+L2JhhtsQ/L38BbL3CRNE8tM= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= @@ -688,8 +688,8 @@ github.com/simonvetter/modbus v1.6.0/go.mod h1:hh90ZaTaPLcK2REj6/fpTbiV0J6S7GWmd github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= github.com/smallnest/chanx v1.2.0 h1:RLyldZBbQZ4O0dSvdkTMHo4+mDw20Bc1jXXTHf+ymZo= @@ -929,7 +929,6 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210426080607-c94f62235c83/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220804214406-8e32c043e418/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1081,8 +1080,8 @@ modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE= modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= -modernc.org/libc v1.67.4 h1:zZGmCMUVPORtKv95c2ReQN5VDjvkoRm9GWPTEPuvlWg= -modernc.org/libc v1.67.4/go.mod h1:QvvnnJ5P7aitu0ReNpVIEyesuhmDLQ8kaEoyMjIFZJA= +modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI= +modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= @@ -1091,8 +1090,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= -modernc.org/sqlite v1.44.0 h1:YjCKJnzZde2mLVy0cMKTSL4PxCmbIguOq9lGp8ZvGOc= -modernc.org/sqlite v1.44.0/go.mod h1:2Dq41ir5/qri7QJJJKNZcP4UF7TsX/KNeykYgPDtGhE= +modernc.org/sqlite v1.44.3 h1:+39JvV/HWMcYslAwRxHb8067w+2zowvFOUrOWIy9PjY= +modernc.org/sqlite v1.44.3/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= From 40b8e35dac741e8d8fb242b87c9ac9bedd8a78f0 Mon Sep 17 00:00:00 2001 From: andig Date: Thu, 29 Jan 2026 14:25:31 +0100 Subject: [PATCH 0037/1747] Plugins/HTTP: include uri in error --- util/request/functions.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/util/request/functions.go b/util/request/functions.go index 9486a3db7..07667bca3 100644 --- a/util/request/functions.go +++ b/util/request/functions.go @@ -51,7 +51,8 @@ func NewStatusError(resp *http.Response) *StatusError { } func (e *StatusError) Error() string { - return fmt.Sprintf("unexpected status: %d (%s)", e.resp.StatusCode, http.StatusText(e.resp.StatusCode)) + req := e.resp.Request + return fmt.Sprintf("unexpected status: %d (%s) %s %s", e.resp.StatusCode, http.StatusText(e.resp.StatusCode), req.Method, req.URL) } // Response returns the response with the unexpected error From ccd0857255629f89f54da4d312a79bffd12efb73 Mon Sep 17 00:00:00 2001 From: andig Date: Thu, 29 Jan 2026 15:05:53 +0100 Subject: [PATCH 0038/1747] Add Xtherma heatpump (#26788) --- templates/definition/charger/xtherma.yaml | 42 +++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 templates/definition/charger/xtherma.yaml diff --git a/templates/definition/charger/xtherma.yaml b/templates/definition/charger/xtherma.yaml new file mode 100644 index 000000000..9f21ea430 --- /dev/null +++ b/templates/definition/charger/xtherma.yaml @@ -0,0 +1,42 @@ +template: xtherma +products: + - brand: Xtherma + description: + generic: experimental +group: heating +# requirements: +# evcc: ["sponsorship"] +params: + - name: modbus + choice: ["tcpip"] +render: | + type: heatpump + setmaxpower: + source: modbus + {{- include "modbus" . | indent 2 }} + register: + address: 72 + type: holding + encoding: uint32 + getmaxpower: + source: modbus + {{- include "modbus" . | indent 2 }} + register: + address: 72 + type: holding + encoding: uint32 + temp: + source: modbus + {{- include "modbus" . | indent 2 }} + register: + address: 124 + type: input + encoding: int32 + scale: 10 + limittemp: + source: modbus + {{- include "modbus" . | indent 2 }} + register: + address: 51 + type: input + encoding: int32 From 1258d5d4c6f08287a144c1b579bdcaccddf28d0b Mon Sep 17 00:00:00 2001 From: spasdev Date: Fri, 30 Jan 2026 09:34:21 +0100 Subject: [PATCH 0039/1747] Add CKW dynamic tariff (#27051) --- templates/definition/tariff/ckw.yaml | 37 ++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 templates/definition/tariff/ckw.yaml diff --git a/templates/definition/tariff/ckw.yaml b/templates/definition/tariff/ckw.yaml new file mode 100644 index 000000000..230f171b3 --- /dev/null +++ b/templates/definition/tariff/ckw.yaml @@ -0,0 +1,37 @@ +template: ckw +products: + - brand: CKW + description: + de: Netz Home/Business dynamic + en: Netz Home/Business dynamic +requirements: + evcc: ["skiptest"] +group: price +countries: ["CH"] +params: + - preset: tariff-base + - name: tariff + required: true + choice: ["home_dynamic", "business_dynamic"] + description: + de: Tarifname + en: Tariff name + help: + de: home_dynamic (unter 50 MWh/Jahr) oder business_dynamic (über 50 MWh/Jahr) + en: home_dynamic (below 50 MWh/year) or business_dynamic (above 50 MWh/year) + - name: tax + default: 0.081 +render: | + type: custom + {{ include "tariff-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" }}` }} + jq: | + [.prices[] | + { + start: (.start_timestamp | sub("Z$"; ":00Z")), + end: (.end_timestamp | sub("Z$"; ":00Z")), + value: (.integrated[] | select(.unit == "CHF_kWh") | .value) + } + ] | tostring From 4d7a29de86307cd803a81f4d5766680b5450b363 Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Fri, 30 Jan 2026 11:19:56 +0100 Subject: [PATCH 0040/1747] OCPP: adjust log level for new connections (#27106) --- charger/ocpp/cs.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charger/ocpp/cs.go b/charger/ocpp/cs.go index 2131daa5c..0babda0db 100644 --- a/charger/ocpp/cs.go +++ b/charger/ocpp/cs.go @@ -208,7 +208,7 @@ func (cs *CS) NewChargePoint(chargePoint ocpp16.ChargePointConnection) { // register unknown charge point cs.regs[chargePoint.ID()] = newRegistration() - cs.log.WARN.Printf("unknown charge point connected: %s", chargePoint.ID()) + cs.log.INFO.Printf("unknown charge point connected: %s", chargePoint.ID()) cs.mu.Unlock() cs.publish() From 6c1831c9cf4789740e19e700ddaad289ce94fbb2 Mon Sep 17 00:00:00 2001 From: VolkerK62 <99199865+VolkerK62@users.noreply.github.com> Date: Fri, 30 Jan 2026 15:08:41 +0100 Subject: [PATCH 0041/1747] chore: update products (#27112) --- templates/definition/charger/daheimladen-pro.yaml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/templates/definition/charger/daheimladen-pro.yaml b/templates/definition/charger/daheimladen-pro.yaml index 14f31b8ce..4dd369dc5 100644 --- a/templates/definition/charger/daheimladen-pro.yaml +++ b/templates/definition/charger/daheimladen-pro.yaml @@ -1,12 +1,11 @@ template: daheimladen-pro products: - - brand: DaheimLaden - description: - generic: Smart/Touch Pro + - { brand: DaheimLaden, description: { generic: Smart/Touch Pro } } + - { brand: DaheimLaden, description: { generic: Business } } requirements: description: - de: Die Phasenumschaltung benötigt mindestens die Firmware-Version "M3W_3.11". Während der Umschaltung pausiert die Ladung für zwei Minuten. - en: Phase switching requires at least firmware version "M3W_3.11." Charging pauses for two minutes during the phase switching. + de: Firmware-Anforderungen= Smart/Touch Pro ab "M3W_3.11STP", Business ab "M3W_3.09PTB". Während der Phasen-Umschaltung pausiert die Ladung für zwei Minuten. + en: Firmware requirements= Smart/Touch Pro from "M3W_3.11STP", Business from "M3W_3.09PTB". Charging pauses for two minutes during phase switching. capabilities: ["1p3p", "mA"] params: - name: host From f668bb7d227de9a25ebeec8e9e9fecbd14d12ca6 Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Fri, 30 Jan 2026 15:51:29 +0100 Subject: [PATCH 0042/1747] Translations update from Hosted Weblate (#27063) --- i18n/ja.json | 233 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 230 insertions(+), 3 deletions(-) diff --git a/i18n/ja.json b/i18n/ja.json index dd60c2139..4530cf994 100644 --- a/i18n/ja.json +++ b/i18n/ja.json @@ -119,6 +119,10 @@ "title": "InfluxDB" }, "loadpoint": { + "addCharger": { + "charging": "充電器を追加", + "heating": "ヒーターを追加" + }, "cancel": "キャンセル", "chargerLabel": { "charging": "充電器", @@ -136,13 +140,23 @@ "titleAdd": { "charging": "充電ポイントを追加" }, + "titleEdit": { + "charging": "充電ポイントを編集" + }, "titleLabel": "タイトル", + "vehicleAutoDetection": "自動認識", + "vehicleInvalid": "車両が存在しません", + "vehicleLabel": "デフォルトの車両", "vehiclesTitle": "車両" }, "main": { + "addGrid": "グリッドメーターを追加", + "addLoadpoint": "充電器もしくはヒーターを追加", + "addPvBattery": "ソーラーもしくはバッテリーを追加", "addVehicle": "車両を追加", "edit": "編集", - "name": "名前" + "name": "名前", + "welcomeBannerTitle": "システムを構成しましょう!" }, "messaging": { "title": "通知" @@ -162,6 +176,7 @@ } }, "modbus": { + "baudrate": "ボーレート", "connectionValueTcpip": "ネットワーク", "device": "デバイス名", "deviceHint": "例: /dev/ttyUSB0", @@ -181,6 +196,7 @@ } }, "mqtt": { + "authentication": "認証", "labelClientId": "クライアントID", "labelClientKey": "クライアントキー", "labelPassword": "パスワード", @@ -195,6 +211,7 @@ "title": "ネットワーク" }, "ocpp": { + "connectedChargers": "接続済み充電器", "status": { "configured": "未接続", "connected": "接続済み", @@ -207,6 +224,22 @@ "boolean": { "no": "いいえ", "yes": "はい" + }, + "endianness": { + "big": "ビッグエンディアン", + "little": "リトルエンディアン" + }, + "operationMode": { + "standby": "スタンバイ" + }, + "schema": { + "http": "HTTP(非暗号化)", + "https": "HTTPS(暗号化)" + }, + "status": { + "A": "A(未接続)", + "B": "B(接続済み)", + "C": "C(充電中)" } }, "pv": { @@ -222,6 +255,13 @@ "labelDeviceId": "デバイスID", "labelVendorId": "ベンダーID" }, + "sponsor": { + "addToken": "トークンを入力", + "changeToken": "トークンを変更", + "error": "スポンサートークンが無効です。", + "invalid": "無効", + "labelToken": "スポンサートークン" + }, "system": { "backupRestore": { "backup": { @@ -229,11 +269,14 @@ }, "cancel": "キャンセル", "reset": { + "action": "リセット...", + "confirmationButton": "リセット&再起動", "title": "リセット" }, "restore": { "title": "復元" - } + }, + "title": "バックアップ&復元" }, "logs": "ログ", "restart": "再起動", @@ -246,7 +289,191 @@ "validation": { "failed": "失敗", "label": "ステータス", - "success": "成功" + "success": "成功", + "unknown": "不明", + "validate": "検証" + }, + "vehicle": { + "cancel": "キャンセル", + "chargingSettings": "充電設定", + "defaultMode": "デフォルトモード", + "delete": "削除", + "priority": "優先度", + "save": "保存", + "scooter": "スクーター", + "titleAdd": "車両を追加", + "titleEdit": "車両を編集", + "validateSave": "検証&保存" + } + }, + "footer": { + "community": { + "greenEnergy": "ソーラー", + "greenShare": "ソーラーシェア" + }, + "savings": { + "co2Title": "CO₂排出量", + "period": { + "thisYear": "今年" + } + }, + "sponsor": { + "becomeSponsor": "スポンサーになる", + "titleTrial": "トライアルモード" + }, + "telemetry": { + "optInMoreDetailsLink": "こちら" + }, + "version": { + "availableLong": "新バージョンが利用可能", + "modalCancel": "キャンセル", + "modalDownload": "ダウンロード", + "modalInstalledVersion": "インストール済みのバージョン", + "modalTitle": "新バージョンが利用可能", + "modalUpdate": "インストール", + "modalUpdateNow": "今すぐインストール", + "modalUpdateStatusStart": "インストール開始:" + } + }, + "forecast": { + "co2": { + "average": "平均" + }, + "price": { + "range": "範囲" + }, + "type": { + "price": "価格", + "solar": "ソーラー" + } + }, + "header": { + "blog": "ブログ", + "docs": "ドキュメント", + "logout": "ログアウト" + }, + "help": { + "documentationButton": "ドキュメント", + "issueButton": "問題を報告", + "logsButton": "ログを確認", + "restart": { + "cancel": "キャンセル" + }, + "restartButton": "再起動" + }, + "issue": { + "additional": { + "logs": "ログ", + "showDetails": "詳細を確認", + "source": "ソース", + "state": "状態", + "title": "追加情報" + }, + "createButtonIssue": "GitHub Issueを作成...", + "issueTitle": "タイトル", + "summary": { + "copied": "コピーしました!", + "copyButton": "追加情報をコピー" + }, + "system": "システム", + "timezone": "タイムゾーン", + "title": "問題を報告", + "version": "バージョン" + }, + "log": { + "search": "検索", + "selectAll": "全てを選択" + }, + "loginModal": { + "invalid": "パスワードが無効です。", + "login": "ログイン", + "reset": "パスワードをリセットしますか?", + "title": "認証" + }, + "main": { + "chargingPlan": { + "active": "アクティブ" + }, + "energyflow": { + "battery": "バッテリー", + "pv": "ソーラーシステム" + }, + "loadpoint": { + "fallbackName": "充電ポイント", + "price": "コスト", + "solar": "ソーラー" + }, + "loadpointSettings": { + "batteryBoost": { + "label": "バッテリーブースト" + }, + "default": "デフォルト", + "disclaimerHint": "注:", + "vehicle": "車両" + }, + "mode": { + "off": "オフ", + "pv": "ソーラー", + "smart": "スマート" + }, + "provider": { + "login": "ログイン", + "logout": "ログアウト" + }, + "targetCharge": { + "activate": "アクティベート", + "currentPlan": "アクティブな計画", + "nextPlan": "次の計画", + "planPeriodLabel": "期間", + "remove": "削除", + "today": "今日", + "tomorrow": "明日", + "update": "アップデート" + }, + "vehicle": { + "fallbackName": "車両", + "none": "車両無し", + "vehicleSoc": "充電" + }, + "vehicleStatus": { + "charging": "充電中…", + "connected": "接続済み。", + "disconnected": "切断済み。" + }, + "vehicles": "駐車場" + }, + "notifications": { + "modalTitle": "通知" + }, + "offline": { + "message": "サーバーに接続されていません。", + "restart": "再起動", + "starting": "サーバーを開始中..." + }, + "passwordModal": { + "labelCurrent": "現在のパスワード", + "labelNew": "新しいパスワード", + "noMatch": "パスワードが一致しません" + }, + "session": { + "cancel": "キャンセル", + "delete": "削除", + "meter": "メーター", + "meterstart": "メーター開始", + "meterstop": "メーター停止", + "price": "価格", + "started": "開始", + "title": "充電セッション" + }, + "sessions": { + "csv": { + "price": "価格", + "vehicle": "車両" + }, + "filter": { + "allLoadpoints": "全ての充電ポイント", + "allVehicles": "全ての車両", + "filter": "フィルター" } } } From 725af06baba0b5305373ab9b0a048feac8262338 Mon Sep 17 00:00:00 2001 From: premultiply <4681172+premultiply@users.noreply.github.com> Date: Fri, 30 Jan 2026 15:53:05 +0100 Subject: [PATCH 0043/1747] ChargeX: fix register decoding (#27110) --- charger/chargex.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/charger/chargex.go b/charger/chargex.go index a056b0af2..9906b6d1c 100644 --- a/charger/chargex.go +++ b/charger/chargex.go @@ -41,11 +41,11 @@ type ChargeX struct { const ( // Module specific base address (module X: 100 + module_index*12) chargexRegModuleBase = 100 // 0x0064 Base address for module 0 - chargexRegModulePower = 0 // PAC_X offset (U32, 2 registers) - chargexRegModuleCurrent1 = 2 // IAC_SUM_1_X offset (U32, 2 registers) - chargexRegModuleCurrent2 = 4 // IAC_SUM_2_X offset (U32, 2 registers) - chargexRegModuleCurrent3 = 6 // IAC_SUM_3_X offset (U32, 2 registers) - chargexRegModuleState = 8 // States_CP_X offset (B16, 1 register!) + chargexRegModulePower = 0 // PAC_X offset + chargexRegModuleCurrent1 = 2 // IAC_SUM_1_X offset + chargexRegModuleCurrent2 = 4 // IAC_SUM_2_X offset + chargexRegModuleCurrent3 = 6 // IAC_SUM_3_X offset + chargexRegModuleState = 8 // States_CP_X offset // Holding registers (read/write) chargexRegTargetPower = 504 // 0x01F8 PAC_Target_Power (W) - U32 @@ -96,7 +96,7 @@ func NewChargeX(ctx context.Context, uri string, id uint8, connector uint16) (ap curr: 6, // assume min current } - // Initialize charging mode to 0 (Full control) to ensure evcc has full control + // Initialize charging mode to 0 (Full control) b := make([]byte, 4) binary.BigEndian.PutUint32(b, 0) if _, err := conn.WriteMultipleRegisters(chargexRegChargingMode, 2, b); err != nil { @@ -136,12 +136,12 @@ func (wb *ChargeX) setCurrent(current float64) error { // Status implements the api.Charger interface func (wb *ChargeX) Status() (api.ChargeStatus, error) { - b, err := wb.conn.ReadHoldingRegisters(wb.moduleReg(chargexRegModuleState), 1) + b, err := wb.conn.ReadHoldingRegisters(wb.moduleReg(chargexRegModuleState), 2) if err != nil { return api.StatusNone, err } - state := binary.BigEndian.Uint16(b) + state := binary.BigEndian.Uint32(b) // Bit 0: Charging (1=charging, 0=not charging) // Bit 1: ChMode (0=single phase, 1=3 phase) @@ -167,13 +167,13 @@ var _ api.StatusReasoner = (*ChargeX)(nil) // StatusReason implements the api.StatusReasoner interface func (wb *ChargeX) StatusReason() (api.Reason, error) { - b, err := wb.conn.ReadHoldingRegisters(wb.moduleReg(chargexRegModuleState), 1) + b, err := wb.conn.ReadHoldingRegisters(wb.moduleReg(chargexRegModuleState), 2) if err != nil { return api.ReasonUnknown, err } // Check if not authorized - if (binary.BigEndian.Uint16(b) & (1 << 2)) == 0 { + if (binary.BigEndian.Uint32(b) & (1 << 2)) == 0 { return api.ReasonWaitingForAuthorization, nil } From 924c4569bec641bf9ed72f48b4b57dc2035e32ec Mon Sep 17 00:00:00 2001 From: andig Date: Sat, 31 Jan 2026 10:00:51 +0100 Subject: [PATCH 0044/1747] MQTT: trim trailing zeros --- server/mqtt.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/mqtt.go b/server/mqtt.go index d383d3ebb..df08689b4 100644 --- a/server/mqtt.go +++ b/server/mqtt.go @@ -62,7 +62,8 @@ func (m *MQTT) encode(v any) string { case string: return val case float64: - return fmt.Sprintf("%.3f", val) + // trim trailing zeros + return strings.TrimRight(strings.TrimRight(fmt.Sprintf("%.3f", val), "0"), ".") case time.Time: if val.IsZero() { return "" From 76ca1ee836c1d4275c94189f2ea48e0bf5ab9fe9 Mon Sep 17 00:00:00 2001 From: andig Date: Sat, 31 Jan 2026 10:07:51 +0100 Subject: [PATCH 0045/1747] chore: fix test --- server/mqtt_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/server/mqtt_test.go b/server/mqtt_test.go index 9f60f99fb..4e1822cbe 100644 --- a/server/mqtt_test.go +++ b/server/mqtt_test.go @@ -109,17 +109,17 @@ func (suite *mqttSuite) TestGrid() { suite.publish("test", false, measurement{}) suite.Equal(topics, suite.topics, "topics") - suite.Equal([]string{"0.000", "", "", "", ""}, suite.payloads, "payloads") + suite.Equal([]string{"0", "", "", "", ""}, suite.payloads, "payloads") suite.publish("test", false, measurement{Energy: 1}) suite.Equal(topics, suite.topics, "topics") - suite.Equal([]string{"0.000", "1.000", "", "", ""}, suite.payloads, "payloads") + suite.Equal([]string{"0", "1", "", "", ""}, suite.payloads, "payloads") suite.publish("test", false, measurement{Controllable: lo.ToPtr(false)}) suite.Equal(topics, suite.topics, "topics") - suite.Equal([]string{"0.000", "", "", "", "false"}, suite.payloads, "payloads") + suite.Equal([]string{"0", "", "", "", "false"}, suite.payloads, "payloads") suite.publish("test", false, measurement{Currents: []float64{1, 2, 3}}) suite.Equal(append(topics, "test/currents/1", "test/currents/2", "test/currents/3"), suite.topics, "topics") - suite.Equal([]string{"0.000", "", "3", "", "", "1.000", "2.000", "3.000"}, suite.payloads, "payloads") + suite.Equal([]string{"0", "", "3", "", "", "1", "2", "3"}, suite.payloads, "payloads") } From c5631123c3a129bf6d8bc60245400e49fb55c3db Mon Sep 17 00:00:00 2001 From: Jordan Rodgers Date: Sat, 31 Jan 2026 01:11:49 -0800 Subject: [PATCH 0046/1747] Add HomeAssistant charger (#27103) --- .../charger/homeassistant-switch.yaml | 2 + .../definition/charger/homeassistant.yaml | 165 ++++++++++++++++++ 2 files changed, 167 insertions(+) create mode 100644 templates/definition/charger/homeassistant.yaml diff --git a/templates/definition/charger/homeassistant-switch.yaml b/templates/definition/charger/homeassistant-switch.yaml index cb8c0f74e..f626107c1 100644 --- a/templates/definition/charger/homeassistant-switch.yaml +++ b/templates/definition/charger/homeassistant-switch.yaml @@ -1,6 +1,8 @@ template: homeassistant-switch products: - brand: Home Assistant + description: + generic: Switch group: switchsockets requirements: evcc: ["skiptest"] diff --git a/templates/definition/charger/homeassistant.yaml b/templates/definition/charger/homeassistant.yaml new file mode 100644 index 000000000..5ad268a7d --- /dev/null +++ b/templates/definition/charger/homeassistant.yaml @@ -0,0 +1,165 @@ +template: homeassistant +products: + - brand: Home Assistant + description: + generic: Charger +group: generic +requirements: + description: + en: Home Assistant instances in your network will be auto-discovered and suitable charger entities and services (e.g. `sensor.*`, `switch.*`, `number.*`) will be suggested. + de: Home Assistant Instanzen in deinem Netzwerk werden automatisch erkannt und passende Wallbox-Entitäten und Services (z.B. `sensor.*`, `switch.*`, `number.*`) werden vorgeschlagen. +auth: + type: homeassistant + params: [uri] +params: + - name: uri + description: + de: Home Assistant URI + en: Home Assistant URI + example: http://homeassistant.local:8123 + help: + en: " " # overwrite default + de: " " # overwrite default + service: homeassistant/instances + required: true + - name: status + description: + de: Ladestatus-Sensor + en: Charging status sensor + service: homeassistant/entities?uri={uri}&domain=sensor,binary_sensor + example: "sensor.charger_status" + required: true + help: + en: Entity ID for charging status (A=ready, B=connected, C=charging, F=error) + de: Entitäts-ID für Ladestatus (A=bereit, B=verbunden, C=laden, F=fehler) + - name: enabled + description: + de: Aktivierungsstatus-Sensor + en: Enabled status sensor + service: homeassistant/entities?uri={uri}&domain=sensor,binary_sensor + example: "binary_sensor.charger_enabled" + required: true + help: + en: Entity ID for enabled state (sensor or binary_sensor with on/off or true/false state) + de: Entitäts-ID für Aktivierungsstatus (sensor oder binary_sensor mit on/off oder true/false Zustand) + - name: enable + description: + de: Aktivierungsschalter + en: Enable switch + service: homeassistant/entities?uri={uri}&domain=switch,input_boolean + example: "switch.charger_enable" + required: true + help: + en: Entity ID for enable/disable control (switch or input_boolean) + de: Entitäts-ID für Aktivierungs-/Deaktivierungs-Steuerung (switch oder input_boolean) + - name: setMaxCurrent + description: + de: Maximale Stromstärke-Entität [A] + en: Maximum current entity [A] + service: homeassistant/entities?uri={uri}&domain=number,input_number + example: "number.charger_max_current" + required: true + help: + en: Entity ID for setting maximum current in amperes (number or input_number entity) + de: Entitäts-ID zum Setzen der maximalen Stromstärke in Ampere (number oder input_number Entität) + - name: power + description: + de: Leistungsentität + en: Power entity + service: homeassistant/entities?uri={uri}&domain=sensor + example: "sensor.charger_power" + advanced: true + help: + en: Entity ID for instantaneous power measurement in watts + de: Entitäts-ID für momentane Leistungsmessung in Watt + - name: energy + description: + de: Energieentität + en: Energy entity + service: homeassistant/entities?uri={uri}&domain=sensor + example: "sensor.charger_energy" + advanced: true + help: + en: Entity ID for cumulative energy measurement in kWh + de: Entitäts-ID für kumulative Energiemessung in kWh + - name: currentL1 + description: + de: L1 Stromentität + en: L1 current entity + service: homeassistant/entities?uri={uri}&domain=sensor + example: "sensor.charger_current_l1" + advanced: true + help: + en: Entity ID for L1 current measurement in amperes + de: Entitäts-ID für L1 Strommessung in Ampere + - name: currentL2 + description: + de: L2 Stromentität + en: L2 current entity + service: homeassistant/entities?uri={uri}&domain=sensor + example: "sensor.charger_current_l2" + advanced: true + help: + en: Entity ID for L2 current measurement in amperes + de: Entitäts-ID für L2 Strommessung in Ampere + - name: currentL3 + description: + de: L3 Stromentität + en: L3 current entity + service: homeassistant/entities?uri={uri}&domain=sensor + example: "sensor.charger_current_l3" + advanced: true + help: + en: Entity ID for L3 current measurement in amperes + de: Entitäts-ID für L3 Strommessung in Ampere + - name: voltageL1 + description: + de: L1 Spannungsentität + en: L1 voltage entity + service: homeassistant/entities?uri={uri}&domain=sensor + example: "sensor.charger_voltage_l1" + advanced: true + help: + en: Entity ID for L1 voltage measurement in volts + de: Entitäts-ID für L1 Spannungsmessung in Volt + - name: voltageL2 + description: + de: L2 Spannungsentität + en: L2 voltage entity + service: homeassistant/entities?uri={uri}&domain=sensor + example: "sensor.charger_voltage_l2" + advanced: true + help: + en: Entity ID for L2 voltage measurement in volts + de: Entitäts-ID für L2 Spannungsmessung in Volt + - name: voltageL3 + description: + de: L3 Spannungsentität + en: L3 voltage entity + service: homeassistant/entities?uri={uri}&domain=sensor + example: "sensor.charger_voltage_l3" + advanced: true + help: + en: Entity ID for L3 voltage measurement in volts + de: Entitäts-ID für L3 Spannungsmessung in Volt +render: | + type: homeassistant + uri: {{ .uri }} + status: {{ .status }} + enabled: {{ .enabled }} + enable: {{ .enable }} + maxcurrent: {{ .setMaxCurrent }} + power: {{ .power }} + energy: {{ .energy }} + {{- if and .currentL1 .currentL2 .currentL3 }} + currents: + - {{ .currentL1 }} + - {{ .currentL2 }} + - {{ .currentL3 }} + {{- end }} + {{- if and .voltageL1 .voltageL2 .voltageL3 }} + voltages: + - {{ .voltageL1 }} + - {{ .voltageL2 }} + - {{ .voltageL3 }} + {{- end }} From 44dcd038880caf88daf75f6a96e225d2b403136e Mon Sep 17 00:00:00 2001 From: andig Date: Sat, 31 Jan 2026 10:17:45 +0100 Subject: [PATCH 0047/1747] MQTT: fix namespace clash (revert #26999) --- templates/definition/vehicle/mg2mqtt.yaml | 15 +++++++-------- templates/definition/vehicle/teslamate.yaml | 13 ++++++------- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/templates/definition/vehicle/mg2mqtt.yaml b/templates/definition/vehicle/mg2mqtt.yaml index 90373afbb..0acacc80d 100644 --- a/templates/definition/vehicle/mg2mqtt.yaml +++ b/templates/definition/vehicle/mg2mqtt.yaml @@ -8,7 +8,6 @@ requirements: en: Required MQTT broker configuration and a SAIC/MQTT Gateway (https://github.com/SAIC-iSmart-API/saic-python-mqtt-gateway or https://github.com/SAIC-iSmart-API/saic-java-client) de: Voraussetzung ist ein konfigurierter MQTT Broker und ein SAIC/MQTT Gateway (https://github.com/SAIC-iSmart-API/saic-python-mqtt-gateway oder https://github.com/SAIC-iSmart-API/saic-java-client) params: - - preset: mqtt - name: user required: true - name: vin @@ -21,22 +20,22 @@ render: | {{- include "vehicle-common" . }} soc: # battery soc (%) source: mqtt - {{- include "mqtt" . | indent 2 }} topic: saic/{{ .user }}/vehicles/{{ .vin }}/drivetrain/soc + timeout: {{ .timeout }} status: source: combined plugged: source: mqtt - {{- include "mqtt" . | indent 4 }} topic: saic/{{ .user }}/vehicles/{{ .vin }}/drivetrain/chargerConnected + timeout: {{ .timeout }} charging: source: mqtt - {{- include "mqtt" . | indent 4 }} topic: saic/{{ .user }}/vehicles/{{ .vin }}/drivetrain/charging + timeout: {{ .timeout }} range: source: mqtt - {{- include "mqtt" . | indent 2 }} topic: saic/{{ .user }}/vehicles/{{ .vin }}/drivetrain/range + timeout: {{ .timeout }} climater: source: go script: | @@ -46,12 +45,12 @@ render: | type: string config: source: mqtt - {{- include "mqtt" . | indent 8 }} topic: saic/{{ .user }}/vehicles/{{ .vin }}/climate/remoteClimateState + timeout: {{ .timeout }} odometer: source: mqtt - {{- include "mqtt" . | indent 2 }} topic: saic/{{ .user }}/vehicles/{{ .vin }}/drivetrain/mileage + timeout: {{ .timeout }} wakeup: source: go script: | @@ -61,5 +60,5 @@ render: | type: string config: source: mqtt - {{- include "mqtt" . | indent 8 }} topic: saic/{{ .user }}/vehicles/{{ .vin }}/refresh/mode/set + timeout: {{ .timeout }} diff --git a/templates/definition/vehicle/teslamate.yaml b/templates/definition/vehicle/teslamate.yaml index c9d9f570b..93301b62f 100644 --- a/templates/definition/vehicle/teslamate.yaml +++ b/templates/definition/vehicle/teslamate.yaml @@ -8,7 +8,6 @@ requirements: en: Open source Tesla data logger [github.com/adriankumpf/teslamate](https://github.com/adriankumpf/teslamate). MQTT broker required. de: Open Source Tesla Datenlogger [github.com/adriankumpf/teslamate](https://github.com/adriankumpf/teslamate). Voraussetzung ist konfigurierter MQTT Broker. params: - - preset: mqtt - preset: vehicle-common - name: id description: @@ -22,29 +21,29 @@ render: | {{- include "vehicle-common" . }} soc: source: mqtt - {{- include "mqtt" . | indent 2 }} topic: teslamate/cars/{{ .id }}/battery_level + timeout: {{ .timeout }} status: source: combined plugged: source: mqtt - {{- include "mqtt" . | indent 4 }} topic: teslamate/cars/{{ .id }}/plugged_in + timeout: {{ .timeout }} charging: source: mqtt - {{- include "mqtt" . | indent 4 }} topic: teslamate/cars/{{ .id }}/charger_actual_current + timeout: {{ .timeout }} jq: . > 0 range: source: mqtt - {{- include "mqtt" . | indent 2 }} topic: teslamate/cars/{{ .id }}/rated_battery_range_km + timeout: {{ .timeout }} odometer: source: mqtt - {{- include "mqtt" . | indent 2 }} topic: teslamate/cars/{{ .id }}/odometer + timeout: {{ .timeout }} limitsoc: source: mqtt - {{- include "mqtt" . | indent 2 }} topic: teslamate/cars/{{ .id }}/charge_limit_soc + timeout: {{ .timeout }} features: ["coarsecurrent"] From ca7bbba0a41b5f91b6d684abd7f38fcbe483999b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Feb 2026 11:30:17 +0100 Subject: [PATCH 0048/1747] chore(deps): bump githubnext/gh-aw from 0.37.22 to 0.38.5 (#27151) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/triage-agent.lock.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/triage-agent.lock.yml b/.github/workflows/triage-agent.lock.yml index 474cb8762..3d0bcfda9 100644 --- a/.github/workflows/triage-agent.lock.yml +++ b/.github/workflows/triage-agent.lock.yml @@ -57,7 +57,7 @@ jobs: comment_repo: "" steps: - name: Setup Scripts - uses: githubnext/gh-aw/actions/setup@afac8d00988d0af8af1eefb0d71cd98122b325b2 # v0.37.22 + uses: githubnext/gh-aw/actions/setup@c62cb1aecba894a20fdb4c6bbc39992bc6ce37e2 # v0.38.5 with: destination: /opt/gh-aw/actions - name: Check workflow file timestamps @@ -95,7 +95,7 @@ jobs: secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} steps: - name: Setup Scripts - uses: githubnext/gh-aw/actions/setup@afac8d00988d0af8af1eefb0d71cd98122b325b2 # v0.37.22 + uses: githubnext/gh-aw/actions/setup@c62cb1aecba894a20fdb4c6bbc39992bc6ce37e2 # v0.38.5 with: destination: /opt/gh-aw/actions - name: Checkout repository @@ -781,7 +781,7 @@ jobs: total_count: ${{ steps.missing_tool.outputs.total_count }} steps: - name: Setup Scripts - uses: githubnext/gh-aw/actions/setup@afac8d00988d0af8af1eefb0d71cd98122b325b2 # v0.37.22 + uses: githubnext/gh-aw/actions/setup@c62cb1aecba894a20fdb4c6bbc39992bc6ce37e2 # v0.38.5 with: destination: /opt/gh-aw/actions - name: Debug job inputs @@ -884,7 +884,7 @@ jobs: success: ${{ steps.parse_results.outputs.success }} steps: - name: Setup Scripts - uses: githubnext/gh-aw/actions/setup@afac8d00988d0af8af1eefb0d71cd98122b325b2 # v0.37.22 + uses: githubnext/gh-aw/actions/setup@c62cb1aecba894a20fdb4c6bbc39992bc6ce37e2 # v0.38.5 with: destination: /opt/gh-aw/actions - name: Download agent artifacts @@ -1022,7 +1022,7 @@ jobs: activated: ${{ steps.check_membership.outputs.is_team_member == 'true' }} steps: - name: Setup Scripts - uses: githubnext/gh-aw/actions/setup@afac8d00988d0af8af1eefb0d71cd98122b325b2 # v0.37.22 + uses: githubnext/gh-aw/actions/setup@c62cb1aecba894a20fdb4c6bbc39992bc6ce37e2 # v0.38.5 with: destination: /opt/gh-aw/actions - name: Check team membership for workflow @@ -1061,7 +1061,7 @@ jobs: process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} steps: - name: Setup Scripts - uses: githubnext/gh-aw/actions/setup@afac8d00988d0af8af1eefb0d71cd98122b325b2 # v0.37.22 + uses: githubnext/gh-aw/actions/setup@c62cb1aecba894a20fdb4c6bbc39992bc6ce37e2 # v0.38.5 with: destination: /opt/gh-aw/actions - name: Download agent output artifact From 768656e0c3182af611bbe58716b239a7ed382a12 Mon Sep 17 00:00:00 2001 From: Maschga <88616799+Maschga@users.noreply.github.com> Date: Sun, 1 Feb 2026 11:59:02 +0100 Subject: [PATCH 0049/1747] EEBUS: configure by default (#26944) --- api/globalconfig/types.go | 28 ++-- assets/js/components/Config/DeviceCard.vue | 13 +- assets/js/components/Config/EebusModal.vue | 147 ++++++++++++++++-- assets/js/components/Config/HemsModal.vue | 2 +- assets/js/components/Config/JsonModal.vue | 12 +- .../components/Config/PropertyCertField.vue | 3 +- assets/js/components/Config/PropertyField.vue | 4 +- .../components/Config/defaultYaml/eebus.yaml | 12 -- assets/js/types/evcc.ts | 25 ++- assets/js/views/Config.vue | 109 +++++-------- cmd/helper.go | 16 ++ cmd/helper_test.go | 34 ++++ cmd/root.go | 34 ++-- cmd/setup.go | 46 ++---- i18n/bg.json | 7 +- i18n/bs.json | 5 +- i18n/cs.json | 7 +- i18n/da.json | 7 +- i18n/de.json | 26 +++- i18n/el.json | 7 +- i18n/en.json | 26 +++- i18n/es.json | 7 +- i18n/fi.json | 7 +- i18n/fr.json | 7 +- i18n/hr.json | 7 +- i18n/hu.json | 7 +- i18n/it.json | 7 +- i18n/lb.json | 7 +- i18n/lt.json | 7 +- i18n/nl.json | 7 +- i18n/no.json | 7 +- i18n/pl.json | 7 +- i18n/pt.json | 7 +- i18n/ro.json | 7 +- i18n/ru.json | 7 +- i18n/sl.json | 7 +- i18n/sv.json | 7 +- i18n/ta.json | 4 +- i18n/tr.json | 7 +- i18n/uk.json | 7 +- i18n/zh-Hans.json | 4 +- server/eebus/eebus.go | 44 ++---- server/eebus/test/cs_test.go | 5 +- server/eebus/types.go | 73 ++++++++- server/http.go | 2 +- server/http_config_site_other_handler.go | 15 +- server/http_global_settings_handler.go | 15 +- tests/config-eebus.evcc.yaml | 27 ++++ tests/config-eebus.spec.ts | 137 ++++++++++++++++ tests/config-ocpp.spec.ts | 2 - util/redact.go | 8 + util/sponsor/auth.go | 4 +- 52 files changed, 712 insertions(+), 324 deletions(-) delete mode 100644 assets/js/components/Config/defaultYaml/eebus.yaml create mode 100644 cmd/helper_test.go create mode 100644 tests/config-eebus.evcc.yaml create mode 100644 tests/config-eebus.spec.ts create mode 100644 util/redact.go diff --git a/api/globalconfig/types.go b/api/globalconfig/types.go index 3756f2f8c..4925bae63 100644 --- a/api/globalconfig/types.go +++ b/api/globalconfig/types.go @@ -18,6 +18,13 @@ import ( "github.com/evcc-io/evcc/util/modbus" ) +// ConfigStatus for publishing config, status and source to UI and external systems +type ConfigStatus struct { + Config any `json:"config,omitempty"` + Status any `json:"status,omitempty"` + FromYaml bool `json:"fromYaml,omitempty"` +} + type All struct { Network Network Ocpp ocpp.Config @@ -79,13 +86,6 @@ func (c Hems) Redacted() any { var _ api.Redactor = (*Mqtt)(nil) -func masked(s any) string { - if s != "" { - return "***" - } - return "" -} - type Mqtt struct { mqtt.Config `mapstructure:",squash"` Topic string `json:"topic"` @@ -97,12 +97,12 @@ func (m Mqtt) Redacted() any { Config: mqtt.Config{ Broker: m.Broker, User: m.User, - Password: masked(m.Password), + Password: util.Masked(m.Password), ClientID: m.ClientID, Insecure: m.Insecure, - CaCert: masked(m.CaCert), - ClientCert: masked(m.ClientCert), - ClientKey: masked(m.ClientKey), + CaCert: util.Masked(m.CaCert), + ClientCert: util.Masked(m.ClientCert), + ClientKey: util.Masked(m.ClientKey), }, Topic: m.Topic, } @@ -124,10 +124,10 @@ func (c Influx) Redacted() any { return Influx{ URL: c.URL, Database: c.Database, - Token: masked(c.Token), + Token: util.Masked(c.Token), Org: c.Org, User: c.User, - Password: masked(c.Password), + Password: util.Masked(c.Password), Insecure: c.Insecure, } } @@ -147,7 +147,7 @@ type MessagingEventTemplate struct { Title, Msg string } -func (c Messaging) Configured() bool { +func (c Messaging) IsConfigured() bool { return len(c.Services) > 0 || len(c.Events) > 0 } diff --git a/assets/js/components/Config/DeviceCard.vue b/assets/js/components/Config/DeviceCard.vue index 5c8eddbf4..aa8c030a8 100644 --- a/assets/js/components/Config/DeviceCard.vue +++ b/assets/js/components/Config/DeviceCard.vue @@ -6,9 +6,10 @@ 'round-box--error': error, 'round-box--warning': warning, 'root--unconfigured': unconfigured, + 'root--with-tags': $slots.tags, }" > -
+
@@ -33,8 +34,10 @@
-
- +
+
+ +
@@ -67,7 +70,7 @@ export default { } let title = `${this.$t("config.main.name")}: ${this.name}`; if (!this.editable) { - title += `
${this.$t("config.main.yaml")}
`; + title += `
${this.$t("config.general.fromYamlHint")}
`; } return `
${title}
`; }, @@ -105,6 +108,8 @@ export default { list-style-type: none; border-radius: 1rem; padding: 1rem 1.5rem; +} +.root--with-tags { min-height: 8rem; } .root--unconfigured { diff --git a/assets/js/components/Config/EebusModal.vue b/assets/js/components/Config/EebusModal.vue index d4e499669..ff7c48df5 100644 --- a/assets/js/components/Config/EebusModal.vue +++ b/assets/js/components/Config/EebusModal.vue @@ -1,26 +1,153 @@ - diff --git a/assets/js/components/Config/HemsModal.vue b/assets/js/components/Config/HemsModal.vue index cd293ef25..cab449f58 100644 --- a/assets/js/components/Config/HemsModal.vue +++ b/assets/js/components/Config/HemsModal.vue @@ -31,7 +31,7 @@

- {{ $t("config.hems.yamlConfigured") }} + {{ $t("config.general.fromYamlHint") }}

diff --git a/assets/js/components/Config/JsonModal.vue b/assets/js/components/Config/JsonModal.vue index e3338bf55..010028e01 100644 --- a/assets/js/components/Config/JsonModal.vue +++ b/assets/js/components/Config/JsonModal.vue @@ -88,6 +88,7 @@ export default { saveMethod: { type: String, default: "post" }, storeValuesInArray: Boolean, size: { type: String }, + confirmRemove: String, }, emits: ["changed", "open"], data() { @@ -121,7 +122,13 @@ export default { await this.load(); }, async load() { - this.serverValues = this.stateKey ? store.state[this.stateKey] : store.state; + if (this.stateKey) { + // Support nested keys like "eebus.config" + const keys = this.stateKey.split("."); + this.serverValues = keys.reduce((obj, key) => obj?.[key], store.state); + } else { + this.serverValues = store.state; + } if (this.transformReadValues) { this.serverValues = this.transformReadValues(this.serverValues); } @@ -155,6 +162,9 @@ export default { this.saving = false; }, async remove() { + if (this.confirmRemove && !window.confirm(this.confirmRemove)) { + return; + } this.removing = true; this.error = ""; try { diff --git a/assets/js/components/Config/PropertyCertField.vue b/assets/js/components/Config/PropertyCertField.vue index c71722301..df64a242d 100644 --- a/assets/js/components/Config/PropertyCertField.vue +++ b/assets/js/components/Config/PropertyCertField.vue @@ -1,6 +1,6 @@ + +
+

{{ $t("issue.downloadHint") }}

+
+ +
+
-
+
@@ -304,6 +330,7 @@ { await expect(email).toHaveAttribute("href", "mailto:support@example.com"); }); + test("issue page uses email flow", async ({ page }) => { + await page.goto("/#/issue"); + + // github-only elements gone + await expect(page.getByRole("radio")).toHaveCount(0); + await expect(page.getByLabel("Steps to Reproduce")).toHaveCount(0); + await expect(page.getByText("Please write your issue in English")).toHaveCount(0); + + await expect(page.getByRole("button", { name: "Send request by email" })).toBeVisible(); + await expect( + page.getByText("Attach this file to your email if more information is needed.") + ).toBeVisible(); + + const downloadButton = page.getByRole("button", { name: "Download debug information" }); + await expect(downloadButton).toBeVisible(); + const downloadPromise = page.waitForEvent("download"); + await downloadButton.click(); + const download = await downloadPromise; + expect(download.suggestedFilename()).toMatch(/^evcc-debug-v.+\.txt$/); + }); + test("help modal shows contact button instead of github discussions", async ({ page }) => { await page.goto("/"); const menu = await openMoreMenu(page); From 7317b3437a45f325522c1dfab10e161956c0efd2 Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Fri, 7 Aug 2026 18:31:18 +0200 Subject: [PATCH 1512/1747] UI: hide chart tooltips on touch release (#32603) --- .../Battery/BatteryHistoryChart.vue | 2 + assets/js/components/Forecast/Chart.vue | 548 ------------------ assets/js/components/Forecast/chartMixin.ts | 9 +- assets/js/components/Forecast/echarts.ts | 14 + assets/js/components/History/GroupChart.vue | 2 + assets/js/components/Optimize/ChargeChart.vue | 4 +- assets/js/components/Optimize/PriceChart.vue | 4 +- assets/js/components/Optimize/SocChart.vue | 11 +- assets/js/components/Sessions/chartConfig.ts | 17 + 9 files changed, 53 insertions(+), 558 deletions(-) delete mode 100644 assets/js/components/Forecast/Chart.vue diff --git a/assets/js/components/Battery/BatteryHistoryChart.vue b/assets/js/components/Battery/BatteryHistoryChart.vue index 3ae4ff3fe..f4d8d786e 100644 --- a/assets/js/components/Battery/BatteryHistoryChart.vue +++ b/assets/js/components/Battery/BatteryHistoryChart.vue @@ -9,6 +9,7 @@ import { FONT_FAMILY, forecastXAxes, forecastYAxis, + registerTouchTooltipReset, tooltipStyle, tooltipTable, type TooltipRow, @@ -241,6 +242,7 @@ export default defineComponent({ const el = this.$refs["chartEl"] as HTMLElement; this.chart = markRaw(echarts.init(el)); this.chart.setOption(this.chartOption); + registerTouchTooltipReset(this.chart, el); this.$nextTick(() => this.updateGraphic()); window.addEventListener("resize", this.resize); // initial render done here (not via the watcher), so paging animates from the first click diff --git a/assets/js/components/Forecast/Chart.vue b/assets/js/components/Forecast/Chart.vue deleted file mode 100644 index 27f4115e1..000000000 --- a/assets/js/components/Forecast/Chart.vue +++ /dev/null @@ -1,548 +0,0 @@ - - - diff --git a/assets/js/components/Forecast/chartMixin.ts b/assets/js/components/Forecast/chartMixin.ts index d8c9cecf3..eed9f6bc2 100644 --- a/assets/js/components/Forecast/chartMixin.ts +++ b/assets/js/components/Forecast/chartMixin.ts @@ -1,5 +1,5 @@ import { defineComponent, markRaw } from "vue"; -import { echarts } from "./echarts"; +import { echarts, registerTouchTooltipReset } from "./echarts"; import "./chartStyles.css"; // chartOption is provided by each consuming component's computed @@ -63,12 +63,7 @@ export default defineComponent({ this.chart.on("hideTip", () => { this.tooltipVisible = false; }); - const resetTouch = () => { - this.chart?.dispatchAction({ type: "hideTip" }); - this.tooltipVisible = false; - }; - el.addEventListener("touchend", resetTouch); - el.addEventListener("touchcancel", resetTouch); + registerTouchTooltipReset(this.chart, el, () => (this.tooltipVisible = false)); }, }, }); diff --git a/assets/js/components/Forecast/echarts.ts b/assets/js/components/Forecast/echarts.ts index f0ed5a3b4..4cfc60f47 100644 --- a/assets/js/components/Forecast/echarts.ts +++ b/assets/js/components/Forecast/echarts.ts @@ -117,6 +117,20 @@ export function tooltipStyle( }; } +// hide tooltip when finger lifts; mouse hover unchanged +export function registerTouchTooltipReset( + chart: Pick, + el: HTMLElement, + onReset?: () => void +) { + const reset = () => { + chart.dispatchAction({ type: "hideTip" }); + onReset?.(); + }; + el.addEventListener("touchend", reset); + el.addEventListener("touchcancel", reset); +} + export interface TooltipRow { name?: string; values: string[]; diff --git a/assets/js/components/History/GroupChart.vue b/assets/js/components/History/GroupChart.vue index 1da173649..8d2951dc4 100644 --- a/assets/js/components/History/GroupChart.vue +++ b/assets/js/components/History/GroupChart.vue @@ -11,6 +11,7 @@ import { FONT_FAMILY, forecastGrid, forecastYAxis, + registerTouchTooltipReset, tooltipStyle, tooltipTable, xAxisLabelStyle, @@ -719,6 +720,7 @@ export default defineComponent({ const zr = this.chart.getZr(); zr.on("mousemove", this.onChartMouseMove); zr.on("globalout", this.clearHighlight); + registerTouchTooltipReset(this.chart, el, this.clearHighlight); this.mediaQuery = window.matchMedia("(max-width: 575.98px)"); this.isMobile = this.mediaQuery.matches; this.mediaQuery.addEventListener("change", this.onMediaChange); diff --git a/assets/js/components/Optimize/ChargeChart.vue b/assets/js/components/Optimize/ChargeChart.vue index 7b4e332c3..61d8a3613 100644 --- a/assets/js/components/Optimize/ChargeChart.vue +++ b/assets/js/components/Optimize/ChargeChart.vue @@ -37,6 +37,7 @@ import formatter from "@/mixins/formatter"; import colors from "@/colors"; import LegendList from "../Sessions/LegendList.vue"; import type { Legend } from "../Sessions/types"; +import { touchDismissPlugin } from "../Sessions/chartConfig"; ChartJS.register( CategoryScale, @@ -48,7 +49,8 @@ ChartJS.register( PointElement, Title, Tooltip, - ChartLegendPlugin + ChartLegendPlugin, + touchDismissPlugin ); export default defineComponent({ diff --git a/assets/js/components/Optimize/PriceChart.vue b/assets/js/components/Optimize/PriceChart.vue index db31b0a1a..87361b6a8 100644 --- a/assets/js/components/Optimize/PriceChart.vue +++ b/assets/js/components/Optimize/PriceChart.vue @@ -35,6 +35,7 @@ import formatter from "@/mixins/formatter"; import colors from "@/colors"; import LegendList from "../Sessions/LegendList.vue"; import type { Legend } from "../Sessions/types"; +import { touchDismissPlugin } from "../Sessions/chartConfig"; const tension = 0; @@ -46,7 +47,8 @@ ChartJS.register( PointElement, Title, Tooltip, - ChartLegendPlugin + ChartLegendPlugin, + touchDismissPlugin ); export default defineComponent({ diff --git a/assets/js/components/Optimize/SocChart.vue b/assets/js/components/Optimize/SocChart.vue index 88d8d64d7..cca83377e 100644 --- a/assets/js/components/Optimize/SocChart.vue +++ b/assets/js/components/Optimize/SocChart.vue @@ -41,8 +41,17 @@ import type { EvoptData } from "./TimeSeriesDataTable.vue"; import type { CURRENCY, BatteryDetail } from "@/types/evcc"; import formatter from "@/mixins/formatter"; import colors from "@/colors"; +import { touchDismissPlugin } from "../Sessions/chartConfig"; -ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, ChartLegendPlugin); +ChartJS.register( + CategoryScale, + LinearScale, + BarElement, + Title, + Tooltip, + ChartLegendPlugin, + touchDismissPlugin +); export default defineComponent({ name: "SocChart", diff --git a/assets/js/components/Sessions/chartConfig.ts b/assets/js/components/Sessions/chartConfig.ts index e2b09193f..f01c008b0 100644 --- a/assets/js/components/Sessions/chartConfig.ts +++ b/assets/js/components/Sessions/chartConfig.ts @@ -3,6 +3,7 @@ import { Tooltip, type ChartComponentLike, type ChartType, + type Plugin, type Point, type TooltipItem, } from "chart.js"; @@ -20,6 +21,22 @@ Chart.defaults.font.family = window Chart.defaults.font.size = 14; Chart.defaults.layout.padding = 0; +// hide tooltip when finger lifts; mouse hover unchanged +export const touchDismissPlugin: Plugin = { + id: "touchDismiss", + afterInit(chart) { + const dismiss = () => { + if (!chart.ctx) return; // destroyed + chart.setActiveElements([]); + chart.tooltip?.setActiveElements([], { x: 0, y: 0 }); + chart.update(); + }; + chart.canvas.addEventListener("touchend", dismiss); + chart.canvas.addEventListener("touchcancel", dismiss); + }, +}; +Chart.register(touchDismissPlugin); + // Custom tooltip positioners (Tooltip.positioners as any)["center"] = function () { const { chart } = this; From 4314a64896fa98c840ff54408d6d7f73320b6471 Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Fri, 7 Aug 2026 18:47:01 +0200 Subject: [PATCH 1513/1747] UI Charts: show/hide tooltips on touch (#32613) --- .../Battery/BatteryHistoryChart.vue | 4 +- assets/js/components/Forecast/chartMixin.ts | 4 +- assets/js/components/Forecast/echarts.ts | 11 +- assets/js/components/History/GroupChart.vue | 4 +- assets/js/components/Optimize/ChargeChart.vue | 5 +- assets/js/components/Optimize/PriceChart.vue | 5 +- assets/js/components/Optimize/SocChart.vue | 12 +-- assets/js/components/Sessions/chartConfig.ts | 11 +- assets/js/utils/swipe.ts | 100 ++++++++++++++++++ 9 files changed, 122 insertions(+), 34 deletions(-) diff --git a/assets/js/components/Battery/BatteryHistoryChart.vue b/assets/js/components/Battery/BatteryHistoryChart.vue index f4d8d786e..59c16366e 100644 --- a/assets/js/components/Battery/BatteryHistoryChart.vue +++ b/assets/js/components/Battery/BatteryHistoryChart.vue @@ -9,7 +9,7 @@ import { FONT_FAMILY, forecastXAxes, forecastYAxis, - registerTouchTooltipReset, + registerTouchTooltip, tooltipStyle, tooltipTable, type TooltipRow, @@ -242,7 +242,7 @@ export default defineComponent({ const el = this.$refs["chartEl"] as HTMLElement; this.chart = markRaw(echarts.init(el)); this.chart.setOption(this.chartOption); - registerTouchTooltipReset(this.chart, el); + registerTouchTooltip(this.chart, el); this.$nextTick(() => this.updateGraphic()); window.addEventListener("resize", this.resize); // initial render done here (not via the watcher), so paging animates from the first click diff --git a/assets/js/components/Forecast/chartMixin.ts b/assets/js/components/Forecast/chartMixin.ts index eed9f6bc2..2c32f105e 100644 --- a/assets/js/components/Forecast/chartMixin.ts +++ b/assets/js/components/Forecast/chartMixin.ts @@ -1,5 +1,5 @@ import { defineComponent, markRaw } from "vue"; -import { echarts, registerTouchTooltipReset } from "./echarts"; +import { echarts, registerTouchTooltip } from "./echarts"; import "./chartStyles.css"; // chartOption is provided by each consuming component's computed @@ -63,7 +63,7 @@ export default defineComponent({ this.chart.on("hideTip", () => { this.tooltipVisible = false; }); - registerTouchTooltipReset(this.chart, el, () => (this.tooltipVisible = false)); + registerTouchTooltip(this.chart, el, () => (this.tooltipVisible = false)); }, }, }); diff --git a/assets/js/components/Forecast/echarts.ts b/assets/js/components/Forecast/echarts.ts index 4cfc60f47..a4c5dabd9 100644 --- a/assets/js/components/Forecast/echarts.ts +++ b/assets/js/components/Forecast/echarts.ts @@ -1,5 +1,6 @@ import * as echarts from "echarts/core"; import colors from "@/colors"; +import { attachTouchTooltipGate } from "@/utils/swipe"; import escapeHtml from "@/utils/escapeHtml"; import type { UiForecastSlot } from "@/types/evcc"; import { BarChart, LineChart } from "echarts/charts"; @@ -117,18 +118,16 @@ export function tooltipStyle( }; } -// hide tooltip when finger lifts; mouse hover unchanged -export function registerTouchTooltipReset( +// touch tooltips: show on dwell, follow the finger, hide when it lifts; mouse hover unchanged +export function registerTouchTooltip( chart: Pick, el: HTMLElement, onReset?: () => void ) { - const reset = () => { + attachTouchTooltipGate(el, () => { chart.dispatchAction({ type: "hideTip" }); onReset?.(); - }; - el.addEventListener("touchend", reset); - el.addEventListener("touchcancel", reset); + }); } export interface TooltipRow { diff --git a/assets/js/components/History/GroupChart.vue b/assets/js/components/History/GroupChart.vue index 8d2951dc4..bb213ef37 100644 --- a/assets/js/components/History/GroupChart.vue +++ b/assets/js/components/History/GroupChart.vue @@ -11,7 +11,7 @@ import { FONT_FAMILY, forecastGrid, forecastYAxis, - registerTouchTooltipReset, + registerTouchTooltip, tooltipStyle, tooltipTable, xAxisLabelStyle, @@ -720,7 +720,7 @@ export default defineComponent({ const zr = this.chart.getZr(); zr.on("mousemove", this.onChartMouseMove); zr.on("globalout", this.clearHighlight); - registerTouchTooltipReset(this.chart, el, this.clearHighlight); + registerTouchTooltip(this.chart, el, this.clearHighlight); this.mediaQuery = window.matchMedia("(max-width: 575.98px)"); this.isMobile = this.mediaQuery.matches; this.mediaQuery.addEventListener("change", this.onMediaChange); diff --git a/assets/js/components/Optimize/ChargeChart.vue b/assets/js/components/Optimize/ChargeChart.vue index 61d8a3613..48c7783a2 100644 --- a/assets/js/components/Optimize/ChargeChart.vue +++ b/assets/js/components/Optimize/ChargeChart.vue @@ -37,7 +37,7 @@ import formatter from "@/mixins/formatter"; import colors from "@/colors"; import LegendList from "../Sessions/LegendList.vue"; import type { Legend } from "../Sessions/types"; -import { touchDismissPlugin } from "../Sessions/chartConfig"; +import "../Sessions/chartConfig"; ChartJS.register( CategoryScale, @@ -49,8 +49,7 @@ ChartJS.register( PointElement, Title, Tooltip, - ChartLegendPlugin, - touchDismissPlugin + ChartLegendPlugin ); export default defineComponent({ diff --git a/assets/js/components/Optimize/PriceChart.vue b/assets/js/components/Optimize/PriceChart.vue index 87361b6a8..c096781ce 100644 --- a/assets/js/components/Optimize/PriceChart.vue +++ b/assets/js/components/Optimize/PriceChart.vue @@ -35,7 +35,7 @@ import formatter from "@/mixins/formatter"; import colors from "@/colors"; import LegendList from "../Sessions/LegendList.vue"; import type { Legend } from "../Sessions/types"; -import { touchDismissPlugin } from "../Sessions/chartConfig"; +import "../Sessions/chartConfig"; const tension = 0; @@ -47,8 +47,7 @@ ChartJS.register( PointElement, Title, Tooltip, - ChartLegendPlugin, - touchDismissPlugin + ChartLegendPlugin ); export default defineComponent({ diff --git a/assets/js/components/Optimize/SocChart.vue b/assets/js/components/Optimize/SocChart.vue index cca83377e..9c27cd3b9 100644 --- a/assets/js/components/Optimize/SocChart.vue +++ b/assets/js/components/Optimize/SocChart.vue @@ -41,17 +41,9 @@ import type { EvoptData } from "./TimeSeriesDataTable.vue"; import type { CURRENCY, BatteryDetail } from "@/types/evcc"; import formatter from "@/mixins/formatter"; import colors from "@/colors"; -import { touchDismissPlugin } from "../Sessions/chartConfig"; +import "../Sessions/chartConfig"; -ChartJS.register( - CategoryScale, - LinearScale, - BarElement, - Title, - Tooltip, - ChartLegendPlugin, - touchDismissPlugin -); +ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, ChartLegendPlugin); export default defineComponent({ name: "SocChart", diff --git a/assets/js/components/Sessions/chartConfig.ts b/assets/js/components/Sessions/chartConfig.ts index f01c008b0..fd228d1e5 100644 --- a/assets/js/components/Sessions/chartConfig.ts +++ b/assets/js/components/Sessions/chartConfig.ts @@ -8,6 +8,7 @@ import { type TooltipItem, } from "chart.js"; import colors from "@/colors"; +import { attachTouchTooltipGate } from "@/utils/swipe"; import type { Context } from "chartjs-plugin-datalabels"; // Register common components export function registerChartComponents(components: ChartComponentLike[]) { @@ -21,18 +22,16 @@ Chart.defaults.font.family = window Chart.defaults.font.size = 14; Chart.defaults.layout.padding = 0; -// hide tooltip when finger lifts; mouse hover unchanged -export const touchDismissPlugin: Plugin = { +// touch tooltips: show on dwell, follow the finger, hide when it lifts; mouse hover unchanged +const touchDismissPlugin: Plugin = { id: "touchDismiss", afterInit(chart) { - const dismiss = () => { + attachTouchTooltipGate(chart.canvas.parentElement ?? chart.canvas, () => { if (!chart.ctx) return; // destroyed chart.setActiveElements([]); chart.tooltip?.setActiveElements([], { x: 0, y: 0 }); chart.update(); - }; - chart.canvas.addEventListener("touchend", dismiss); - chart.canvas.addEventListener("touchcancel", dismiss); + }); }, }; Chart.register(touchDismissPlugin); diff --git a/assets/js/utils/swipe.ts b/assets/js/utils/swipe.ts index c74d2ae1a..0dd00250c 100644 --- a/assets/js/utils/swipe.ts +++ b/assets/js/utils/swipe.ts @@ -6,6 +6,106 @@ interface SwipeOptions { maxDuration?: number; } +const DWELL_MS = 100; +const DWELL_SLOP_PX = 10; + +// Touch tooltip gate: the chart never sees raw touch events (zrender would otherwise +// ignore mouse events for 700ms after every touchend). The tooltip appears only after +// the finger rests for a moment, then follows it; fast movement (swipes, scrolling) +// keeps it hidden and a pause re-arms it. The tooltip is driven by a synthetic +// mousemove at the finger position, which both chart libs handle natively. +export function attachTouchTooltipGate(el: HTMLElement, hide: () => void): () => void { + let timer: ReturnType | null = null; + let shown = false; + let latched = false; + let x = 0; + let y = 0; + let anchorX = 0; + let anchorY = 0; + let gestureX = 0; + let gestureY = 0; + + const show = () => { + const target = document.elementFromPoint(x, y); + if (!target || !el.contains(target)) return; + shown = true; + const event = new MouseEvent("mousemove", { clientX: x, clientY: y, bubbles: true }); + // WebKit computes offsetX/Y of synthetic events against the wrong base element, + // and both chart libs read them; provide correct values relative to the chart + const rect = el.getBoundingClientRect(); + Object.defineProperty(event, "offsetX", { value: x - rect.left }); + Object.defineProperty(event, "offsetY", { value: y - rect.top }); + target.dispatchEvent(event); + }; + + const restartDwell = () => { + if (timer) clearTimeout(timer); + anchorX = x; + anchorY = y; + timer = setTimeout(show, DWELL_MS); + }; + + const onTouchStart = (e: TouchEvent) => { + const touch = e.touches[0]; + if (!touch) return; + e.stopPropagation(); + shown = false; + latched = false; + x = touch.clientX; + y = touch.clientY; + gestureX = x; + gestureY = y; + restartDwell(); + }; + + const onTouchMove = (e: TouchEvent) => { + const touch = e.touches[0]; + if (!touch) return; + e.stopPropagation(); + x = touch.clientX; + y = touch.clientY; + // large displacement before the tooltip ever showed = scroll or swipe, + // no tooltip for the rest of this gesture (scroll deceleration would + // otherwise pass the dwell check while the page is still moving) + if (!shown && !latched && Math.hypot(x - gestureX, y - gestureY) > 30) { + latched = true; + if (timer) clearTimeout(timer); + } + if (latched) return; + if (shown) { + show(); + } else if (Math.hypot(x - anchorX, y - anchorY) > DWELL_SLOP_PX) { + restartDwell(); + } + }; + + const onTouchEnd = (e: TouchEvent) => { + e.stopPropagation(); + // suppress compat mouse events after touch, they would re-trigger the tooltip + if (e.cancelable) e.preventDefault(); + if (timer) clearTimeout(timer); + shown = false; + hide(); + }; + + // long-press must not start text selection of chart or tooltip content + el.classList.add("user-select-none"); + + el.addEventListener("touchstart", onTouchStart, { capture: true, passive: true }); + el.addEventListener("touchmove", onTouchMove, { capture: true, passive: true }); + el.addEventListener("touchend", onTouchEnd, { capture: true }); + el.addEventListener("touchcancel", onTouchEnd, { capture: true }); + + return () => { + if (timer) clearTimeout(timer); + el.classList.remove("user-select-none"); + el.removeEventListener("touchstart", onTouchStart, { capture: true }); + el.removeEventListener("touchmove", onTouchMove, { capture: true }); + el.removeEventListener("touchend", onTouchEnd, { capture: true }); + el.removeEventListener("touchcancel", onTouchEnd, { capture: true }); + }; +} + export function attachSwipeHandler(el: HTMLElement, options: SwipeOptions): () => void { const minDistance = options.minDistance ?? 60; const maxDuration = options.maxDuration ?? 600; From 950ca489e5ee7606f88ec4752d8fec6eb9415b5d Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Fri, 7 Aug 2026 18:47:25 +0200 Subject: [PATCH 1514/1747] UI Charts: swipe on charts to switch time range (#32614) --- .../components/Battery/BatteryHistoryCard.vue | 33 +++++++++++++++++++ assets/js/components/Battery/WindowNav.vue | 4 +++ .../js/components/Sessions/DateNavigator.vue | 4 ++- assets/js/utils/haptic.ts | 5 +-- assets/js/utils/swipe.ts | 14 ++++---- 5 files changed, 50 insertions(+), 10 deletions(-) diff --git a/assets/js/components/Battery/BatteryHistoryCard.vue b/assets/js/components/Battery/BatteryHistoryCard.vue index a7ded3a3b..27241fd49 100644 --- a/assets/js/components/Battery/BatteryHistoryCard.vue +++ b/assets/js/components/Battery/BatteryHistoryCard.vue @@ -5,6 +5,8 @@ :label="windowLabel" :prev-disabled="prevDisabled" :next-disabled="nextDisabled" + :highlight-prev="highlightPrev" + :highlight-next="highlightNext" @prev="changeOffset(-1)" @next="changeOffset(1)" /> @@ -44,6 +46,8 @@ import { batteryColor } from "@/colors"; import type { Legend } from "../Sessions/types"; import type { BatterySeries } from "./types"; import Card from "../Helper/Card.vue"; +import { attachSwipeHandler } from "@/utils/swipe"; +import { hapticFeedback } from "@/utils/haptic"; import SelectGroup from "../Helper/SelectGroup.vue"; import LegendList from "../Sessions/LegendList.vue"; import WindowNav from "./WindowNav.vue"; @@ -71,6 +75,10 @@ export default defineComponent({ dayOffset: 0, focusedBattery: null as number | null, selectedUnit: (settings.batteryUnit || "soc") as "soc" | "energy", + highlightPrev: false, + highlightNext: false, + highlightTimer: null as ReturnType | null, + detachSwipe: null as (() => void) | null, }; }, computed: { @@ -145,7 +153,32 @@ export default defineComponent({ } }, }, + mounted() { + this.detachSwipe = attachSwipeHandler(this.$el as HTMLElement, { + onSwipeLeft: () => this.swipe(1), + onSwipeRight: () => this.swipe(-1), + }); + }, + unmounted() { + this.detachSwipe?.(); + if (this.highlightTimer) clearTimeout(this.highlightTimer); + }, methods: { + swipe(dir: number) { + if (dir < 0 ? this.prevDisabled : this.nextDisabled) return; + this.changeOffset(dir); + this.flashHighlight(dir < 0 ? "prev" : "next"); + hapticFeedback("medium"); + }, + flashHighlight(dir: "prev" | "next") { + if (this.highlightTimer) clearTimeout(this.highlightTimer); + this.highlightPrev = dir === "prev"; + this.highlightNext = dir === "next"; + this.highlightTimer = setTimeout(() => { + this.highlightPrev = false; + this.highlightNext = false; + }, 300); + }, updateUnit(value: string | number | boolean | null) { this.selectedUnit = value === "energy" ? "energy" : "soc"; settings.batteryUnit = this.selectedUnit; diff --git a/assets/js/components/Battery/WindowNav.vue b/assets/js/components/Battery/WindowNav.vue index 2fb1249bd..0b5f63ccc 100644 --- a/assets/js/components/Battery/WindowNav.vue +++ b/assets/js/components/Battery/WindowNav.vue @@ -3,6 +3,7 @@ @@ -12,6 +13,7 @@ @@ -29,6 +31,8 @@ export default defineComponent({ label: { type: String, default: "" }, prevDisabled: Boolean, nextDisabled: Boolean, + highlightPrev: Boolean, + highlightNext: Boolean, }, emits: ["prev", "next"], }); diff --git a/assets/js/components/Sessions/DateNavigator.vue b/assets/js/components/Sessions/DateNavigator.vue index f8cca98a3..a8bb37084 100644 --- a/assets/js/components/Sessions/DateNavigator.vue +++ b/assets/js/components/Sessions/DateNavigator.vue @@ -146,6 +146,7 @@ import DateNavigatorButton from "./DateNavigatorButton.vue"; import formatter from "@/mixins/formatter"; import type { SelectOption } from "@/types/evcc"; import { attachSwipeHandler } from "@/utils/swipe"; +import { hapticFeedback } from "@/utils/haptic"; function daysInMonth(year: number, month: number) { return new Date(year, month, 0).getDate(); @@ -279,7 +280,6 @@ export default defineComponent({ this.detachSwipe = attachSwipeHandler(document.body, { onSwipeLeft: () => this.swipeNext(), onSwipeRight: () => this.swipePrev(), - ignoreSelector: "canvas, [_echarts_instance_]", }); }, unmounted() { @@ -393,6 +393,7 @@ export default defineComponent({ else if (this.showYear && this.hasPrevYear) this.emitPrevYear(); else return; this.flashHighlight("prev"); + hapticFeedback("medium"); }, swipeNext() { if (this.showDay && this.hasNextDay) this.emitNextDay(); @@ -400,6 +401,7 @@ export default defineComponent({ else if (this.showYear && this.hasNextYear) this.emitNextYear(); else return; this.flashHighlight("next"); + hapticFeedback("medium"); }, }, }); diff --git a/assets/js/utils/haptic.ts b/assets/js/utils/haptic.ts index 272d134cd..a1270b6f0 100644 --- a/assets/js/utils/haptic.ts +++ b/assets/js/utils/haptic.ts @@ -1,3 +1,4 @@ -export function hapticFeedback(): void { - navigator.vibrate?.(5); +// the native app shims navigator.vibrate; duration maps to impact strength (<=50 light, <=100 medium) +export function hapticFeedback(style: "light" | "medium" = "light"): void { + navigator.vibrate?.(style === "medium" ? 60 : 5); } diff --git a/assets/js/utils/swipe.ts b/assets/js/utils/swipe.ts index 0dd00250c..3dd49f984 100644 --- a/assets/js/utils/swipe.ts +++ b/assets/js/utils/swipe.ts @@ -1,7 +1,6 @@ interface SwipeOptions { onSwipeLeft?: () => void; onSwipeRight?: () => void; - ignoreSelector?: string; minDistance?: number; maxDuration?: number; } @@ -119,8 +118,7 @@ export function attachSwipeHandler(el: HTMLElement, options: SwipeOptions): () = ignored = true; return; } - const target = e.target as Element | null; - ignored = !!(options.ignoreSelector && target?.closest(options.ignoreSelector)); + ignored = false; startX = e.touches[0].clientX; startY = e.touches[0].clientY; startTime = Date.now(); @@ -139,11 +137,13 @@ export function attachSwipeHandler(el: HTMLElement, options: SwipeOptions): () = else options.onSwipeRight?.(); }; - el.addEventListener("touchstart", onTouchStart, { passive: true }); - el.addEventListener("touchend", onTouchEnd, { passive: true }); + // capture phase, so the touch tooltip gate's stopPropagation on chart elements + // cannot starve the swipe detection + el.addEventListener("touchstart", onTouchStart, { capture: true, passive: true }); + el.addEventListener("touchend", onTouchEnd, { capture: true, passive: true }); return () => { - el.removeEventListener("touchstart", onTouchStart); - el.removeEventListener("touchend", onTouchEnd); + el.removeEventListener("touchstart", onTouchStart, { capture: true }); + el.removeEventListener("touchend", onTouchEnd, { capture: true }); }; } From 6f8a6b8c36024afb29a3afe02cc1fa3976de3c55 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 7 Aug 2026 16:52:58 +0000 Subject: [PATCH 1515/1747] core: fix nil pointer panic when deleting active grid meter (#32607) --- core/site.go | 6 +- core/site_delete_test.go | 161 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 165 insertions(+), 2 deletions(-) create mode 100644 core/site_delete_test.go diff --git a/core/site.go b/core/site.go index 9711fef09..329f464a6 100644 --- a/core/site.go +++ b/core/site.go @@ -881,7 +881,7 @@ func (site *Site) updateGridMeter() error { return nil } - mm := types.Measurement{Name: site.Meters.GridMeterRef} + mm := types.Measurement{Name: site.gridMeter.Config().Name} meter := site.gridMeter.Instance() @@ -935,7 +935,9 @@ func (site *Site) updateGridMeter() error { } } - site.collectors[site.Meters.GridMeterRef].AddEnergy(mm.Energy, mm.ReturnEnergy, mm.Power) + if c, ok := site.collectors[site.gridMeter.Config().Name]; ok { + c.AddEnergy(mm.Energy, mm.ReturnEnergy, mm.Power) + } site.publish(keys.Grid, mm) diff --git a/core/site_delete_test.go b/core/site_delete_test.go new file mode 100644 index 000000000..06681e2d3 --- /dev/null +++ b/core/site_delete_test.go @@ -0,0 +1,161 @@ +package core + +import ( + "testing" + "time" + + "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/core/keys" + "github.com/evcc-io/evcc/core/metrics" + "github.com/evcc-io/evcc/core/types" + "github.com/evcc-io/evcc/server/db" + "github.com/evcc-io/evcc/tariff" + "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" +) + +// deleteTestSite mirrors what Prepare wires up: cached devices plus one energy +// collector per meter, keyed by the meter's config name. +type deleteTestSite struct { + *Site + params chan util.Param +} + +func newDeleteTestSite(t *testing.T, ctrl *gomock.Controller) *deleteTestSite { + t.Helper() + + require.NoError(t, db.NewInstance("sqlite", ":memory:")) + require.NoError(t, metrics.SetupSchema()) + + meter := func(power float64) api.Meter { + m := api.NewMockMeter(ctrl) + m.EXPECT().CurrentPower().Return(power, nil).AnyTimes() + return m + } + + battery := func(power, soc float64) api.Meter { + m := api.NewMockMeter(ctrl) + m.EXPECT().CurrentPower().Return(power, nil).AnyTimes() + b := api.NewMockBattery(ctrl) + b.EXPECT().Soc().Return(soc, nil).AnyTimes() + return &struct { + api.Meter + api.Battery + }{Meter: m, Battery: b} + } + + dev := func(name string, m api.Meter) config.Device[api.Meter] { + return config.NewStaticDevice(config.Named{Name: name}, m) + } + + params := make(chan util.Param, 1024) + + site := NewSite() + site.log = util.NewLogger("foo") + site.valueChan = params + + site.gridMeter = dev("grid", meter(1e3)) + site.pvMeters = []config.Device[api.Meter]{dev("pv", meter(2e3))} + site.batteryMeters = []config.Device[api.Meter]{dev("battery", battery(-500, 42))} + + // collectors as Prepare registers them: keyed by the meter's config name + for _, c := range []struct{ group, name string }{ + {metrics.Grid, "grid"}, + {metrics.PV, "pv"}, + {metrics.Battery, "battery"}, + {metrics.Forecast, metrics.Forecast}, + {metrics.Temperature, metrics.Temperature}, + } { + col, err := metrics.NewCollector(c.group, c.name, c.name) + require.NoError(t, err) + site.collectors[c.name] = col + } + + // site settings pointing at those meters + site.Meters.GridMeterRef = "grid" + site.Meters.PVMetersRef = []string{"pv"} + site.Meters.BatteryMetersRef = []string{"battery"} + + return &deleteTestSite{Site: site, params: params} +} + +// published drains the value channel and returns the last value per key +func (s *deleteTestSite) published() map[string]any { + res := make(map[string]any) + for { + select { + case p := <-s.params: + res[p.Key] = p.Val + default: + return res + } + } +} + +// TestSiteUpdateAfterMeterDeletion deletes the active grid, pv and battery meters while +// running. Only the refs are cleared, the cached devices keep being polled (#32605). +func TestSiteUpdateAfterMeterDeletion(t *testing.T) { + ctrl := gomock.NewController(t) + s := newDeleteTestSite(t, ctrl) + + // baseline: everything still referenced + require.NoError(t, s.updateGridMeter()) + s.updatePvMeters() + s.updateBatteryMeters() + + grid, ok := s.published()[keys.Grid].(types.Measurement) + require.True(t, ok, "grid measurement published") + assert.Equal(t, "grid", grid.Name, "grid measurement carries the meter name") + + // delete all three meters- exactly what deleteDeviceHandler does to the site + s.SetGridMeterRef("") + s.SetPVMeterRefs(nil) + s.SetBatteryMeterRefs(nil) + + require.NotPanics(t, func() { + require.NoError(t, s.updateGridMeter()) + s.updatePvMeters() + s.updateBatteryMeters() + }, "update loop must survive the refs being cleared") + + // the devices are still cached, so the loop keeps producing measurements + pub := s.published() + assert.Equal(t, 2e3, s.pvPower, "pv still polled") + assert.Equal(t, -500.0, s.battery.Power, "battery still polled") + + grid, ok = pub[keys.Grid].(types.Measurement) + require.True(t, ok, "grid measurement still published") + assert.Equal(t, "grid", grid.Name, "grid measurement must keep the meter name, not the cleared ref") +} + +// TestSiteUpdateAfterTariffDeletion deletes a tariff device. cleanupTariffRef only +// rewrites the persisted refs, so the publish path must cope either way. +func TestSiteUpdateAfterTariffDeletion(t *testing.T) { + ctrl := gomock.NewController(t) + s := newDeleteTestSite(t, ctrl) + + solar := api.NewMockTariff(ctrl) + solar.EXPECT().Type().Return(api.TariffTypeSolar).AnyTimes() + solar.EXPECT().Rates().Return(api.Rates{ + {Start: time.Now().Add(-time.Hour), End: time.Now(), Value: 1e3}, + {Start: time.Now(), End: time.Now().Add(time.Hour), Value: 2e3}, + }, nil).AnyTimes() + + s.tariffs = &tariff.Tariffs{Solar: solar} + + require.NotPanics(t, func() { + s.publishTariffs(0, 0) + }, "tariff publish with a solar tariff") + + require.NotNil(t, s.published()[keys.Forecast], "forecast published while the tariff exists") + + // delete the tariff + s.tariffs = &tariff.Tariffs{} + + require.NotPanics(t, func() { + s.publishTariffs(0, 0) + }, "tariff publish must survive the tariff being deleted") +} From afac7ab56d5b84739bb1b33fecb61bbe2a4aedc9 Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Fri, 7 Aug 2026 21:35:14 +0200 Subject: [PATCH 1516/1747] Config UI: fix duration template param handling (#32572) --- .../Config/DeviceModal/ModbusAdvanced.vue | 2 + .../js/components/Config/LoadpointModal.vue | 3 + assets/js/components/Config/PropertyEntry.vue | 10 +- assets/js/components/Config/PropertyField.vue | 69 ++++++++++++-- assets/js/utils/goDuration.test.ts | 76 +++++++++++++++ assets/js/utils/goDuration.ts | 48 ++++++++++ i18n/de.json | 1 + i18n/en.json | 1 + templates/definition/meter/sessy-p1.yaml | 2 +- tests/config-decimal-input.spec.ts | 4 +- tests/config-duration-demo.tpl.yaml | 25 +++++ tests/config-duration-values.sql | 23 +++++ tests/config-duration.spec.ts | 94 +++++++++++++++++++ util/templates/defaults.yaml | 1 + 14 files changed, 346 insertions(+), 13 deletions(-) create mode 100644 assets/js/utils/goDuration.test.ts create mode 100644 assets/js/utils/goDuration.ts create mode 100644 tests/config-duration-demo.tpl.yaml create mode 100644 tests/config-duration-values.sql create mode 100644 tests/config-duration.spec.ts diff --git a/assets/js/components/Config/DeviceModal/ModbusAdvanced.vue b/assets/js/components/Config/DeviceModal/ModbusAdvanced.vue index 212823c84..8193c660c 100644 --- a/assets/js/components/Config/DeviceModal/ModbusAdvanced.vue +++ b/assets/js/components/Config/DeviceModal/ModbusAdvanced.vue @@ -8,6 +8,7 @@ :id="formId('modbusTimeout')" property="timeout" type="Duration" + :label="$t('config.modbus.timeout')" class="me-2" :model-value="timeout" @update:model-value="(v) => $emit('update:timeout', v)" @@ -22,6 +23,7 @@ :id="formId('modbusDelay')" property="delay" type="Duration" + :label="$t('config.modbus.delay')" class="me-2" :model-value="delay" @update:model-value="(v) => $emit('update:delay', v)" diff --git a/assets/js/components/Config/LoadpointModal.vue b/assets/js/components/Config/LoadpointModal.vue index 3f2703bd0..b48f9d1dc 100644 --- a/assets/js/components/Config/LoadpointModal.vue +++ b/assets/js/components/Config/LoadpointModal.vue @@ -192,6 +192,7 @@ id="loadpointEnableDelay" v-model="values.thresholds.enable.delay" type="Duration" + legacy-duration unit="minute" size="w-25 w-min-200" required @@ -255,6 +256,7 @@ id="loadpointDisableDelay" v-model="values.thresholds.disable.delay" type="Duration" + legacy-duration unit="minute" size="w-25 w-min-200" required @@ -519,6 +521,7 @@ id="loadpointPollInterval" v-model="values.soc.poll.interval" type="Duration" + legacy-duration unit="minute" size="w-25 w-min-200" class="me-2" diff --git a/assets/js/components/Config/PropertyEntry.vue b/assets/js/components/Config/PropertyEntry.vue index 7ef34e012..d14c214e6 100644 --- a/assets/js/components/Config/PropertyEntry.vue +++ b/assets/js/components/Config/PropertyEntry.vue @@ -28,10 +28,13 @@ /* oxlint-disable vue/prop-name-casing */ import FormRow from "./FormRow.vue"; import PropertyField from "./PropertyField.vue"; +import formatter from "@/mixins/formatter"; +import { goDurationToUnit, goDurationUnit } from "@/utils/goDuration"; export default { name: "PropertyEntry", components: { FormRow, PropertyField }, + mixins: [formatter], props: { id: String, Name: String, @@ -66,8 +69,11 @@ export default { return this.Description === this.Help ? undefined : this.Help; }, example() { - // hide example text since config ui doesnt use go duration format (e.g. 5m) - return this.Type === "Duration" ? undefined : this.Example; + // show duration example in its own unit, field unit is user-changeable + const unit = this.Type === "Duration" ? goDurationUnit(this.Example) : null; + if (!unit) return this.Example; + const value = goDurationToUnit(this.Example, unit); + return `${value} ${this.fmtDurationUnit(value, unit)}`; }, }, }; diff --git a/assets/js/components/Config/PropertyField.vue b/assets/js/components/Config/PropertyField.vue index 3e016e0b7..b1ba72ff6 100644 --- a/assets/js/components/Config/PropertyField.vue +++ b/assets/js/components/Config/PropertyField.vue @@ -122,8 +122,23 @@
+ + {{ unitValue }} + { - return { selectMode: false }; + return { selectMode: false, unitOverride: null }; }, computed: { patternRegex() { @@ -240,7 +264,7 @@ export default { }, unitValue() { if (this.type === "Duration") { - return this.fmtDurationUnit(this.value, this.unit); + return this.fmtDurationUnit(this.value, this.durationUnit); } if (this.pricePerKWh) { return this.pricePerKWhUnit(this.currency); @@ -297,7 +321,19 @@ export default { return this.choice.length > 0 || this.chargeModes; }, durationFactor() { - return this.unit === "minute" ? 60 : 1; + return displayFactors[this.durationUnit] ?? 1; + }, + durationUnit() { + return this.unitOverride ?? goDurationUnit(this.modelValue) ?? this.unit ?? "second"; + }, + unitSelectable() { + return this.type === "Duration" && !this.legacyDuration && !this.disabled; + }, + unitOptions() { + return durationUnits.map((value) => ({ + value, + name: this.fmtDurationUnit(2, value), + })); }, selectOptions() { if (this.chargeModes) { @@ -343,8 +379,15 @@ export default { return Array.isArray(this.modelValue) ? this.modelValue.join("\n") : ""; } - if (this.type === "Duration" && typeof this.modelValue === "number") { - return this.modelValue / this.durationFactor / NS_PER_SECOND; + if (this.type === "Duration") { + const ns = + typeof this.modelValue === "string" + ? parseGoDuration(this.modelValue) + : this.modelValue; + if (typeof ns === "number") { + return ns / this.durationFactor / NS_PER_SECOND; + } + return ""; } if (this.pricePerKWh) { @@ -367,7 +410,9 @@ export default { } if (this.type === "Duration" && typeof newValue === "number") { - newValue = newValue * this.durationFactor * NS_PER_SECOND; + newValue = this.legacyDuration + ? newValue * this.durationFactor * NS_PER_SECOND + : toGoDuration(newValue, this.durationUnit); } if (this.pricePerKWh) { @@ -385,6 +430,14 @@ export default { } return val; }, + onUnitChange(e) { + // read display value before override changes the getter's unit + const num = this.value; + this.unitOverride = e.target.value; + if (typeof num === "number") { + this.$emit("update:modelValue", toGoDuration(num, this.unitOverride)); + } + }, onFieldChange(e) { // unparsable input (e.g. locale decimal separator mismatch) if (e.target.validity?.badInput) return; diff --git a/assets/js/utils/goDuration.test.ts b/assets/js/utils/goDuration.test.ts new file mode 100644 index 000000000..7ceb51d14 --- /dev/null +++ b/assets/js/utils/goDuration.test.ts @@ -0,0 +1,76 @@ +import { describe, expect, test } from "vite-plus/test"; +import parseGoDuration, { goDurationToUnit, goDurationUnit, toGoDuration } from "./goDuration"; + +describe("parseGoDuration", () => { + test("parses single unit", () => { + expect(parseGoDuration("24h")).toBe(24 * 3.6e12); + expect(parseGoDuration("30m")).toBe(30 * 6e10); + expect(parseGoDuration("45s")).toBe(45e9); + expect(parseGoDuration("500ms")).toBe(5e8); + expect(parseGoDuration("10us")).toBe(1e4); + expect(parseGoDuration("10µs")).toBe(1e4); + expect(parseGoDuration("100ns")).toBe(100); + }); + + test("parses compound durations", () => { + expect(parseGoDuration("1h30m")).toBe(3.6e12 + 30 * 6e10); + expect(parseGoDuration("1m30s")).toBe(6e10 + 30e9); + }); + + test("parses decimals", () => { + expect(parseGoDuration("0.1s")).toBe(1e8); + expect(parseGoDuration("1.5h")).toBe(1.5 * 3.6e12); + }); + + test("rejects invalid input", () => { + expect(parseGoDuration("")).toBeNull(); + expect(parseGoDuration("6")).toBeNull(); + expect(parseGoDuration("1h30")).toBeNull(); + expect(parseGoDuration("abc")).toBeNull(); + expect(parseGoDuration("6 h")).toBeNull(); + }); +}); + +describe("goDurationToUnit", () => { + test("converts to display unit", () => { + expect(goDurationToUnit("10s")).toBe(10); + expect(goDurationToUnit("5m")).toBe(300); + expect(goDurationToUnit("90s", "minute")).toBe(1.5); + expect(goDurationToUnit("3h", "hour")).toBe(3); + expect(goDurationToUnit("6", "hour")).toBeNull(); + }); +}); + +describe("toGoDuration", () => { + test("formats value with unit suffix", () => { + expect(toGoDuration(6, "hour")).toBe("6h"); + expect(toGoDuration(90, "second")).toBe("90s"); + expect(toGoDuration(30, "minute")).toBe("30m"); + expect(toGoDuration(1.5, "hour")).toBe("1.5h"); + expect(toGoDuration(0.1, "second")).toBe("0.1s"); + expect(toGoDuration(0)).toBe("0s"); + expect(toGoDuration(5, "fortnight")).toBe("5s"); + }); + + test("roundtrips through parseGoDuration", () => { + expect(parseGoDuration(toGoDuration(6, "hour"))).toBe(6 * 3.6e12); + expect(parseGoDuration(toGoDuration(1.5, "minute"))).toBe(90e9); + }); +}); + +describe("goDurationUnit", () => { + test("detects single-unit strings", () => { + expect(goDurationUnit("12h")).toBe("hour"); + expect(goDurationUnit("90s")).toBe("second"); + expect(goDurationUnit("30m")).toBe("minute"); + expect(goDurationUnit("1.5h")).toBe("hour"); + }); + + test("null for composite, numbers, empty", () => { + expect(goDurationUnit("1h30m")).toBeNull(); + expect(goDurationUnit("")).toBeNull(); + expect(goDurationUnit(15000000000)).toBeNull(); + expect(goDurationUnit("6")).toBeNull(); + expect(goDurationUnit("500ms")).toBeNull(); + }); +}); diff --git a/assets/js/utils/goDuration.ts b/assets/js/utils/goDuration.ts new file mode 100644 index 000000000..de68d11b3 --- /dev/null +++ b/assets/js/utils/goDuration.ts @@ -0,0 +1,48 @@ +const factors: Record = { + ns: 1, + us: 1e3, + µs: 1e3, + ms: 1e6, + s: 1e9, + m: 6e10, + h: 3.6e12, +}; + +export const displayFactors: Record = { minute: 60, hour: 3600 }; + +export type DurationUnit = "second" | "minute" | "hour"; +type DurationSuffix = "s" | "m" | "h"; + +const unitSuffixes: Record = { second: "s", minute: "m", hour: "h" }; +const suffixUnits: Record = { s: "second", m: "minute", h: "hour" }; + +export const durationUnits = Object.keys(unitSuffixes) as DurationUnit[]; + +// display value in given Intl unit to go duration string ("6h", "90s", "1.5h") +export function toGoDuration(value: number, unit: string = "second"): string { + return `${value}${unitSuffixes[unit as DurationUnit] ?? "s"}`; +} + +// Intl unit of a single-unit go duration string ("12h" > "hour"), null for composite/number/empty +export function goDurationUnit(value: unknown): DurationUnit | null { + if (typeof value !== "string") return null; + const match = value.match(/^\d+(?:\.\d+)?(s|m|h)$/); + return match ? suffixUnits[match[1] as DurationSuffix] : null; +} + +// go duration string to display value in given Intl unit (default second), null if invalid +export function goDurationToUnit(value: string, unit?: string): number | null { + const ns = parseGoDuration(value); + return ns === null ? null : ns / (displayFactors[unit ?? ""] ?? 1) / 1e9; +} + +// go duration string ("24h", "1h30m", "0.1s") to nanoseconds, null if invalid +export default function parseGoDuration(value: string): number | null { + let total = 0; + let matched = ""; + for (const [all, num, unit] of value.matchAll(/(\d+(?:\.\d+)?)(ns|us|µs|ms|s|m|h)/g)) { + total += parseFloat(num!) * factors[unit!]!; + matched += all; + } + return matched && matched === value ? total : null; +} diff --git a/i18n/de.json b/i18n/de.json index 6c651dd52..bd29b8201 100644 --- a/i18n/de.json +++ b/i18n/de.json @@ -306,6 +306,7 @@ "form": { "danger": "Achtung", "deprecated": "veraltet", + "durationUnit": "Zeiteinheit für {label}", "example": "Beispiel", "optional": "optional" }, diff --git a/i18n/en.json b/i18n/en.json index 48a81d482..91eb2eddb 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -306,6 +306,7 @@ "form": { "danger": "Danger", "deprecated": "deprecated", + "durationUnit": "Time unit for {label}", "example": "Example", "optional": "optional" }, diff --git a/templates/definition/meter/sessy-p1.yaml b/templates/definition/meter/sessy-p1.yaml index 8fc06991b..68a56c375 100644 --- a/templates/definition/meter/sessy-p1.yaml +++ b/templates/definition/meter/sessy-p1.yaml @@ -8,7 +8,7 @@ params: choice: ["grid"] - name: host - name: cache - default: 10 + default: 10s render: | type: custom power: diff --git a/tests/config-decimal-input.spec.ts b/tests/config-decimal-input.spec.ts index 7974117d4..1ba0c8108 100644 --- a/tests/config-decimal-input.spec.ts +++ b/tests/config-decimal-input.spec.ts @@ -31,12 +31,12 @@ test("decimal input with german separator", async ({ page }) => { await modal.getByRole("button", { name: "Show advanced settings" }).click(); // comma accepted as decimal separator - const timeout = modal.getByLabel("Timeout"); + const timeout = modal.getByRole("spinbutton", { name: "Timeout" }); await timeout.pressSequentially("0,5"); await expect(timeout).toHaveValue("0.5"); // intermediate zeros survive typing - const delay = modal.getByLabel("Delay"); + const delay = modal.getByRole("spinbutton", { name: "Delay" }); await delay.pressSequentially("0.004"); await expect(delay).toHaveValue("0.004"); await delay.blur(); diff --git a/tests/config-duration-demo.tpl.yaml b/tests/config-duration-demo.tpl.yaml new file mode 100644 index 000000000..420fe1336 --- /dev/null +++ b/tests/config-duration-demo.tpl.yaml @@ -0,0 +1,25 @@ +template: duration-demo +group: generic +products: + - description: + generic: Duration Demo Meter +params: + - name: usage + choice: ["grid"] + - name: shortduration + type: duration + default: 30s + example: 10s + description: + generic: Short duration + - name: longduration + type: duration + unit: hour + default: 3h + description: + generic: Long duration +render: | + type: custom + power: + source: const + value: 0 diff --git a/tests/config-duration-values.sql b/tests/config-duration-values.sql new file mode 100644 index 000000000..7fa46fbe1 --- /dev/null +++ b/tests/config-duration-values.sql @@ -0,0 +1,23 @@ +BEGIN; + +CREATE TABLE `configs` ( + `id` integer PRIMARY KEY AUTOINCREMENT + , `class` integer + , `type` text + , `title` text + , `icon` text + , `product` text + , `value` text +); +CREATE TABLE `settings` ( + `key` text + , `value` text + , PRIMARY KEY(`key`) +); + +-- longduration as duration string (UI-written format), shortduration as legacy nanosecond number +INSERT INTO configs(id, class, type, title, icon, product, value) VALUES(1, 2, 'template', 'Duration Test', '', 'Duration Demo Meter', '{"template":"duration-demo","usage":"grid","longduration":"12h","shortduration":15000000000}'); + +INSERT INTO settings("key", value) VALUES('gridMeter', 'db:1'); + +COMMIT; diff --git a/tests/config-duration.spec.ts b/tests/config-duration.spec.ts new file mode 100644 index 000000000..e1f3f9be3 --- /dev/null +++ b/tests/config-duration.spec.ts @@ -0,0 +1,94 @@ +import { test, expect, type Page, type Locator } from "@playwright/test"; +import { start, stop, restart, baseUrl } from "./evcc"; +import { expectModalVisible, expectModalHidden } from "./utils"; + +const templateFlags = [ + "--disable-auth", + "--template-type", + "meter", + "--template", + "tests/config-duration-demo.tpl.yaml", +]; + +test.use({ baseURL: baseUrl() }); + +test.afterEach(async () => { + await stop(); +}); + +async function openGridMeterModal(page: Page): Promise { + await page.goto("/#/config"); + await page.getByTestId("grid").getByRole("button", { name: "edit" }).click(); + const modal = page.getByTestId("meter-modal"); + await expectModalVisible(modal); + return modal; +} + +test.describe("duration fields", async () => { + test("template defaults and roundtrip", async ({ page }) => { + await start(undefined, undefined, templateFlags); + await page.goto("/#/config"); + await page.getByRole("button", { name: "Add grid meter" }).click(); + const modal = page.getByTestId("meter-modal"); + await expectModalVisible(modal); + await modal.getByLabel("Manufacturer").selectOption("Duration Demo Meter"); + + await expect(modal.getByRole("spinbutton", { name: "Short duration" })).toHaveValue("30"); + await expect(modal.getByText("Example: 10 seconds", { exact: true })).toBeVisible(); + await expect(modal.getByLabel("Time unit for Short duration")).toHaveValue("second"); + await expect(modal.getByRole("spinbutton", { name: "Long duration" })).toHaveValue("3"); + await expect(modal.getByLabel("Time unit for Long duration")).toHaveValue("hour"); + + await modal.getByRole("spinbutton", { name: "Long duration" }).fill("6"); + await modal.getByRole("button", { name: "Save" }).click(); + await expectModalHidden(modal); + + await restart(undefined, templateFlags); + await page.reload(); + + // edited value and untouched default are both stored as duration strings + const reopened = await openGridMeterModal(page); + await expect(reopened.getByRole("spinbutton", { name: "Long duration" })).toHaveValue("6"); + await expect(reopened.getByRole("spinbutton", { name: "Short duration" })).toHaveValue("30"); + }); + + test("existing values as string and nanoseconds", async ({ page }) => { + await start(undefined, "config-duration-values.sql", templateFlags); + + const modal = await openGridMeterModal(page); + await expect(modal.getByRole("spinbutton", { name: "Long duration" })).toHaveValue("12"); + await expect(modal.getByRole("spinbutton", { name: "Short duration" })).toHaveValue("15"); + + await modal.getByRole("button", { name: "Save" }).click(); + await expectModalHidden(modal); + + await restart(undefined, templateFlags); + await page.reload(); + + const reopened = await openGridMeterModal(page); + await expect(reopened.getByRole("spinbutton", { name: "Long duration" })).toHaveValue("12"); + await expect(reopened.getByRole("spinbutton", { name: "Short duration" })).toHaveValue("15"); + }); + + test("unit switching keeps number", async ({ page }) => { + await start(undefined, "config-duration-values.sql", templateFlags); + + // stored "12h" drives the unit, ns number falls back to template default + const modal = await openGridMeterModal(page); + await expect(modal.getByLabel("Time unit for Long duration")).toHaveValue("hour"); + await expect(modal.getByLabel("Time unit for Short duration")).toHaveValue("second"); + + await modal.getByLabel("Time unit for Long duration").selectOption("minutes"); + await expect(modal.getByRole("spinbutton", { name: "Long duration" })).toHaveValue("12"); + await modal.getByRole("button", { name: "Save" }).click(); + await expectModalHidden(modal); + + await restart(undefined, templateFlags); + await page.reload(); + + // stored "12m" restores the selected unit + const reopened = await openGridMeterModal(page); + await expect(reopened.getByRole("spinbutton", { name: "Long duration" })).toHaveValue("12"); + await expect(reopened.getByLabel("Time unit for Long duration")).toHaveValue("minute"); + }); +}); diff --git a/util/templates/defaults.yaml b/util/templates/defaults.yaml index f392968a6..262a67f1e 100644 --- a/util/templates/defaults.yaml +++ b/util/templates/defaults.yaml @@ -489,6 +489,7 @@ params: en: Battery storage unit index de: Nummer des Batteriespeichers - name: delay + type: duration description: en: Delay de: Verzögerung From daa3df4b4ead65a25786b096c65139b589253755 Mon Sep 17 00:00:00 2001 From: mi-ch-a Date: Sat, 8 Aug 2026 10:44:45 +0200 Subject: [PATCH 1517/1747] Solplanet AI dongle: add battery control (#31908) --- .../definition/meter/solplanet-ai-dongle.yaml | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/templates/definition/meter/solplanet-ai-dongle.yaml b/templates/definition/meter/solplanet-ai-dongle.yaml index 979dbb046..4db87cec8 100644 --- a/templates/definition/meter/solplanet-ai-dongle.yaml +++ b/templates/definition/meter/solplanet-ai-dongle.yaml @@ -8,9 +8,12 @@ requirements: de: | Nutzt die lokale HTTP-Schnittstelle des AI-Dongle (z.B. BA121T-30) statt Modbus TCP. Die Seriennummer (isn) des Wechselrichters kann über `http://:8484/getdev.cgi?device=2` ausgelesen werden. + Ladeleistung der Batteriesteuerung ist auf 5000 W fest limitiert. en: | Uses the AI-Dongle local HTTP API (e.g. BA121T-30) instead of Modbus TCP. The inverter serial number (isn) can be read via `http://:8484/getdev.cgi?device=2`. + Battery control charge power is fixed at 5000 W. +capabilities: ["battery-control"] params: - name: usage choice: ["grid", "pv", "battery"] @@ -44,5 +47,38 @@ render: | source: http uri: http://{{ .host }}:8484/getdevdata.cgi?device=4&sn={{ .serial }} jq: .soc + batterymode: + source: switch + switch: + - case: 1 # normal + set: + source: http + <<: &fdbg + uri: http://{{ .host }}:8484/fdbg.cgi + method: POST + timeout: 20s + headers: + Content-Type: application/json + body: '{"data":"0306044f0002390e"}' # write reg 1103 (mode) = 2 (normal) + - case: 2 # hold: custom mode + 0 W + set: + source: sequence + set: + - source: http + <<: *fdbg + body: '{"data":"0306044f0004b90c"}' # write reg 1103 (mode) = 4 (custom) + - source: http + <<: *fdbg + body: '{"data":"03060480000088f0"}' # write reg 1152 (setpoint) = 0 W + - case: 3 # charge: custom mode + fixed 5000 W (see requirements) + set: + source: sequence + set: + - source: http + <<: *fdbg + body: '{"data":"0306044f0004b90c"}' # write reg 1103 (mode) = 4 (custom) + - source: http + <<: *fdbg + body: '{"data":"03060480ec78c412"}' # write reg 1152 (setpoint) = -5000 W {{- include "battery-params" . }} {{- end }} From 6a976286d1808527d6b2440e942770946c35ece4 Mon Sep 17 00:00:00 2001 From: andig Date: Sat, 8 Aug 2026 12:45:07 +0200 Subject: [PATCH 1518/1747] chore: ugprade modules --- go.mod | 56 +++++++++++++-------------- go.sum | 120 ++++++++++++++++++++++++++++----------------------------- 2 files changed, 88 insertions(+), 88 deletions(-) diff --git a/go.mod b/go.mod index 1ebe485dc..2bd90efe2 100644 --- a/go.mod +++ b/go.mod @@ -8,15 +8,15 @@ require ( github.com/Masterminds/sprig/v3 v3.3.0 github.com/PanterSoft/comlynx-go v0.1.0 github.com/PuerkitoBio/goquery v1.12.0 - github.com/WulfgarW/sensonet v0.0.7 + github.com/WulfgarW/sensonet v0.0.8 github.com/andig/go-powerwall v0.3.0 github.com/andig/gosunspec v0.0.0-20260705113727-6d585e133512 github.com/andig/mbserver v0.0.0-20230310211055-1d29cbb5820e github.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef - github.com/aws/aws-sdk-go-v2 v1.43.2 - github.com/aws/aws-sdk-go-v2/config v1.32.33 - github.com/aws/aws-sdk-go-v2/credentials v1.19.32 - github.com/aws/aws-sdk-go-v2/service/cognitoidentity v1.36.2 + github.com/aws/aws-sdk-go-v2 v1.43.4 + github.com/aws/aws-sdk-go-v2/config v1.32.35 + github.com/aws/aws-sdk-go-v2/credentials v1.19.34 + github.com/aws/aws-sdk-go-v2/service/cognitoidentity v1.36.4 github.com/basgys/goxml2json v1.1.0 github.com/benbjohnson/clock v1.3.5 github.com/bogosj/tesla v1.3.2-0.20250818120641-a31b7b6396c9 @@ -33,14 +33,14 @@ require ( github.com/enbility/ship-go v0.6.1-0.20260720110450-0aa90f64ac76 github.com/enbility/spine-go v0.7.1-0.20260629113257-b3bcc643f323 github.com/evcc-io/openapi-mcp v0.6.1-0.20260701153510-26c442199ef4 - github.com/evcc-io/optimizer v0.0.0-20260728084523-a51031d7607f + github.com/evcc-io/optimizer v0.0.0-20260808102051-bc47435b4068 github.com/evcc-io/rct v0.2.0 github.com/evcc-io/tesla-proxy-client v0.0.0-20260722080120-05ada06c7368 github.com/fatih/structs v1.1.0 - github.com/getkin/kin-openapi v0.145.0 + github.com/getkin/kin-openapi v0.146.0 github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1 github.com/go-playground/validator/v10 v10.30.3 - github.com/go-telegram/bot v1.22.0 + github.com/go-telegram/bot v1.23.0 github.com/go-viper/mapstructure/v2 v2.5.0 github.com/godbus/dbus/v5 v5.2.2 github.com/gokrazy/updater v0.0.0-20260620140544-0a84d8ab3878 @@ -69,17 +69,17 @@ require ( github.com/koron/go-ssdp v0.9.1 github.com/korylprince/ipnetgen v1.0.1 github.com/libp2p/zeroconf/v2 v2.2.0 - github.com/libtnb/sqlite v1.2.1 + github.com/libtnb/sqlite v1.2.2 github.com/lorenzodonini/ocpp-go v0.19.0 github.com/lunixbochs/struc v0.0.0-20241101090106-8d528fa2c543 github.com/mabunixda/wattpilot v1.8.5 github.com/mitchellh/go-homedir v1.1.0 github.com/modelcontextprotocol/go-sdk v1.7.0 github.com/muka/go-bluetooth v0.0.0-20240701044517-04c4f09c514e - github.com/nicholas-fedor/shoutrrr v0.16.3 + github.com/nicholas-fedor/shoutrrr v0.17.0 github.com/nicksnyder/go-i18n/v2 v2.6.1 github.com/olekukonko/tablewriter v1.1.4 - github.com/philippseith/signalr v0.8.0 + github.com/philippseith/signalr v0.10.0 github.com/prometheus-community/pro-bing v0.9.1 github.com/prometheus/client_golang v1.24.1 github.com/prometheus/common v0.70.1 @@ -99,7 +99,7 @@ require ( github.com/teslamotors/vehicle-command v0.4.1 github.com/tess1o/go-ecoflow v1.1.1-0.20251003083510-2ccc15a17e29 github.com/traefik/yaegi v0.16.1 - github.com/volkszaehler/mbmd v0.0.0-20260623211432-fb8f9c22f121 + github.com/volkszaehler/mbmd v0.0.0-20260803093438-96221ccf34be github.com/warthog618/go-gpiocdev v0.9.1 github.com/xuri/excelize/v2 v2.11.0 gitlab.com/bboehmke/sunny v0.17.0 @@ -107,7 +107,7 @@ require ( go.uber.org/mock v0.6.0 go.yaml.in/yaml/v4 v4.0.0-rc.6 golang.org/x/crypto v0.54.0 - golang.org/x/crypto/x509roots/fallback v0.0.0-20260723152544-d701c51f7e4e + golang.org/x/crypto/x509roots/fallback v0.0.0-20260804050112-5ed494470b06 golang.org/x/exp v0.0.0-20260727155853-b88d891fe743 golang.org/x/net v0.57.0 golang.org/x/oauth2 v0.36.0 @@ -117,7 +117,7 @@ require ( google.golang.org/grpc v1.83.0 google.golang.org/protobuf v1.36.11 gorm.io/gorm v1.31.2 - modernc.org/sqlite v1.55.0 + modernc.org/sqlite v1.56.0 ) require ( @@ -126,17 +126,17 @@ require ( github.com/andybalholm/cascadia v1.3.3 // indirect github.com/antihax/optional v1.0.0 // indirect github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.33 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.33 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.33 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.34 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.14 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.33 // indirect - github.com/aws/aws-sdk-go-v2/service/signin v1.5.2 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.33.2 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.2 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.45.2 // indirect - github.com/aws/smithy-go v1.27.5 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.35 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.35 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.35 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.36 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.15 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.35 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.5.4 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.33.4 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.4 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.45.4 // indirect + github.com/aws/smithy-go v1.27.6 // indirect github.com/azihsoyn/rijndael256 v0.0.0-20200316065338-d14eefa2b66b // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bitly/go-simplejson v0.5.1 // indirect @@ -164,7 +164,7 @@ require ( github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a // indirect github.com/go-jose/go-jose/v4 v4.1.4 // indirect github.com/go-kit/log v0.2.1 // indirect - github.com/go-logfmt/logfmt v0.6.0 // indirect + github.com/go-logfmt/logfmt v0.6.1 // indirect github.com/go-openapi/jsonpointer v0.22.5 // indirect github.com/go-openapi/swag/jsonname v0.25.5 // indirect github.com/go-playground/locales v0.14.1 // indirect @@ -213,7 +213,7 @@ require ( github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/procfs v0.21.1 // indirect github.com/quic-go/qpack v0.6.0 // indirect - github.com/quic-go/quic-go v0.60.0 // indirect + github.com/quic-go/quic-go v0.61.0 // indirect github.com/quic-go/webtransport-go v0.11.1 // indirect github.com/relvacode/iso8601 v1.6.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect @@ -250,7 +250,7 @@ require ( gopkg.in/go-playground/validator.v9 v9.30.0 // indirect gopkg.in/sourcemap.v1 v1.0.5 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - modernc.org/libc v1.74.1 // indirect + modernc.org/libc v1.74.4 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect ) diff --git a/go.sum b/go.sum index ecbbad8af..4cd766dd8 100644 --- a/go.sum +++ b/go.sum @@ -20,8 +20,8 @@ github.com/PuerkitoBio/goquery v1.12.0/go.mod h1:802ej+gV2y7bbIhOIoPY5sT183ZW0YF github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= github.com/Shopify/toxiproxy v2.1.4+incompatible h1:TKdv8HiTLgE5wdJuEML90aBgNWsokNbMijUGhmcoBJc= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= -github.com/WulfgarW/sensonet v0.0.7 h1:WkrZF7UNoQVyQIvpaUPqZfKTvHfFXTRmSF/zMQKJo+Q= -github.com/WulfgarW/sensonet v0.0.7/go.mod h1:GNOik1nTPswwtZ4bLpIRglEn9dNgEG79iOhVUq4IKgA= +github.com/WulfgarW/sensonet v0.0.8 h1:CkKzQuGTmZiYQPOI7t9tYCynM0mVnl1UNWyiSY5a3cE= +github.com/WulfgarW/sensonet v0.0.8/go.mod h1:uId+Lbatrz6CsUMXfj+zsRPdT5KHrh3H8YOk0xZU/9E= github.com/andig/eebus-go v0.0.0-20260725155950-e735091ff165 h1:mgx3N1XD29NtCtvbm1CEMJpz8lBUgMiSdH0Ku72pDP4= github.com/andig/eebus-go v0.0.0-20260725155950-e735091ff165/go.mod h1:L1nBEy/dBASQRoz/9JF7KEmqFdnXHLbZqDpxiA0ezr4= github.com/andig/go-powerwall v0.3.0 h1:6g50+8xEwxrnzoSaPsxNRpc2fld8gARk0X78NEexFGI= @@ -40,36 +40,36 @@ github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7D github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= github.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef h1:2JGTg6JapxP9/R33ZaagQtAM4EkkSYnIAlOG5EI8gkM= github.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef/go.mod h1:JS7hed4L1fj0hXcyEejnW57/7LCetXggd+vwrRnYeII= -github.com/aws/aws-sdk-go-v2 v1.43.2 h1:cl+IXwWb3qazClUcm08tGSsB6OiuV83JVJO9B0jQcPc= -github.com/aws/aws-sdk-go-v2 v1.43.2/go.mod h1:WEzLKBh/mEjXvx1FtQMWgSxMSTVqxQzjkRtk5fa3wkg= -github.com/aws/aws-sdk-go-v2/config v1.32.33 h1:M1m/Q6f0OKDEDGwhiNOqx1OjTdrewe3v+GDbHmKczWk= -github.com/aws/aws-sdk-go-v2/config v1.32.33/go.mod h1:fGj1iQj2QpIZzp7jE4aQQ+71TE8cd4z9K4+xCd6EqmE= -github.com/aws/aws-sdk-go-v2/credentials v1.19.32 h1:eNE0JnIblBo1NCvd3tqEYuZz9XDefn69R74CHd3nT7U= -github.com/aws/aws-sdk-go-v2/credentials v1.19.32/go.mod h1:yYJu+6tqKUYZuJSYcpSGjz/6sV/SUaAaKIufnWKx2OU= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.33 h1:MobhiR6KIerWxmO74Zit5I3379+mSc2DOdZ3DeRFB9w= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.33/go.mod h1:xu02847OdZfNr/jAfZpHtyRk0b3v4d0kaoxNHxZGG/w= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.33 h1:HAp1wLFZzch054uh3FK7rcVYg4v7J2FxVf3h3IGNZas= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.33/go.mod h1:mJk5fmqnF+WUlMdPG37pR2Fh3oh6r8F6ZGUgPKvzu0c= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.33 h1:0YA0aCKgsJyno6xkFfaIgjE3/wK08+Qxo9nQfe1UrWM= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.33/go.mod h1:UZqj4WIdTH+ga8Y/DgpAuy/8cGjM3h7gDCliJYGg2SE= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.34 h1:HQYnjFnXpX8EbPW5M1QT8mXzesRPwly0HEPTcFlS02Y= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.34/go.mod h1:tGzj56niKYZBbDIRhwPGDqrULzmWv5b6uBQGqyNaFZw= -github.com/aws/aws-sdk-go-v2/service/cognitoidentity v1.36.2 h1:ir+wsl0RWI1HMe1OeJmKMhDjq0GGdnmkQnAxlDaliJA= -github.com/aws/aws-sdk-go-v2/service/cognitoidentity v1.36.2/go.mod h1:EqAg5u9NN5mlqjCJUj+eQkf0kjDPb47SrYdXWp4SCA8= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.14 h1:SA43nfaY7+1jjMNIc2ywu99JLJLButtIdLP6j+bT870= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.14/go.mod h1:Du3llKcwbQvHsTXSLzTOGQz0DTDBMEzdg7DAGu7inrY= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.33 h1:mqI7OrxN/DUH85F5OqVn3cIfuZ3+HVcebUm2N8mLlgQ= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.33/go.mod h1:eZ5jdEpvaaOU8nWWE4cTAJETSEA5FZoWxvNRao4piHY= -github.com/aws/aws-sdk-go-v2/service/signin v1.5.2 h1:EjI1CZzDcBxPkTa3j1BdtIrUDbqnOGssFMeyUS+6W0I= -github.com/aws/aws-sdk-go-v2/service/signin v1.5.2/go.mod h1:vN3eb5H8MEAZ4dx0F5Wc9LT8eb3eW7bZZ5BjGJdbw9k= -github.com/aws/aws-sdk-go-v2/service/sso v1.33.2 h1:zMP1FDFE08L7sM5f1QqkH/ZgKKg8Uc0Dz7KhSSYqWkw= -github.com/aws/aws-sdk-go-v2/service/sso v1.33.2/go.mod h1:0LoIZSUKjdo2BleHfT1hv/jlD33LQS00IrBlzoUsoUQ= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.2 h1:9eTqUYl+SyVmaRPMyBXSO9wwqC6TRwZB82pKENK2hdQ= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.2/go.mod h1:DThweuz22kiLc7lGHop5vQ9c3bx5W6Azs/YqSHa2fu8= -github.com/aws/aws-sdk-go-v2/service/sts v1.45.2 h1:EJd8vZO3E8SE6nmPqxuxlQ1NeSb8as50sf6eGdV4Saw= -github.com/aws/aws-sdk-go-v2/service/sts v1.45.2/go.mod h1:OgpPvKzsO2Ranjpli/20djMkg6UrV5mw4W3pZpq1Mqo= -github.com/aws/smithy-go v1.27.5 h1:d1ro7KpYOYwP6m73YFa+Kc/A130VsAdX68SpsJwARMM= -github.com/aws/smithy-go v1.27.5/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/aws/aws-sdk-go-v2 v1.43.4 h1:b9FTvbRwy+JCsfp2Wp6wV/KbOx3Aj7nkoFb2cRX0IhE= +github.com/aws/aws-sdk-go-v2 v1.43.4/go.mod h1:70vwSy16txshwG+g55WkpgPKDIByzHI8ccBsOteo3bQ= +github.com/aws/aws-sdk-go-v2/config v1.32.35 h1:UEzXuET8E42lxBPijuACu/tEK7v5lFPlk0Q+GT5WD9E= +github.com/aws/aws-sdk-go-v2/config v1.32.35/go.mod h1:KaMtJpFa2JlL2BStjjHQVwQpzZEmw+ND/EgVrfFoo2g= +github.com/aws/aws-sdk-go-v2/credentials v1.19.34 h1:y6GkSmcv5myd1ngrYbGmiLlwQqB6TQhOuN/tbSSuWDY= +github.com/aws/aws-sdk-go-v2/credentials v1.19.34/go.mod h1:w3dTcnDVoQIewjo7JG45hduAToikiIFLC4FIO7fndvw= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.35 h1:+S7kbJoLDDQ5tE+lHrUBgMkzC8NLgsaioS2F3dVoFAE= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.35/go.mod h1:Ak7xXviIARfFdNUJ9Etb0bdVDt/KAvKjMGJVLWXDzik= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.35 h1:kzVuGlatQtYinwBJEEyLAbggepCoavosiaHHX9+fD+c= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.35/go.mod h1:0yLx0yEI+SfqeJMPvOtIEFoZbiQYXMGszBueiutQyaI= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.35 h1:WK6CjihTuLisCjSKKbildJ79sGZZgbBz3iNa7VsKIhU= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.35/go.mod h1:KYleN57luLoe97R7vTnx8PMcVrr9gAcRECtOjl91DNg= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.36 h1:jbGY4CXLzZElOXgGsexlC3Hi+3YM0rSmk4opFXKqg/k= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.36/go.mod h1:uBu/9aKsS/UQGc72RAt3y54kjgYQxmhut8ZD2dXCDNE= +github.com/aws/aws-sdk-go-v2/service/cognitoidentity v1.36.4 h1:ajrCz05CyO6ijM0WXGO4Mb3NNzrL9h+KjJgOS24x25M= +github.com/aws/aws-sdk-go-v2/service/cognitoidentity v1.36.4/go.mod h1:nOtzj3w1wGSA1kn0yWw1jObneu9GBhcKZfZkcOWxpdM= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.15 h1:JJLBQxwY+AFwuPAi5ivGc1ChnTdUt4cXMv7e76m2c/Y= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.15/go.mod h1:lQknBIe78MVL0cQOQDlag8KGflMbMEVFx9mB6O8ENvk= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.35 h1:BBEElKh4a+rKshvjrfpajTe9CbpZvrbb4Jkg2PB7RzA= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.35/go.mod h1:zaZk983w//8beSruBVec/mr4CmDwgZitW/qzGhAAX0g= +github.com/aws/aws-sdk-go-v2/service/signin v1.5.4 h1:cOJELVNrq5Q3Udry2GLuHUM7MhwpeaQRdYaoa6GI/yI= +github.com/aws/aws-sdk-go-v2/service/signin v1.5.4/go.mod h1:f4LxzKBtaTxD7xh3PiVg3CE1tchQemfmghaJr+NbK2c= +github.com/aws/aws-sdk-go-v2/service/sso v1.33.4 h1:AMW7a7S8iQaHjBYZdU3PCq4GKRPijTPRAc7e6XtEThY= +github.com/aws/aws-sdk-go-v2/service/sso v1.33.4/go.mod h1:QQNsFV1DVXoXcZt18FS8lI8rtUrlDyAuWZLQ5shunv4= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.4 h1:AsbZcJAQPRmHDJG8K1N0pof/1zPWjVT8TFlTWuGLSvo= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.4/go.mod h1:6imqztH0//t0mKbl6yWl7swSEl7F/w32oAmqB3vP1ag= +github.com/aws/aws-sdk-go-v2/service/sts v1.45.4 h1:w/AryDYMjSUANSQ2uoZxJovUsMTwWJNTv3IMex30Y+4= +github.com/aws/aws-sdk-go-v2/service/sts v1.45.4/go.mod h1:WeBiAa67azG7Su9Vf+ChGDBLiAozJCXzdjXiPBUwtbc= +github.com/aws/smithy-go v1.27.6 h1:0zjT8jgK3jbrTT7JJ3EE6JsMhX8JTrZ+f1sEndYDXrA= +github.com/aws/smithy-go v1.27.6/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/azihsoyn/rijndael256 v0.0.0-20200316065338-d14eefa2b66b h1:/2dABok/UswXOj5rjbR5bZ411ApGBq1pAEZdy5rvFrY= github.com/azihsoyn/rijndael256 v0.0.0-20200316065338-d14eefa2b66b/go.mod h1:ef+2vMUkiKcy2Tz7HykB01KbgUnkK4gQKq4ZeR4RYVs= github.com/basgys/goxml2json v1.1.0 h1:4ln5i4rseYfXNd86lGEB+Vi652IsIXIvggKM/BhUKVw= @@ -152,8 +152,8 @@ github.com/evcc-io/ocpp-go v0.0.0-20260727074919-195c10b8758d h1:UxgSUmwiuVURrma github.com/evcc-io/ocpp-go v0.0.0-20260727074919-195c10b8758d/go.mod h1:2kcukDdhui4u730VfnYVWuwzDLgw+mBRGDir/QAyBhg= github.com/evcc-io/openapi-mcp v0.6.1-0.20260701153510-26c442199ef4 h1:aGM3NwDsKbnmiaO10ptbvEolRAUThWJEsObiAWFDU/c= github.com/evcc-io/openapi-mcp v0.6.1-0.20260701153510-26c442199ef4/go.mod h1:YyOx4zwr6xVUcchAETHERZ+75cZMIrdcjVIZFwBlE1Q= -github.com/evcc-io/optimizer v0.0.0-20260728084523-a51031d7607f h1:mwExmgCCH8ldBLe8qNIXmMy7qT+OTsptXwnI6LQo+mQ= -github.com/evcc-io/optimizer v0.0.0-20260728084523-a51031d7607f/go.mod h1:cHMeq6GfoXQ8LxqY4LH1wn/hrgYEn+ka5RPI1CF37SY= +github.com/evcc-io/optimizer v0.0.0-20260808102051-bc47435b4068 h1:qB2mxyFU9kLz4eXtVWTLnKoW/FrPLclxtDYS21fgNfc= +github.com/evcc-io/optimizer v0.0.0-20260808102051-bc47435b4068/go.mod h1:cHMeq6GfoXQ8LxqY4LH1wn/hrgYEn+ka5RPI1CF37SY= github.com/evcc-io/rct v0.2.0 h1:qjXKI7NKwW5YpEKEb27MwLMBaR4ne2Kd2cPV2PYTKn4= github.com/evcc-io/rct v0.2.0/go.mod h1:n6MTBU36QOadGlxxiADu86VaT5l0eYdbmFBHF14AD/s= github.com/evcc-io/tesla-proxy-client v0.0.0-20260722080120-05ada06c7368 h1:Vek18PwCpXWBVaNfzq7qwru0VsQgWtlL5yIQbLqFwWg= @@ -171,8 +171,8 @@ github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx5 github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= -github.com/getkin/kin-openapi v0.145.0 h1:htBX+Q7SevVaCUqymFegUKzH2WCbewl9tsmyn2FMGWY= -github.com/getkin/kin-openapi v0.145.0/go.mod h1:3BH9M9XDe/y9M5DSvEocVYAYq1w0qrhJHjC/vZi0AaY= +github.com/getkin/kin-openapi v0.146.0 h1:RA/1RdxrSJW4oc1+6IfnYB6AO9CaGy8GTKPh0k4Ordo= +github.com/getkin/kin-openapi v0.146.0/go.mod h1:3BH9M9XDe/y9M5DSvEocVYAYq1w0qrhJHjC/vZi0AaY= github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1 h1:zga7zaRE8HCbWjcXMDlfvmQtH0/kMVLo7cQ48dy6kWg= github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1/go.mod h1:PumS+5d59wmAGsZo6IfRpVNaJUq+6xjC4Utt/k8GO6Q= github.com/go-http-utils/fresh v0.0.0-20161124030543-7231e26a4b27 h1:O6yi4xa9b2DMosGsXzlMe2E9qXgXCVkRLCoRX+5amxI= @@ -183,8 +183,8 @@ github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= -github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= -github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE= +github.com/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/z6CO0XAk= github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8= github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= @@ -207,8 +207,8 @@ github.com/go-playground/validator/v10 v10.30.3 h1:4MU6YkEwx7GbcPJOZxrtbu+QfF3pJ github.com/go-playground/validator/v10 v10.30.3/go.mod h1:4Axh7oCNGcoGkqLoE4YWt6n20mcEIsPRlB7vPk3lpyc= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= -github.com/go-telegram/bot v1.22.0 h1:zK29OoTYMmR5emJrCtGa2SjaGleeZiUB/C1i7kc2lXE= -github.com/go-telegram/bot v1.22.0/go.mod h1:i2TRs7fXWIeaceF3z7KzsMt/he0TwkVC680mvdTFYeM= +github.com/go-telegram/bot v1.23.0 h1:CKKQq115G/GUGBG8uuWl5uXbiBHyVjZBp/qqOLWZjJk= +github.com/go-telegram/bot v1.23.0/go.mod h1:i2TRs7fXWIeaceF3z7KzsMt/he0TwkVC680mvdTFYeM= github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= @@ -245,8 +245,8 @@ github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/jsonschema-go v0.4.3 h1:/DBOLZTfDow7pe2GmaJNhltueGTtDKICi8V8p+DQPd0= github.com/google/jsonschema-go v0.4.3/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= -github.com/google/pprof v0.0.0-20260709232956-b9395ee17fa0 h1:du0WGc8xSKq/++e0cglxhS/mXVqsR7+c7jLEi5Vqduw= -github.com/google/pprof v0.0.0-20260709232956-b9395ee17fa0/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= +github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3 h1:LMLX+LgTNWpfvCBdFebv6EsYotImrt/Ppc5cXIriCSo= +github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3/go.mod h1:jl5iWTm0/hd5PjEYEOuwAJ57L/CibdZfrqZ5XA5GrCk= github.com/google/renameio/v2 v2.0.0 h1:UifI23ZTGY8Tt29JbYFiuyIU3eX+RNFtUwefq9qAhxg= github.com/google/renameio/v2 v2.0.0/go.mod h1:BtmJXm5YlszgC+TD4HOEEUFgkJP3nLxehU6hfe7jRt4= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -343,8 +343,8 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/libp2p/zeroconf/v2 v2.2.0 h1:Cup06Jv6u81HLhIj1KasuNM/RHHrJ8T7wOTS4+Tv53Q= github.com/libp2p/zeroconf/v2 v2.2.0/go.mod h1:fuJqLnUwZTshS3U/bMRJ3+ow/v9oid1n0DmyYyNO1Xs= -github.com/libtnb/sqlite v1.2.1 h1:lrhN3yG49pj1gmtMMP6Es0lhKIkzDeqBul2EWOLyjpY= -github.com/libtnb/sqlite v1.2.1/go.mod h1:pGq13BirCjkM6ihkWWxs0PGFhBiwUyA0PqdSmgDWfmM= +github.com/libtnb/sqlite v1.2.2 h1:Ku5hAPP5B3A4kQcDn4Z3qyNMafKzuAbnFZX4oD9wR4I= +github.com/libtnb/sqlite v1.2.2/go.mod h1:JkAuxM7HHo0tc7dQENjIGpp/yIfrLX5nKr5ME9wuvmI= github.com/lunixbochs/struc v0.0.0-20241101090106-8d528fa2c543 h1:GxMuVb9tJajC1QpbQwYNY1ZAo1EIE8I+UclBjOfjz/M= github.com/lunixbochs/struc v0.0.0-20241101090106-8d528fa2c543/go.mod h1:vy1vK6wD6j7xX6O6hXe621WabdtNkou2h7uRtTfRMyg= github.com/mabunixda/wattpilot v1.8.5 h1:pjm//7dnEiR0OHfMZ+SLlLV557Y0LMrxgjwudOsuHbQ= @@ -389,8 +389,8 @@ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= -github.com/nicholas-fedor/shoutrrr v0.16.3 h1:8n2Sp6futI8Q/zEzQXQKUJhn1izZEwr4DGiLgeLdC5k= -github.com/nicholas-fedor/shoutrrr v0.16.3/go.mod h1:XTUvtSfZxjo9f0mqk9W7ygAjfS9bw5Bhcvoz01Fk+Tc= +github.com/nicholas-fedor/shoutrrr v0.17.0 h1:xfp3z5QbE8jXvUhUEwWDk47SJ/b912VoB8MJJDU+q4E= +github.com/nicholas-fedor/shoutrrr v0.17.0/go.mod h1:s4ldyLs6uwBy9lIjYrY+8lyTqJtPvZSrILw0CyMLock= github.com/nicksnyder/go-i18n/v2 v2.6.1 h1:JDEJraFsQE17Dut9HFDHzCoAWGEQJom5s0TRd17NIEQ= github.com/nicksnyder/go-i18n/v2 v2.6.1/go.mod h1:Vee0/9RD3Quc/NmwEjzzD7VTZ+Ir7QbXocrkhOzmUKA= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= @@ -424,8 +424,8 @@ github.com/pelletier/go-toml/v2 v2.4.3/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8 github.com/peterbourgon/ff v1.2.0/go.mod h1:ljiF7yxtUvZaxUDyUqQa0+uiEOgwVboj+Q2S2+0nq40= github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 h1:Dx7Ovyv/SFnMFw3fD4oEoeorXc6saIiQ23LrGLth0Gw= github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= -github.com/philippseith/signalr v0.8.0 h1:CvylMNn7TkJi4adUlk75K08OwljdmBqo6jd12Pz2Guw= -github.com/philippseith/signalr v0.8.0/go.mod h1:ZIAyv2b3xIsh+8j++0Omtp0Xe4CwDnwfyyZBEh5Z9uk= +github.com/philippseith/signalr v0.10.0 h1:VTL/e36Id8ImVLLF5QttNNvNk3JUs8PszlfTBirrIKA= +github.com/philippseith/signalr v0.10.0/go.mod h1:HUuLr3FuirOWkZTJaBnnCChHNOupoE8tcvu6QWBuBqU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -445,8 +445,8 @@ github.com/quic-go/go-ossfuzz-seeds v0.1.0 h1:APacT+iIaNF6fd8AGEiN3bT/Jtkd2jz4v4 github.com/quic-go/go-ossfuzz-seeds v0.1.0/go.mod h1:3IOHRbJIc+L6YKMwfDtJAM9Vj9k0YY4muhuyUYk5tbk= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= -github.com/quic-go/quic-go v0.60.0 h1:xcQioE8OM66UQLeUMHltK1CCcOu3JbVB4JAQdDQSB+0= -github.com/quic-go/quic-go v0.60.0/go.mod h1:wpKpjmPpftl30sL6pFh7REVpjbcCVy4zt2vDyK1TuJk= +github.com/quic-go/quic-go v0.61.0 h1:ui88A53s8MSVYLC56en0KQ17HARk+9986Dn0SBfKNvA= +github.com/quic-go/quic-go v0.61.0/go.mod h1:9So2anK4Tp22URSQq00k+Vo2PNkle96ycDPDHL4s9vs= github.com/quic-go/webtransport-go v0.11.1 h1:rrFQMO+7/52ZDJ04fsrjIaWqn6q1z1MYo9iVFq6JtbA= github.com/quic-go/webtransport-go v0.11.1/go.mod h1:SHgEzUFVyj+9WUSuGB1P6Zd351Pww2leWV3SwlTovkA= github.com/relvacode/iso8601 v1.6.0 h1:eFXUhMJN3Gz8Rcq82f9DTMW0svjtAVuIEULglM7QHTU= @@ -553,8 +553,8 @@ github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IU github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= -github.com/volkszaehler/mbmd v0.0.0-20260623211432-fb8f9c22f121 h1:eDW8HHhwxp1HQQu9uNPMs7TUKZQkTKhe5LkBfhQ+bZA= -github.com/volkszaehler/mbmd v0.0.0-20260623211432-fb8f9c22f121/go.mod h1:hx00iJeEiVti6jY7HwLIiShGWdjAAxY7vDdhx/Ys1ys= +github.com/volkszaehler/mbmd v0.0.0-20260803093438-96221ccf34be h1:q0kgjSngsgSWiKGhLNOfUJDO76+Ctx31f5Y50MO3Bf4= +github.com/volkszaehler/mbmd v0.0.0-20260803093438-96221ccf34be/go.mod h1:hx00iJeEiVti6jY7HwLIiShGWdjAAxY7vDdhx/Ys1ys= github.com/warthog618/go-gpiocdev v0.9.1 h1:pwHPaqjJfhCipIQl78V+O3l9OKHivdRDdmgXYbmhuCI= github.com/warthog618/go-gpiocdev v0.9.1/go.mod h1:dN3e3t/S2aSNC+hgigGE/dBW8jE1ONk9bDSEYfoPyl8= github.com/warthog618/go-gpiosim v0.1.1 h1:MRAEv+T+itmw+3GeIGpQJBfanUVyg0l3JCTwHtwdre4= @@ -609,8 +609,8 @@ golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= -golang.org/x/crypto/x509roots/fallback v0.0.0-20260723152544-d701c51f7e4e h1:9TjMDOuGaMMTP5f7GXeHeA0JvFqGmv4DYRIWkzoePGI= -golang.org/x/crypto/x509roots/fallback v0.0.0-20260723152544-d701c51f7e4e/go.mod h1:+UoQFNBq2p2wO+Q6ddVtYc25GZ6VNdOMyyrd4nrqrKs= +golang.org/x/crypto/x509roots/fallback v0.0.0-20260804050112-5ed494470b06 h1:/63HmWYvASiBqlJK1XRa/ZJWz/u4cHpfKk389DTfTEE= +golang.org/x/crypto/x509roots/fallback v0.0.0-20260804050112-5ed494470b06/go.mod h1:+UoQFNBq2p2wO+Q6ddVtYc25GZ6VNdOMyyrd4nrqrKs= golang.org/x/exp v0.0.0-20260727155853-b88d891fe743 h1:ex206bKw+v3K0dm3andkrIF+ijyQKJG1pLgwQ2PYdQM= golang.org/x/exp v0.0.0-20260727155853-b88d891fe743/go.mod h1:EdfpwwqSu+0Li0mzskwHU6FWDV3t9Q+RZDo3QMUtL3Q= golang.org/x/image v0.38.0 h1:5l+q+Y9JDC7mBOMjo4/aPhMDcxEptsX+Tt3GgRQRPuE= @@ -749,8 +749,8 @@ gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= gorm.io/gorm v1.31.2 h1:3o8FXNo9v9S858gil+3LlZA1LkCOzgb4g5BL64FgaCo= gorm.io/gorm v1.31.2/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= -modernc.org/cc/v4 v4.29.0 h1:CXgwL8cvxmyzBQZzbSl/6xFtMCryb6u8IOqDci39cgc= -modernc.org/cc/v4 v4.29.0/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI= +modernc.org/cc/v4 v4.29.1 h1:MKgdCV3WykTSPqpVrnxdEDS0HEd2FHpKZDzxzU5LyeI= +modernc.org/cc/v4 v4.29.1/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI= modernc.org/ccgo/v4 v4.34.6 h1:sBgfIwyN0TQ9C5hwIeuqyeAKyMWnbvj2fvpF4L11uzU= modernc.org/ccgo/v4 v4.34.6/go.mod h1:SZ8YcN9NG7XVsQYdm6jYBvi8PQP1qi+kqB6OhjqI3Fk= modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= @@ -761,8 +761,8 @@ modernc.org/gc/v3 v3.1.4 h1:2g65LGVSmFQrXeITAw97x7hCRvZFcyE1uDP+7Vng7JI= modernc.org/gc/v3 v3.1.4/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= -modernc.org/libc v1.74.1 h1:bdR4VTKFMC4966QSNZ05XLGI/VwzVa2kTUX51Dm0riQ= -modernc.org/libc v1.74.1/go.mod h1:uH4t5bOx3G3g9Xcmj10YKlTcVISlRDwv8VoQJG9n8Os= +modernc.org/libc v1.74.4 h1:fX1Omw4o2/1C2iRkkIsrQTasJQldLhRmuPreXLoWs9k= +modernc.org/libc v1.74.4/go.mod h1:eeQAS9W3sZeKYMFubydxJpII9ybHWshk+7or7bLG9co= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= @@ -771,8 +771,8 @@ modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg= modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= -modernc.org/sqlite v1.55.0 h1:hIFh0MCH0rGinQ/4KYb5/UbCkRkb+UP+OkLCVWa5MTM= -modernc.org/sqlite v1.55.0/go.mod h1:4ntCLuNmnH8+GNqjka1wNg7KJd5/Hi5FYp8K+XQ7GZw= +modernc.org/sqlite v1.56.0 h1:/D8e2RfFqoy/Zc6PuC76U28zFwmI/sYx1Kjm4yEn9e0= +modernc.org/sqlite v1.56.0/go.mod h1:yCJ2cmAaIkHQ25oXWrF8H4O1lIfPYPR26yCEDj2P3pQ= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= From 29137181f1b520dd733de538d47f739b982679a6 Mon Sep 17 00:00:00 2001 From: andig Date: Sat, 8 Aug 2026 12:58:44 +0200 Subject: [PATCH 1519/1747] Optimizer: stop demanding charge once the vehicle is full (#32633) --- core/site_optimizer.go | 25 ++++++++++- core/site_optimizer_demand_test.go | 66 ++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 core/site_optimizer_demand_test.go diff --git a/core/site_optimizer.go b/core/site_optimizer.go index 17d50e69c..87027d4b6 100644 --- a/core/site_optimizer.go +++ b/core/site_optimizer.go @@ -800,12 +800,35 @@ func (site *Site) loadpointRequest(lp loadpoint.API, minLen int, firstSlotDurati } if demand != nil { - bat.PDemand = prorate(demand, firstSlotDuration) + // after prorate, so the shortened first slot counts with the energy it really carries + bat.PDemand = clearDemandWhenFull(prorate(demand, firstSlotDuration), bat.SMax-bat.SInitial) } return bat, detail } +// clearDemandWhenFull zeroes the charge demand from the slot the accumulated energy fills the +// vehicle. The optimizer drops the demand at s_max anyway, but pays two binaries per slot to +// detect it, so slots that cannot bind are worth not asking about. Losses are accounted for. +// +// The cut assumes the demand is met every slot. A grid import limit can throttle charging below +// it, moving the real fill point later than the estimate - the next request corrects that from +// the measured soc, and the near slots are never affected because the cut sits a full charge away. +func clearDemandWhenFull(demand []float32, headroom float32) []float32 { + res := slices.Clone(demand) + + var acc float32 + for i, d := range res { + if acc >= headroom { + res[i] = 0 + continue + } + acc += d * eta + } + + return res +} + func (site *Site) batteryRequest(dev config.Device[api.Meter], b types.Measurement, grid api.Rates, minLen int, firstSlotDuration time.Duration) (optimizer.BatteryConfig, batteryDetail) { bat := optimizer.BatteryConfig{ CMax: batteryPower, diff --git a/core/site_optimizer_demand_test.go b/core/site_optimizer_demand_test.go new file mode 100644 index 000000000..4fa89f024 --- /dev/null +++ b/core/site_optimizer_demand_test.go @@ -0,0 +1,66 @@ +package core + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestClearDemandWhenFull(t *testing.T) { + for _, tc := range []struct { + name string + demand []float32 + headroom float32 + expected []float32 + }{ + { + // 345Wh demanded per slot stores 310.5Wh, so 1000Wh of headroom is covered + // during the fourth slot- that slot still binds, the ones after it cannot + "cut once the accumulated energy fills the vehicle", + []float32{345, 345, 345, 345, 345, 345}, + 1000, + []float32{345, 345, 345, 345, 0, 0}, + }, + { + "headroom beyond the horizon leaves the demand untouched", + []float32{345, 345, 345}, + 1e6, + []float32{345, 345, 345}, + }, + { + // already at the soc limit: every slot is relaxed at s_max anyway + "no headroom clears the whole demand", + []float32{345, 345, 345}, + 0, + []float32{0, 0, 0}, + }, + { + // the shortened first slot carries less energy and must not pull the cut in + "partial first slot counts with its own energy", + []float32{100, 345, 345, 345}, + 600, + []float32{100, 345, 345, 0}, + }, + { + "empty demand stays empty", + []float32{}, + 1000, + []float32{}, + }, + } { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.expected, clearDemandWhenFull(tc.demand, tc.headroom)) + }) + } +} + +// the request contract requires every series to have the same length +func TestClearDemandWhenFullKeepsLength(t *testing.T) { + demand := make([]float32, 96) + for i := range demand { + demand[i] = 345 + } + + assert.Len(t, clearDemandWhenFull(demand, 1000), len(demand)) + assert.Len(t, clearDemandWhenFull(demand, 0), len(demand)) +} From 856ac800840a95c7b46dac70d62d6f7f6fb9a9c7 Mon Sep 17 00:00:00 2001 From: andig Date: Sat, 8 Aug 2026 13:16:38 +0200 Subject: [PATCH 1520/1747] chore: upgrade modules --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 2bd90efe2..d89270a01 100644 --- a/go.mod +++ b/go.mod @@ -79,7 +79,7 @@ require ( github.com/nicholas-fedor/shoutrrr v0.17.0 github.com/nicksnyder/go-i18n/v2 v2.6.1 github.com/olekukonko/tablewriter v1.1.4 - github.com/philippseith/signalr v0.10.0 + github.com/philippseith/signalr v0.10.1-0.20260806083734-d62f30bf7a90 github.com/prometheus-community/pro-bing v0.9.1 github.com/prometheus/client_golang v1.24.1 github.com/prometheus/common v0.70.1 @@ -214,7 +214,7 @@ require ( github.com/prometheus/procfs v0.21.1 // indirect github.com/quic-go/qpack v0.6.0 // indirect github.com/quic-go/quic-go v0.61.0 // indirect - github.com/quic-go/webtransport-go v0.11.1 // indirect + github.com/quic-go/webtransport-go v0.12.0 // indirect github.com/relvacode/iso8601 v1.6.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/richardlehane/mscfb v1.0.7 // indirect diff --git a/go.sum b/go.sum index 4cd766dd8..259dfba03 100644 --- a/go.sum +++ b/go.sum @@ -424,8 +424,8 @@ github.com/pelletier/go-toml/v2 v2.4.3/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8 github.com/peterbourgon/ff v1.2.0/go.mod h1:ljiF7yxtUvZaxUDyUqQa0+uiEOgwVboj+Q2S2+0nq40= github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 h1:Dx7Ovyv/SFnMFw3fD4oEoeorXc6saIiQ23LrGLth0Gw= github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= -github.com/philippseith/signalr v0.10.0 h1:VTL/e36Id8ImVLLF5QttNNvNk3JUs8PszlfTBirrIKA= -github.com/philippseith/signalr v0.10.0/go.mod h1:HUuLr3FuirOWkZTJaBnnCChHNOupoE8tcvu6QWBuBqU= +github.com/philippseith/signalr v0.10.1-0.20260806083734-d62f30bf7a90 h1:s5PiRL6iuCdjV7iwYhi/YxrhKfdRoaIqIGSeBoBiWuo= +github.com/philippseith/signalr v0.10.1-0.20260806083734-d62f30bf7a90/go.mod h1:9VAFjplkNr3RKc3mH3L/L0KvCdF1xeV9Y7cDhNwXvZ8= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -447,8 +447,8 @@ github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= github.com/quic-go/quic-go v0.61.0 h1:ui88A53s8MSVYLC56en0KQ17HARk+9986Dn0SBfKNvA= github.com/quic-go/quic-go v0.61.0/go.mod h1:9So2anK4Tp22URSQq00k+Vo2PNkle96ycDPDHL4s9vs= -github.com/quic-go/webtransport-go v0.11.1 h1:rrFQMO+7/52ZDJ04fsrjIaWqn6q1z1MYo9iVFq6JtbA= -github.com/quic-go/webtransport-go v0.11.1/go.mod h1:SHgEzUFVyj+9WUSuGB1P6Zd351Pww2leWV3SwlTovkA= +github.com/quic-go/webtransport-go v0.12.0 h1:CpnKNwZvdV0LD73xoHO8QaR0NI3llqpWRwnazdZS0sE= +github.com/quic-go/webtransport-go v0.12.0/go.mod h1:GHne8aRFJ24h73pAMrcywXtuaz/ShBXCLXLvG/NPFdU= github.com/relvacode/iso8601 v1.6.0 h1:eFXUhMJN3Gz8Rcq82f9DTMW0svjtAVuIEULglM7QHTU= github.com/relvacode/iso8601 v1.6.0/go.mod h1:FlNp+jz+TXpyRqgmM7tnzHHzBnz776kmAH2h3sZCn0I= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= From 9a276f3ff51712163c371630af18340ac7b59d65 Mon Sep 17 00:00:00 2001 From: andig Date: Sat, 8 Aug 2026 13:26:29 +0200 Subject: [PATCH 1521/1747] Skoda: expose welcomecharge parameter (#32636) --- templates/definition/vehicle/skoda.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/templates/definition/vehicle/skoda.yaml b/templates/definition/vehicle/skoda.yaml index 79d47266a..6b7ea1df3 100644 --- a/templates/definition/vehicle/skoda.yaml +++ b/templates/definition/vehicle/skoda.yaml @@ -6,6 +6,8 @@ products: generic: MyŠkoda params: - preset: vehicle-base + - name: welcomecharge + advanced: true - preset: vehicle-online - name: timeout render: | From aaaa28a42084199fc7ee52d24c0632d9739daab7 Mon Sep 17 00:00:00 2001 From: andig Date: Sat, 8 Aug 2026 13:57:02 +0200 Subject: [PATCH 1522/1747] Solcast: keep forecast alive outside active hours (#32639) --- tariff/helper_test.go | 27 +++++++++++++++++++++++++++ tariff/solcast.go | 2 ++ 2 files changed, 29 insertions(+) diff --git a/tariff/helper_test.go b/tariff/helper_test.go index 93e7ddee3..06bf96af7 100644 --- a/tariff/helper_test.go +++ b/tariff/helper_test.go @@ -62,6 +62,33 @@ func TestMergeRatesAfter(t *testing.T) { } } +// TestMergeRatesAfterRefreshesTimestamp asserts that merging without new rates +// keeps the cached rates and un-expires the monitor - this is how solcast keeps +// its forecast alive while fetching is paused outside the from/to window. +func TestMergeRatesAfterRefreshesTimestamp(t *testing.T) { + clock := clock.NewMock() + clock.Set(now.BeginningOfDay()) + + rates := api.Rates{{ + Start: clock.Now().Add(time.Hour), + End: clock.Now().Add(2 * time.Hour), + Value: 1, + }} + + data := util.NewMonitor[api.Rates](time.Hour).WithClock(clock) + data.Set(rates) + + clock.Add(2 * time.Hour) + _, err := data.Get() + require.ErrorIs(t, err, api.ErrOutdated) + + mergeRatesAfter(data, nil, clock.Now().Add(-2*time.Hour)) + + res, err := data.Get() + require.NoError(t, err) + assert.Equal(t, rates, res) +} + type runner struct { res error } diff --git a/tariff/solcast.go b/tariff/solcast.go index 957885167..f6227a44f 100644 --- a/tariff/solcast.go +++ b/tariff/solcast.go @@ -82,6 +82,8 @@ func (t *Solcast) run(interval time.Duration, done chan error) { select { case <-t.data.Done(): if !t.fromTo.IsActive(time.Now().Hour()) { + // keep cached forecast alive while fetching is paused + mergeRatesAfter(t.data, nil, beginningOfDay()) continue } default: From 19f750c417cab26969443fc30668a61b656140f2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 8 Aug 2026 12:00:40 +0000 Subject: [PATCH 1523/1747] core: keep limitSoc/limitEnergy on integrated device disconnect (#32623) --- core/loadpoint.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/core/loadpoint.go b/core/loadpoint.go index cd3618be5..e05835cb7 100644 --- a/core/loadpoint.go +++ b/core/loadpoint.go @@ -628,8 +628,10 @@ func (lp *Loadpoint) evVehicleDisconnectHandler() { } // reset session - lp.SetLimitSoc(0) - lp.SetLimitEnergy(0) + if !lp.chargerHasFeature(api.IntegratedDevice) { + lp.SetLimitSoc(0) + lp.SetLimitEnergy(0) + } // mark plan slot as inactive // this will force a deletion of an outdated plan once plan time is expired in GetPlan() From b8130ea48d2022e1c15ce965fc95f46a9ffc22e8 Mon Sep 17 00:00:00 2001 From: andig Date: Sat, 8 Aug 2026 14:56:43 +0200 Subject: [PATCH 1524/1747] Shelly: fix PV energy for devices with reverse power measurement enabled (#32550) --- meter/shelly.go | 25 ++++-- meter/shelly/connection.go | 2 + meter/shelly/connection_test.go | 140 ++++++++++++++++++++++++++++++++ meter/shelly/gen1.go | 10 +++ meter/shelly/gen2.go | 68 ++++++++++++++-- meter/shelly/gen2_test.go | 86 ++++++++++++++++++++ meter/shelly_test.go | 15 ++-- 7 files changed, 326 insertions(+), 20 deletions(-) create mode 100644 meter/shelly/connection_test.go diff --git a/meter/shelly.go b/meter/shelly.go index fe54f2cca..4801e80c1 100644 --- a/meter/shelly.go +++ b/meter/shelly.go @@ -49,12 +49,17 @@ func NewShellyFromConfig(other map[string]any) (api.Meter, error) { // making their totals unsuitable for bidirectional grid metering. if !(c.usage == "grid" && c.conn.IsThreePhase()) { total, ret := c.conn.TotalEnergy, c.conn.ReturnEnergy - if c.usage == "pv" { - // reverse direction + // production is measured in return direction, unless the device has no return + // register at all or already reverses the direction itself + if c.usage == "pv" && c.conn.HasReturnEnergy() && !c.conn.IsReversed() { total, ret = ret, total } implement.Has(c, implement.MeterEnergy(total)) - implement.Has(c, implement.MeterReturnEnergy(ret)) + + // without a return register the second reading is a constant zero + if c.conn.HasReturnEnergy() { + implement.Has(c, implement.MeterReturnEnergy(ret)) + } } if phases, ok := c.conn.Generation.(shelly.Phases); ok { @@ -88,16 +93,20 @@ func (c *Shelly) CurrentPower() (float64, error) { if err != nil { return 0, err } - return c.currentPowerForUsage(power, c.conn.SignedPower()), nil + return c.currentPowerForUsage(power, c.conn.SignedPower(), c.conn.IsReversed()), nil } -// PV usage inverts directional power, otherwise the magnitude is used. -func (c *Shelly) currentPowerForUsage(power float64, signed bool) float64 { +// PV usage inverts directional power unless the device already reverses it, otherwise the magnitude is used. +func (c *Shelly) currentPowerForUsage(power float64, signed, reversed bool) float64 { if c.usage != "pv" { return power } - if signed { + switch { + case !signed: + return math.Abs(power) + case reversed: + return power + default: return -power } - return math.Abs(power) } diff --git a/meter/shelly/connection.go b/meter/shelly/connection.go index 46150008a..c584b9127 100644 --- a/meter/shelly/connection.go +++ b/meter/shelly/connection.go @@ -19,6 +19,8 @@ type Generation interface { api.MeterEnergy api.MeterReturnEnergy IsThreePhase() bool + IsReversed() bool + HasReturnEnergy() bool } type Phases interface { diff --git a/meter/shelly/connection_test.go b/meter/shelly/connection_test.go new file mode 100644 index 000000000..7185c9280 --- /dev/null +++ b/meter/shelly/connection_test.go @@ -0,0 +1,140 @@ +package shelly + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// shellyServer emulates a gen2+ device serving the given rpc responses. Methods +// not listed are reported as unavailable by Shelly.ListMethods and answered 404. +func shellyServer(t *testing.T, rpc map[string]string) *httptest.Server { + t.Helper() + + methods := make([]string, 0, len(rpc)) + for m := range rpc { + methods = append(methods, m) + } + + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + switch r.URL.Path { + case "/shelly": + json.NewEncoder(w).Encode(map[string]any{"gen": 2, "model": "SNSW-001X16EU"}) + + case "/rpc/Shelly.ListMethods": + json.NewEncoder(w).Encode(Gen2Methods{Methods: methods}) + + default: + method := strings.TrimPrefix(r.URL.Path, "/rpc/") + res, ok := rpc[method] + if !ok { + http.Error(w, `{"code":-105,"message":"no handler"}`, http.StatusNotFound) + return + } + w.Write([]byte(res)) + } + })) +} + +// TestNakedSwitchConnection asserts that a switch without power measurement +// (Shelly Plus 1) connects - its status has neither aenergy nor ret_aenergy. +func TestNakedSwitchConnection(t *testing.T) { + srv := shellyServer(t, map[string]string{ + "Switch.GetStatus": `{"id":0,"source":"init","output":true,"temperature":{"tC":45.2,"tF":113.4}}`, + "Switch.GetConfig": `{"id":0,"name":null,"in_mode":"follow","initial_state":"match_input","auto_on":false}`, + }) + defer srv.Close() + + conn, err := NewConnection(srv.URL, "", "", 0, time.Second) + require.NoError(t, err, "naked switch must connect") + + assert.False(t, conn.IsReversed()) + assert.False(t, conn.HasReturnEnergy()) + + enabled, err := conn.Enabled() + require.NoError(t, err) + assert.True(t, enabled) + + total, err := conn.TotalEnergy() + require.NoError(t, err) + assert.Zero(t, total) + + ret, err := conn.ReturnEnergy() + require.NoError(t, err) + assert.Zero(t, ret) +} + +// TestSwitchConnectionStatusError asserts that a temporarily unavailable switch +// status does not break connecting- the error surfaces on read instead. +func TestSwitchConnectionStatusError(t *testing.T) { + // Switch.GetStatus advertised but erroring, e.g. component busy right after boot + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/shelly": + json.NewEncoder(w).Encode(map[string]any{"gen": 2, "model": "SNSW-001X16EU"}) + case "/rpc/Shelly.ListMethods": + json.NewEncoder(w).Encode(Gen2Methods{Methods: []string{"Switch.GetStatus", "Switch.GetConfig"}}) + case "/rpc/Switch.GetConfig": + w.Write([]byte(`{"id":0}`)) + default: + http.Error(w, `{"code":-114,"message":"component not found"}`, http.StatusNotFound) + } + })) + defer srv.Close() + + conn, err := NewConnection(srv.URL, "", "", 0, time.Second) + require.NoError(t, err, "status error must not break connecting") + + _, err = conn.CurrentPower() + require.Error(t, err, "error surfaces on read") +} + +// TestPlugConnection covers the metered plug from #32213: aenergy without ret_aenergy. +func TestPlugConnection(t *testing.T) { + srv := shellyServer(t, map[string]string{ + "Switch.GetStatus": `{"id":0,"output":true,"apower":399.2,"voltage":240.8,"current":1.886,"aenergy":{"total":2574466.629}}`, + "Switch.GetConfig": `{"id":0,"power_limit":2800,"voltage_limit":280}`, + }) + defer srv.Close() + + conn, err := NewConnection(srv.URL, "", "", 0, time.Second) + require.NoError(t, err) + + assert.False(t, conn.IsReversed()) + assert.False(t, conn.HasReturnEnergy(), "plug has no return register") + + total, err := conn.TotalEnergy() + require.NoError(t, err) + assert.Equal(t, 2574.466629, total) +} + +// TestReversedSwitchConnection covers a switch with device-side reverse measurement. +func TestReversedSwitchConnection(t *testing.T) { + srv := shellyServer(t, map[string]string{ + "Switch.GetStatus": `{"id":0,"output":true,"apower":-350,"aenergy":{"total":10000},"ret_aenergy":{"total":4000}}`, + "Switch.GetConfig": `{"id":0,"reverse":true}`, + }) + defer srv.Close() + + conn, err := NewConnection(srv.URL, "", "", 0, time.Second) + require.NoError(t, err) + + assert.True(t, conn.IsReversed()) + assert.True(t, conn.HasReturnEnergy()) + + total, err := conn.TotalEnergy() + require.NoError(t, err) + assert.Equal(t, 6.0, total) + + ret, err := conn.ReturnEnergy() + require.NoError(t, err) + assert.Equal(t, 4.0, ret) +} diff --git a/meter/shelly/gen1.go b/meter/shelly/gen1.go index 65175a15e..a8e007ef5 100644 --- a/meter/shelly/gen1.go +++ b/meter/shelly/gen1.go @@ -144,6 +144,16 @@ func (c *gen1) ReturnEnergy() (float64, error) { return c.energy(energy) / 1000, nil } +// IsReversed reports whether the device reverses its measurement direction. Gen1 devices cannot. +func (c *gen1) IsReversed() bool { + return false +} + +// HasReturnEnergy reports whether the device measures energy in the return direction +func (c *gen1) HasReturnEnergy() bool { + return true +} + // IsThreePhase reports whether the device is a three-phase energy meter. func (c *gen1) IsThreePhase() bool { res, err := c.status.Get() diff --git a/meter/shelly/gen2.go b/meter/shelly/gen2.go index e98841cfc..a34cc6822 100644 --- a/meter/shelly/gen2.go +++ b/meter/shelly/gen2.go @@ -28,6 +28,10 @@ type Gen2Methods struct { Methods []string } +type Gen2Config struct { + Reverse bool +} + type Gen2SwitchStatus struct { Output bool Apower float64 @@ -36,11 +40,23 @@ type Gen2SwitchStatus struct { Aenergy struct { Total float64 } - Ret_Aenergy struct { + // nil on devices without reverse energy metering- they omit the register entirely + Ret_Aenergy *struct { Total float64 } } +// switchEnergy splits the switch registers into import and return energy (kWh). +// https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/Switch#status +// NOTE: energy added to ret_aenergy is also added to aenergy, so aenergy holds +// both directions. Without the register aenergy is import only. +func switchEnergy(res Gen2SwitchStatus) (total, ret float64) { + if res.Ret_Aenergy == nil { + return res.Aenergy.Total / 1000, 0 + } + return max(0, res.Aenergy.Total-res.Ret_Aenergy.Total) / 1000, res.Ret_Aenergy.Total / 1000 +} + type Gen2EMStatus struct { TotalActPower float64 `json:"total_act_power"` ACurrent float64 `json:"a_current"` @@ -84,6 +100,7 @@ type gen2 struct { switchchannel int model string methods []string + reversed bool switchstatus util.Cacheable[Gen2SwitchStatus] em1status func() (Gen2EM1Status, error) em1data func() (Gen2EM1Data, error) @@ -139,6 +156,27 @@ func newGen2(helper *request.Helper, uri, model string, channel int, user, passw } else { c.switchstatus = util.ResettableCached(apiCall[Gen2SwitchStatus](c, c.switchchannel, "Switch.GetStatus"), cache) } + // device-side "Reverse power measurement" setting (requires restart, hence static) + var cfgMethod string + cfgChannel := channel + switch { + case c.hasEM1Endpoint(): + cfgMethod = "EM1.GetConfig" + case c.hasMethod("PM1.GetStatus"): + cfgMethod = "PM1.GetConfig" + case c.hasMethod("Switch.GetStatus"): + cfgMethod = "Switch.GetConfig" + cfgChannel = c.switchchannel + } + + if c.hasMethod(cfgMethod) { + var cfg Gen2Config + if err := c.execCmd(cfgChannel, cfgMethod, &cfg); err != nil { + return nil, err + } + 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) @@ -232,10 +270,8 @@ func (c *gen2) TotalEnergy() (float64, error) { case c.hasSwitchEndpoint(): res, err := c.switchstatus.Get() - // https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/Switch#status - // NOTE: ret_aenergy - the active energy added to this container is also added to aenergy container. - // All the consumed energy is collected in aenergy regardless of the direction(consumed or returned) of the active energy. - return max(0, res.Aenergy.Total-res.Ret_Aenergy.Total) / 1000, err + total, _ := switchEnergy(res) + return total, err default: return 0, fmt.Errorf("unknown shelly model: %s", c.model) @@ -255,7 +291,8 @@ func (c *gen2) ReturnEnergy() (float64, error) { case c.hasSwitchEndpoint(): res, err := c.switchstatus.Get() - return res.Ret_Aenergy.Total / 1000, err + _, ret := switchEnergy(res) + return ret, err default: return 0, fmt.Errorf("unknown shelly model: %s", c.model) @@ -340,6 +377,25 @@ func (c *gen2) IsThreePhase() bool { return c.hasEMEndpoint() } +// IsReversed reports whether the device-side "Reverse power measurement" setting is enabled +func (c *gen2) IsReversed() bool { + return c.reversed +} + +// HasReturnEnergy reports whether the device measures energy in the return direction. +// Plain plugs omit ret_aenergy entirely, so the (cached) status decides. +func (c *gen2) HasReturnEnergy() bool { + switch { + case c.hasEM1Endpoint(), c.hasEMEndpoint(): + return true + case c.hasSwitchEndpoint(): + res, err := c.switchstatus.Get() + return err == nil && res.Ret_Aenergy != nil + default: + return false + } +} + // Gen2+ models using EM1.GetStatus endpoint for power and EM1Data.GetStatus for energy // https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/EM1#em1getstatus-example // https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/EM1Data#em1datagetstatus-example diff --git a/meter/shelly/gen2_test.go b/meter/shelly/gen2_test.go index b0e577486..fa2995522 100644 --- a/meter/shelly/gen2_test.go +++ b/meter/shelly/gen2_test.go @@ -2,8 +2,11 @@ package shelly import ( "encoding/json" + "errors" "testing" + "time" + "github.com/evcc-io/evcc/util" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -80,3 +83,86 @@ func TestUnmarshalGen2StatusResponse(t *testing.T) { assert.Equal(t, 0, parseAddOnSwitchID(channel, res)) } } + +func TestSwitchEnergy(t *testing.T) { + tests := []struct { + name string + status string + total, ret float64 + hasReturnReg bool + }{ + { + // Shelly Plug S Gen3 as pv meter, https://github.com/evcc-io/evcc/issues/32213 + // no ret_aenergy register: aenergy is production, swapping it would report 0 + name: "plug without reverse metering", + status: `{"id":0,"source":"init","output":true,"apower":399.2,"voltage":240.8,"current":1.886,"aenergy":{"total":2574466.629,"by_minute":[7200.069,6121.044,5088.795],"minute_ts":1786033200},"temperature":{"tC":46.3,"tF":115.4}}`, + total: 2574.466629, + }, + { + // aenergy holds both directions, so import is the difference + name: "switch with reverse metering", + status: `{"id":0,"output":true,"apower":-350,"aenergy":{"total":10000},"ret_aenergy":{"total":4000}}`, + total: 6, + ret: 4, + hasReturnReg: true, + }, + { + // pure production: everything lands in ret_aenergy, import must not go negative + name: "switch measuring return only", + status: `{"id":0,"output":true,"aenergy":{"total":4000},"ret_aenergy":{"total":4000}}`, + ret: 4, + hasReturnReg: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var res Gen2SwitchStatus + require.NoError(t, json.Unmarshal([]byte(tc.status), &res)) + + assert.Equal(t, tc.hasReturnReg, res.Ret_Aenergy != nil, "ret_aenergy presence") + + total, ret := switchEnergy(res) + assert.Equal(t, tc.total, total, "total energy") + assert.Equal(t, tc.ret, ret, "return energy") + + // same values through the endpoint dispatch + c := &gen2{ + methods: []string{"Switch.GetStatus"}, + switchstatus: util.ResettableCached(func() (Gen2SwitchStatus, error) { + return res, nil + }, time.Minute), + } + + assert.Equal(t, tc.hasReturnReg, c.HasReturnEnergy(), "HasReturnEnergy") + + totalEnergy, err := c.TotalEnergy() + require.NoError(t, err) + assert.Equal(t, tc.total, totalEnergy, "TotalEnergy") + + returnEnergy, err := c.ReturnEnergy() + require.NoError(t, err) + assert.Equal(t, tc.ret, returnEnergy, "ReturnEnergy") + }) + } +} + +// a failed read yields the zero status, whose ret_aenergy register is nil too +func TestSwitchEnergyReadError(t *testing.T) { + c := &gen2{ + methods: []string{"Switch.GetStatus"}, + switchstatus: util.ResettableCached(func() (Gen2SwitchStatus, error) { + return Gen2SwitchStatus{}, errors.New("offline") + }, time.Minute), + } + + require.NotPanics(t, func() { + total, err := c.TotalEnergy() + require.Error(t, err) + assert.Zero(t, total) + + ret, err := c.ReturnEnergy() + require.Error(t, err) + assert.Zero(t, ret) + }) +} diff --git a/meter/shelly_test.go b/meter/shelly_test.go index 7ebf35cd4..a23bfb8cd 100644 --- a/meter/shelly_test.go +++ b/meter/shelly_test.go @@ -8,23 +8,26 @@ import ( func TestShellyCurrentPowerForUsage(t *testing.T) { tests := []struct { - name string - usage string - signed bool - power float64 - want float64 + name string + usage string + signed bool + reversed bool + power float64 + want float64 }{ {name: "grid keeps sign", usage: "grid", power: -350, want: -350}, {name: "unsigned pv uses absolute value", usage: "pv", power: -350, want: 350}, {name: "unsigned pv keeps positive values", usage: "pv", power: 350, want: 350}, {name: "signed pv inverts positive values", usage: "pv", signed: true, power: 350, want: -350}, {name: "signed pv inverts negative values", usage: "pv", signed: true, power: -350, want: 350}, + {name: "signed reversed pv keeps sign", usage: "pv", signed: true, reversed: true, power: 350, want: 350}, + {name: "signed reversed pv keeps negative sign", usage: "pv", signed: true, reversed: true, power: -350, want: -350}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { m := &Shelly{usage: tc.usage} - assert.Equal(t, tc.want, m.currentPowerForUsage(tc.power, tc.signed)) + assert.Equal(t, tc.want, m.currentPowerForUsage(tc.power, tc.signed, tc.reversed)) }) } } From b70ab2792806591dcb9b8eb4e3ca58008306221c Mon Sep 17 00:00:00 2001 From: andig Date: Sat, 8 Aug 2026 15:15:41 +0200 Subject: [PATCH 1525/1747] Shelly: query return energy support once (#32644) --- meter/shelly.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/meter/shelly.go b/meter/shelly.go index 4801e80c1..35568f960 100644 --- a/meter/shelly.go +++ b/meter/shelly.go @@ -49,15 +49,18 @@ func NewShellyFromConfig(other map[string]any) (api.Meter, error) { // making their totals unsuitable for bidirectional grid metering. if !(c.usage == "grid" && c.conn.IsThreePhase()) { total, ret := c.conn.TotalEnergy, c.conn.ReturnEnergy + + hasReturn := c.conn.HasReturnEnergy() + // production is measured in return direction, unless the device has no return // register at all or already reverses the direction itself - if c.usage == "pv" && c.conn.HasReturnEnergy() && !c.conn.IsReversed() { + if c.usage == "pv" && hasReturn && !c.conn.IsReversed() { total, ret = ret, total } implement.Has(c, implement.MeterEnergy(total)) // without a return register the second reading is a constant zero - if c.conn.HasReturnEnergy() { + if hasReturn { implement.Has(c, implement.MeterReturnEnergy(ret)) } } From b29b49e3056ead3c99ff1f4711b2bb17083114b0 Mon Sep 17 00:00:00 2001 From: andig Date: Sat, 8 Aug 2026 16:44:51 +0200 Subject: [PATCH 1526/1747] core: simplify site/optimizer integration (#32646) --- core/site.go | 14 +- core/site_api.go | 6 +- core/site_optimizer.go | 268 +++++++++++++++++++----------------- core/site_optimizer_test.go | 44 ++++-- 4 files changed, 183 insertions(+), 149 deletions(-) diff --git a/core/site.go b/core/site.go index 329f464a6..ad83a1420 100644 --- a/core/site.go +++ b/core/site.go @@ -34,7 +34,6 @@ import ( "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/config" "github.com/evcc-io/evcc/util/modbus" - "github.com/evcc-io/evcc/util/sponsor" "github.com/evcc-io/evcc/util/telemetry" "github.com/samber/lo" "github.com/smallnest/chanx" @@ -116,9 +115,11 @@ 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 - batterySuggestions map[string]types.Suggestion // Optimizer suggestions by battery meter name - loadpointSuggestions map[int]types.Suggestion // Optimizer suggestions by loadpoint id + suggestions map[string]types.Suggestion // Optimizer suggestions by device key suggestionActions map[string]string // last notified actionable optimizer action by device key + + optimizerMu sync.Mutex // guards optimizer runs + optimizerUpdated time.Time // last optimizer run, guarded by optimizerMu } // MetersConfig contains the site's meter configuration @@ -788,8 +789,9 @@ func (site *Site) updateBatteryMeters() { // publishBattery applies the optimizer suggestions and publishes the battery state func (site *Site) publishBattery() { + mode := site.GetBatteryMode().String() for i, d := range site.battery.Devices { - site.battery.Devices[i].Suggestion = site.batterySuggestion(d.Name) + site.battery.Devices[i].Suggestion = site.suggestion(batteryKey(d.Name), mode) } site.publish(keys.Battery, site.battery) @@ -959,9 +961,7 @@ func (site *Site) updateMeters() error { return err } - if sponsor.IsAuthorized() && optimizerEnabled() && time.Since(optimizerUpdated) >= tariff.SlotDuration { - go site.optimizerUpdateAsync() - } + go site.optimizerUpdateAsync(tariff.SlotDuration) return nil } diff --git a/core/site_api.go b/core/site_api.go index e6830c562..eb755a8cd 100644 --- a/core/site_api.go +++ b/core/site_api.go @@ -44,7 +44,7 @@ func (site *Site) Optimize() error { return api.ErrNotAvailable } - go site.optimizerUpdateAsync() + go site.optimizerUpdateAsync(optimizerDebounce) return nil } @@ -369,7 +369,7 @@ func (site *Site) SetGridExportLimit(power float64) error { site.publish(keys.GridExportLimit, power) // re-run the optimizer so the new limit takes effect immediately - site.triggerOptimizer() + go site.optimizerUpdateAsync(0) } return nil @@ -519,7 +519,7 @@ func (site *Site) SetOptimizerChargingStrategy(strategy string) error { site.publish(keys.OptimizerChargingStrategy, strategy) // re-run the optimizer so the new strategy takes effect immediately - site.triggerOptimizer() + go site.optimizerUpdateAsync(0) } return nil diff --git a/core/site_optimizer.go b/core/site_optimizer.go index 87027d4b6..bdd34a44b 100644 --- a/core/site_optimizer.go +++ b/core/site_optimizer.go @@ -10,7 +10,6 @@ import ( "os" "slices" "strings" - "sync" "time" "github.com/evcc-io/evcc/api" @@ -30,12 +29,15 @@ import ( "golang.org/x/exp/constraints" ) -var ( - eta = float32(0.9) // efficiency of the battery charging/discharging - batteryPower = float32(6000) // default power of the battery in W +const ( + // eta is the efficiency of the battery charging/discharging + eta = 0.9 - mu sync.Mutex - optimizerUpdated time.Time + // batteryPower is the default power of the battery in W + batteryPower = 6000 + + // optimizerDebounce limits how often on-demand optimizer runs execute + optimizerDebounce = 2 * time.Minute ) // optimizerChargingStrategies are the valid grid charging strategies; the first @@ -53,23 +55,6 @@ const defaultOptimizerChargingStrategy = string(optimizer.OptimizerStrategyCharg // optimizerDecaySlots is the number of slots over which measured values decay into the forecast const optimizerDecaySlots = 4 -// triggerOptimizer re-runs the optimizer immediately so a changed setting takes -// effect without waiting for the next slot. It is a no-op when the optimizer is -// not active or a run is already in progress; the running update reflects the -// change on its next slot. -func (site *Site) triggerOptimizer() { - if !sponsor.IsAuthorized() || !optimizerEnabled() { - return - } - if !mu.TryLock() { - return - } - optimizerUpdated = time.Time{} // bypass the slot/debounce gate - mu.Unlock() - - go site.optimizerUpdateAsync() -} - // optimizerResult wraps the optimizer publish payload to implement BytesMarshaler. // This ensures publishComplex serializes it as a single JSON message instead of // recursively decomposing each struct field and array element into individual MQTT @@ -104,7 +89,34 @@ type batteryDetail struct { Capacity float64 `json:"capacity,omitempty"` loadpoint *int // originating loadpoint id for loadpoint/vehicle entries - controllable bool // battery exposes a controller; only these get suggestions + controllable bool // device can act on suggestions +} + +// batteryKey and loadpointKey build the canonical device keys used for +// suggestion routing and notifications +func batteryKey(name string) string { return "battery:" + name } +func loadpointKey(id int) string { return fmt.Sprintf("loadpoint:%d", id) } + +// key identifies the device across optimizer runs; an empty key means the +// device can't act on a suggestion. +func (d batteryDetail) key() string { + switch { + case d.Type == batteryTypeBattery: + return batteryKey(d.Name) + case d.loadpoint != nil: + return loadpointKey(*d.loadpoint) + default: + return "" + } +} + +// currentAction returns the device's current operating mode for suggestion +// comparison. Must only be called for devices with a non-empty key. +func (d batteryDetail) currentAction(site *Site) string { + if d.Type == batteryTypeBattery { + return site.GetBatteryMode().String() + } + return loadpointCurrentAction(site.loadpoints[*d.loadpoint]) } type batteryResult struct { @@ -136,10 +148,8 @@ type pendingSuggestion struct { event messenger.Event } -// suggestionEvent builds the notification key and event for a device suggestion. -// The key ("loadpoint:" / "battery:") identifies the device across -// runs; an empty key means the device can't act on a suggestion. -func suggestionEvent(detail batteryDetail, s types.Suggestion) (string, messenger.Event) { +// suggestionEvent builds the notification event for a device suggestion +func suggestionEvent(detail batteryDetail, s types.Suggestion) messenger.Event { ev := messenger.Event{Event: evSuggestion, Attributes: map[string]any{ "suggestionAction": s.Action, "suggestionTitle": detail.Title, @@ -148,14 +158,12 @@ func suggestionEvent(detail batteryDetail, s types.Suggestion) (string, messenge switch { case detail.Type == batteryTypeBattery: ev.Attributes["suggestionName"] = detail.Name - return "battery:" + detail.Name, ev case detail.loadpoint != nil: id := *detail.loadpoint ev.Loadpoint = &id - return fmt.Sprintf("loadpoint:%d", id), ev - default: - return "", ev } + + return ev } // currentSlotSuggestion maps the optimizer's first-slot corner result onto an advisory action. @@ -216,55 +224,35 @@ func loadpointCurrentAction(lp *Loadpoint) string { } // setSuggestions replaces the suggestions applied on each publish -func (site *Site) setSuggestions(batteries map[string]types.Suggestion, loadpoints map[int]types.Suggestion) { +func (site *Site) setSuggestions(suggestions map[string]types.Suggestion) { site.Lock() defer site.Unlock() - site.batterySuggestions = batteries - site.loadpointSuggestions = loadpoints + site.suggestions = suggestions } -// batterySuggestion returns the optimizer suggestion for the given battery meter. -// The actionable flag is evaluated on read since the battery mode changes between -// optimizer runs. -func (site *Site) batterySuggestion(name string) *types.Suggestion { - mode := site.GetBatteryMode().String() - +// suggestion returns the optimizer suggestion for the given device key. +// The actionable flag is evaluated on read against the device's current +// action since that changes between optimizer runs. +func (site *Site) suggestion(key, currentAction string) *types.Suggestion { site.RLock() - defer site.RUnlock() - - s, ok := site.batterySuggestions[name] - if !ok { - return nil - } - - s.Actionable = s.Action != mode - - return &s -} - -// loadpointSuggestion returns the optimizer suggestion for the given loadpoint. -// The actionable flag is evaluated on read since the loadpoint's action changes -// between optimizer runs. -func (site *Site) loadpointSuggestion(id int) *types.Suggestion { - site.RLock() - s, ok := site.loadpointSuggestions[id] + s, ok := site.suggestions[key] site.RUnlock() if !ok { return nil } - s.Actionable = s.Action != loadpointCurrentAction(site.loadpoints[id]) + s.Actionable = s.Action != currentAction return &s } // publishSuggestions publishes the loadpoints' suggestions func (site *Site) publishSuggestions() { - for id := range site.loadpoints { + for id, lp := range site.loadpoints { var val any - if s := site.loadpointSuggestion(id); s != nil { + if s := site.suggestion(loadpointKey(id), loadpointCurrentAction(lp)); s != nil { val = *s } site.publishLoadpoint(id, keys.Suggestion, val) @@ -274,7 +262,7 @@ func (site *Site) publishSuggestions() { // clearSuggestions removes all suggestions and the battery forecast when the // optimizer result is stale func (site *Site) clearSuggestions() { - site.setSuggestions(nil, nil) + site.setSuggestions(nil) site.battery.Forecast = nil site.publishBattery() @@ -291,21 +279,17 @@ func (site *Site) pendingSuggestions(details []batteryDetail) map[string]pending pending := make(map[string]pendingSuggestion, len(details)) for _, detail := range details { - var s *types.Suggestion - - switch { - case detail.Type == batteryTypeBattery: - s = site.batterySuggestion(detail.Name) - case detail.loadpoint != nil: - s = site.loadpointSuggestion(*detail.loadpoint) + key := detail.key() + if key == "" { + continue } + s := site.suggestion(key, detail.currentAction(site)) if s == nil { continue } - key, ev := suggestionEvent(detail, *s) - pending[key] = pendingSuggestion{suggestion: *s, event: ev} + pending[key] = pendingSuggestion{suggestion: *s, event: suggestionEvent(detail, *s)} } return pending @@ -346,19 +330,41 @@ type requestDetails struct { BatteryDetails []batteryDetail `json:"batteryDetails"` } +// optimizerBattery pairs a battery request entry with its device detail +type optimizerBattery struct { + cfg optimizer.BatteryConfig + detail batteryDetail +} + +func optimizerURI() string { + return cmp.Or(os.Getenv("OPTIMIZER_URI"), OPTIMIZER_URI) +} + const slotsPerHour = float64(time.Hour / tariff.SlotDuration) // errOptimizerNotReady means battery measurements aren't available yet (e.g. at // startup); the slot gate is left open so the next cycle retries. var errOptimizerNotReady = errors.New("battery measurements not ready") -func (site *Site) optimizerUpdateAsync() { - if !mu.TryLock() { +// optimizerUpdateAsync runs the optimizer unless the last run is younger than +// minAge. Pass 0 to force a run, e.g. when a changed setting should take effect +// without waiting for the next slot. It is a no-op when the optimizer is not +// active or a run is already in progress; the running update reflects the +// change on its next slot. +func (site *Site) optimizerUpdateAsync(minAge time.Duration) { + if !sponsor.IsAuthorized() || !optimizerEnabled() { return } - defer mu.Unlock() - if time.Since(optimizerUpdated) < 2*time.Minute { + if !site.optimizerMu.TryLock() { + return + } + defer site.optimizerMu.Unlock() + + if minAge == 0 { + // keep the gate open so a not-ready run is retried on the next cycle + site.optimizerUpdated = time.Time{} + } else if time.Since(site.optimizerUpdated) < minAge { return } @@ -374,7 +380,7 @@ func (site *Site) optimizerUpdateAsync() { return } - optimizerUpdated = time.Now() + site.optimizerUpdated = time.Now() if err != nil { site.log.ERROR.Println("optimizer:", err) @@ -387,7 +393,12 @@ func (site *Site) optimizerUpdateAsync() { err = site.optimizerUpdate(site.battery.Devices) } -func (site *Site) optimizerUpdate(battery []types.Measurement) error { +// optimizerRequest assembles the optimizer request and the matching device +// details from tariffs, home profile, loadpoints and battery meters +func (site *Site) optimizerRequest(battery []types.Measurement) (optimizer.OptimizationInput, requestDetails, error) { + var req optimizer.OptimizationInput + var details requestDetails + solarTariff := site.GetTariff(api.TariffUsageSolar) solar := currentRates(solarTariff) @@ -400,17 +411,16 @@ func (site *Site) optimizerUpdate(battery []types.Measurement) error { minLen = min(minLen, len(solar)) } - uri := cmp.Or(os.Getenv("OPTIMIZER_URI"), OPTIMIZER_URI) - if uri == OPTIMIZER_URI { + if optimizerURI() == OPTIMIZER_URI { // limit to 2 days for sake of performance minLen = min(2*96, minLen) } if expectedSlots := 8; minLen < expectedSlots { if solarTariff != nil { - return fmt.Errorf("not enough forecast slots for meaningful optimization: %d < %d (grid=%d, feedIn=%d, solar=%d)", minLen, expectedSlots, len(grid), len(feedIn), len(solar)) + return req, details, fmt.Errorf("not enough forecast slots for meaningful optimization: %d < %d (grid=%d, feedIn=%d, solar=%d)", minLen, expectedSlots, len(grid), len(feedIn), len(solar)) } - return fmt.Errorf("not enough forecast slots for meaningful optimization: %d < %d (grid=%d, feedIn=%d)", minLen, expectedSlots, len(grid), len(feedIn)) + return req, details, fmt.Errorf("not enough forecast slots for meaningful optimization: %d < %d (grid=%d, feedIn=%d)", minLen, expectedSlots, len(grid), len(feedIn)) } now := time.Now() @@ -426,7 +436,7 @@ func (site *Site) optimizerUpdate(battery []types.Measurement) error { gt, err := site.homeProfile(minLen) if err != nil { - return err + return req, details, err } // blend measured energy of the last metrics slot into the first slots @@ -441,7 +451,7 @@ func (site *Site) optimizerUpdate(battery []types.Measurement) error { if solarTariff != nil && len(solar) > 0 { solarEnergy, err := solarRatesToEnergy(solar) if err != nil { - return err + return req, details, err } scale := site.effectiveSolarScale() @@ -456,7 +466,7 @@ func (site *Site) optimizerUpdate(battery []types.Measurement) error { ft = prorate(ftSlots, firstSlotDuration) } - req := optimizer.OptimizationInput{ + req = optimizer.OptimizationInput{ Strategy: optimizer.OptimizerStrategy{ ChargingStrategy: optimizer.OptimizerStrategyChargingStrategy(site.GetOptimizerChargingStrategy()), DischargingStrategy: optimizer.OptimizerStrategyDischargingStrategyDischargeBeforeImport, @@ -475,7 +485,7 @@ func (site *Site) optimizerUpdate(battery []types.Measurement) error { // end of horizon Wh value pa := lo.Min(req.TimeSeries.PN) * eta * 0.99 - details := requestDetails{ + details = requestDetails{ Timestamps: asTimestamps(dt, now), } @@ -500,11 +510,7 @@ func (site *Site) optimizerUpdate(battery []types.Measurement) error { } } - add := func(battery optimizer.BatteryConfig, detail batteryDetail) { - battery.PA = pa - req.Batteries = append(req.Batteries, battery) - details.BatteryDetails = append(details.BatteryDetails, detail) - } + var batteries []optimizerBattery for id, lp := range site.Loadpoints() { // ignore disconnected loadpoints, including StatusNone @@ -517,9 +523,9 @@ func (site *Site) optimizerUpdate(battery []types.Measurement) error { } // skip disabled loadpoints - if req, detail := site.loadpointRequest(lp, minLen, firstSlotDuration, grid); req.CMax > 0 { + if cfg, detail := site.loadpointRequest(lp, minLen, firstSlotDuration, grid); cfg.CMax > 0 { detail.loadpoint = &id - add(req, detail) + batteries = append(batteries, optimizerBattery{cfg, detail}) } } @@ -534,7 +540,23 @@ func (site *Site) optimizerUpdate(battery []types.Measurement) error { continue } - add(site.batteryRequest(dev, b, grid, minLen, firstSlotDuration)) + cfg, detail := site.batteryRequest(dev, b, grid, minLen, firstSlotDuration) + batteries = append(batteries, optimizerBattery{cfg, detail}) + } + + for _, b := range batteries { + b.cfg.PA = pa + req.Batteries = append(req.Batteries, b.cfg) + details.BatteryDetails = append(details.BatteryDetails, b.detail) + } + + return req, details, nil +} + +func (site *Site) optimizerUpdate(battery []types.Measurement) error { + req, details, err := site.optimizerRequest(battery) + if err != nil { + return err } if len(req.Batteries) == 0 { @@ -549,7 +571,7 @@ func (site *Site) optimizerUpdate(battery []types.Measurement) error { httpClient := request.NewClient(site.log) httpClient.Timeout = 90 * time.Second - apiClient, err := optimizer.NewClientWithResponses(uri, optimizer.WithHTTPClient(httpClient)) + apiClient, err := optimizer.NewClientWithResponses(optimizerURI(), optimizer.WithHTTPClient(httpClient)) if err != nil { return err } @@ -581,51 +603,50 @@ func (site *Site) optimizerUpdate(battery []types.Measurement) error { return errors.New(string(resp.JSON200.Status)) } - slotHours := firstSlotDuration.Hours() - gridImporting := len(resp.JSON200.GridImport) > 0 && resp.JSON200.GridImport[0] > 0 - gridExporting := len(resp.JSON200.GridExport) > 0 && resp.JSON200.GridExport[0] > 0 + site.applyOptimizerResult(req, details.BatteryDetails, *resp.JSON200) + + return nil +} + +// applyOptimizerResult maps the optimizer response onto suggestions, battery +// forecast and notifications +func (site *Site) applyOptimizerResult(req optimizer.OptimizationInput, details []batteryDetail, res optimizer.OptimizationResult) { + slotHours := (time.Duration(req.TimeSeries.Dt[0]) * time.Second).Hours() + gridImporting := len(res.GridImport) > 0 && res.GridImport[0] > 0 + gridExporting := len(res.GridExport) > 0 && res.GridExport[0] > 0 var batteries []batteryResult suggestions := make(map[string]types.Suggestion, len(req.Batteries)) - lpSuggestions := make(map[int]types.Suggestion) for i, batReq := range req.Batteries { - batResp := resp.JSON200.Batteries[i] - detail := details.BatteryDetails[i] + batRes := res.Batteries[i] + detail := details[i] - batResult := batteryResult{ + batteries = append(batteries, batteryResult{ batteryDetail: detail, - Full: matchSoc(batResp.StateOfCharge, func(soc float32) bool { + Full: matchSoc(batRes.StateOfCharge, func(soc float32) bool { return soc >= batReq.SMax }), - Empty: matchSoc(batResp.StateOfCharge, func(soc float32) bool { + Empty: matchSoc(batRes.StateOfCharge, func(soc float32) bool { return soc <= batReq.SMin }), - } + }) - batteries = append(batteries, batResult) - - suggestion := currentSlotSuggestion(detail, batResp, gridImporting, gridExporting, slotHours) + suggestion := currentSlotSuggestion(detail, batRes, gridImporting, gridExporting, slotHours) if suggestion.Action == "" { continue } - switch { - case detail.Type == batteryTypeBattery: - // uncontrollable batteries can't act on a suggestion - if !detail.controllable { - continue - } - suggestions[detail.Name] = suggestion - case detail.loadpoint != nil: - lpSuggestions[*detail.loadpoint] = suggestion + // uncontrollable devices can't act on a suggestion + if key := detail.key(); key != "" && detail.controllable { + suggestions[key] = suggestion } } site.publish("evopt-batteries", batteries) - site.setSuggestions(suggestions, lpSuggestions) - site.battery.Forecast = site.addBatteryForecastTotals(req.Batteries, resp.JSON200.Batteries) + site.setSuggestions(suggestions) + site.battery.Forecast = site.addBatteryForecastTotals(req.Batteries, res.Batteries) site.publishBattery() @@ -633,11 +654,9 @@ func (site *Site) optimizerUpdate(battery []types.Measurement) error { site.publishSuggestions() // notify on actionable suggestion changes (advisory only, see #31903) - for _, ev := range site.diffSuggestions(site.pendingSuggestions(details.BatteryDetails)) { + for _, ev := range site.diffSuggestions(site.pendingSuggestions(details)) { site.pushEvent(ev) } - - return nil } func (site *Site) addBatteryForecastTotals(req []optimizer.BatteryConfig, resp []optimizer.BatteryResult) *types.BatteryForecast { @@ -741,8 +760,9 @@ func (site *Site) loadpointRequest(lp loadpoint.API, minLen int, firstSlotDurati } detail := batteryDetail{ - Type: batteryTypeLoadpoint, - Title: lp.GetTitle(), + Type: batteryTypeLoadpoint, + Title: lp.GetTitle(), + controllable: true, } // vehicle diff --git a/core/site_optimizer_test.go b/core/site_optimizer_test.go index 2d5133ba9..9e8d5bbb6 100644 --- a/core/site_optimizer_test.go +++ b/core/site_optimizer_test.go @@ -355,40 +355,49 @@ func TestSuggestionActionable(t *testing.T) { batteryMode: api.BatteryNormal, loadpoints: []*Loadpoint{lp}, } - site.setSuggestions( - map[string]types.Suggestion{"bat": {Action: api.BatteryCharge.String()}}, - map[int]types.Suggestion{0: {Action: actionCharge}}, - ) + site.setSuggestions(map[string]types.Suggestion{ + batteryKey("bat"): {Action: api.BatteryCharge.String()}, + loadpointKey(0): {Action: actionCharge}, + }) + + batterySuggestion := func(name string) *types.Suggestion { + return site.suggestion(batteryKey(name), site.GetBatteryMode().String()) + } + loadpointSuggestion := func(id int) *types.Suggestion { + return site.suggestion(loadpointKey(id), loadpointCurrentAction(lp)) + } // battery mode differs from suggestion - s := site.batterySuggestion("bat") + s := batterySuggestion("bat") require.NotNil(t, s) assert.True(t, s.Actionable) site.batteryMode = api.BatteryCharge - assert.False(t, site.batterySuggestion("bat").Actionable) + assert.False(t, batterySuggestion("bat").Actionable) - assert.Nil(t, site.batterySuggestion("unknown")) + assert.Nil(t, batterySuggestion("unknown")) // loadpoint stopped, suggestion is to charge - s = site.loadpointSuggestion(0) + s = loadpointSuggestion(0) require.NotNil(t, s) assert.True(t, s.Actionable) // loadpoint charging matches the suggestion lp.enabled = true lp.status = api.StatusC - assert.False(t, site.loadpointSuggestion(0).Actionable) + assert.False(t, loadpointSuggestion(0).Actionable) - assert.Nil(t, site.loadpointSuggestion(1)) + assert.Nil(t, loadpointSuggestion(1)) } func TestSuggestionEvent(t *testing.T) { id := 2 // battery: no loadpoint id, carries name - key, ev := suggestionEvent(batteryDetail{Type: batteryTypeBattery, Name: "home", Title: "Home"}, types.Suggestion{Action: api.BatteryCharge.String()}) - assert.Equal(t, "battery:home", key) + detail := batteryDetail{Type: batteryTypeBattery, Name: "home", Title: "Home"} + assert.Equal(t, "battery:home", detail.key()) + + ev := suggestionEvent(detail, types.Suggestion{Action: api.BatteryCharge.String()}) assert.Nil(t, ev.Loadpoint) assert.Equal(t, evSuggestion, ev.Event) assert.Equal(t, api.BatteryCharge.String(), ev.Attributes["suggestionAction"]) @@ -396,18 +405,23 @@ func TestSuggestionEvent(t *testing.T) { assert.Equal(t, "Home", ev.Attributes["suggestionTitle"]) // loadpoint: carries id, no name - key, ev = suggestionEvent(batteryDetail{Type: batteryTypeVehicle, loadpoint: &id, Title: "Garage"}, types.Suggestion{Action: actionCharge}) - assert.Equal(t, "loadpoint:2", key) + detail = batteryDetail{Type: batteryTypeVehicle, loadpoint: &id, Title: "Garage"} + assert.Equal(t, "loadpoint:2", detail.key()) + + ev = suggestionEvent(detail, types.Suggestion{Action: actionCharge}) require.NotNil(t, ev.Loadpoint) assert.Equal(t, id, *ev.Loadpoint) assert.NotContains(t, ev.Attributes, "suggestionName") + + // vehicle without loadpoint can't act on a suggestion + assert.Empty(t, batteryDetail{Type: batteryTypeVehicle}.key()) } func TestDiffSuggestions(t *testing.T) { site := &Site{} pending := func(s types.Suggestion) map[string]pendingSuggestion { - _, ev := suggestionEvent(batteryDetail{loadpoint: new(int)}, s) + ev := suggestionEvent(batteryDetail{loadpoint: new(int)}, s) return map[string]pendingSuggestion{"loadpoint:0": {suggestion: s, event: ev}} } From d09a952cbabb8e3f14532adcf670f8cf7d53a4d1 Mon Sep 17 00:00:00 2001 From: andig Date: Sun, 9 Aug 2026 10:37:22 +0200 Subject: [PATCH 1527/1747] Optimizer: add feasible status (#32632) --- .../js/components/Config/StatusIndicator.vue | 4 ++- .../Optimize/OptimizeHeader.stories.ts | 4 +++ .../js/components/Optimize/OptimizeHeader.vue | 35 +++++++++++-------- assets/js/types/evcc.ts | 1 + core/site_optimizer.go | 5 +-- 5 files changed, 32 insertions(+), 17 deletions(-) diff --git a/assets/js/components/Config/StatusIndicator.vue b/assets/js/components/Config/StatusIndicator.vue index 17c92d037..d27f33fb2 100644 --- a/assets/js/components/Config/StatusIndicator.vue +++ b/assets/js/components/Config/StatusIndicator.vue @@ -16,7 +16,7 @@ import { defineComponent, type PropType } from "vue"; import Tooltip from "bootstrap/js/dist/tooltip"; -type Variant = "success" | "warning" | "muted"; +type Variant = "success" | "warning" | "danger" | "muted"; export default defineComponent({ name: "StatusIndicator", @@ -34,6 +34,8 @@ export default defineComponent({ return "bg-success"; case "warning": return "bg-warning"; + case "danger": + return "bg-danger"; default: return "border border-secondary"; } diff --git a/assets/js/components/Optimize/OptimizeHeader.stories.ts b/assets/js/components/Optimize/OptimizeHeader.stories.ts index f443ac6a5..601eb24a2 100644 --- a/assets/js/components/Optimize/OptimizeHeader.stories.ts +++ b/assets/js/components/Optimize/OptimizeHeader.stories.ts @@ -41,6 +41,10 @@ Payment.args = { ...base, status: OptimizationStatus.OPTIMAL, netCost: 12.4 }; export const Credit = Template.bind({}); Credit.args = { ...base, status: OptimizationStatus.OPTIMAL, netCost: -3.8 }; +// usable but not proven optimal plan +export const Feasible = Template.bind({}); +Feasible.args = { ...base, status: OptimizationStatus.FEASIBLE, netCost: 12.4 }; + // solver could not produce a plan export const Infeasible = Template.bind({}); Infeasible.args = { ...base, status: OptimizationStatus.INFEASIBLE, netCost: 0 }; diff --git a/assets/js/components/Optimize/OptimizeHeader.vue b/assets/js/components/Optimize/OptimizeHeader.vue index a7cd81b65..5736ecd75 100644 --- a/assets/js/components/Optimize/OptimizeHeader.vue +++ b/assets/js/components/Optimize/OptimizeHeader.vue @@ -11,7 +11,9 @@
always, fixed
- Lowest cost + Lowest cost
@@ -32,7 +34,7 @@ @change="onStrategyChange" > {{ secondaryGoalLabel }} @@ -60,7 +62,7 @@
- {{ + {{ status }} @@ -88,7 +90,7 @@
{{ netCostDisplay }} @@ -132,6 +134,7 @@ const STRATEGY_LABELS: Record = { const STATUS_TOOLTIP = "The optimizer result:

" + "Optimal: the best plan was found.
" + + "Feasible: a usable plan was found, but not proven optimal.
" + "Infeasible: the constraints cannot all be met.
" + "Not solved: optimization hasn't finished yet."; @@ -180,13 +183,15 @@ export default defineComponent({ if (Math.abs(elapsed) < 60 * 1000) return "just now"; return this.fmtTimeAgo(elapsed); }, - statusVariant(): "success" | "warning" | "muted" { + statusVariant(): "success" | "warning" | "danger" | "muted" { switch (this.status) { case OptimizationStatus.OPTIMAL: return "success"; + case OptimizationStatus.FEASIBLE: + return "warning"; case OptimizationStatus.INFEASIBLE: case OptimizationStatus.UNBOUNDED: - return "warning"; + return "danger"; default: return "muted"; } @@ -276,20 +281,22 @@ export default defineComponent({ .info-icon { cursor: help; } +/* body size on small screens, larger from md up; bold carries the emphasis */ +.value-text { + font-size: 1rem; + line-height: 1.2; +} +@media (min-width: 768px) { + .value-text { + font-size: 1.25rem; + } +} .secondary-goal { text-decoration: underline; text-decoration-color: var(--evcc-gray); text-underline-offset: 3px; } -/* small: slightly reduced value size, rows are tighter */ -@media (max-width: 767.98px) { - .field-value .large, - .field-value .fs-4 { - font-size: 1.125rem !important; - } -} - /* md and up: each field a vertical stack (label, value, caption); columns via Bootstrap grid */ @media (min-width: 768px) { .field { diff --git a/assets/js/types/evcc.ts b/assets/js/types/evcc.ts index 30a2bb32a..97cd268e5 100644 --- a/assets/js/types/evcc.ts +++ b/assets/js/types/evcc.ts @@ -1429,6 +1429,7 @@ export interface TimeSeries { // Solver status enum export enum OptimizationStatus { OPTIMAL = "Optimal", + FEASIBLE = "Feasible", INFEASIBLE = "Infeasible", UNBOUNDED = "Unbounded", UNDEFINED = "Undefined", diff --git a/core/site_optimizer.go b/core/site_optimizer.go index bdd34a44b..ef1a7b983 100644 --- a/core/site_optimizer.go +++ b/core/site_optimizer.go @@ -599,8 +599,9 @@ func (site *Site) optimizerUpdate(battery []types.Measurement) error { Details: details, }) - if resp.JSON200.Status != optimizer.Optimal { - return errors.New(string(resp.JSON200.Status)) + // feasible results are usable, they are just not proven optimal + if status := resp.JSON200.Status; status != optimizer.Optimal && status != optimizer.Feasible { + return errors.New(string(status)) } site.applyOptimizerResult(req, details.BatteryDetails, *resp.JSON200) From 27aca97bab947c7a782a1e116398812c027c3a50 Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Sun, 9 Aug 2026 11:22:04 +0200 Subject: [PATCH 1528/1747] Tasmota: mask password in error messages (#32659) --- meter/tasmota/connection.go | 39 ++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/meter/tasmota/connection.go b/meter/tasmota/connection.go index 3442d3932..9f1b65fb9 100644 --- a/meter/tasmota/connection.go +++ b/meter/tasmota/connection.go @@ -43,7 +43,7 @@ func NewConnection(uri, user, password string, channels []int, cache time.Durati used[c] = true } - log := util.NewLogger("tasmota") + log := util.NewLogger("tasmota").Redact(user, password) c := &Connection{ Helper: request.NewHelper(log), uri: util.DefaultScheme(strings.TrimRight(uri, "/"), "http"), @@ -55,30 +55,35 @@ func NewConnection(uri, user, password string, channels []int, cache time.Durati c.Client.Transport = request.NewTripper(log, transport.Insecure()) c.statusSnsG = util.ResettableCached(func() (StatusSNSResponse, error) { - parameters := url.Values{ - "user": {c.user}, - "password": {c.password}, - "cmnd": {"Status 8"}, - } var res StatusSNSResponse - err := c.GetJSON(fmt.Sprintf("%s/cm?%s", c.uri, parameters.Encode()), &res) + err := c.cmd("Status 8", &res) return res, err }, cache) c.statusStsG = util.ResettableCached(func() (StatusSTSResponse, error) { - parameters := url.Values{ - "user": {c.user}, - "password": {c.password}, - "cmnd": {"Status 0"}, - } var res StatusSTSResponse - err := c.GetJSON(fmt.Sprintf("%s/cm?%s", c.uri, parameters.Encode()), &res) + err := c.cmd("Status 0", &res) return res, err }, cache) return c, nil } +// cmd executes a Tasmota command, masking the password in returned errors +func (c *Connection) cmd(cmnd string, res any) error { + parameters := url.Values{ + "user": {c.user}, + "password": {c.password}, + "cmnd": {cmnd}, + } + + err := c.GetJSON(fmt.Sprintf("%s/cm?%s", c.uri, parameters.Encode()), res) + if err != nil && c.password != "" { + return errors.New(strings.ReplaceAll(err.Error(), url.QueryEscape(c.password), "***")) + } + return err +} + // channelExists checks the existence of the configured relay channel interface func (c *Connection) RelayExists() error { res, err := c.statusStsG.Get() @@ -123,14 +128,8 @@ func (c *Connection) Enable(enable bool) error { cmd = fmt.Sprintf("Power%d on", channel) } - parameters := url.Values{ - "user": {c.user}, - "password": {c.password}, - "cmnd": {cmd}, - } - var res PowerResponse - if err := c.GetJSON(fmt.Sprintf("%s/cm?%s", c.uri, parameters.Encode()), &res); err != nil { + if err := c.cmd(cmd, &res); err != nil { return err } From 11cc118002a32dca1b3c7f29713427274b1ad669 Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Sun, 9 Aug 2026 11:29:39 +0200 Subject: [PATCH 1529/1747] chore: remove prettier editor leftovers, align oxfmt config (#32657) --- .vscode/extensions.json | 2 -- oxfmt.config.ts | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.vscode/extensions.json b/.vscode/extensions.json index ec190d867..4978ed1ba 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,9 +1,7 @@ { "recommendations": [ "Vue.volar", - "vitest.explorer", "golang.go", - "esbenp.prettier-vscode", "yoavbls.pretty-ts-errors", "VoidZero.vite-plus-extension-pack" ] diff --git a/oxfmt.config.ts b/oxfmt.config.ts index a304002f7..d3b80482c 100644 --- a/oxfmt.config.ts +++ b/oxfmt.config.ts @@ -1,6 +1,8 @@ import { defineConfig } from "oxfmt"; export default defineConfig({ + printWidth: 100, + trailingComma: "es5", ignorePatterns: ["tests/custom-css.css"], overrides: [{ files: ["**/*.vue"], options: { tabWidth: 4 } }], }); From 7d4bb61636f13e4cffb42348624b1adf15979b3d Mon Sep 17 00:00:00 2001 From: mfuchs1984 <57141790+mfuchs1984@users.noreply.github.com> Date: Sun, 9 Aug 2026 11:36:51 +0200 Subject: [PATCH 1530/1747] Hoymiles AhoyDTU: add cache (#32652) --- templates/definition/meter/hoymiles-ahoydtu.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/templates/definition/meter/hoymiles-ahoydtu.yaml b/templates/definition/meter/hoymiles-ahoydtu.yaml index 85a3c9111..8a7369351 100644 --- a/templates/definition/meter/hoymiles-ahoydtu.yaml +++ b/templates/definition/meter/hoymiles-ahoydtu.yaml @@ -16,13 +16,17 @@ params: de: "Falls mehrere vorhanden. Die Nummerierung beginnt bei 0. Siehe AhoyDTU Webinterface -> Inverter #[ID]" en: "If multiple exist. The numbering starts at 0. See AhoyDTU webinterface -> Inverter #[ID]" default: 0 + - name: cache + default: 1s render: | type: custom power: source: http uri: http://{{ .host }}/api/inverter/id/{{ .id }} jq: .ch[0][2] + cache: {{ .cache }} energy: source: http uri: http://{{ .host }}/api/inverter/id/{{ .id }} jq: .ch[0][6] + cache: {{ .cache }} From 99a9c889480faad431ed0df695218ed2e5e6802f Mon Sep 17 00:00:00 2001 From: CiNcH83 Date: Sun, 9 Aug 2026 12:19:03 +0200 Subject: [PATCH 1531/1747] Huawei SmartLogger: add returnenergy to grid (#32650) --- templates/definition/meter/huawei-smartlogger.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/templates/definition/meter/huawei-smartlogger.yaml b/templates/definition/meter/huawei-smartlogger.yaml index 85b69967c..cf40df6a1 100644 --- a/templates/definition/meter/huawei-smartlogger.yaml +++ b/templates/definition/meter/huawei-smartlogger.yaml @@ -34,6 +34,14 @@ render: | type: holding decode: int64 scale: 0.01 + returnenergy: + source: modbus + {{- include "modbus" . | indent 2 }} + register: + address: 32357 # Positive active electricity + type: holding + decode: int64 + scale: 0.01 currents: - source: modbus {{- include "modbus" . | indent 2 }} From 28e41bdd09f59cbbe36ee9d094c5fbb7e4c95c0b Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Sun, 9 Aug 2026 13:01:19 +0200 Subject: [PATCH 1532/1747] chore: fix lint:format skipping assets and tests (#32662) --- assets/js/components/BottomTabs/Bar.vue | 9 +-- .../components/ChargingPlans/PlanStrategy.vue | 10 ++- .../js/components/Config/PropertyZoneForm.vue | 1 - i18n/check.ts | 72 ++++++++++--------- package.json | 2 +- tests/hems.spec.ts | 5 +- tests/plan.spec.ts | 4 +- 7 files changed, 52 insertions(+), 51 deletions(-) diff --git a/assets/js/components/BottomTabs/Bar.vue b/assets/js/components/BottomTabs/Bar.vue index a4ae2ae74..3628df075 100644 --- a/assets/js/components/BottomTabs/Bar.vue +++ b/assets/js/components/BottomTabs/Bar.vue @@ -52,14 +52,7 @@ import BatteryIcon from "../Energyflow/BatteryIcon.vue"; import Item from "./Item.vue"; import MoreItem from "./MoreItem.vue"; import { defineComponent, type PropType } from "vue"; -import type { - FatalError, - Sponsor, - EvOpt, - AuthProviders, - Battery, - Vehicle, -} from "@/types/evcc"; +import type { FatalError, Sponsor, EvOpt, AuthProviders, Battery, Vehicle } from "@/types/evcc"; export default defineComponent({ name: "BottomTabBar", diff --git a/assets/js/components/ChargingPlans/PlanStrategy.vue b/assets/js/components/ChargingPlans/PlanStrategy.vue index 7e38251e0..13bb6a8c9 100644 --- a/assets/js/components/ChargingPlans/PlanStrategy.vue +++ b/assets/js/components/ChargingPlans/PlanStrategy.vue @@ -31,7 +31,10 @@
-