From 58c4b446d6056154c0b7158546d39599be17310b Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Sat, 1 Aug 2026 12:44:11 +0200 Subject: [PATCH 1/5] Cardata: delete obsolete streaming containers (#32376) --- vehicle/bmw/cardata/api.go | 5 ++++- vehicle/bmw/cardata/mqtt.go | 2 +- vehicle/bmw/cardata/provider.go | 21 +++++++++++++++++++-- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/vehicle/bmw/cardata/api.go b/vehicle/bmw/cardata/api.go index 219baa824..a8857708c 100644 --- a/vehicle/bmw/cardata/api.go +++ b/vehicle/bmw/cardata/api.go @@ -33,7 +33,10 @@ var requiredKeys = []string{ "vehicle.vehicle.travelledDistance", } -const requiredVersion = "v6" +const ( + containerName = "evcc.io" + requiredVersion = "v6" +) type API struct { *request.Helper diff --git a/vehicle/bmw/cardata/mqtt.go b/vehicle/bmw/cardata/mqtt.go index 8b0801139..94fb9cf6c 100644 --- a/vehicle/bmw/cardata/mqtt.go +++ b/vehicle/bmw/cardata/mqtt.go @@ -205,7 +205,7 @@ func (v *MqttConnector) handler(_ mqtt.Client, m mqtt.Message) { return } - v.log.TRACE.Println("recv: " + string(m.Payload())) + v.log.TRACE.Printf("recv %s: %s", m.Topic(), string(m.Payload())) v.mu.RLock() defer v.mu.RUnlock() diff --git a/vehicle/bmw/cardata/provider.go b/vehicle/bmw/cardata/provider.go index cf9ab0e93..dd47620f1 100644 --- a/vehicle/bmw/cardata/provider.go +++ b/vehicle/bmw/cardata/provider.go @@ -71,14 +71,17 @@ func (v *Provider) findOrCreateContainer() (string, error) { return "", err } + // obsolete containers keep streaming, resulting in duplicate messages + defer v.deleteObsoleteContainers(containers) + if i := slices.IndexFunc(containers, func(c Container) bool { - return c.Name == "evcc.io" && c.Purpose == requiredVersion + return c.Name == containerName && c.Purpose == requiredVersion }); i >= 0 { return containers[i].ContainerId, nil } res, err := v.api.CreateContainer(CreateContainer{ - Name: "evcc.io", + Name: containerName, Purpose: requiredVersion, TechnicalDescriptors: requiredKeys, }) @@ -86,6 +89,20 @@ func (v *Provider) findOrCreateContainer() (string, error) { return res.ContainerId, err } +func (v *Provider) deleteObsoleteContainers(containers []Container) { + for _, c := range containers { + if c.Name != containerName || c.Purpose == requiredVersion { + continue + } + + v.log.DEBUG.Printf("deleting obsolete container %s (%s)", c.ContainerId, c.Purpose) + + if err := v.api.DeleteContainer(c.ContainerId); err != nil { + v.log.WARN.Printf("delete container %s: %v", c.ContainerId, err) + } + } +} + func (v *Provider) setupContainer() error { container, err := v.findOrCreateContainer() if err != nil { From fff64791152366541b299e6649c765211b98b9f2 Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Sat, 1 Aug 2026 12:56:12 +0200 Subject: [PATCH 2/5] 0.313 backport: chore: fix request cache warning (#32382) --- plugin/http.go | 13 ++----------- plugin/http_test.go | 7 +++---- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/plugin/http.go b/plugin/http.go index 44db1ddc4..5d54ec8b3 100644 --- a/plugin/http.go +++ b/plugin/http.go @@ -229,8 +229,8 @@ func (p *HTTP) request(url string, body string) ([]byte, error) { // warn on uncached GET polling: a repeated roundtrip means neither a configured // cache nor the device's own response headers spared it. cache hits are exempt. if p.method == http.MethodGet && p.mu == nil && resp.Header.Get(httpcache.XFromCache) == "" { - if key := stripQuery(url); repeatedGet(key, time.Now()) { - p.log.WARN.Printf("uncached request repeated within 1s, please report at https://github.com/evcc-io/evcc/issues: %s", key) + if repeatedGet(url, time.Now()) { + p.log.WARN.Printf("uncached request repeated within 1s, please report at https://github.com/evcc-io/evcc/issues: %s", url) } } @@ -254,15 +254,6 @@ var ( httpSeen = make(map[string]httpAccess) ) -// stripQuery drops the query and fragment so cache-busting params do not make -// each poll look like a distinct url. -func stripQuery(url string) string { - if i := strings.IndexAny(url, "?#"); i >= 0 { - return url[:i] - } - return url -} - // repeatedGet reports the first time url is fetched again within a second, a sign // the response should be cached. It fires once per url to avoid log spam. func repeatedGet(url string, now time.Time) bool { diff --git a/plugin/http_test.go b/plugin/http_test.go index 3a70d68a3..e71c65da1 100644 --- a/plugin/http_test.go +++ b/plugin/http_test.go @@ -176,8 +176,7 @@ func TestRepeatedGet(t *testing.T) { require.False(t, repeatedGet(spaced, t0)) require.False(t, repeatedGet(spaced, t0.Add(2*time.Second))) // >1s apart: no warn - // query params are stripped before keying, so cache-busting still counts as a repeat - require.Equal(t, "http://q.test/path", stripQuery("http://q.test/path?ts=1&x=2#frag")) - require.False(t, repeatedGet(stripQuery("http://q.test/path?ts=1"), t0)) - require.True(t, repeatedGet(stripQuery("http://q.test/path?ts=2"), t0.Add(300*time.Millisecond))) + // query params are part of the key, so cache-busting urls are distinct requests + require.False(t, repeatedGet("http://q.test/path?ts=1", t0)) + require.False(t, repeatedGet("http://q.test/path?ts=2", t0.Add(300*time.Millisecond))) } From 0990bd97843952ae993004d5d2a32c71f8cdb9dc Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Sat, 1 Aug 2026 13:00:13 +0200 Subject: [PATCH 3/5] 0.313 backport: Sessions UI: fix grouped chart layout and legend styling (#32383) --- assets/js/colors.ts | 3 +++ assets/js/components/Sessions/AvgCostGroupedChart.vue | 6 +++--- assets/js/components/Sessions/CostGroupedChart.vue | 6 +++--- assets/js/components/Sessions/EnergyGroupedChart.vue | 6 +++--- assets/js/components/Sessions/LegendList.vue | 4 ++-- assets/js/components/Sessions/SessionTable.vue | 2 +- assets/js/components/Sessions/SolarGroupedChart.vue | 6 +++--- assets/js/components/Sessions/SolarYearChart.vue | 2 +- assets/js/views/Sessions.vue | 6 +++--- 9 files changed, 22 insertions(+), 19 deletions(-) diff --git a/assets/js/colors.ts b/assets/js/colors.ts index d9425d322..ffa039a14 100644 --- a/assets/js/colors.ts +++ b/assets/js/colors.ts @@ -27,6 +27,7 @@ const colors: { temperature: string | null; export: string | null; background: string | null; + box: string | null; light: string | null; selfPalette: string[]; palette: string[]; @@ -44,6 +45,7 @@ const colors: { temperature: null, export: null, background: null, + box: null, light: null, selfPalette: ["#0FDE41", "#FFBD2F", "#FD6158", "#03C1EF", "#0F662D", "#FF922E"], palette: [ @@ -152,6 +154,7 @@ export function updateCssColors() { colors.temperature = style.getPropertyValue("--evcc-temperature"); colors.export = style.getPropertyValue("--evcc-export-contrast"); colors.background = style.getPropertyValue("--evcc-background"); + colors.box = style.getPropertyValue("--evcc-box"); colors.pricePerKWh = style.getPropertyValue("--bs-gray-medium"); colors.co2PerKWh = style.getPropertyValue("--bs-gray-medium"); colors.light = style.getPropertyValue("--bs-gray-light"); diff --git a/assets/js/components/Sessions/AvgCostGroupedChart.vue b/assets/js/components/Sessions/AvgCostGroupedChart.vue index a62e8446f..814572f0c 100644 --- a/assets/js/components/Sessions/AvgCostGroupedChart.vue +++ b/assets/js/components/Sessions/AvgCostGroupedChart.vue @@ -1,9 +1,9 @@