From 16b170624e46b3146d00fe6a0f2df350b1eb7475 Mon Sep 17 00:00:00 2001 From: andig Date: Thu, 9 Apr 2026 11:18:24 +0200 Subject: [PATCH 0001/1293] Homeassistant charger: fix phases (2nd attempt) --- templates/definition/charger/homeassistant.yaml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/templates/definition/charger/homeassistant.yaml b/templates/definition/charger/homeassistant.yaml index 66707e0b0..1d80e29a3 100644 --- a/templates/definition/charger/homeassistant.yaml +++ b/templates/definition/charger/homeassistant.yaml @@ -143,13 +143,16 @@ params: en: Entity ID for L3 voltage measurement in volts de: Entitäts-ID für L3 Spannungsmessung in Volt + # cannot uses phases as it has type int - name: phases + deprecated: true + + - name: phaseswitch description: de: Phasenumschaltungs-Entität en: Phase switching entity service: homeassistant/entities?uri={uri}&domain=select,input_select example: "select.charger_phases" - type: string # override default advanced: true help: en: Entity ID for 1p/3p phase switching (select entity with options "1" and "3") @@ -175,6 +178,4 @@ render: | - {{ .voltageL2 }} - {{ .voltageL3 }} {{- end }} - {{- if .phases }} - phases: {{ .phases }} - {{- end }} + phases: {{ .phaseswitch }} From d9fbd9d06066411e83a007ac4634e5c940aa8767 Mon Sep 17 00:00:00 2001 From: andig Date: Thu, 9 Apr 2026 12:48:16 +0200 Subject: [PATCH 0002/1293] Tariffs: speed up combining forecasts --- tariff/combined.go | 45 ++++++++++++++--------------------------- tariff/combined_test.go | 19 +++++++++++++++++ 2 files changed, 34 insertions(+), 30 deletions(-) diff --git a/tariff/combined.go b/tariff/combined.go index 4a9f20121..db3a70e6c 100644 --- a/tariff/combined.go +++ b/tariff/combined.go @@ -1,11 +1,10 @@ package tariff import ( - "errors" - "slices" "time" "github.com/evcc-io/evcc/api" + "github.com/samber/lo" ) type combined struct { @@ -19,45 +18,31 @@ func NewCombined(tariffs []api.Tariff) api.Tariff { } func (t *combined) Rates() (api.Rates, error) { - var keys []time.Time + var rates api.Rates + for _, t := range t.tariffs { rr, err := t.Rates() if err != nil { return nil, err } - for _, r := range rr { - if !slices.ContainsFunc(keys, r.Start.Equal) { - keys = append(keys, r.Start) - } - } + rates = append(rates, rr...) } var res api.Rates - keys = slices.SortedFunc(slices.Values(keys), time.Time.Compare) - for _, ts := range keys { - var rate api.Rate + partitions := lo.PartitionBy(rates, func(r api.Rate) time.Time { + return r.Start + }) - for _, t := range t.tariffs { - r, err := At(t, ts) - if err != nil { - continue - } - - if rate.Start.IsZero() { - rate = r - continue - } - - if !r.End.Equal(rate.End) { - return nil, errors.New("combined tariffs must have the same period length") - } - - rate.Value += r.Value - } - - res = append(res, rate) + for _, rr := range partitions { + res = append(res, api.Rate{ + Start: rr[0].Start, + End: rr[0].End, + Value: lo.SumBy(rr, func(r api.Rate) float64 { + return r.Value + }), + }) } return res, nil diff --git a/tariff/combined_test.go b/tariff/combined_test.go index 5c1e359c3..d470d38dc 100644 --- a/tariff/combined_test.go +++ b/tariff/combined_test.go @@ -40,3 +40,22 @@ func TestCombined(t *testing.T) { require.NoError(t, err) assert.Equal(t, api.Rates{rate(1, 1), rate(2, 4), rate(3, 3)}, rr) } + +func BenchmarkCombined(bench *testing.B) { + clock := clock.NewMock() + rate := func(start int, val float64) api.Rate { + return api.Rate{ + Start: clock.Now().Add(time.Duration(start) * time.Hour), + End: clock.Now().Add(time.Duration(start+1) * time.Hour), + Value: val, + } + } + + a := &tariff{api.Rates{rate(1, 1), rate(2, 2)}} + b := &tariff{api.Rates{rate(2, 2), rate(3, 3)}} + c := &combined{[]api.Tariff{a, b}} + + for bench.Loop() { + c.Rates() + } +} From 9e562fa28fca94075dbe18216eabd6b906bcd067 Mon Sep 17 00:00:00 2001 From: Ajinkya Gokhale <121028346+AjinkyaGokhale@users.noreply.github.com> Date: Thu, 9 Apr 2026 17:31:10 +0200 Subject: [PATCH 0003/1293] Add stromleser.one (#28976) --- templates/definition/meter/stromleser.yaml | 35 ++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 templates/definition/meter/stromleser.yaml diff --git a/templates/definition/meter/stromleser.yaml b/templates/definition/meter/stromleser.yaml new file mode 100644 index 000000000..c1768bb91 --- /dev/null +++ b/templates/definition/meter/stromleser.yaml @@ -0,0 +1,35 @@ +template: stromleser +products: + - brand: stromleser.one + description: + generic: IR Reader +requirements: + description: + en: Requires a stromleser.one device accessible on the local network. + de: Erfordert ein stromleser.one-Gerät im lokalen Netzwerk. +params: + - name: usage + choice: ["grid", "pv"] + - name: host + - name: cache + default: 15s +render: | + type: custom + power: + source: http + uri: http://{{ .host }}/v1/data + cache: {{ .cache }} + {{- if eq .usage "pv" }} + jq: 'def toW: split(" ") | (.[0]|tonumber) * (if .[1]=="kW" then 1000 elif .[1]=="mW" then 0.001 else 1 end); try (.["2.7.0"]|toW) // (0 - (.["16.7.0"]|toW))' + {{- else }} + jq: 'def toW: split(" ") | (.[0]|tonumber) * (if .[1]=="kW" then 1000 elif .[1]=="mW" then 0.001 else 1 end); try (.["16.7.0"]|toW) // ((.["1.7.0"]|toW) - (.["2.7.0"]|toW))' + {{- end }} + energy: + source: http + uri: http://{{ .host }}/v1/data + cache: {{ .cache }} + {{- if eq .usage "pv" }} + jq: 'def tokWh: split(" ") | (.[0]|tonumber) * (if .[1]=="Wh" then 0.001 elif .[1]=="MWh" then 1000 else 1 end); (try (.["2.8.0"]|tokWh)) // 0' + {{- else }} + jq: 'def tokWh: split(" ") | (.[0]|tonumber) * (if .[1]=="Wh" then 0.001 elif .[1]=="MWh" then 1000 else 1 end); (try (.["1.8.0"]|tokWh)) // 0' + {{- end }} From 8eb7412a74e02a96a3fff5b5ab6873a6c5c3e077 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20He=C3=9F?= Date: Thu, 9 Apr 2026 17:38:58 +0200 Subject: [PATCH 0004/1293] Easee: restore noop signal from dispatcher send (#28972) --- charger/easee.go | 20 +++++++++++-------- charger/easee/dispatcher.go | 29 +++++++++++++++------------- charger/easee/dispatcher_test.go | 33 ++++++++++++++++++++++---------- charger/easee_test.go | 4 ++-- 4 files changed, 53 insertions(+), 33 deletions(-) diff --git a/charger/easee.go b/charger/easee.go index 1e40d5b7f..ec41b2824 100644 --- a/charger/easee.go +++ b/charger/easee.go @@ -486,7 +486,7 @@ func (c *Easee) Enable(enable bool) (err error) { } uri := fmt.Sprintf("%s/chargers/%s/settings", easee.API, c.charger) - if err := c.dispatcher.Send(uri, data); err != nil { + if _, err := c.dispatcher.Send(uri, data); err != nil { return err } } @@ -508,7 +508,7 @@ func (c *Easee) Enable(enable bool) (err error) { } uri := fmt.Sprintf("%s/chargers/%s/commands/%s", easee.API, c.charger, action) - if err := c.dispatcher.Send(uri, nil); err != nil { + if _, err := c.dispatcher.Send(uri, nil); err != nil { return err } @@ -620,11 +620,15 @@ func (c *Easee) MaxCurrent(current int64) error { } uri := fmt.Sprintf("%s/chargers/%s/settings", easee.API, c.charger) - if err := c.dispatcher.Send(uri, data); err != nil { + noop, err := c.dispatcher.Send(uri, data) + if err != nil { return err } - if err := c.waitForDynamicChargerCurrent(cur); err != nil { - return err + + if !noop { + if err := c.waitForDynamicChargerCurrent(cur); err != nil { + return err + } } c.mux.Lock() @@ -733,7 +737,7 @@ func (c *Easee) Phases1p3p(phases int) error { // cloud sends on HTTP 200 (sync) responses is silently consumed rather // than logged as rogue. On error we undo the registration. c.dispatcher.ExpectOrphan(easee.CIRCUIT_MAX_CURRENT_P1) - err = c.dispatcher.Send(uri, data) + _, err = c.dispatcher.Send(uri, data) if err != nil { c.dispatcher.CancelOrphan(easee.CIRCUIT_MAX_CURRENT_P1) } @@ -751,7 +755,7 @@ func (c *Easee) Phases1p3p(phases int) error { uri := fmt.Sprintf("%s/chargers/%s/settings", easee.API, c.charger) - if err = c.dispatcher.Send(uri, data); err != nil { + if _, err = c.dispatcher.Send(uri, data); err != nil { return err } } @@ -829,7 +833,7 @@ func (c *Easee) updateSmartCharging() { uri := fmt.Sprintf("%s/chargers/%s/settings", easee.API, c.charger) - if err := c.dispatcher.Send(uri, data); err != nil { + if _, err := c.dispatcher.Send(uri, data); err != nil { c.log.WARN.Printf("smart charging: %v", err) return } diff --git a/charger/easee/dispatcher.go b/charger/easee/dispatcher.go index 97ef12cd7..c874b2812 100644 --- a/charger/easee/dispatcher.go +++ b/charger/easee/dispatcher.go @@ -104,18 +104,21 @@ func (d *CommandDispatcher) CancelOrphan(id ObservationID) bool { // if the response is asynchronous (HTTP 202), waits for the matching SignalR // CommandResponse. // -// Returns nil on success (both synchronous HTTP 200 and confirmed async HTTP 202, -// including noops where Ticks == 0). Returns an error on HTTP failure, decode -// failure, command rejection, or timeout. -func (d *CommandDispatcher) Send(uri string, data any) error { +// Returns noop=true when the API indicates no state change was needed (HTTP 200 +// or HTTP 202 with an empty settings array / Ticks == 0). Callers that wait for +// a subsequent state observation (e.g. waitForDynamicChargerCurrent) must skip +// the wait on noop to avoid a timeout. +// +// Returns an error on HTTP failure, decode failure, command rejection, or timeout. +func (d *CommandDispatcher) Send(uri string, data any) (bool, error) { resp, err := d.helper.Post(uri, request.JSONContent, request.MarshalJSON(data)) if err != nil { - return err + return false, err } defer resp.Body.Close() if resp.StatusCode == 200 { - return nil + return true, nil } // Any status other than 200 or 202 is unexpected — return an error. @@ -123,7 +126,7 @@ func (d *CommandDispatcher) Send(uri string, data any) error { // not on HTTP error responses, so this guard is the actual defense against // 4xx/5xx responses from the Easee API. if resp.StatusCode != 202 { - return fmt.Errorf("unexpected status: %d", resp.StatusCode) + return false, fmt.Errorf("unexpected status: %d", resp.StatusCode) } // HTTP 202: parse the response body to get the command correlation info. @@ -131,13 +134,13 @@ func (d *CommandDispatcher) Send(uri string, data any) error { if strings.Contains(uri, "/commands/") { // Command endpoints return a single object. if err := json.NewDecoder(resp.Body).Decode(&cmd); err != nil { - return err + return false, err } } else { // Settings endpoints return an array; take index 0 if present. var cmdArr []RestCommandResponse if err := json.NewDecoder(resp.Body).Decode(&cmdArr); err != nil { - return err + return false, err } if len(cmdArr) != 0 { cmd = cmdArr[0] @@ -149,7 +152,7 @@ func (d *CommandDispatcher) Send(uri string, data any) error { if cmd.Ticks == 0 { // Noop: the API indicates no state change was needed. - return nil + return true, nil } // Create a buffered channel (capacity 1) so Dispatch never blocks even if @@ -177,10 +180,10 @@ func (d *CommandDispatcher) Send(uri string, data any) error { select { case res := <-ch: if !res.WasAccepted { - return fmt.Errorf("command rejected: %d", res.Ticks) + return false, fmt.Errorf("command rejected: %d", res.Ticks) } - return nil + return false, nil case <-timer.C: - return api.ErrTimeout + return false, api.ErrTimeout } } diff --git a/charger/easee/dispatcher_test.go b/charger/easee/dispatcher_test.go index 6f603d804..9c0ccc630 100644 --- a/charger/easee/dispatcher_test.go +++ b/charger/easee/dispatcher_test.go @@ -103,7 +103,9 @@ func TestDispatcher_Send_HTTP200Sync(t *testing.T) { httpmock.RegisterResponder(http.MethodPost, testURI, httpmock.NewStringResponder(http.StatusOK, "")) - assert.NoError(t, d.Send(testURI, nil)) + noop, err := d.Send(testURI, nil) + assert.NoError(t, err) + assert.True(t, noop) } func TestDispatcher_Send_Noop(t *testing.T) { @@ -115,7 +117,9 @@ func TestDispatcher_Send_Noop(t *testing.T) { httpmock.RegisterResponder(http.MethodPost, testURI, httpmock.NewStringResponder(http.StatusAccepted, "[]")) - assert.NoError(t, d.Send(testURI, nil)) + noop, err := d.Send(testURI, nil) + assert.NoError(t, err) + assert.True(t, noop) } func TestDispatcher_Send_HTTP202_InvalidJSON(t *testing.T) { @@ -125,7 +129,8 @@ func TestDispatcher_Send_HTTP202_InvalidJSON(t *testing.T) { httpmock.RegisterResponder(http.MethodPost, testURI, httpmock.NewStringResponder(http.StatusAccepted, "{not-json")) - assert.Error(t, d.Send(testURI, nil)) + _, err := d.Send(testURI, nil) + assert.Error(t, err) } func TestDispatcher_Send_HTTP202_NonNumericTicks(t *testing.T) { @@ -135,7 +140,8 @@ func TestDispatcher_Send_HTTP202_NonNumericTicks(t *testing.T) { httpmock.RegisterResponder(http.MethodPost, testURI, httpmock.NewStringResponder(http.StatusAccepted, `[{"ticks":"NaN"}]`)) - assert.Error(t, d.Send(testURI, nil)) + _, err := d.Send(testURI, nil) + assert.Error(t, err) } func TestDispatcher_Send_HTTPError(t *testing.T) { @@ -146,7 +152,7 @@ func TestDispatcher_Send_HTTPError(t *testing.T) { httpmock.RegisterResponder(http.MethodPost, testURI, httpmock.NewStringResponder(http.StatusBadRequest, "")) - err := d.Send(testURI, nil) + _, err := d.Send(testURI, nil) assert.Error(t, err) } @@ -166,7 +172,9 @@ func TestDispatcher_Send_TicksMatch(t *testing.T) { d.Dispatch(SignalRCommandResponse{Ticks: ticks, WasAccepted: true}) }() - assert.NoError(t, d.Send(testURI, nil)) + noop, err := d.Send(testURI, nil) + assert.NoError(t, err) + assert.False(t, noop) } func TestDispatcher_Send_IDFallback(t *testing.T) { @@ -187,7 +195,9 @@ func TestDispatcher_Send_IDFallback(t *testing.T) { d.Dispatch(SignalRCommandResponse{ID: int(obsID), Ticks: ticks + 1, WasAccepted: true}) }() - assert.NoError(t, d.Send(testURI, nil)) + noop, err := d.Send(testURI, nil) + assert.NoError(t, err) + assert.False(t, noop) } func TestDispatcher_Send_Timeout(t *testing.T) { @@ -202,7 +212,8 @@ func TestDispatcher_Send_Timeout(t *testing.T) { httpmock.NewStringResponder(http.StatusAccepted, body)) // No Dispatch call → Send times out - assert.ErrorIs(t, d.Send(testURI, nil), api.ErrTimeout) + _, err := d.Send(testURI, nil) + assert.ErrorIs(t, err, api.ErrTimeout) } func TestDispatcher_Send_Rejected(t *testing.T) { @@ -221,7 +232,7 @@ func TestDispatcher_Send_Rejected(t *testing.T) { d.Dispatch(SignalRCommandResponse{Ticks: ticks, WasAccepted: false}) }() - err := d.Send(testURI, nil) + _, err := d.Send(testURI, nil) assert.Error(t, err) assert.Contains(t, err.Error(), "rejected") } @@ -243,5 +254,7 @@ func TestDispatcher_Send_CommandURI(t *testing.T) { d.Dispatch(SignalRCommandResponse{Ticks: ticks, WasAccepted: true}) }() - assert.NoError(t, d.Send(testCmdURI, nil)) + noop, err := d.Send(testCmdURI, nil) + assert.NoError(t, err) + assert.False(t, noop) } diff --git a/charger/easee_test.go b/charger/easee_test.go index ab4e1e33b..8744ad78b 100644 --- a/charger/easee_test.go +++ b/charger/easee_test.go @@ -330,7 +330,7 @@ func TestEasee_CommandResponse_legitimate(t *testing.T) { errCh <- nil }() - err = e.dispatcher.Send(uri, nil) + _, err = e.dispatcher.Send(uri, nil) assert.NoError(t, err) <-errCh } @@ -420,7 +420,7 @@ func TestEasee_CommandResponse_matchedByID(t *testing.T) { errCh <- nil }() - err = e.dispatcher.Send(uri, nil) + _, err = e.dispatcher.Send(uri, nil) assert.NoError(t, err) <-errCh } From 411f194645b522e6221fd7d588e8b57f8c59e628 Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Thu, 9 Apr 2026 17:49:00 +0200 Subject: [PATCH 0005/1293] Config UI: fix card min height (#28977) --- assets/js/components/Config/DeviceCard.vue | 23 ++++++++++------------ 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/assets/js/components/Config/DeviceCard.vue b/assets/js/components/Config/DeviceCard.vue index 7e0c1e4ac..3c5c18166 100644 --- a/assets/js/components/Config/DeviceCard.vue +++ b/assets/js/components/Config/DeviceCard.vue @@ -77,20 +77,17 @@ export default { const el = this.$refs.tagsContainer; const content = this.$refs.tagsContent; if (!el || !content) return; - const initialHeight = content.offsetHeight; - this.resizeObserver = new ResizeObserver(() => { - if (content.offsetHeight <= initialHeight) return; - const prev = el.style.minHeight; + const minContentHeight = 10; + const check = () => { + if (content.offsetHeight <= minContentHeight) return; + // measure natural height without cached min-height el.style.minHeight = ""; - const naturalHeight = Math.round(el.getBoundingClientRect().height); - el.style.minHeight = prev; - if (!this.tagsMinHeight || naturalHeight >= this.tagsMinHeight) { - settings.cardHeights[this.id] = naturalHeight; - this.tagsMinHeight = null; - this.resizeObserver?.disconnect(); - this.resizeObserver = null; - } - }); + settings.cardHeights[this.id] = Math.round(el.getBoundingClientRect().height); + this.tagsMinHeight = null; + this.resizeObserver?.disconnect(); + this.resizeObserver = null; + }; + this.resizeObserver = new ResizeObserver(check); this.resizeObserver.observe(content); }); }, From 7df9224d8ccee06346770b221bd545ba7505b318 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 07:55:23 +0200 Subject: [PATCH 0006/1293] chore(deps): bump axios from 1.14.0 to 1.15.0 (#28989) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 06b68f900..2c6999e9e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@h2d2/shopicons": "^1.9.0", "@popperjs/core": "^2.11.8", "@unhead/vue": "^2.1.12", - "axios": "^1.14.0", + "axios": "^1.15.0", "bootstrap": "^5.3.8", "canvas-confetti": "^1.9.4", "chart.js": "^4.5.1", @@ -4712,9 +4712,9 @@ } }, "node_modules/axios": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.14.0.tgz", - "integrity": "sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz", + "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.11", diff --git a/package.json b/package.json index c75824b6b..9705a4c7c 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "@h2d2/shopicons": "^1.9.0", "@popperjs/core": "^2.11.8", "@unhead/vue": "^2.1.12", - "axios": "^1.14.0", + "axios": "^1.15.0", "bootstrap": "^5.3.8", "canvas-confetti": "^1.9.4", "chart.js": "^4.5.1", From ac779d080f5537c2d474951703de43b347cb5810 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 06:00:29 +0000 Subject: [PATCH 0007/1293] chore(deps): bump unhead and @unhead/vue (#28988) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 16 ++++++++-------- package.json | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2c6999e9e..829f0c83a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "@guolao/vue-monaco-editor": "1.5.5", "@h2d2/shopicons": "^1.9.0", "@popperjs/core": "^2.11.8", - "@unhead/vue": "^2.1.12", + "@unhead/vue": "^2.1.13", "axios": "^1.15.0", "bootstrap": "^5.3.8", "canvas-confetti": "^1.9.4", @@ -3766,13 +3766,13 @@ } }, "node_modules/@unhead/vue": { - "version": "2.1.12", - "resolved": "https://registry.npmjs.org/@unhead/vue/-/vue-2.1.12.tgz", - "integrity": "sha512-zEWqg0nZM8acpuTZE40wkeUl8AhIe0tU0OkilVi1D4fmVjACrwoh5HP6aNqJ8kUnKsoy6D+R3Vi/O+fmdNGO7g==", + "version": "2.1.13", + "resolved": "https://registry.npmjs.org/@unhead/vue/-/vue-2.1.13.tgz", + "integrity": "sha512-HYy0shaHRnLNW9r85gppO8IiGz0ONWVV3zGdlT8CQ0tbTwixznJCIiyqV4BSV1aIF1jJIye0pd1p/k6Eab8Z/A==", "license": "MIT", "dependencies": { "hookable": "^6.0.1", - "unhead": "2.1.12" + "unhead": "2.1.13" }, "funding": { "url": "https://github.com/sponsors/harlan-zw" @@ -11517,9 +11517,9 @@ "license": "MIT" }, "node_modules/unhead": { - "version": "2.1.12", - "resolved": "https://registry.npmjs.org/unhead/-/unhead-2.1.12.tgz", - "integrity": "sha512-iTHdWD9ztTunOErtfUFk6Wr11BxvzumcYJ0CzaSCBUOEtg+DUZ9+gnE99i8QkLFT2q1rZD48BYYGXpOZVDLYkA==", + "version": "2.1.13", + "resolved": "https://registry.npmjs.org/unhead/-/unhead-2.1.13.tgz", + "integrity": "sha512-jO9M1sI6b2h/1KpIu4Jeu+ptumLmUKboRRLxys5pYHFeT+lqTzfNHbYUX9bxVDhC1FBszAGuWcUVlmvIPsah8Q==", "license": "MIT", "dependencies": { "hookable": "^6.0.1" diff --git a/package.json b/package.json index 9705a4c7c..17d725fde 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "@guolao/vue-monaco-editor": "1.5.5", "@h2d2/shopicons": "^1.9.0", "@popperjs/core": "^2.11.8", - "@unhead/vue": "^2.1.12", + "@unhead/vue": "^2.1.13", "axios": "^1.15.0", "bootstrap": "^5.3.8", "canvas-confetti": "^1.9.4", From 07ce10fb9f40e980c22f451a0e22e52b097f2b25 Mon Sep 17 00:00:00 2001 From: andig Date: Fri, 10 Apr 2026 10:40:03 +0200 Subject: [PATCH 0008/1293] chore: upgrade modules --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 951979826..732ea8cc7 100644 --- a/go.mod +++ b/go.mod @@ -160,7 +160,7 @@ require ( github.com/glebarez/go-sqlite v1.22.0 // indirect github.com/go-http-utils/fresh v0.0.0-20161124030543-7231e26a4b27 // indirect github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a // indirect - github.com/go-jose/go-jose/v4 v4.1.3 // 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-openapi/jsonpointer v0.22.0 // indirect diff --git a/go.sum b/go.sum index e88854466..3ef475b07 100644 --- a/go.sum +++ b/go.sum @@ -232,8 +232,8 @@ github.com/go-http-utils/fresh v0.0.0-20161124030543-7231e26a4b27 h1:O6yi4xa9b2D github.com/go-http-utils/fresh v0.0.0-20161124030543-7231e26a4b27/go.mod h1:AYvN8omj7nKLmbcXS2dyABYU6JB1Lz1bHmkkq1kf4I4= github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a h1:v6zMvHuY9yue4+QkG/HQ/W67wvtQmWJ4SDo9aK/GIno= github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a/go.mod h1:I79BieaU4fxrw4LMXby6q5OS9XnoR9UIKLOzDFjUmuw= -github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= -github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= +github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= From 28d2e1ec974b3577009e42457cdce512dd2da8af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szczepan=20R=C4=99dzioch?= Date: Fri, 10 Apr 2026 10:42:52 +0200 Subject: [PATCH 0009/1293] Pstryk: update api (#28983) --- templates/definition/tariff/pstryk.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/templates/definition/tariff/pstryk.yaml b/templates/definition/tariff/pstryk.yaml index d00583967..8a3426619 100644 --- a/templates/definition/tariff/pstryk.yaml +++ b/templates/definition/tariff/pstryk.yaml @@ -37,17 +37,17 @@ render: | {{ include "tariff-base" . }} forecast: source: http - uri: https://api.pstryk.pl/integrations/{{ .plan }}/?resolution=hour&window_start={{ `{{ now | mustDateModify "-24h" | date "2006-01-02" }}` }}&window_end={{ `{{ now | mustDateModify "+72h" | date "2006-01-02" }}` }} + uri: https://api.pstryk.pl/integrations/meter-data/unified-metrics/?metrics=pricing&resolution=hour&window_start={{ `{{ now.UTC | mustDateModify "-24h" | date "2006-01-02" }}` }}:00:00Z&window_end={{ `{{ now.UTC | mustDateModify "+72h" | date "2006-01-02" }}` }}:00:00Z headers: Authorization: {{ .token }} Accept: application/json jq: | [(.frames // [])[] - | select(.is_cheap != null or .is_expensive != null) + | select(.metrics?.pricing? != null) | { start: (.start | sub("\\+00:00$"; "Z")), end: (.end | sub("\\+00:00$"; "Z")), - value: .price_gross + value: .metrics.pricing.{{ if eq .plan "prosumer-pricing" }}price_prosumer_gross{{ else }}price_gross{{ end }} } ] | tostring interval: {{ .interval }} From 3f107d17785b9a1002258ac0a7fdc9ec03f274df Mon Sep 17 00:00:00 2001 From: andig Date: Fri, 10 Apr 2026 11:50:47 +0200 Subject: [PATCH 0010/1293] chore: align naming --- charger/alfen.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/charger/alfen.go b/charger/alfen.go index 3170744d1..9f6b94fd6 100644 --- a/charger/alfen.go +++ b/charger/alfen.go @@ -229,18 +229,18 @@ var _ api.PhaseCurrents = (*Alfen)(nil) // Currents implements the api.PhaseCurrents interface func (wb *Alfen) Currents() (float64, float64, float64, error) { - return wb.voltagesOrCurrents(alfenRegCurrents) + return wb.getPhaseValues(alfenRegCurrents) } var _ api.PhaseVoltages = (*Alfen)(nil) // Voltages implements the api.PhaseVoltages interface (tbc) func (wb *Alfen) Voltages() (float64, float64, float64, error) { - return wb.voltagesOrCurrents(alfenRegVoltages) + return wb.getPhaseValues(alfenRegVoltages) } -// voltagesOrCurrents returns 3 sequential float registers -func (wb *Alfen) voltagesOrCurrents(reg uint16) (float64, float64, float64, error) { +// getPhaseValues returns 3 sequential float registers +func (wb *Alfen) getPhaseValues(reg uint16) (float64, float64, float64, error) { b, err := wb.conn.ReadHoldingRegisters(reg, 6) if err != nil { return 0, 0, 0, err From 94c2606797503bf2ce5381fb81522710e7cfe708 Mon Sep 17 00:00:00 2001 From: andig Date: Fri, 10 Apr 2026 15:48:23 +0200 Subject: [PATCH 0011/1293] Fix docorating by promoting embedded type methods as capabilities (#28995) --- charger/abl_decorators.go | 3 + charger/alfen_decorators.go | 3 + charger/amperfied_decorators.go | 3 + charger/bender_decorators.go | 3 + charger/cfos_decorators.go | 3 + charger/charger_decorators.go | 3 + charger/daheimladen_decorators.go | 3 + charger/eebus_decorators.go | 3 + charger/em2go_decorators.go | 3 + charger/etek_decorators.go | 3 + charger/evecube_decorators.go | 3 + charger/evsedin_decorators.go | 3 + charger/evsewifi_decorators.go | 3 + charger/ghosteebus_decorators.go | 3 + charger/go-e_decorators.go | 3 + charger/hardybarth-salia_decorators.go | 3 + charger/heatpump_decorators.go | 3 + charger/homeassistant_decorators.go | 3 + charger/innogy_decorators.go | 3 + charger/keba-modbus_decorators.go | 3 + charger/keba-udp_decorators.go | 3 + charger/kse_decorators.go | 3 + charger/mennekes-compact_decorators.go | 3 + charger/nrggen2_decorators.go | 3 + charger/ocpp_decorators.go | 3 + charger/openevse_decorators.go | 3 + charger/openwb-2.0_decorators.go | 3 + charger/openwb-native_decorators_linux.go | 3 + charger/openwb-pro_decorators.go | 3 + charger/openwb_decorators.go | 3 + charger/pcelectric_decorators.go | 3 + charger/peblar_decorators.go | 3 + charger/phoenix-charx_decorators.go | 3 + charger/phoenix-em-eth_decorators.go | 3 + charger/phoenix-ev-eth_decorators.go | 3 + charger/pulsares_decorators.go | 3 + charger/semp_decorators.go | 3 + charger/sgready_decorators.go | 3 + charger/shelly_decorators.go | 3 + charger/solax_decorators.go | 3 + charger/switchsocket_decorators.go | 3 + charger/tasmota_decorators.go | 3 + charger/vaillant_decorators.go | 3 + charger/vestel_decorators.go | 3 + charger/victron_decorators.go | 3 + charger/wallbe_decorators.go | 3 + charger/warp-ws_decorators.go | 3 + charger/warp2-mqtt_decorators.go | 3 + charger/weidmüller_decorators.go | 3 + charger/zaptec_decorators.go | 3 + cmd/decorate/decorate.tpl | 3 + cmd/decorate/decorate_decorators.go | 3 + core/capable_test.go | 106 ++++++++++++++++++++++ meter/dsmr_decorators.go | 3 + meter/e3dc_decorators.go | 3 + meter/goodwe-wifi_decorators.go | 3 + meter/lgess_decorators.go | 3 + meter/meter_decorators.go | 6 ++ meter/powerwall_decorators.go | 3 + meter/rct_decorators.go | 3 + meter/shelly_decorators.go | 3 + meter/tasmota_decorators.go | 3 + meter/tq-em_decorators.go | 3 + vehicle/tronity_decorators.go | 3 + vehicle/vehicle_decorators.go | 3 + 65 files changed, 301 insertions(+) create mode 100644 core/capable_test.go diff --git a/charger/abl_decorators.go b/charger/abl_decorators.go index 98c12b083..18714b77d 100644 --- a/charger/abl_decorators.go +++ b/charger/abl_decorators.go @@ -33,6 +33,9 @@ type decorateABLeMHCapable struct { func (d *decorateABLeMHCapable) Capability(typ reflect.Type) (any, bool) { c, ok := d.caps[typ] + if !ok && reflect.TypeOf(d).Implements(typ) { + return d, true + } return c, ok } diff --git a/charger/alfen_decorators.go b/charger/alfen_decorators.go index ecf840de9..610538e87 100644 --- a/charger/alfen_decorators.go +++ b/charger/alfen_decorators.go @@ -33,6 +33,9 @@ type decorateAlfenCapable struct { func (d *decorateAlfenCapable) Capability(typ reflect.Type) (any, bool) { c, ok := d.caps[typ] + if !ok && reflect.TypeOf(d).Implements(typ) { + return d, true + } return c, ok } diff --git a/charger/amperfied_decorators.go b/charger/amperfied_decorators.go index cf1d039c5..67548cbd6 100644 --- a/charger/amperfied_decorators.go +++ b/charger/amperfied_decorators.go @@ -33,6 +33,9 @@ type decorateAmperfiedCapable struct { func (d *decorateAmperfiedCapable) Capability(typ reflect.Type) (any, bool) { c, ok := d.caps[typ] + if !ok && reflect.TypeOf(d).Implements(typ) { + return d, true + } return c, ok } diff --git a/charger/bender_decorators.go b/charger/bender_decorators.go index 152d92cb5..140fec094 100644 --- a/charger/bender_decorators.go +++ b/charger/bender_decorators.go @@ -61,6 +61,9 @@ type decorateBenderCCCapable struct { func (d *decorateBenderCCCapable) Capability(typ reflect.Type) (any, bool) { c, ok := d.caps[typ] + if !ok && reflect.TypeOf(d).Implements(typ) { + return d, true + } return c, ok } diff --git a/charger/cfos_decorators.go b/charger/cfos_decorators.go index 03574566a..f58b588fe 100644 --- a/charger/cfos_decorators.go +++ b/charger/cfos_decorators.go @@ -41,6 +41,9 @@ type decorateCfosCapable struct { func (d *decorateCfosCapable) Capability(typ reflect.Type) (any, bool) { c, ok := d.caps[typ] + if !ok && reflect.TypeOf(d).Implements(typ) { + return d, true + } return c, ok } diff --git a/charger/charger_decorators.go b/charger/charger_decorators.go index a25543a4d..38a738f45 100644 --- a/charger/charger_decorators.go +++ b/charger/charger_decorators.go @@ -65,6 +65,9 @@ type decorateCustomCapable struct { func (d *decorateCustomCapable) Capability(typ reflect.Type) (any, bool) { c, ok := d.caps[typ] + if !ok && reflect.TypeOf(d).Implements(typ) { + return d, true + } return c, ok } diff --git a/charger/daheimladen_decorators.go b/charger/daheimladen_decorators.go index 3dc12d3a5..4ab19bab3 100644 --- a/charger/daheimladen_decorators.go +++ b/charger/daheimladen_decorators.go @@ -33,6 +33,9 @@ type decorateDaheimLadenCapable struct { func (d *decorateDaheimLadenCapable) Capability(typ reflect.Type) (any, bool) { c, ok := d.caps[typ] + if !ok && reflect.TypeOf(d).Implements(typ) { + return d, true + } return c, ok } diff --git a/charger/eebus_decorators.go b/charger/eebus_decorators.go index f285b7d65..207f27d17 100644 --- a/charger/eebus_decorators.go +++ b/charger/eebus_decorators.go @@ -37,6 +37,9 @@ type decorateEEBusCapable struct { func (d *decorateEEBusCapable) Capability(typ reflect.Type) (any, bool) { c, ok := d.caps[typ] + if !ok && reflect.TypeOf(d).Implements(typ) { + return d, true + } return c, ok } diff --git a/charger/em2go_decorators.go b/charger/em2go_decorators.go index 8ae05d41b..73e0c9eea 100644 --- a/charger/em2go_decorators.go +++ b/charger/em2go_decorators.go @@ -37,6 +37,9 @@ type decorateEm2GoCapable struct { func (d *decorateEm2GoCapable) Capability(typ reflect.Type) (any, bool) { c, ok := d.caps[typ] + if !ok && reflect.TypeOf(d).Implements(typ) { + return d, true + } return c, ok } diff --git a/charger/etek_decorators.go b/charger/etek_decorators.go index bf970e440..dfd008e7f 100644 --- a/charger/etek_decorators.go +++ b/charger/etek_decorators.go @@ -37,6 +37,9 @@ type decorateEtekCapable struct { func (d *decorateEtekCapable) Capability(typ reflect.Type) (any, bool) { c, ok := d.caps[typ] + if !ok && reflect.TypeOf(d).Implements(typ) { + return d, true + } return c, ok } diff --git a/charger/evecube_decorators.go b/charger/evecube_decorators.go index da37e04c3..a80f9a02a 100644 --- a/charger/evecube_decorators.go +++ b/charger/evecube_decorators.go @@ -33,6 +33,9 @@ type decorateEVECUBECapable struct { func (d *decorateEVECUBECapable) Capability(typ reflect.Type) (any, bool) { c, ok := d.caps[typ] + if !ok && reflect.TypeOf(d).Implements(typ) { + return d, true + } return c, ok } diff --git a/charger/evsedin_decorators.go b/charger/evsedin_decorators.go index de9709da9..8eacdecb2 100644 --- a/charger/evsedin_decorators.go +++ b/charger/evsedin_decorators.go @@ -29,6 +29,9 @@ type decorateEvseDINCapable struct { func (d *decorateEvseDINCapable) Capability(typ reflect.Type) (any, bool) { c, ok := d.caps[typ] + if !ok && reflect.TypeOf(d).Implements(typ) { + return d, true + } return c, ok } diff --git a/charger/evsewifi_decorators.go b/charger/evsewifi_decorators.go index 362b50b4e..0409faae6 100644 --- a/charger/evsewifi_decorators.go +++ b/charger/evsewifi_decorators.go @@ -49,6 +49,9 @@ type decorateEVSECapable struct { func (d *decorateEVSECapable) Capability(typ reflect.Type) (any, bool) { c, ok := d.caps[typ] + if !ok && reflect.TypeOf(d).Implements(typ) { + return d, true + } return c, ok } diff --git a/charger/ghosteebus_decorators.go b/charger/ghosteebus_decorators.go index bfaa947f9..6e60a779b 100644 --- a/charger/ghosteebus_decorators.go +++ b/charger/ghosteebus_decorators.go @@ -45,6 +45,9 @@ type decorateGhostEEBusCapable struct { func (d *decorateGhostEEBusCapable) Capability(typ reflect.Type) (any, bool) { c, ok := d.caps[typ] + if !ok && reflect.TypeOf(d).Implements(typ) { + return d, true + } return c, ok } diff --git a/charger/go-e_decorators.go b/charger/go-e_decorators.go index 8c81e75a3..8f2c9dd4c 100644 --- a/charger/go-e_decorators.go +++ b/charger/go-e_decorators.go @@ -33,6 +33,9 @@ type decorateGoECapable struct { func (d *decorateGoECapable) Capability(typ reflect.Type) (any, bool) { c, ok := d.caps[typ] + if !ok && reflect.TypeOf(d).Implements(typ) { + return d, true + } return c, ok } diff --git a/charger/hardybarth-salia_decorators.go b/charger/hardybarth-salia_decorators.go index e8c1fe4df..20188cbba 100644 --- a/charger/hardybarth-salia_decorators.go +++ b/charger/hardybarth-salia_decorators.go @@ -45,6 +45,9 @@ type decorateSaliaCapable struct { func (d *decorateSaliaCapable) Capability(typ reflect.Type) (any, bool) { c, ok := d.caps[typ] + if !ok && reflect.TypeOf(d).Implements(typ) { + return d, true + } return c, ok } diff --git a/charger/heatpump_decorators.go b/charger/heatpump_decorators.go index d97d5d883..68c876185 100644 --- a/charger/heatpump_decorators.go +++ b/charger/heatpump_decorators.go @@ -41,6 +41,9 @@ type decorateHeatpumpCapable struct { func (d *decorateHeatpumpCapable) Capability(typ reflect.Type) (any, bool) { c, ok := d.caps[typ] + if !ok && reflect.TypeOf(d).Implements(typ) { + return d, true + } return c, ok } diff --git a/charger/homeassistant_decorators.go b/charger/homeassistant_decorators.go index 2040ad1fb..4dac8fd14 100644 --- a/charger/homeassistant_decorators.go +++ b/charger/homeassistant_decorators.go @@ -49,6 +49,9 @@ type decorateHomeAssistantCapable struct { func (d *decorateHomeAssistantCapable) Capability(typ reflect.Type) (any, bool) { c, ok := d.caps[typ] + if !ok && reflect.TypeOf(d).Implements(typ) { + return d, true + } return c, ok } diff --git a/charger/innogy_decorators.go b/charger/innogy_decorators.go index 6bba9854d..c73120052 100644 --- a/charger/innogy_decorators.go +++ b/charger/innogy_decorators.go @@ -33,6 +33,9 @@ type decorateInnogyCapable struct { func (d *decorateInnogyCapable) Capability(typ reflect.Type) (any, bool) { c, ok := d.caps[typ] + if !ok && reflect.TypeOf(d).Implements(typ) { + return d, true + } return c, ok } diff --git a/charger/keba-modbus_decorators.go b/charger/keba-modbus_decorators.go index 8c25c376b..31aeda7ee 100644 --- a/charger/keba-modbus_decorators.go +++ b/charger/keba-modbus_decorators.go @@ -53,6 +53,9 @@ type decorateKebaCapable struct { func (d *decorateKebaCapable) Capability(typ reflect.Type) (any, bool) { c, ok := d.caps[typ] + if !ok && reflect.TypeOf(d).Implements(typ) { + return d, true + } return c, ok } diff --git a/charger/keba-udp_decorators.go b/charger/keba-udp_decorators.go index 64ac7a55a..f3ae427ab 100644 --- a/charger/keba-udp_decorators.go +++ b/charger/keba-udp_decorators.go @@ -37,6 +37,9 @@ type decorateKebaUdpCapable struct { func (d *decorateKebaUdpCapable) Capability(typ reflect.Type) (any, bool) { c, ok := d.caps[typ] + if !ok && reflect.TypeOf(d).Implements(typ) { + return d, true + } return c, ok } diff --git a/charger/kse_decorators.go b/charger/kse_decorators.go index 35901d756..ab78ee55e 100644 --- a/charger/kse_decorators.go +++ b/charger/kse_decorators.go @@ -37,6 +37,9 @@ type decorateKSECapable struct { func (d *decorateKSECapable) Capability(typ reflect.Type) (any, bool) { c, ok := d.caps[typ] + if !ok && reflect.TypeOf(d).Implements(typ) { + return d, true + } return c, ok } diff --git a/charger/mennekes-compact_decorators.go b/charger/mennekes-compact_decorators.go index a6df54f85..3da5e4095 100644 --- a/charger/mennekes-compact_decorators.go +++ b/charger/mennekes-compact_decorators.go @@ -29,6 +29,9 @@ type decorateMennekesCompactCapable struct { func (d *decorateMennekesCompactCapable) Capability(typ reflect.Type) (any, bool) { c, ok := d.caps[typ] + if !ok && reflect.TypeOf(d).Implements(typ) { + return d, true + } return c, ok } diff --git a/charger/nrggen2_decorators.go b/charger/nrggen2_decorators.go index 943f70891..c09c2d9c2 100644 --- a/charger/nrggen2_decorators.go +++ b/charger/nrggen2_decorators.go @@ -29,6 +29,9 @@ type decorateNRGKickGen2Capable struct { func (d *decorateNRGKickGen2Capable) Capability(typ reflect.Type) (any, bool) { c, ok := d.caps[typ] + if !ok && reflect.TypeOf(d).Implements(typ) { + return d, true + } return c, ok } diff --git a/charger/ocpp_decorators.go b/charger/ocpp_decorators.go index 2add164f9..cb1a1939b 100644 --- a/charger/ocpp_decorators.go +++ b/charger/ocpp_decorators.go @@ -53,6 +53,9 @@ type decorateOCPPCapable struct { func (d *decorateOCPPCapable) Capability(typ reflect.Type) (any, bool) { c, ok := d.caps[typ] + if !ok && reflect.TypeOf(d).Implements(typ) { + return d, true + } return c, ok } diff --git a/charger/openevse_decorators.go b/charger/openevse_decorators.go index 3fcdc6359..7e76f0dea 100644 --- a/charger/openevse_decorators.go +++ b/charger/openevse_decorators.go @@ -29,6 +29,9 @@ type decorateOpenEVSECapable struct { func (d *decorateOpenEVSECapable) Capability(typ reflect.Type) (any, bool) { c, ok := d.caps[typ] + if !ok && reflect.TypeOf(d).Implements(typ) { + return d, true + } return c, ok } diff --git a/charger/openwb-2.0_decorators.go b/charger/openwb-2.0_decorators.go index 5f28551ce..b990a7062 100644 --- a/charger/openwb-2.0_decorators.go +++ b/charger/openwb-2.0_decorators.go @@ -33,6 +33,9 @@ type decorateOpenWB20Capable struct { func (d *decorateOpenWB20Capable) Capability(typ reflect.Type) (any, bool) { c, ok := d.caps[typ] + if !ok && reflect.TypeOf(d).Implements(typ) { + return d, true + } return c, ok } diff --git a/charger/openwb-native_decorators_linux.go b/charger/openwb-native_decorators_linux.go index b18205488..b714e329f 100644 --- a/charger/openwb-native_decorators_linux.go +++ b/charger/openwb-native_decorators_linux.go @@ -37,6 +37,9 @@ type decorateOpenWbNativeCapable struct { func (d *decorateOpenWbNativeCapable) Capability(typ reflect.Type) (any, bool) { c, ok := d.caps[typ] + if !ok && reflect.TypeOf(d).Implements(typ) { + return d, true + } return c, ok } diff --git a/charger/openwb-pro_decorators.go b/charger/openwb-pro_decorators.go index e109bdef0..41b9e4c90 100644 --- a/charger/openwb-pro_decorators.go +++ b/charger/openwb-pro_decorators.go @@ -29,6 +29,9 @@ type decorateOpenWBProCapable struct { func (d *decorateOpenWBProCapable) Capability(typ reflect.Type) (any, bool) { c, ok := d.caps[typ] + if !ok && reflect.TypeOf(d).Implements(typ) { + return d, true + } return c, ok } diff --git a/charger/openwb_decorators.go b/charger/openwb_decorators.go index 7aa5b6f2f..a88cac499 100644 --- a/charger/openwb_decorators.go +++ b/charger/openwb_decorators.go @@ -33,6 +33,9 @@ type decorateOpenWBCapable struct { func (d *decorateOpenWBCapable) Capability(typ reflect.Type) (any, bool) { c, ok := d.caps[typ] + if !ok && reflect.TypeOf(d).Implements(typ) { + return d, true + } return c, ok } diff --git a/charger/pcelectric_decorators.go b/charger/pcelectric_decorators.go index adb8848ad..e51d5e2bd 100644 --- a/charger/pcelectric_decorators.go +++ b/charger/pcelectric_decorators.go @@ -37,6 +37,9 @@ type decoratePCECapable struct { func (d *decoratePCECapable) Capability(typ reflect.Type) (any, bool) { c, ok := d.caps[typ] + if !ok && reflect.TypeOf(d).Implements(typ) { + return d, true + } return c, ok } diff --git a/charger/peblar_decorators.go b/charger/peblar_decorators.go index da4e64e5d..4146b7fe7 100644 --- a/charger/peblar_decorators.go +++ b/charger/peblar_decorators.go @@ -33,6 +33,9 @@ type decoratePeblarCapable struct { func (d *decoratePeblarCapable) Capability(typ reflect.Type) (any, bool) { c, ok := d.caps[typ] + if !ok && reflect.TypeOf(d).Implements(typ) { + return d, true + } return c, ok } diff --git a/charger/phoenix-charx_decorators.go b/charger/phoenix-charx_decorators.go index 37fec3e1e..c36619a10 100644 --- a/charger/phoenix-charx_decorators.go +++ b/charger/phoenix-charx_decorators.go @@ -41,6 +41,9 @@ type decoratePhoenixCharxCapable struct { func (d *decoratePhoenixCharxCapable) Capability(typ reflect.Type) (any, bool) { c, ok := d.caps[typ] + if !ok && reflect.TypeOf(d).Implements(typ) { + return d, true + } return c, ok } diff --git a/charger/phoenix-em-eth_decorators.go b/charger/phoenix-em-eth_decorators.go index 5e8af9605..780a9c36e 100644 --- a/charger/phoenix-em-eth_decorators.go +++ b/charger/phoenix-em-eth_decorators.go @@ -41,6 +41,9 @@ type decoratePhoenixEMEthCapable struct { func (d *decoratePhoenixEMEthCapable) Capability(typ reflect.Type) (any, bool) { c, ok := d.caps[typ] + if !ok && reflect.TypeOf(d).Implements(typ) { + return d, true + } return c, ok } diff --git a/charger/phoenix-ev-eth_decorators.go b/charger/phoenix-ev-eth_decorators.go index 886219277..dd6a6dbfb 100644 --- a/charger/phoenix-ev-eth_decorators.go +++ b/charger/phoenix-ev-eth_decorators.go @@ -49,6 +49,9 @@ type decoratePhoenixEVEthCapable struct { func (d *decoratePhoenixEVEthCapable) Capability(typ reflect.Type) (any, bool) { c, ok := d.caps[typ] + if !ok && reflect.TypeOf(d).Implements(typ) { + return d, true + } return c, ok } diff --git a/charger/pulsares_decorators.go b/charger/pulsares_decorators.go index 0068a86f8..fb252ee26 100644 --- a/charger/pulsares_decorators.go +++ b/charger/pulsares_decorators.go @@ -29,6 +29,9 @@ type decoratePulsaresCapable struct { func (d *decoratePulsaresCapable) Capability(typ reflect.Type) (any, bool) { c, ok := d.caps[typ] + if !ok && reflect.TypeOf(d).Implements(typ) { + return d, true + } return c, ok } diff --git a/charger/semp_decorators.go b/charger/semp_decorators.go index c6fc75c9e..ed11668c4 100644 --- a/charger/semp_decorators.go +++ b/charger/semp_decorators.go @@ -37,6 +37,9 @@ type decorateSEMPCapable struct { func (d *decorateSEMPCapable) Capability(typ reflect.Type) (any, bool) { c, ok := d.caps[typ] + if !ok && reflect.TypeOf(d).Implements(typ) { + return d, true + } return c, ok } diff --git a/charger/sgready_decorators.go b/charger/sgready_decorators.go index d981ce092..056a490a9 100644 --- a/charger/sgready_decorators.go +++ b/charger/sgready_decorators.go @@ -41,6 +41,9 @@ type decorateSgReadyCapable struct { func (d *decorateSgReadyCapable) Capability(typ reflect.Type) (any, bool) { c, ok := d.caps[typ] + if !ok && reflect.TypeOf(d).Implements(typ) { + return d, true + } return c, ok } diff --git a/charger/shelly_decorators.go b/charger/shelly_decorators.go index 945665799..3c80eb071 100644 --- a/charger/shelly_decorators.go +++ b/charger/shelly_decorators.go @@ -37,6 +37,9 @@ type decorateShellyCapable struct { func (d *decorateShellyCapable) Capability(typ reflect.Type) (any, bool) { c, ok := d.caps[typ] + if !ok && reflect.TypeOf(d).Implements(typ) { + return d, true + } return c, ok } diff --git a/charger/solax_decorators.go b/charger/solax_decorators.go index 14feae8ba..40f10cd7a 100644 --- a/charger/solax_decorators.go +++ b/charger/solax_decorators.go @@ -33,6 +33,9 @@ type decorateSolaxCapable struct { func (d *decorateSolaxCapable) Capability(typ reflect.Type) (any, bool) { c, ok := d.caps[typ] + if !ok && reflect.TypeOf(d).Implements(typ) { + return d, true + } return c, ok } diff --git a/charger/switchsocket_decorators.go b/charger/switchsocket_decorators.go index 349b1dfd2..0e3e9d91c 100644 --- a/charger/switchsocket_decorators.go +++ b/charger/switchsocket_decorators.go @@ -33,6 +33,9 @@ type decorateSwitchSocketCapable struct { func (d *decorateSwitchSocketCapable) Capability(typ reflect.Type) (any, bool) { c, ok := d.caps[typ] + if !ok && reflect.TypeOf(d).Implements(typ) { + return d, true + } return c, ok } diff --git a/charger/tasmota_decorators.go b/charger/tasmota_decorators.go index 1c02f67e1..fdde72fa8 100644 --- a/charger/tasmota_decorators.go +++ b/charger/tasmota_decorators.go @@ -33,6 +33,9 @@ type decorateTasmotaCapable struct { func (d *decorateTasmotaCapable) Capability(typ reflect.Type) (any, bool) { c, ok := d.caps[typ] + if !ok && reflect.TypeOf(d).Implements(typ) { + return d, true + } return c, ok } diff --git a/charger/vaillant_decorators.go b/charger/vaillant_decorators.go index b942619ac..1e84858e9 100644 --- a/charger/vaillant_decorators.go +++ b/charger/vaillant_decorators.go @@ -33,6 +33,9 @@ type decorateVaillantCapable struct { func (d *decorateVaillantCapable) Capability(typ reflect.Type) (any, bool) { c, ok := d.caps[typ] + if !ok && reflect.TypeOf(d).Implements(typ) { + return d, true + } return c, ok } diff --git a/charger/vestel_decorators.go b/charger/vestel_decorators.go index cf55dd7e2..b498fbe90 100644 --- a/charger/vestel_decorators.go +++ b/charger/vestel_decorators.go @@ -37,6 +37,9 @@ type decorateVestelCapable struct { func (d *decorateVestelCapable) Capability(typ reflect.Type) (any, bool) { c, ok := d.caps[typ] + if !ok && reflect.TypeOf(d).Implements(typ) { + return d, true + } return c, ok } diff --git a/charger/victron_decorators.go b/charger/victron_decorators.go index 3306784db..24573317a 100644 --- a/charger/victron_decorators.go +++ b/charger/victron_decorators.go @@ -33,6 +33,9 @@ type decorateVictronCapable struct { func (d *decorateVictronCapable) Capability(typ reflect.Type) (any, bool) { c, ok := d.caps[typ] + if !ok && reflect.TypeOf(d).Implements(typ) { + return d, true + } return c, ok } diff --git a/charger/wallbe_decorators.go b/charger/wallbe_decorators.go index 39b877394..6467878b6 100644 --- a/charger/wallbe_decorators.go +++ b/charger/wallbe_decorators.go @@ -41,6 +41,9 @@ type decorateWallbeCapable struct { func (d *decorateWallbeCapable) Capability(typ reflect.Type) (any, bool) { c, ok := d.caps[typ] + if !ok && reflect.TypeOf(d).Implements(typ) { + return d, true + } return c, ok } diff --git a/charger/warp-ws_decorators.go b/charger/warp-ws_decorators.go index 5f2179b55..9c3df9117 100644 --- a/charger/warp-ws_decorators.go +++ b/charger/warp-ws_decorators.go @@ -53,6 +53,9 @@ type decorateWarpWSCapable struct { func (d *decorateWarpWSCapable) Capability(typ reflect.Type) (any, bool) { c, ok := d.caps[typ] + if !ok && reflect.TypeOf(d).Implements(typ) { + return d, true + } return c, ok } diff --git a/charger/warp2-mqtt_decorators.go b/charger/warp2-mqtt_decorators.go index ed5871ac6..ecbd4ecb5 100644 --- a/charger/warp2-mqtt_decorators.go +++ b/charger/warp2-mqtt_decorators.go @@ -53,6 +53,9 @@ type decorateWarp2Capable struct { func (d *decorateWarp2Capable) Capability(typ reflect.Type) (any, bool) { c, ok := d.caps[typ] + if !ok && reflect.TypeOf(d).Implements(typ) { + return d, true + } return c, ok } diff --git a/charger/weidmüller_decorators.go b/charger/weidmüller_decorators.go index b35215673..489556d45 100644 --- a/charger/weidmüller_decorators.go +++ b/charger/weidmüller_decorators.go @@ -29,6 +29,9 @@ type decorateWeidmüllerCapable struct { func (d *decorateWeidmüllerCapable) Capability(typ reflect.Type) (any, bool) { c, ok := d.caps[typ] + if !ok && reflect.TypeOf(d).Implements(typ) { + return d, true + } return c, ok } diff --git a/charger/zaptec_decorators.go b/charger/zaptec_decorators.go index 344ee1eec..eb486e4e6 100644 --- a/charger/zaptec_decorators.go +++ b/charger/zaptec_decorators.go @@ -29,6 +29,9 @@ type decorateZaptecCapable struct { func (d *decorateZaptecCapable) Capability(typ reflect.Type) (any, bool) { c, ok := d.caps[typ] + if !ok && reflect.TypeOf(d).Implements(typ) { + return d, true + } return c, ok } diff --git a/cmd/decorate/decorate.tpl b/cmd/decorate/decorate.tpl index a4dcd362c..ca7e4660b 100644 --- a/cmd/decorate/decorate.tpl +++ b/cmd/decorate/decorate.tpl @@ -22,6 +22,9 @@ type {{.Function}}Capable struct { func (d *{{.Function}}Capable) Capability(typ reflect.Type) (any, bool) { c, ok := d.caps[typ] + if !ok && reflect.TypeOf(d).Implements(typ) { + return d, true + } return c, ok } diff --git a/cmd/decorate/decorate_decorators.go b/cmd/decorate/decorate_decorators.go index 17b8c4ae1..2a3a16b4a 100644 --- a/cmd/decorate/decorate_decorators.go +++ b/cmd/decorate/decorate_decorators.go @@ -37,6 +37,9 @@ type decorateTestCapable struct { func (d *decorateTestCapable) Capability(typ reflect.Type) (any, bool) { c, ok := d.caps[typ] + if !ok && reflect.TypeOf(d).Implements(typ) { + return d, true + } return c, ok } diff --git a/core/capable_test.go b/core/capable_test.go new file mode 100644 index 000000000..40b9b9157 --- /dev/null +++ b/core/capable_test.go @@ -0,0 +1,106 @@ +package core + +import ( + "reflect" + "testing" + + "github.com/evcc-io/evcc/api" + "github.com/stretchr/testify/assert" +) + +type charger struct { + caps map[reflect.Type]any +} + +var _ api.Capable = (*charger)(nil) + +func (c *charger) Capability(typ reflect.Type) (any, bool) { + cap, ok := c.caps[typ] + if !ok && reflect.TypeOf(c).Implements(typ) { + return c, true + } + return cap, ok +} + +var _ api.Meter = (*charger)(nil) + +func (c *charger) CurrentPower() (float64, error) { + return 0, nil +} + +var _ api.BatteryCapacity = (*charger)(nil) + +func (c *charger) Capacity() float64 { + return 0 +} + +var _ api.MeterEnergy = (*charger)(nil) + +func (c *charger) TotalEnergy() (float64, error) { + return 0, nil +} + +var _ api.Battery = (*batteryImpl)(nil) + +type batteryImpl struct { + soc func() (float64, error) +} + +func (impl *batteryImpl) Soc() (float64, error) { + return impl.soc() +} + +func TestCapsWrapping(t *testing.T) { + // type is just a shortcut for something simple that is not a meter + var c api.BatteryCapacity + + c = &charger{ + caps: make(map[reflect.Type]any), + } + + c.(*charger).caps[reflect.TypeFor[api.Battery]()] = &batteryImpl{ + soc: func() (float64, error) { + return 0, nil + }, + } + + { + _, ok := c.(api.Meter) + assert.True(t, ok) + } + { + _, ok := c.(api.MeterEnergy) + assert.True(t, ok) + } + { + _, ok := c.(api.Battery) + assert.False(t, ok) + assert.True(t, api.HasCap[api.Battery](c)) + } + + var m api.Meter + + if mt, ok := api.Cap[api.Meter](c); ok { + if c, ok := c.(api.Capable); ok { + m = &capableMeter{Meter: mt, Capable: c} + } else { + m = mt + } + } + + { + _, ok := m.(api.MeterEnergy) + assert.False(t, ok, "unexpected promoted energy") + assert.True(t, api.HasCap[api.MeterEnergy](m), "missing promoted energy cap") + + var mm any = m.(*capableMeter).Meter + _, ok = mm.(api.MeterEnergy) + assert.True(t, ok, "missing embedded energy") + assert.True(t, api.HasCap[api.MeterEnergy](mm), "missing embedded energy cap") + } + { + _, ok := m.(api.Battery) + assert.False(t, ok) + assert.True(t, api.HasCap[api.Battery](m), "missing battery cap") + } +} diff --git a/meter/dsmr_decorators.go b/meter/dsmr_decorators.go index cf10cad14..be00f203c 100644 --- a/meter/dsmr_decorators.go +++ b/meter/dsmr_decorators.go @@ -33,6 +33,9 @@ type decorateDsmrCapable struct { func (d *decorateDsmrCapable) Capability(typ reflect.Type) (any, bool) { c, ok := d.caps[typ] + if !ok && reflect.TypeOf(d).Implements(typ) { + return d, true + } return c, ok } diff --git a/meter/e3dc_decorators.go b/meter/e3dc_decorators.go index cbd04db0c..82d479a3e 100644 --- a/meter/e3dc_decorators.go +++ b/meter/e3dc_decorators.go @@ -49,6 +49,9 @@ type decorateE3dcCapable struct { func (d *decorateE3dcCapable) Capability(typ reflect.Type) (any, bool) { c, ok := d.caps[typ] + if !ok && reflect.TypeOf(d).Implements(typ) { + return d, true + } return c, ok } diff --git a/meter/goodwe-wifi_decorators.go b/meter/goodwe-wifi_decorators.go index 0288610fd..b0d41e2c7 100644 --- a/meter/goodwe-wifi_decorators.go +++ b/meter/goodwe-wifi_decorators.go @@ -33,6 +33,9 @@ type decorateGoodWeWifiCapable struct { func (d *decorateGoodWeWifiCapable) Capability(typ reflect.Type) (any, bool) { c, ok := d.caps[typ] + if !ok && reflect.TypeOf(d).Implements(typ) { + return d, true + } return c, ok } diff --git a/meter/lgess_decorators.go b/meter/lgess_decorators.go index b2613af99..4c607cb6b 100644 --- a/meter/lgess_decorators.go +++ b/meter/lgess_decorators.go @@ -49,6 +49,9 @@ type decorateLgEssCapable struct { func (d *decorateLgEssCapable) Capability(typ reflect.Type) (any, bool) { c, ok := d.caps[typ] + if !ok && reflect.TypeOf(d).Implements(typ) { + return d, true + } return c, ok } diff --git a/meter/meter_decorators.go b/meter/meter_decorators.go index f30e1a774..71e1399c7 100644 --- a/meter/meter_decorators.go +++ b/meter/meter_decorators.go @@ -45,6 +45,9 @@ type decorateMeterCapable struct { func (d *decorateMeterCapable) Capability(typ reflect.Type) (any, bool) { c, ok := d.caps[typ] + if !ok && reflect.TypeOf(d).Implements(typ) { + return d, true + } return c, ok } @@ -129,6 +132,9 @@ type decorateMeterBatteryCapable struct { func (d *decorateMeterBatteryCapable) Capability(typ reflect.Type) (any, bool) { c, ok := d.caps[typ] + if !ok && reflect.TypeOf(d).Implements(typ) { + return d, true + } return c, ok } diff --git a/meter/powerwall_decorators.go b/meter/powerwall_decorators.go index fc68c0880..12c7cefbf 100644 --- a/meter/powerwall_decorators.go +++ b/meter/powerwall_decorators.go @@ -49,6 +49,9 @@ type decoratePowerWallCapable struct { func (d *decoratePowerWallCapable) Capability(typ reflect.Type) (any, bool) { c, ok := d.caps[typ] + if !ok && reflect.TypeOf(d).Implements(typ) { + return d, true + } return c, ok } diff --git a/meter/rct_decorators.go b/meter/rct_decorators.go index 828761eb9..44857b73c 100644 --- a/meter/rct_decorators.go +++ b/meter/rct_decorators.go @@ -57,6 +57,9 @@ type decorateRCTCapable struct { func (d *decorateRCTCapable) Capability(typ reflect.Type) (any, bool) { c, ok := d.caps[typ] + if !ok && reflect.TypeOf(d).Implements(typ) { + return d, true + } return c, ok } diff --git a/meter/shelly_decorators.go b/meter/shelly_decorators.go index 2a7e3b3ec..cc2e48ff7 100644 --- a/meter/shelly_decorators.go +++ b/meter/shelly_decorators.go @@ -37,6 +37,9 @@ type decorateShellyCapable struct { func (d *decorateShellyCapable) Capability(typ reflect.Type) (any, bool) { c, ok := d.caps[typ] + if !ok && reflect.TypeOf(d).Implements(typ) { + return d, true + } return c, ok } diff --git a/meter/tasmota_decorators.go b/meter/tasmota_decorators.go index dee171478..0ad3cc223 100644 --- a/meter/tasmota_decorators.go +++ b/meter/tasmota_decorators.go @@ -37,6 +37,9 @@ type decorateTasmotaCapable struct { func (d *decorateTasmotaCapable) Capability(typ reflect.Type) (any, bool) { c, ok := d.caps[typ] + if !ok && reflect.TypeOf(d).Implements(typ) { + return d, true + } return c, ok } diff --git a/meter/tq-em_decorators.go b/meter/tq-em_decorators.go index 838a28b61..e4424c290 100644 --- a/meter/tq-em_decorators.go +++ b/meter/tq-em_decorators.go @@ -29,6 +29,9 @@ type decorateTqEmCapable struct { func (d *decorateTqEmCapable) Capability(typ reflect.Type) (any, bool) { c, ok := d.caps[typ] + if !ok && reflect.TypeOf(d).Implements(typ) { + return d, true + } return c, ok } diff --git a/vehicle/tronity_decorators.go b/vehicle/tronity_decorators.go index 07e3da4a5..43d96e5e1 100644 --- a/vehicle/tronity_decorators.go +++ b/vehicle/tronity_decorators.go @@ -37,6 +37,9 @@ type decorateTronityCapable struct { func (d *decorateTronityCapable) Capability(typ reflect.Type) (any, bool) { c, ok := d.caps[typ] + if !ok && reflect.TypeOf(d).Implements(typ) { + return d, true + } return c, ok } diff --git a/vehicle/vehicle_decorators.go b/vehicle/vehicle_decorators.go index 3949aaecb..aff3271a9 100644 --- a/vehicle/vehicle_decorators.go +++ b/vehicle/vehicle_decorators.go @@ -74,6 +74,9 @@ type decorateVehicleCapable struct { func (d *decorateVehicleCapable) Capability(typ reflect.Type) (any, bool) { c, ok := d.caps[typ] + if !ok && reflect.TypeOf(d).Implements(typ) { + return d, true + } return c, ok } From 21efd23fcfe53ce411acc5e01fae9e246d401f0d Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Sun, 12 Apr 2026 08:07:46 +0200 Subject: [PATCH 0012/1293] iOS: fix modal+validation scrolling (#29023) --- assets/css/app.css | 5 +++++ .../Config/DeviceModal/DeviceModalBase.vue | 3 ++- .../Config/utils/reportValidityInModal.ts | 20 +++++++++++++++++++ assets/js/components/Config/utils/test.ts | 3 ++- 4 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 assets/js/components/Config/utils/reportValidityInModal.ts diff --git a/assets/css/app.css b/assets/css/app.css index 992b23c68..cc748436c 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -444,6 +444,11 @@ a:hover { } } +.modal { + /* prevent touch-scroll chaining from the modal to on iOS Safari */ + overscroll-behavior: contain; +} + .modal-body { padding: 1rem 0 0; } diff --git a/assets/js/components/Config/DeviceModal/DeviceModalBase.vue b/assets/js/components/Config/DeviceModal/DeviceModalBase.vue index 5bd10b632..104b45f9a 100644 --- a/assets/js/components/Config/DeviceModal/DeviceModalBase.vue +++ b/assets/js/components/Config/DeviceModal/DeviceModalBase.vue @@ -188,6 +188,7 @@ import YamlEntry from "./YamlEntry.vue"; import AuthCodeDisplay from "../AuthCodeDisplay.vue"; import AuthConnectButton from "../AuthConnectButton.vue"; import { initialTestState, performTest } from "../utils/test"; +import { reportValidityInModal } from "../utils/reportValidityInModal"; import { initialAuthState, prepareAuthLogin } from "../utils/authProvider"; import sleep from "@/utils/sleep"; import { ConfigType } from "@/types/evcc"; @@ -618,7 +619,7 @@ export default defineComponent({ // trigger browser validation if (this.$refs["form"]) { - if (!(this.$refs["form"] as HTMLFormElement).reportValidity()) { + if (!reportValidityInModal(this.$refs["form"] as HTMLFormElement)) { return; } } diff --git a/assets/js/components/Config/utils/reportValidityInModal.ts b/assets/js/components/Config/utils/reportValidityInModal.ts new file mode 100644 index 000000000..9a47b9f30 --- /dev/null +++ b/assets/js/components/Config/utils/reportValidityInModal.ts @@ -0,0 +1,20 @@ +/** + * `form.reportValidity()` with the document scroll restored afterwards. + * + * The native call scroll-into-views the first invalid field and leaks scroll + * up to `` (overflow:hidden only blocks user scroll). On iOS Safari + * that wedges the modal's touch scrolling onto the page underneath. + */ +export function reportValidityInModal(form: HTMLFormElement): boolean { + const { scrollX, scrollY } = window; + const valid = form.reportValidity(); + if (window.scrollX !== scrollX || window.scrollY !== scrollY) { + // Chromium ignores scrollTop writes while overflow-y:hidden; relax briefly. + const html = document.documentElement; + const prev = html.style.overflowY; + html.style.overflowY = "auto"; + window.scrollTo(scrollX, scrollY); + html.style.overflowY = prev; + } + return valid; +} diff --git a/assets/js/components/Config/utils/test.ts b/assets/js/components/Config/utils/test.ts index 7a0e1723c..f6e8d6047 100644 --- a/assets/js/components/Config/utils/test.ts +++ b/assets/js/components/Config/utils/test.ts @@ -1,5 +1,6 @@ import type { AxiosResponse } from "axios"; import sleep from "@/utils/sleep"; +import { reportValidityInModal } from "./reportValidityInModal"; export type TestState = { isUnknown: boolean; @@ -28,7 +29,7 @@ export const performTest = async ( api: () => Promise>, form: HTMLElement | undefined ) => { - if (form && !(form as HTMLFormElement).reportValidity()) return false; + if (form && !reportValidityInModal(form as HTMLFormElement)) return false; state.isUnknown = false; state.isSuccess = false; state.isRunning = true; From e3709a0739ab553ce813262052732ec57c43ce80 Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Sun, 12 Apr 2026 09:29:08 +0200 Subject: [PATCH 0013/1293] Issue UI: add residualpower to essential data (#29018) --- assets/js/views/Issue.vue | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/assets/js/views/Issue.vue b/assets/js/views/Issue.vue index fcf572171..a15a2b4c2 100644 --- a/assets/js/views/Issue.vue +++ b/assets/js/views/Issue.vue @@ -538,14 +538,20 @@ export default defineComponent({ } // read essential config data from state - ["modbusproxy", "mqtt", "influx", "shm", "interval", "experimental"].forEach( - (key) => { - const value = store.state[key as keyof State]; - if (value !== undefined && value !== null) { - configs[key] = value; - } + [ + "modbusproxy", + "mqtt", + "influx", + "shm", + "interval", + "residualPower", + "experimental", + ].forEach((key) => { + const value = store.state[key as keyof State]; + if (value !== undefined && value !== null) { + configs[key] = value; } - ); + }); this.sections.uiConfig.content = formatJson(configs, EXPAND_KEYS); } catch (error) { From b59b71f854ee81c0fff5ca569333127f47849d48 Mon Sep 17 00:00:00 2001 From: premultiply <4681172+premultiply@users.noreply.github.com> Date: Sun, 12 Apr 2026 10:23:07 +0200 Subject: [PATCH 0014/1293] SMA Hybrid: remove wrong solar energy register (#29034) --- templates/definition/meter/sma-hybrid.yaml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/templates/definition/meter/sma-hybrid.yaml b/templates/definition/meter/sma-hybrid.yaml index fbd992181..7a37f46f6 100644 --- a/templates/definition/meter/sma-hybrid.yaml +++ b/templates/definition/meter/sma-hybrid.yaml @@ -151,14 +151,6 @@ render: | address: 30967 # SMA Modbus Profile: DcMs.Watt [2] type: holding decode: int32nan - energy: - source: modbus - {{- include "modbus" . | indent 2 }} - register: - address: 30513 # SMA Modbus Profile: Metering.TotWhOut - type: holding - decode: uint64nan - scale: 0.001 maxacpower: {{ .maxacpower }} # W {{- end }} {{- if eq .usage "battery" }} From 7135ab055814a0132a814bc31dffd8677276d301 Mon Sep 17 00:00:00 2001 From: Jens0001 <88800396+Jens0001@users.noreply.github.com> Date: Sun, 12 Apr 2026 10:25:39 +0200 Subject: [PATCH 0015/1293] Cupra: treat "NotReadyForCharging" aus connected (#29030) --- vehicle/seat/cupra/provider.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vehicle/seat/cupra/provider.go b/vehicle/seat/cupra/provider.go index 162cefaaf..26d74e9cb 100644 --- a/vehicle/seat/cupra/provider.go +++ b/vehicle/seat/cupra/provider.go @@ -66,7 +66,7 @@ func (v *Provider) Status() (api.ChargeStatus, error) { res, err := v.statusG() if err == nil { switch strings.ToLower(res.Services.Charging.Status) { - case "connected", "readyforcharging", "error": + case "connected", "readyforcharging", "notreadyforcharging", "error": status = api.StatusB case "charging": status = api.StatusC From d5c9db02c45964aaf0919d36a82598bdce3f635b Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Sun, 12 Apr 2026 10:37:38 +0200 Subject: [PATCH 0016/1293] Config UI: vehicle priority (#29021) --- assets/js/components/Config/VehicleModal.vue | 1 - 1 file changed, 1 deletion(-) diff --git a/assets/js/components/Config/VehicleModal.vue b/assets/js/components/Config/VehicleModal.vue index e362b70b3..de78a77f6 100644 --- a/assets/js/components/Config/VehicleModal.vue +++ b/assets/js/components/Config/VehicleModal.vue @@ -124,7 +124,6 @@ size="w-100" class="me-2" :choice="priorityOptions" - required /> From 20de0b9da1e9fb4a5c9cab619b0e3ab80b2883df Mon Sep 17 00:00:00 2001 From: Nick Veenhof Date: Sun, 12 Apr 2026 13:41:43 +0200 Subject: [PATCH 0017/1293] OCPP: proactively trigger BootNotification after WebSocket connect (#28540) --- charger/ocpp/const.go | 5 +++++ charger/ocpp/cp.go | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/charger/ocpp/const.go b/charger/ocpp/const.go index 4a5433d4c..5ccf8672d 100644 --- a/charger/ocpp/const.go +++ b/charger/ocpp/const.go @@ -4,6 +4,11 @@ import "time" var Timeout = time.Minute // default request / response timeout on protocol level +// triggerBootDelay defines how long to wait after WebSocket connect before +// proactively triggering a BootNotification. This allows the connection to +// stabilize and gives the charger a chance to send a spontaneous BootNotification. +const triggerBootDelay = 5 * time.Second + const ( // Core profile keys KeyMeterValueSampleInterval = "MeterValueSampleInterval" diff --git a/charger/ocpp/cp.go b/charger/ocpp/cp.go index 52419fa04..0f8c8e51c 100644 --- a/charger/ocpp/cp.go +++ b/charger/ocpp/cp.go @@ -8,6 +8,7 @@ import ( "github.com/evcc-io/evcc/util" "github.com/lorenzodonini/ocpp-go/ocpp1.6/core" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/remotetrigger" "github.com/lorenzodonini/ocpp-go/ocpp1.6/types" ) @@ -142,12 +143,47 @@ func (cp *CP) connect(connect bool) { // onTransportConnect is called when the WebSocket connection is established. // Instead of marking the CP as connected immediately, it waits for the // BootNotification handshake to complete (or a timeout to expire). +// +// Some chargers (e.g. Wallbox Pulsar Pro FW 6.x) do not send BootNotification +// spontaneously on connect. For these chargers, we proactively trigger it +// after a short delay, which typically yields a response within 1 second +// instead of waiting for the full timeout. func (cp *CP) onTransportConnect() { cp.mu.Lock() defer cp.mu.Unlock() cp.stopBootTimer() cp.bootTimer = time.AfterFunc(Timeout, cp.onBootTimeout) + + // Proactively trigger BootNotification after a short delay. + // This helps chargers that don't send it spontaneously (e.g. Wallbox FW 6.x). + // The TriggerMessage is sent directly via the OCPP instance, bypassing the + // Connected() check which would fail at this point. + time.AfterFunc(triggerBootDelay, func() { + cp.mu.RLock() + // If BootNotification already arrived or timer was cancelled (disconnect), + // there is nothing to do. + if cp.bootTimer == nil || cp.BootNotificationResult != nil { + cp.mu.RUnlock() + return + } + cp.mu.RUnlock() + + cp.log.DEBUG.Printf("proactively triggering BootNotification") + + if err := Instance().TriggerMessage( + cp.id, + func(conf *remotetrigger.TriggerMessageConfirmation, err error) { + if err != nil { + cp.log.ERROR.Printf("trigger BootNotification response error: %v", err) + } + }, + core.BootNotificationFeatureName, + func(request *remotetrigger.TriggerMessageRequest) {}, + ); err != nil { + cp.log.ERROR.Printf("failed to trigger BootNotification: %v", err) + } + }) } // onBootTimeout is called when the BootNotification wait timer expires. From b28be96d0ae9f77540aa5f7ae15c8f2b52a36869 Mon Sep 17 00:00:00 2001 From: lehmanju Date: Sun, 12 Apr 2026 14:04:40 +0200 Subject: [PATCH 0018/1293] Config UI: fix editing existing OAuth devices (#28012) --- .../Config/DeviceModal/DeviceModalBase.vue | 6 ++-- .../js/components/Config/DeviceModal/index.ts | 14 ++++++-- plugin/auth/demo.go | 10 ++++-- server/http.go | 1 + server/http_config_helper.go | 27 +++++++++++++++ server/http_config_metadata_handler.go | 33 +++++++++++++++++++ tests/config-device-auth-demo.tpl.yaml | 7 +++- tests/config-device-auth.spec.ts | 9 +++++ 8 files changed, 98 insertions(+), 9 deletions(-) diff --git a/assets/js/components/Config/DeviceModal/DeviceModalBase.vue b/assets/js/components/Config/DeviceModal/DeviceModalBase.vue index 104b45f9a..f855f7967 100644 --- a/assets/js/components/Config/DeviceModal/DeviceModalBase.vue +++ b/assets/js/components/Config/DeviceModal/DeviceModalBase.vue @@ -429,7 +429,6 @@ export default defineComponent({ return this.template?.Auth && !this.auth.ok; }, authValuesMissing() { - console.log("authValuesMissing", this.authValues); return this.template?.Auth && Object.values(this.authValues).some((value) => !value); }, authValues() { @@ -628,9 +627,10 @@ export default defineComponent({ if (this.authValuesMissing) return; const { type } = this.template.Auth; - const values = this.authValues; + // include the template name so the backend can resolve masked fields + const values = { ...this.authValues, template: this.templateName }; this.auth.loading = true; - const result = await this.device.checkAuth(type, values); + const result = await this.device.checkAuth(type, values, this.id); this.auth.loading = false; if (result.success) { // login already exists diff --git a/assets/js/components/Config/DeviceModal/index.ts b/assets/js/components/Config/DeviceModal/index.ts index afa1123d8..bf2cd6edb 100644 --- a/assets/js/components/Config/DeviceModal/index.ts +++ b/assets/js/components/Config/DeviceModal/index.ts @@ -236,10 +236,18 @@ export function createDeviceUtils(deviceType: DeviceType) { return response.data; } - async function checkAuth(type: string, values: Record): Promise { - const params = { type, ...values }; + async function checkAuth( + type: string, + values: Record, + id?: number + ): Promise { + const body = { type, ...values }; + let url = `config/auth`; + if (id !== undefined) { + url += `/${deviceType}/merge/${id}`; + } try { - const { status, data = {} } = await api.post(`config/auth`, params, { + const { status, data = {} } = await api.post(url, body, { validateStatus: (status) => [204, 400].includes(status), }); // already set up diff --git a/plugin/auth/demo.go b/plugin/auth/demo.go index 5aec084da..deaa7cb7a 100644 --- a/plugin/auth/demo.go +++ b/plugin/auth/demo.go @@ -2,6 +2,7 @@ package auth import ( "context" + "errors" "fmt" "net/url" "strings" @@ -32,16 +33,21 @@ func NewDemoFromConfig(_ context.Context, other map[string]any) (oauth2.TokenSou Server string Method string RedirectUri string + Secret string } if err := util.DecodeOther(other, &cc); err != nil { return nil, err } - return NewDemo(cc.Server, cc.Method, cc.RedirectUri) + return NewDemo(cc.Server, cc.Method, cc.RedirectUri, cc.Secret) } -func NewDemo(server string, method string, redirectUri string) (oauth2.TokenSource, error) { +func NewDemo(server, method, redirectUri, secret string) (oauth2.TokenSource, error) { + if secret != "topsecret" { + return nil, errors.New("invalid secret") + } + // reuse instance (similar to oauth.go getInstance pattern) if demoInstance != nil { // update existing instance with new values diff --git a/server/http.go b/server/http.go index 4279605d1..a0c4cfe4f 100644 --- a/server/http.go +++ b/server/http.go @@ -279,6 +279,7 @@ func (s *HTTPd) RegisterSystemHandler(site *core.Site, pub publisher, cache *uti routes := map[string]route{ "auth": {"POST", "/auth", authHandler}, + "authmerged": {"POST", "/auth/{class:[a-z]+}/merge/{id:[0-9.]+}", authHandler}, "templates": {"GET", "/templates/{class:[a-z]+}", templatesHandler}, "products": {"GET", "/products/{class:[a-z]+}", productsHandler}, "devices": {"GET", "/devices/{class:[a-z]+}", devicesConfigHandler}, diff --git a/server/http_config_helper.go b/server/http_config_helper.go index 3930942a1..84f5060f8 100644 --- a/server/http_config_helper.go +++ b/server/http_config_helper.go @@ -107,6 +107,7 @@ func templateForConfig(class templates.Class, conf map[string]any) (templates.Te return templates.ByName(class, typ) } +// filterValidTemplateParams removes all configuration properties that are not part of the template definition func filterValidTemplateParams(tmpl *templates.Template, conf map[string]any) map[string]any { res := make(map[string]any) @@ -169,6 +170,32 @@ func mergeMasked(class templates.Class, conf, old map[string]any) (map[string]an }) } +// deviceOther looks up a stored device's `Other` config by class and id. +func deviceOther(class templates.Class, id int) (map[string]any, error) { + name := config.NameForID(id) + switch class { + case templates.Charger: + return deviceOtherFromHandler(name, config.Chargers()) + case templates.Meter: + return deviceOtherFromHandler(name, config.Meters()) + case templates.Vehicle: + return deviceOtherFromHandler(name, config.Vehicles()) + case templates.Tariff: + return deviceOtherFromHandler(name, config.Tariffs()) + case templates.Messenger: + return deviceOtherFromHandler(name, config.Messengers()) + } + return nil, errors.New("unsupported class: " + class.String()) +} + +func deviceOtherFromHandler[T any](name string, h config.Handler[T]) (map[string]any, error) { + dev, err := h.ByName(name) + if err != nil { + return nil, err + } + return dev.Config().Other, nil +} + func startDeviceTimeout() (context.Context, context.CancelFunc, chan struct{}) { done := make(chan struct{}) ctx, cancel := context.WithCancel(context.Background()) diff --git a/server/http_config_metadata_handler.go b/server/http_config_metadata_handler.go index bcd331093..14eeef9a9 100644 --- a/server/http_config_metadata_handler.go +++ b/server/http_config_metadata_handler.go @@ -5,6 +5,7 @@ import ( "encoding/json" "net/http" "slices" + "strconv" "strings" "github.com/evcc-io/evcc/plugin/auth" @@ -42,6 +43,38 @@ func authHandler(w http.ResponseWriter, r *http.Request) { return } + // when editing existing device, merge masked values with stored config + if vars := mux.Vars(r); vars["class"] != "" && vars["id"] != "" { + id, err := strconv.Atoi(vars["id"]) + if err != nil { + jsonError(w, http.StatusBadRequest, err) + return + } + + class, err := templates.ClassString(vars["class"]) + if err != nil { + jsonError(w, http.StatusBadRequest, err) + return + } + + old, err := deviceOther(class, id) + if err != nil { + jsonError(w, http.StatusBadRequest, err) + return + } + + merged, err := mergeMasked(class, cc.Other, old) + if err != nil { + jsonError(w, http.StatusBadRequest, err) + return + } + + cc.Other = merged + } + + // template is only needed by mergeMasked above; the auth decoder is strict + delete(cc.Other, typeTemplate) + ts, err := auth.NewFromConfig(context.Background(), cc.Type, cc.Other) if err != nil { jsonError(w, http.StatusBadRequest, err) diff --git a/tests/config-device-auth-demo.tpl.yaml b/tests/config-device-auth-demo.tpl.yaml index 46e3395e7..b33aa1fba 100644 --- a/tests/config-device-auth-demo.tpl.yaml +++ b/tests/config-device-auth-demo.tpl.yaml @@ -6,7 +6,7 @@ products: en: Auth Demo Meter auth: type: demo - params: ["server", "method", "redirectUri"] + params: ["server", "method", "redirectUri", "secret"] params: - name: usage choice: ["grid"] @@ -25,6 +25,11 @@ params: generic: Authentication Method choice: ["redirect", "device-code"] required: true + - name: secret + description: + generic: Secret + required: true + mask: true - name: power description: generic: Power diff --git a/tests/config-device-auth.spec.ts b/tests/config-device-auth.spec.ts index 7b1116795..d5b1b7267 100644 --- a/tests/config-device-auth.spec.ts +++ b/tests/config-device-auth.spec.ts @@ -5,6 +5,8 @@ import { simulatorUrl, startSimulator, stopSimulator } from "./simulator"; test.use({ baseURL: baseUrl() }); +const secret = "topsecret"; + const templateFlags = [ "--disable-auth", "--template-type", @@ -51,6 +53,7 @@ test.describe("config device auth", async () => { await meterModal.getByLabel("Server").fill(simulatorUrl()); await meterModal.getByLabel("Redirect URI").fill(getRedirectUri(page.url())); await meterModal.getByLabel("Authentication Method").selectOption("redirect"); + await meterModal.getByLabel("Secret").fill(secret); await meterModal.getByRole("button", { name: "Prepare connection" }).click(); // Get the login link and remove target="_blank" to keep it in same page @@ -85,6 +88,7 @@ test.describe("config device auth", async () => { await meterModal.getByLabel("Server").fill(simulatorUrl()); await meterModal.getByLabel("Redirect URI").fill(getRedirectUri(page.url())); await meterModal.getByLabel("Authentication Method").selectOption("redirect"); + await meterModal.getByLabel("Secret").fill(secret); // Even though auth is already done, still need to click prepare connection to proceed to device fields await meterModal.getByRole("button", { name: "Prepare connection" }).click(); @@ -109,6 +113,7 @@ test.describe("config device auth", async () => { await expectModalVisible(meterModal); await expect(meterModal.getByLabel("Server")).toHaveValue(simulatorUrl()); await expect(meterModal.getByLabel("Authentication Method")).toHaveValue("redirect"); + await expect(meterModal.getByLabel("Secret")).toHaveValue("***"); await expect(meterModal.getByLabel("Power")).toHaveValue("5000"); await expect(meterModal.getByRole("button", { name: "Prepare connection" })).not.toBeVisible(); await expect(meterModal.getByRole("button", { name: "Validate & save" })).toBeVisible(); @@ -143,6 +148,7 @@ test.describe("config device auth", async () => { await meterModal.getByLabel("Server").fill(simulatorUrl()); await meterModal.getByLabel("Redirect URI").fill(getRedirectUri(page.url())); await meterModal.getByLabel("Authentication Method").selectOption("device-code"); + await meterModal.getByLabel("Secret").fill(secret); await meterModal.getByRole("button", { name: "Prepare connection" }).click(); // verify device code is displayed @@ -162,6 +168,7 @@ test.describe("config device auth", async () => { await meterModal.getByLabel("Server").fill("invalid-url-without-scheme"); await meterModal.getByLabel("Redirect URI").fill(getRedirectUri(page.url())); await meterModal.getByLabel("Authentication Method").selectOption("redirect"); + await meterModal.getByLabel("Secret").fill(secret); await meterModal.getByRole("button", { name: "Prepare connection" }).click(); await expect(meterModal).toContainText("server must start with http:// or https://"); @@ -200,6 +207,7 @@ test.describe("config device auth", async () => { await meterModal.getByLabel("Server").fill(simulatorUrl()); await meterModal.getByLabel("Redirect URI").fill(getRedirectUri(page.url())); await meterModal.getByLabel("Authentication Method").selectOption("redirect"); + await meterModal.getByLabel("Secret").fill(secret); await meterModal.getByRole("button", { name: "Prepare connection" }).click(); // Get the login link and remove target="_blank" to keep it in same page @@ -236,6 +244,7 @@ test.describe("config device auth", async () => { await meterModal.getByLabel("Server").fill(simulatorUrl()); await meterModal.getByLabel("Redirect URI").fill(getRedirectUri(page.url())); await meterModal.getByLabel("Authentication Method").selectOption("redirect"); + await meterModal.getByLabel("Secret").fill(secret); await meterModal.getByRole("button", { name: "Prepare connection" }).click(); // verify connection link is ready but close modal instead of clicking it From d44bc7f8fc6e73cfddb9d67e33c153663014c375 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20He=C3=9F?= Date: Sun, 12 Apr 2026 14:19:56 +0200 Subject: [PATCH 0019/1293] Easee: align observation IDs with official developer docs (#29044) --- charger/easee.go | 6 +- charger/easee/observationid_enumer.go | 1198 +++++++++++++------------ charger/easee/signalr.go | 16 +- 3 files changed, 628 insertions(+), 592 deletions(-) diff --git a/charger/easee.go b/charger/easee.go index ec41b2824..8297e9235 100644 --- a/charger/easee.go +++ b/charger/easee.go @@ -362,11 +362,11 @@ func (c *Easee) ProductUpdate(i json.RawMessage) { } case easee.LIFETIME_ENERGY: c.totalEnergy = value.(float64) - case easee.IN_CURRENT_T3: + case easee.INT_CURRENT_T3: c.currentL1 = value.(float64) - case easee.IN_CURRENT_T4: + case easee.INT_CURRENT_T4: c.currentL2 = value.(float64) - case easee.IN_CURRENT_T5: + case easee.INT_CURRENT_T5: c.currentL3 = value.(float64) case easee.PHASE_MODE: c.phaseMode = value.(int) diff --git a/charger/easee/observationid_enumer.go b/charger/easee/observationid_enumer.go index 15c0eb970..396774719 100644 --- a/charger/easee/observationid_enumer.go +++ b/charger/easee/observationid_enumer.go @@ -7,8 +7,8 @@ import ( "strings" ) -const _ObservationIDName = "SELF_TEST_RESULTSELF_TEST_DETAILSWIFI_EVENTCHARGER_OFFLINE_REASONEASEE_LINK_COMMAND_RESPONSEEASEE_LINK_DATA_RECEIVEDLOCAL_PRE_AUTHORIZE_ENABLEDLOCAL_AUTHORIZE_OFFLINE_ENABLEDALLOW_OFFLINE_TX_FOR_UNKNOWN_IDERRATIC_EVMAX_TOGGLESBACKPLATE_TYPESITE_STRUCTUREDETECTED_POWER_GRID_TYPECIRCUIT_MAX_CURRENT_P1CIRCUIT_MAX_CURRENT_P2CIRCUIT_MAX_CURRENT_P3LOCATIONSITE_IDSTRINGSITE_IDNUMERICLOCK_CABLE_PERMANENTLYIS_ENABLEDCIRCUIT_SEQUENCE_NUMBERSINGLE_PHASE_NUMBERENABLE3_PHASES_DEPRECATEDWI_FI_SSIDENABLE_IDLE_CURRENTPHASE_MODEFORCED_THREE_PHASE_ON_ITWITH_GND_FAULTLED_STRIP_BRIGHTNESSLOCAL_AUTHORIZATION_REQUIREDAUTHORIZATION_REQUIREDREMOTE_START_REQUIREDSMART_BUTTON_ENABLEDOFFLINE_CHARGING_MODELEDMODEMAX_CHARGER_CURRENTDYNAMIC_CHARGER_CURRENTMAX_CURRENT_OFFLINE_FALLBACK_P1MAX_CURRENT_OFFLINE_FALLBACK_P2MAX_CURRENT_OFFLINE_FALLBACK_P3LISTEN_TO_CONTROL_PULSECONTROL_PULSE_RTTCHARGING_SCHEDULEPAIRED_EQUALIZERWI_FI_APENABLEDPAIRED_USER_IDTOKENCIRCUIT_TOTAL_ALLOCATED_PHASE_CONDUCTOR_CURRENT_L1CIRCUIT_TOTAL_ALLOCATED_PHASE_CONDUCTOR_CURRENT_L2CIRCUIT_TOTAL_ALLOCATED_PHASE_CONDUCTOR_CURRENT_L3CIRCUIT_TOTAL_PHASE_CONDUCTOR_CURRENT_L1CIRCUIT_TOTAL_PHASE_CONDUCTOR_CURRENT_L2CIRCUIT_TOTAL_PHASE_CONDUCTOR_CURRENT_L3NUMBER_OF_CARS_CONNECTEDNUMBER_OF_CARS_CHARGINGNUMBER_OF_CARS_IN_QUEUENUMBER_OF_CARS_FULLY_CHARGEDSOFTWARE_RELEASEICCIDMODEM_FW_IDOTAERROR_CODEMOBILE_NETWORK_OPERATORREBOOT_REASONPOWER_PCBVERSIONCOM_PCBVERSIONREASON_FOR_NO_CURRENTLOAD_BALANCING_NUMBER_OF_CONNECTED_CHARGERSUDPNUM_OF_CONNECTED_NODESLOCAL_CONNECTIONPILOT_MODECAR_CONNECTED_DEPRECATEDSMART_CHARGINGCABLE_LOCKEDCABLE_RATINGPILOT_HIGHPILOT_LOWBACK_PLATE_IDUSER_IDTOKEN_REVERSEDCHARGER_OP_MODEOUTPUT_PHASEDYNAMIC_CIRCUIT_CURRENT_P1DYNAMIC_CIRCUIT_CURRENT_P2DYNAMIC_CIRCUIT_CURRENT_P3OUTPUT_CURRENTDERATED_CURRENTDERATING_ACTIVEDEBUG_STRINGERROR_STRINGERROR_CODETOTAL_POWERSESSION_ENERGYENERGY_PER_HOURLEGACY_EV_STATUSLIFETIME_ENERGYLIFETIME_RELAY_SWITCHESLIFETIME_HOURSDYNAMIC_CURRENT_OFFLINE_FALLBACK_DEPRICATEDUSER_IDTOKENCHARGING_SESSIONCELL_RSSICELL_RATWI_FI_RSSICELL_ADDRESSWI_FI_ADDRESSWI_FI_TYPELOCAL_RSSIMASTER_BACK_PLATE_IDLOCAL_TX_POWERLOCAL_STATEFOUND_WI_FICHARGER_RATCELLULAR_INTERFACE_ERROR_COUNTCELLULAR_INTERFACE_RESET_COUNTWIFI_INTERFACE_ERROR_COUNTWIFI_INTERFACE_RESET_COUNTLOCAL_NODE_TYPELOCAL_RADIO_CHANNELLOCAL_SHORT_ADDRESSLOCAL_PARENT_ADDR_OR_NUM_OF_NODESTEMP_MAXTEMP_AMBIENT_POWER_BOARDTEMP_INPUT_T2TEMP_INPUT_T3TEMP_INPUT_T4TEMP_INPUT_T5TEMP_OUTPUT_NTEMP_OUTPUT_L1TEMP_OUTPUT_L2TEMP_OUTPUT_L3TEMP_AMBIENTLIGHT_AMBIENTINT_REL_HUMIDITYBACK_PLATE_LOCKEDCURRENT_MOTORBACK_PLATE_HALL_SENSORIN_CURRENT_T2IN_CURRENT_T3IN_CURRENT_T4IN_CURRENT_T5IN_VOLT_T1_T2IN_VOLT_T1_T3IN_VOLT_T1_T4IN_VOLT_T1_T5IN_VOLT_T2_T3IN_VOLT_T2_T4IN_VOLT_T2_T5IN_VOLT_T3_T4IN_VOLT_T3_T5IN_VOLT_T4_T5OUT_VOLT_PIN1_2OUT_VOLT_PIN1_3OUT_VOLT_PIN1_4OUT_VOLT_PIN1_5VOLT_LEVEL33VOLT_LEVEL5VOLT_LEVEL12LTE_RSRPLTE_SINRLTE_RSRQEQ_AVAILABLE_CURRENT_P1EQ_AVAILABLE_CURRENT_P2EQ_AVAILABLE_CURRENT_P3CONNECTED_TO_CLOUDCLOUD_DISCONNECT_REASON" -const _ObservationIDLowerName = "self_test_resultself_test_detailswifi_eventcharger_offline_reasoneasee_link_command_responseeasee_link_data_receivedlocal_pre_authorize_enabledlocal_authorize_offline_enabledallow_offline_tx_for_unknown_iderratic_evmax_togglesbackplate_typesite_structuredetected_power_grid_typecircuit_max_current_p1circuit_max_current_p2circuit_max_current_p3locationsite_idstringsite_idnumericlock_cable_permanentlyis_enabledcircuit_sequence_numbersingle_phase_numberenable3_phases_deprecatedwi_fi_ssidenable_idle_currentphase_modeforced_three_phase_on_itwith_gnd_faultled_strip_brightnesslocal_authorization_requiredauthorization_requiredremote_start_requiredsmart_button_enabledoffline_charging_modeledmodemax_charger_currentdynamic_charger_currentmax_current_offline_fallback_p1max_current_offline_fallback_p2max_current_offline_fallback_p3listen_to_control_pulsecontrol_pulse_rttcharging_schedulepaired_equalizerwi_fi_apenabledpaired_user_idtokencircuit_total_allocated_phase_conductor_current_l1circuit_total_allocated_phase_conductor_current_l2circuit_total_allocated_phase_conductor_current_l3circuit_total_phase_conductor_current_l1circuit_total_phase_conductor_current_l2circuit_total_phase_conductor_current_l3number_of_cars_connectednumber_of_cars_chargingnumber_of_cars_in_queuenumber_of_cars_fully_chargedsoftware_releaseiccidmodem_fw_idotaerror_codemobile_network_operatorreboot_reasonpower_pcbversioncom_pcbversionreason_for_no_currentload_balancing_number_of_connected_chargersudpnum_of_connected_nodeslocal_connectionpilot_modecar_connected_deprecatedsmart_chargingcable_lockedcable_ratingpilot_highpilot_lowback_plate_iduser_idtoken_reversedcharger_op_modeoutput_phasedynamic_circuit_current_p1dynamic_circuit_current_p2dynamic_circuit_current_p3output_currentderated_currentderating_activedebug_stringerror_stringerror_codetotal_powersession_energyenergy_per_hourlegacy_ev_statuslifetime_energylifetime_relay_switcheslifetime_hoursdynamic_current_offline_fallback_depricateduser_idtokencharging_sessioncell_rssicell_ratwi_fi_rssicell_addresswi_fi_addresswi_fi_typelocal_rssimaster_back_plate_idlocal_tx_powerlocal_statefound_wi_ficharger_ratcellular_interface_error_countcellular_interface_reset_countwifi_interface_error_countwifi_interface_reset_countlocal_node_typelocal_radio_channellocal_short_addresslocal_parent_addr_or_num_of_nodestemp_maxtemp_ambient_power_boardtemp_input_t2temp_input_t3temp_input_t4temp_input_t5temp_output_ntemp_output_l1temp_output_l2temp_output_l3temp_ambientlight_ambientint_rel_humidityback_plate_lockedcurrent_motorback_plate_hall_sensorin_current_t2in_current_t3in_current_t4in_current_t5in_volt_t1_t2in_volt_t1_t3in_volt_t1_t4in_volt_t1_t5in_volt_t2_t3in_volt_t2_t4in_volt_t2_t5in_volt_t3_t4in_volt_t3_t5in_volt_t4_t5out_volt_pin1_2out_volt_pin1_3out_volt_pin1_4out_volt_pin1_5volt_level33volt_level5volt_level12lte_rsrplte_sinrlte_rsrqeq_available_current_p1eq_available_current_p2eq_available_current_p3connected_to_cloudcloud_disconnect_reason" +const _ObservationIDName = "SELF_TEST_RESULTSELF_TEST_DETAILSWIFI_EVENTCHARGER_OFFLINE_REASONEASEE_LINK_COMMAND_RESPONSEEASEE_LINK_DATA_RECEIVEDLOCAL_PRE_AUTHORIZE_ENABLEDLOCAL_AUTHORIZE_OFFLINE_ENABLEDALLOW_OFFLINE_TX_FOR_UNKNOWN_IDERRATIC_EVMAX_TOGGLESBACKPLATE_TYPESITE_STRUCTUREDETECTED_POWER_GRID_TYPECIRCUIT_MAX_CURRENT_P1CIRCUIT_MAX_CURRENT_P2CIRCUIT_MAX_CURRENT_P3LOCATIONSITE_IDSTRINGSITE_IDNUMERICRFID_TIMEOUT_AUTHLOCK_CABLE_PERMANENTLYIS_ENABLEDTEMPERATURE_MONITOR_STATECIRCUIT_SEQUENCE_NUMBERSINGLE_PHASE_NUMBERENABLE3_PHASES_DEPRECATEDWI_FI_SSIDENABLE_IDLE_CURRENTPHASE_MODEFORCED_THREE_PHASE_ON_ITWITH_GND_FAULTLED_STRIP_BRIGHTNESSLOCAL_AUTHORIZATION_REQUIREDAUTHORIZATION_REQUIREDREMOTE_START_REQUIREDSMART_BUTTON_ENABLEDOFFLINE_CHARGING_MODELEDMODEMAX_CHARGER_CURRENTDYNAMIC_CHARGER_CURRENTMAX_CURRENT_OFFLINE_FALLBACK_P1MAX_CURRENT_OFFLINE_FALLBACK_P2MAX_CURRENT_OFFLINE_FALLBACK_P3RELEASE_CABLE_AT_POWER_OFFLISTEN_TO_CONTROL_PULSECONTROL_PULSE_RTTCHARGING_SESSION_SIGNEDCHARGING_SCHEDULEPAIRED_EQUALIZERWI_FI_APENABLEDPAIRED_USER_IDTOKENCIRCUIT_TOTAL_ALLOCATED_PHASE_CONDUCTOR_CURRENT_L1CIRCUIT_TOTAL_ALLOCATED_PHASE_CONDUCTOR_CURRENT_L2CIRCUIT_TOTAL_ALLOCATED_PHASE_CONDUCTOR_CURRENT_L3CIRCUIT_TOTAL_PHASE_CONDUCTOR_CURRENT_L1CIRCUIT_TOTAL_PHASE_CONDUCTOR_CURRENT_L2CIRCUIT_TOTAL_PHASE_CONDUCTOR_CURRENT_L3NUMBER_OF_CARS_CONNECTEDNUMBER_OF_CARS_CHARGINGNUMBER_OF_CARS_IN_QUEUENUMBER_OF_CARS_FULLY_CHARGEDSOFTWARE_RELEASEICCIDMODEM_FW_IDOTAERROR_CODEMOBILE_NETWORK_OPERATORREBOOT_REASONPOWER_PCBVERSIONCOM_PCBVERSIONREASON_FOR_NO_CURRENTLOAD_BALANCING_NUMBER_OF_CONNECTED_CHARGERSUDPNUM_OF_CONNECTED_NODESLOCAL_CONNECTIONPILOT_MODECAR_CONNECTED_DEPRECATEDSMART_CHARGINGCABLE_LOCKEDCABLE_RATINGPILOT_HIGHPILOT_LOWBACK_PLATE_IDUSER_IDTOKEN_REVERSEDCHARGER_OP_MODEOUTPUT_PHASEDYNAMIC_CIRCUIT_CURRENT_P1DYNAMIC_CIRCUIT_CURRENT_P2DYNAMIC_CIRCUIT_CURRENT_P3OUTPUT_CURRENTDERATED_CURRENTDERATING_ACTIVEDEBUG_STRINGERROR_STRINGERROR_CODETOTAL_POWERSESSION_ENERGYENERGY_PER_HOURLEGACY_EV_STATUSLIFETIME_ENERGYLIFETIME_RELAY_SWITCHESLIFETIME_HOURSDYNAMIC_CURRENT_OFFLINE_FALLBACK_DEPRICATEDUSER_IDTOKENCHARGING_SESSIONCELL_RSSICELL_RATWI_FI_RSSICELL_ADDRESSWI_FI_ADDRESSWI_FI_TYPELOCAL_RSSIMASTER_BACK_PLATE_IDLOCAL_TX_POWERLOCAL_STATEFOUND_WI_FICURRENT_CONNECTIONCELLULAR_INTERFACE_ERROR_COUNTCELLULAR_INTERFACE_RESET_COUNTWIFI_INTERFACE_ERROR_COUNTWIFI_INTERFACE_RESET_COUNTLOCAL_NODE_TYPELOCAL_RADIO_CHANNELLOCAL_SHORT_ADDRESSLOCAL_PARENT_ADDR_OR_NUM_OF_NODESTEMP_MAXTEMP_AMBIENT_POWER_BOARDTEMP_INPUT_T2TEMP_INPUT_T3TEMP_INPUT_T4TEMP_INPUT_T5TEMP_OUTPUT_NTEMP_OUTPUT_L1TEMP_OUTPUT_L2TEMP_OUTPUT_L3TEMP_AMBIENTLIGHT_AMBIENTINT_REL_HUMIDITYBACK_PLATE_LOCKEDCURRENT_MOTORBACK_PLATE_HALL_SENSORINT_CURRENT_T2INT_CURRENT_T3INT_CURRENT_T4INT_CURRENT_T5IN_VOLT_T1_T2IN_VOLT_T1_T3IN_VOLT_T1_T4IN_VOLT_T1_T5IN_VOLT_T2_T3IN_VOLT_T2_T4IN_VOLT_T2_T5IN_VOLT_T3_T4IN_VOLT_T3_T5IN_VOLT_T4_T5OUT_VOLT_PIN1_2OUT_VOLT_PIN1_3OUT_VOLT_PIN1_4OUT_VOLT_PIN1_5OUT_VOLT_PIN2_3VOLT_LEVEL33VOLT_LEVEL5VOLT_LEVEL12LTE_RSRPLTE_SINRLTE_RSRQCHARGE_SESSION_STARTEQ_AVAILABLE_CURRENT_P1EQ_AVAILABLE_CURRENT_P2EQ_AVAILABLE_CURRENT_P3CONNECTED_TO_CLOUDCLOUD_DISCONNECT_REASON" +const _ObservationIDLowerName = "self_test_resultself_test_detailswifi_eventcharger_offline_reasoneasee_link_command_responseeasee_link_data_receivedlocal_pre_authorize_enabledlocal_authorize_offline_enabledallow_offline_tx_for_unknown_iderratic_evmax_togglesbackplate_typesite_structuredetected_power_grid_typecircuit_max_current_p1circuit_max_current_p2circuit_max_current_p3locationsite_idstringsite_idnumericrfid_timeout_authlock_cable_permanentlyis_enabledtemperature_monitor_statecircuit_sequence_numbersingle_phase_numberenable3_phases_deprecatedwi_fi_ssidenable_idle_currentphase_modeforced_three_phase_on_itwith_gnd_faultled_strip_brightnesslocal_authorization_requiredauthorization_requiredremote_start_requiredsmart_button_enabledoffline_charging_modeledmodemax_charger_currentdynamic_charger_currentmax_current_offline_fallback_p1max_current_offline_fallback_p2max_current_offline_fallback_p3release_cable_at_power_offlisten_to_control_pulsecontrol_pulse_rttcharging_session_signedcharging_schedulepaired_equalizerwi_fi_apenabledpaired_user_idtokencircuit_total_allocated_phase_conductor_current_l1circuit_total_allocated_phase_conductor_current_l2circuit_total_allocated_phase_conductor_current_l3circuit_total_phase_conductor_current_l1circuit_total_phase_conductor_current_l2circuit_total_phase_conductor_current_l3number_of_cars_connectednumber_of_cars_chargingnumber_of_cars_in_queuenumber_of_cars_fully_chargedsoftware_releaseiccidmodem_fw_idotaerror_codemobile_network_operatorreboot_reasonpower_pcbversioncom_pcbversionreason_for_no_currentload_balancing_number_of_connected_chargersudpnum_of_connected_nodeslocal_connectionpilot_modecar_connected_deprecatedsmart_chargingcable_lockedcable_ratingpilot_highpilot_lowback_plate_iduser_idtoken_reversedcharger_op_modeoutput_phasedynamic_circuit_current_p1dynamic_circuit_current_p2dynamic_circuit_current_p3output_currentderated_currentderating_activedebug_stringerror_stringerror_codetotal_powersession_energyenergy_per_hourlegacy_ev_statuslifetime_energylifetime_relay_switcheslifetime_hoursdynamic_current_offline_fallback_depricateduser_idtokencharging_sessioncell_rssicell_ratwi_fi_rssicell_addresswi_fi_addresswi_fi_typelocal_rssimaster_back_plate_idlocal_tx_powerlocal_statefound_wi_ficurrent_connectioncellular_interface_error_countcellular_interface_reset_countwifi_interface_error_countwifi_interface_reset_countlocal_node_typelocal_radio_channellocal_short_addresslocal_parent_addr_or_num_of_nodestemp_maxtemp_ambient_power_boardtemp_input_t2temp_input_t3temp_input_t4temp_input_t5temp_output_ntemp_output_l1temp_output_l2temp_output_l3temp_ambientlight_ambientint_rel_humidityback_plate_lockedcurrent_motorback_plate_hall_sensorint_current_t2int_current_t3int_current_t4int_current_t5in_volt_t1_t2in_volt_t1_t3in_volt_t1_t4in_volt_t1_t5in_volt_t2_t3in_volt_t2_t4in_volt_t2_t5in_volt_t3_t4in_volt_t3_t5in_volt_t4_t5out_volt_pin1_2out_volt_pin1_3out_volt_pin1_4out_volt_pin1_5out_volt_pin2_3volt_level33volt_level5volt_level12lte_rsrplte_sinrlte_rsrqcharge_session_starteq_available_current_p1eq_available_current_p2eq_available_current_p3connected_to_cloudcloud_disconnect_reason" var _ObservationIDMap = map[ObservationID]string{ 1: _ObservationIDName[0:16], @@ -30,150 +30,156 @@ var _ObservationIDMap = map[ObservationID]string{ 25: _ObservationIDName[344:352], 26: _ObservationIDName[352:365], 27: _ObservationIDName[365:379], - 30: _ObservationIDName[379:401], - 31: _ObservationIDName[401:411], - 33: _ObservationIDName[411:434], - 34: _ObservationIDName[434:453], - 35: _ObservationIDName[453:478], - 36: _ObservationIDName[478:488], - 37: _ObservationIDName[488:507], - 38: _ObservationIDName[507:517], - 39: _ObservationIDName[517:555], - 40: _ObservationIDName[555:575], - 41: _ObservationIDName[575:603], - 42: _ObservationIDName[603:625], - 43: _ObservationIDName[625:646], - 44: _ObservationIDName[646:666], - 45: _ObservationIDName[666:687], - 46: _ObservationIDName[687:694], - 47: _ObservationIDName[694:713], - 48: _ObservationIDName[713:736], - 50: _ObservationIDName[736:767], - 51: _ObservationIDName[767:798], - 52: _ObservationIDName[798:829], - 56: _ObservationIDName[829:852], - 57: _ObservationIDName[852:869], - 62: _ObservationIDName[869:886], - 65: _ObservationIDName[886:902], - 68: _ObservationIDName[902:917], - 69: _ObservationIDName[917:936], - 70: _ObservationIDName[936:986], - 71: _ObservationIDName[986:1036], - 72: _ObservationIDName[1036:1086], - 73: _ObservationIDName[1086:1126], - 74: _ObservationIDName[1126:1166], - 75: _ObservationIDName[1166:1206], - 76: _ObservationIDName[1206:1230], - 77: _ObservationIDName[1230:1253], - 78: _ObservationIDName[1253:1276], - 79: _ObservationIDName[1276:1304], - 80: _ObservationIDName[1304:1320], - 81: _ObservationIDName[1320:1325], - 82: _ObservationIDName[1325:1336], - 83: _ObservationIDName[1336:1349], - 84: _ObservationIDName[1349:1372], - 89: _ObservationIDName[1372:1385], - 90: _ObservationIDName[1385:1401], - 91: _ObservationIDName[1401:1415], - 96: _ObservationIDName[1415:1436], - 97: _ObservationIDName[1436:1479], - 98: _ObservationIDName[1479:1504], - 99: _ObservationIDName[1504:1520], - 100: _ObservationIDName[1520:1530], - 101: _ObservationIDName[1530:1554], - 102: _ObservationIDName[1554:1568], - 103: _ObservationIDName[1568:1580], - 104: _ObservationIDName[1580:1592], - 105: _ObservationIDName[1592:1602], - 106: _ObservationIDName[1602:1611], - 107: _ObservationIDName[1611:1624], - 108: _ObservationIDName[1624:1645], - 109: _ObservationIDName[1645:1660], - 110: _ObservationIDName[1660:1672], - 111: _ObservationIDName[1672:1698], - 112: _ObservationIDName[1698:1724], - 113: _ObservationIDName[1724:1750], - 114: _ObservationIDName[1750:1764], - 115: _ObservationIDName[1764:1779], - 116: _ObservationIDName[1779:1794], - 117: _ObservationIDName[1794:1806], - 118: _ObservationIDName[1806:1818], - 119: _ObservationIDName[1818:1828], - 120: _ObservationIDName[1828:1839], - 121: _ObservationIDName[1839:1853], - 122: _ObservationIDName[1853:1868], - 123: _ObservationIDName[1868:1884], - 124: _ObservationIDName[1884:1899], - 125: _ObservationIDName[1899:1922], - 126: _ObservationIDName[1922:1936], - 127: _ObservationIDName[1936:1979], - 128: _ObservationIDName[1979:1991], - 129: _ObservationIDName[1991:2007], - 130: _ObservationIDName[2007:2016], - 131: _ObservationIDName[2016:2024], - 132: _ObservationIDName[2024:2034], - 133: _ObservationIDName[2034:2046], - 134: _ObservationIDName[2046:2059], - 135: _ObservationIDName[2059:2069], - 136: _ObservationIDName[2069:2079], - 137: _ObservationIDName[2079:2099], - 138: _ObservationIDName[2099:2113], - 139: _ObservationIDName[2113:2124], - 140: _ObservationIDName[2124:2135], - 141: _ObservationIDName[2135:2146], - 142: _ObservationIDName[2146:2176], - 143: _ObservationIDName[2176:2206], - 144: _ObservationIDName[2206:2232], - 145: _ObservationIDName[2232:2258], - 146: _ObservationIDName[2258:2273], - 147: _ObservationIDName[2273:2292], - 148: _ObservationIDName[2292:2311], - 149: _ObservationIDName[2311:2344], - 150: _ObservationIDName[2344:2352], - 151: _ObservationIDName[2352:2376], - 152: _ObservationIDName[2376:2389], - 153: _ObservationIDName[2389:2402], - 154: _ObservationIDName[2402:2415], - 155: _ObservationIDName[2415:2428], - 160: _ObservationIDName[2428:2441], - 161: _ObservationIDName[2441:2455], - 162: _ObservationIDName[2455:2469], - 163: _ObservationIDName[2469:2483], - 170: _ObservationIDName[2483:2495], - 171: _ObservationIDName[2495:2508], - 172: _ObservationIDName[2508:2524], - 173: _ObservationIDName[2524:2541], - 174: _ObservationIDName[2541:2554], - 175: _ObservationIDName[2554:2576], - 182: _ObservationIDName[2576:2589], - 183: _ObservationIDName[2589:2602], - 184: _ObservationIDName[2602:2615], - 185: _ObservationIDName[2615:2628], - 190: _ObservationIDName[2628:2641], - 191: _ObservationIDName[2641:2654], - 192: _ObservationIDName[2654:2667], - 193: _ObservationIDName[2667:2680], - 194: _ObservationIDName[2680:2693], - 195: _ObservationIDName[2693:2706], - 196: _ObservationIDName[2706:2719], - 197: _ObservationIDName[2719:2732], - 198: _ObservationIDName[2732:2745], - 199: _ObservationIDName[2745:2758], - 202: _ObservationIDName[2758:2773], - 203: _ObservationIDName[2773:2788], - 204: _ObservationIDName[2788:2803], - 205: _ObservationIDName[2803:2818], - 210: _ObservationIDName[2818:2830], - 211: _ObservationIDName[2830:2841], - 212: _ObservationIDName[2841:2853], - 220: _ObservationIDName[2853:2861], - 221: _ObservationIDName[2861:2869], - 222: _ObservationIDName[2869:2877], - 230: _ObservationIDName[2877:2900], - 231: _ObservationIDName[2900:2923], - 232: _ObservationIDName[2923:2946], - 250: _ObservationIDName[2946:2964], - 251: _ObservationIDName[2964:2987], + 28: _ObservationIDName[379:396], + 30: _ObservationIDName[396:418], + 31: _ObservationIDName[418:428], + 32: _ObservationIDName[428:453], + 33: _ObservationIDName[453:476], + 34: _ObservationIDName[476:495], + 35: _ObservationIDName[495:520], + 36: _ObservationIDName[520:530], + 37: _ObservationIDName[530:549], + 38: _ObservationIDName[549:559], + 39: _ObservationIDName[559:597], + 40: _ObservationIDName[597:617], + 41: _ObservationIDName[617:645], + 42: _ObservationIDName[645:667], + 43: _ObservationIDName[667:688], + 44: _ObservationIDName[688:708], + 45: _ObservationIDName[708:729], + 46: _ObservationIDName[729:736], + 47: _ObservationIDName[736:755], + 48: _ObservationIDName[755:778], + 50: _ObservationIDName[778:809], + 51: _ObservationIDName[809:840], + 52: _ObservationIDName[840:871], + 54: _ObservationIDName[871:897], + 56: _ObservationIDName[897:920], + 57: _ObservationIDName[920:937], + 60: _ObservationIDName[937:960], + 62: _ObservationIDName[960:977], + 65: _ObservationIDName[977:993], + 68: _ObservationIDName[993:1008], + 69: _ObservationIDName[1008:1027], + 70: _ObservationIDName[1027:1077], + 71: _ObservationIDName[1077:1127], + 72: _ObservationIDName[1127:1177], + 73: _ObservationIDName[1177:1217], + 74: _ObservationIDName[1217:1257], + 75: _ObservationIDName[1257:1297], + 76: _ObservationIDName[1297:1321], + 77: _ObservationIDName[1321:1344], + 78: _ObservationIDName[1344:1367], + 79: _ObservationIDName[1367:1395], + 80: _ObservationIDName[1395:1411], + 81: _ObservationIDName[1411:1416], + 82: _ObservationIDName[1416:1427], + 83: _ObservationIDName[1427:1440], + 84: _ObservationIDName[1440:1463], + 89: _ObservationIDName[1463:1476], + 90: _ObservationIDName[1476:1492], + 91: _ObservationIDName[1492:1506], + 96: _ObservationIDName[1506:1527], + 97: _ObservationIDName[1527:1570], + 98: _ObservationIDName[1570:1595], + 99: _ObservationIDName[1595:1611], + 100: _ObservationIDName[1611:1621], + 101: _ObservationIDName[1621:1645], + 102: _ObservationIDName[1645:1659], + 103: _ObservationIDName[1659:1671], + 104: _ObservationIDName[1671:1683], + 105: _ObservationIDName[1683:1693], + 106: _ObservationIDName[1693:1702], + 107: _ObservationIDName[1702:1715], + 108: _ObservationIDName[1715:1736], + 109: _ObservationIDName[1736:1751], + 110: _ObservationIDName[1751:1763], + 111: _ObservationIDName[1763:1789], + 112: _ObservationIDName[1789:1815], + 113: _ObservationIDName[1815:1841], + 114: _ObservationIDName[1841:1855], + 115: _ObservationIDName[1855:1870], + 116: _ObservationIDName[1870:1885], + 117: _ObservationIDName[1885:1897], + 118: _ObservationIDName[1897:1909], + 119: _ObservationIDName[1909:1919], + 120: _ObservationIDName[1919:1930], + 121: _ObservationIDName[1930:1944], + 122: _ObservationIDName[1944:1959], + 123: _ObservationIDName[1959:1975], + 124: _ObservationIDName[1975:1990], + 125: _ObservationIDName[1990:2013], + 126: _ObservationIDName[2013:2027], + 127: _ObservationIDName[2027:2070], + 128: _ObservationIDName[2070:2082], + 129: _ObservationIDName[2082:2098], + 130: _ObservationIDName[2098:2107], + 131: _ObservationIDName[2107:2115], + 132: _ObservationIDName[2115:2125], + 133: _ObservationIDName[2125:2137], + 134: _ObservationIDName[2137:2150], + 135: _ObservationIDName[2150:2160], + 136: _ObservationIDName[2160:2170], + 137: _ObservationIDName[2170:2190], + 138: _ObservationIDName[2190:2204], + 139: _ObservationIDName[2204:2215], + 140: _ObservationIDName[2215:2226], + 141: _ObservationIDName[2226:2244], + 142: _ObservationIDName[2244:2274], + 143: _ObservationIDName[2274:2304], + 144: _ObservationIDName[2304:2330], + 145: _ObservationIDName[2330:2356], + 146: _ObservationIDName[2356:2371], + 147: _ObservationIDName[2371:2390], + 148: _ObservationIDName[2390:2409], + 149: _ObservationIDName[2409:2442], + 150: _ObservationIDName[2442:2450], + 151: _ObservationIDName[2450:2474], + 152: _ObservationIDName[2474:2487], + 153: _ObservationIDName[2487:2500], + 154: _ObservationIDName[2500:2513], + 155: _ObservationIDName[2513:2526], + 160: _ObservationIDName[2526:2539], + 161: _ObservationIDName[2539:2553], + 162: _ObservationIDName[2553:2567], + 163: _ObservationIDName[2567:2581], + 170: _ObservationIDName[2581:2593], + 171: _ObservationIDName[2593:2606], + 172: _ObservationIDName[2606:2622], + 173: _ObservationIDName[2622:2639], + 174: _ObservationIDName[2639:2652], + 175: _ObservationIDName[2652:2674], + 182: _ObservationIDName[2674:2688], + 183: _ObservationIDName[2688:2702], + 184: _ObservationIDName[2702:2716], + 185: _ObservationIDName[2716:2730], + 190: _ObservationIDName[2730:2743], + 191: _ObservationIDName[2743:2756], + 192: _ObservationIDName[2756:2769], + 193: _ObservationIDName[2769:2782], + 194: _ObservationIDName[2782:2795], + 195: _ObservationIDName[2795:2808], + 196: _ObservationIDName[2808:2821], + 197: _ObservationIDName[2821:2834], + 198: _ObservationIDName[2834:2847], + 199: _ObservationIDName[2847:2860], + 202: _ObservationIDName[2860:2875], + 203: _ObservationIDName[2875:2890], + 204: _ObservationIDName[2890:2905], + 205: _ObservationIDName[2905:2920], + 206: _ObservationIDName[2920:2935], + 210: _ObservationIDName[2935:2947], + 211: _ObservationIDName[2947:2958], + 212: _ObservationIDName[2958:2970], + 220: _ObservationIDName[2970:2978], + 221: _ObservationIDName[2978:2986], + 222: _ObservationIDName[2986:2994], + 223: _ObservationIDName[2994:3014], + 230: _ObservationIDName[3014:3037], + 231: _ObservationIDName[3037:3060], + 232: _ObservationIDName[3060:3083], + 250: _ObservationIDName[3083:3101], + 251: _ObservationIDName[3101:3124], } func (i ObservationID) String() string { @@ -206,8 +212,10 @@ func _ObservationIDNoOp() { _ = x[LOCATION-(25)] _ = x[SITE_IDSTRING-(26)] _ = x[SITE_IDNUMERIC-(27)] + _ = x[RFID_TIMEOUT_AUTH-(28)] _ = x[LOCK_CABLE_PERMANENTLY-(30)] _ = x[IS_ENABLED-(31)] + _ = x[TEMPERATURE_MONITOR_STATE-(32)] _ = x[CIRCUIT_SEQUENCE_NUMBER-(33)] _ = x[SINGLE_PHASE_NUMBER-(34)] _ = x[ENABLE3_PHASES_DEPRECATED-(35)] @@ -227,8 +235,10 @@ func _ObservationIDNoOp() { _ = x[MAX_CURRENT_OFFLINE_FALLBACK_P1-(50)] _ = x[MAX_CURRENT_OFFLINE_FALLBACK_P2-(51)] _ = x[MAX_CURRENT_OFFLINE_FALLBACK_P3-(52)] + _ = x[RELEASE_CABLE_AT_POWER_OFF-(54)] _ = x[LISTEN_TO_CONTROL_PULSE-(56)] _ = x[CONTROL_PULSE_RTT-(57)] + _ = x[CHARGING_SESSION_SIGNED-(60)] _ = x[CHARGING_SCHEDULE-(62)] _ = x[PAIRED_EQUALIZER-(65)] _ = x[WI_FI_APENABLED-(68)] @@ -296,7 +306,7 @@ func _ObservationIDNoOp() { _ = x[LOCAL_TX_POWER-(138)] _ = x[LOCAL_STATE-(139)] _ = x[FOUND_WI_FI-(140)] - _ = x[CHARGER_RAT-(141)] + _ = x[CURRENT_CONNECTION-(141)] _ = x[CELLULAR_INTERFACE_ERROR_COUNT-(142)] _ = x[CELLULAR_INTERFACE_RESET_COUNT-(143)] _ = x[WIFI_INTERFACE_ERROR_COUNT-(144)] @@ -321,10 +331,10 @@ func _ObservationIDNoOp() { _ = x[BACK_PLATE_LOCKED-(173)] _ = x[CURRENT_MOTOR-(174)] _ = x[BACK_PLATE_HALL_SENSOR-(175)] - _ = x[IN_CURRENT_T2-(182)] - _ = x[IN_CURRENT_T3-(183)] - _ = x[IN_CURRENT_T4-(184)] - _ = x[IN_CURRENT_T5-(185)] + _ = x[INT_CURRENT_T2-(182)] + _ = x[INT_CURRENT_T3-(183)] + _ = x[INT_CURRENT_T4-(184)] + _ = x[INT_CURRENT_T5-(185)] _ = x[IN_VOLT_T1_T2-(190)] _ = x[IN_VOLT_T1_T3-(191)] _ = x[IN_VOLT_T1_T4-(192)] @@ -339,12 +349,14 @@ func _ObservationIDNoOp() { _ = x[OUT_VOLT_PIN1_3-(203)] _ = x[OUT_VOLT_PIN1_4-(204)] _ = x[OUT_VOLT_PIN1_5-(205)] + _ = x[OUT_VOLT_PIN2_3-(206)] _ = x[VOLT_LEVEL33-(210)] _ = x[VOLT_LEVEL5-(211)] _ = x[VOLT_LEVEL12-(212)] _ = x[LTE_RSRP-(220)] _ = x[LTE_SINR-(221)] _ = x[LTE_RSRQ-(222)] + _ = x[CHARGE_SESSION_START-(223)] _ = x[EQ_AVAILABLE_CURRENT_P1-(230)] _ = x[EQ_AVAILABLE_CURRENT_P2-(231)] _ = x[EQ_AVAILABLE_CURRENT_P3-(232)] @@ -352,7 +364,7 @@ func _ObservationIDNoOp() { _ = x[CLOUD_DISCONNECT_REASON-(251)] } -var _ObservationIDValues = []ObservationID{SELF_TEST_RESULT, SELF_TEST_DETAILS, WIFI_EVENT, CHARGER_OFFLINE_REASON, EASEE_LINK_COMMAND_RESPONSE, EASEE_LINK_DATA_RECEIVED, LOCAL_PRE_AUTHORIZE_ENABLED, LOCAL_AUTHORIZE_OFFLINE_ENABLED, ALLOW_OFFLINE_TX_FOR_UNKNOWN_ID, ERRATIC_EVMAX_TOGGLES, BACKPLATE_TYPE, SITE_STRUCTURE, DETECTED_POWER_GRID_TYPE, CIRCUIT_MAX_CURRENT_P1, CIRCUIT_MAX_CURRENT_P2, CIRCUIT_MAX_CURRENT_P3, LOCATION, SITE_IDSTRING, SITE_IDNUMERIC, LOCK_CABLE_PERMANENTLY, IS_ENABLED, CIRCUIT_SEQUENCE_NUMBER, SINGLE_PHASE_NUMBER, ENABLE3_PHASES_DEPRECATED, WI_FI_SSID, ENABLE_IDLE_CURRENT, PHASE_MODE, FORCED_THREE_PHASE_ON_ITWITH_GND_FAULT, LED_STRIP_BRIGHTNESS, LOCAL_AUTHORIZATION_REQUIRED, AUTHORIZATION_REQUIRED, REMOTE_START_REQUIRED, SMART_BUTTON_ENABLED, OFFLINE_CHARGING_MODE, LEDMODE, MAX_CHARGER_CURRENT, DYNAMIC_CHARGER_CURRENT, MAX_CURRENT_OFFLINE_FALLBACK_P1, MAX_CURRENT_OFFLINE_FALLBACK_P2, MAX_CURRENT_OFFLINE_FALLBACK_P3, LISTEN_TO_CONTROL_PULSE, CONTROL_PULSE_RTT, CHARGING_SCHEDULE, PAIRED_EQUALIZER, WI_FI_APENABLED, PAIRED_USER_IDTOKEN, CIRCUIT_TOTAL_ALLOCATED_PHASE_CONDUCTOR_CURRENT_L1, CIRCUIT_TOTAL_ALLOCATED_PHASE_CONDUCTOR_CURRENT_L2, CIRCUIT_TOTAL_ALLOCATED_PHASE_CONDUCTOR_CURRENT_L3, CIRCUIT_TOTAL_PHASE_CONDUCTOR_CURRENT_L1, CIRCUIT_TOTAL_PHASE_CONDUCTOR_CURRENT_L2, CIRCUIT_TOTAL_PHASE_CONDUCTOR_CURRENT_L3, NUMBER_OF_CARS_CONNECTED, NUMBER_OF_CARS_CHARGING, NUMBER_OF_CARS_IN_QUEUE, NUMBER_OF_CARS_FULLY_CHARGED, SOFTWARE_RELEASE, ICCID, MODEM_FW_ID, OTAERROR_CODE, MOBILE_NETWORK_OPERATOR, REBOOT_REASON, POWER_PCBVERSION, COM_PCBVERSION, REASON_FOR_NO_CURRENT, LOAD_BALANCING_NUMBER_OF_CONNECTED_CHARGERS, UDPNUM_OF_CONNECTED_NODES, LOCAL_CONNECTION, PILOT_MODE, CAR_CONNECTED_DEPRECATED, SMART_CHARGING, CABLE_LOCKED, CABLE_RATING, PILOT_HIGH, PILOT_LOW, BACK_PLATE_ID, USER_IDTOKEN_REVERSED, CHARGER_OP_MODE, OUTPUT_PHASE, DYNAMIC_CIRCUIT_CURRENT_P1, DYNAMIC_CIRCUIT_CURRENT_P2, DYNAMIC_CIRCUIT_CURRENT_P3, OUTPUT_CURRENT, DERATED_CURRENT, DERATING_ACTIVE, DEBUG_STRING, ERROR_STRING, ERROR_CODE, TOTAL_POWER, SESSION_ENERGY, ENERGY_PER_HOUR, LEGACY_EV_STATUS, LIFETIME_ENERGY, LIFETIME_RELAY_SWITCHES, LIFETIME_HOURS, DYNAMIC_CURRENT_OFFLINE_FALLBACK_DEPRICATED, USER_IDTOKEN, CHARGING_SESSION, CELL_RSSI, CELL_RAT, WI_FI_RSSI, CELL_ADDRESS, WI_FI_ADDRESS, WI_FI_TYPE, LOCAL_RSSI, MASTER_BACK_PLATE_ID, LOCAL_TX_POWER, LOCAL_STATE, FOUND_WI_FI, CHARGER_RAT, CELLULAR_INTERFACE_ERROR_COUNT, CELLULAR_INTERFACE_RESET_COUNT, WIFI_INTERFACE_ERROR_COUNT, WIFI_INTERFACE_RESET_COUNT, LOCAL_NODE_TYPE, LOCAL_RADIO_CHANNEL, LOCAL_SHORT_ADDRESS, LOCAL_PARENT_ADDR_OR_NUM_OF_NODES, TEMP_MAX, TEMP_AMBIENT_POWER_BOARD, TEMP_INPUT_T2, TEMP_INPUT_T3, TEMP_INPUT_T4, TEMP_INPUT_T5, TEMP_OUTPUT_N, TEMP_OUTPUT_L1, TEMP_OUTPUT_L2, TEMP_OUTPUT_L3, TEMP_AMBIENT, LIGHT_AMBIENT, INT_REL_HUMIDITY, BACK_PLATE_LOCKED, CURRENT_MOTOR, BACK_PLATE_HALL_SENSOR, IN_CURRENT_T2, IN_CURRENT_T3, IN_CURRENT_T4, IN_CURRENT_T5, IN_VOLT_T1_T2, IN_VOLT_T1_T3, IN_VOLT_T1_T4, IN_VOLT_T1_T5, IN_VOLT_T2_T3, IN_VOLT_T2_T4, IN_VOLT_T2_T5, IN_VOLT_T3_T4, IN_VOLT_T3_T5, IN_VOLT_T4_T5, OUT_VOLT_PIN1_2, OUT_VOLT_PIN1_3, OUT_VOLT_PIN1_4, OUT_VOLT_PIN1_5, VOLT_LEVEL33, VOLT_LEVEL5, VOLT_LEVEL12, LTE_RSRP, LTE_SINR, LTE_RSRQ, EQ_AVAILABLE_CURRENT_P1, EQ_AVAILABLE_CURRENT_P2, EQ_AVAILABLE_CURRENT_P3, CONNECTED_TO_CLOUD, CLOUD_DISCONNECT_REASON} +var _ObservationIDValues = []ObservationID{SELF_TEST_RESULT, SELF_TEST_DETAILS, WIFI_EVENT, CHARGER_OFFLINE_REASON, EASEE_LINK_COMMAND_RESPONSE, EASEE_LINK_DATA_RECEIVED, LOCAL_PRE_AUTHORIZE_ENABLED, LOCAL_AUTHORIZE_OFFLINE_ENABLED, ALLOW_OFFLINE_TX_FOR_UNKNOWN_ID, ERRATIC_EVMAX_TOGGLES, BACKPLATE_TYPE, SITE_STRUCTURE, DETECTED_POWER_GRID_TYPE, CIRCUIT_MAX_CURRENT_P1, CIRCUIT_MAX_CURRENT_P2, CIRCUIT_MAX_CURRENT_P3, LOCATION, SITE_IDSTRING, SITE_IDNUMERIC, RFID_TIMEOUT_AUTH, LOCK_CABLE_PERMANENTLY, IS_ENABLED, TEMPERATURE_MONITOR_STATE, CIRCUIT_SEQUENCE_NUMBER, SINGLE_PHASE_NUMBER, ENABLE3_PHASES_DEPRECATED, WI_FI_SSID, ENABLE_IDLE_CURRENT, PHASE_MODE, FORCED_THREE_PHASE_ON_ITWITH_GND_FAULT, LED_STRIP_BRIGHTNESS, LOCAL_AUTHORIZATION_REQUIRED, AUTHORIZATION_REQUIRED, REMOTE_START_REQUIRED, SMART_BUTTON_ENABLED, OFFLINE_CHARGING_MODE, LEDMODE, MAX_CHARGER_CURRENT, DYNAMIC_CHARGER_CURRENT, MAX_CURRENT_OFFLINE_FALLBACK_P1, MAX_CURRENT_OFFLINE_FALLBACK_P2, MAX_CURRENT_OFFLINE_FALLBACK_P3, RELEASE_CABLE_AT_POWER_OFF, LISTEN_TO_CONTROL_PULSE, CONTROL_PULSE_RTT, CHARGING_SESSION_SIGNED, CHARGING_SCHEDULE, PAIRED_EQUALIZER, WI_FI_APENABLED, PAIRED_USER_IDTOKEN, CIRCUIT_TOTAL_ALLOCATED_PHASE_CONDUCTOR_CURRENT_L1, CIRCUIT_TOTAL_ALLOCATED_PHASE_CONDUCTOR_CURRENT_L2, CIRCUIT_TOTAL_ALLOCATED_PHASE_CONDUCTOR_CURRENT_L3, CIRCUIT_TOTAL_PHASE_CONDUCTOR_CURRENT_L1, CIRCUIT_TOTAL_PHASE_CONDUCTOR_CURRENT_L2, CIRCUIT_TOTAL_PHASE_CONDUCTOR_CURRENT_L3, NUMBER_OF_CARS_CONNECTED, NUMBER_OF_CARS_CHARGING, NUMBER_OF_CARS_IN_QUEUE, NUMBER_OF_CARS_FULLY_CHARGED, SOFTWARE_RELEASE, ICCID, MODEM_FW_ID, OTAERROR_CODE, MOBILE_NETWORK_OPERATOR, REBOOT_REASON, POWER_PCBVERSION, COM_PCBVERSION, REASON_FOR_NO_CURRENT, LOAD_BALANCING_NUMBER_OF_CONNECTED_CHARGERS, UDPNUM_OF_CONNECTED_NODES, LOCAL_CONNECTION, PILOT_MODE, CAR_CONNECTED_DEPRECATED, SMART_CHARGING, CABLE_LOCKED, CABLE_RATING, PILOT_HIGH, PILOT_LOW, BACK_PLATE_ID, USER_IDTOKEN_REVERSED, CHARGER_OP_MODE, OUTPUT_PHASE, DYNAMIC_CIRCUIT_CURRENT_P1, DYNAMIC_CIRCUIT_CURRENT_P2, DYNAMIC_CIRCUIT_CURRENT_P3, OUTPUT_CURRENT, DERATED_CURRENT, DERATING_ACTIVE, DEBUG_STRING, ERROR_STRING, ERROR_CODE, TOTAL_POWER, SESSION_ENERGY, ENERGY_PER_HOUR, LEGACY_EV_STATUS, LIFETIME_ENERGY, LIFETIME_RELAY_SWITCHES, LIFETIME_HOURS, DYNAMIC_CURRENT_OFFLINE_FALLBACK_DEPRICATED, USER_IDTOKEN, CHARGING_SESSION, CELL_RSSI, CELL_RAT, WI_FI_RSSI, CELL_ADDRESS, WI_FI_ADDRESS, WI_FI_TYPE, LOCAL_RSSI, MASTER_BACK_PLATE_ID, LOCAL_TX_POWER, LOCAL_STATE, FOUND_WI_FI, CURRENT_CONNECTION, CELLULAR_INTERFACE_ERROR_COUNT, CELLULAR_INTERFACE_RESET_COUNT, WIFI_INTERFACE_ERROR_COUNT, WIFI_INTERFACE_RESET_COUNT, LOCAL_NODE_TYPE, LOCAL_RADIO_CHANNEL, LOCAL_SHORT_ADDRESS, LOCAL_PARENT_ADDR_OR_NUM_OF_NODES, TEMP_MAX, TEMP_AMBIENT_POWER_BOARD, TEMP_INPUT_T2, TEMP_INPUT_T3, TEMP_INPUT_T4, TEMP_INPUT_T5, TEMP_OUTPUT_N, TEMP_OUTPUT_L1, TEMP_OUTPUT_L2, TEMP_OUTPUT_L3, TEMP_AMBIENT, LIGHT_AMBIENT, INT_REL_HUMIDITY, BACK_PLATE_LOCKED, CURRENT_MOTOR, BACK_PLATE_HALL_SENSOR, INT_CURRENT_T2, INT_CURRENT_T3, INT_CURRENT_T4, INT_CURRENT_T5, IN_VOLT_T1_T2, IN_VOLT_T1_T3, IN_VOLT_T1_T4, IN_VOLT_T1_T5, IN_VOLT_T2_T3, IN_VOLT_T2_T4, IN_VOLT_T2_T5, IN_VOLT_T3_T4, IN_VOLT_T3_T5, IN_VOLT_T4_T5, OUT_VOLT_PIN1_2, OUT_VOLT_PIN1_3, OUT_VOLT_PIN1_4, OUT_VOLT_PIN1_5, OUT_VOLT_PIN2_3, VOLT_LEVEL33, VOLT_LEVEL5, VOLT_LEVEL12, LTE_RSRP, LTE_SINR, LTE_RSRQ, CHARGE_SESSION_START, EQ_AVAILABLE_CURRENT_P1, EQ_AVAILABLE_CURRENT_P2, EQ_AVAILABLE_CURRENT_P3, CONNECTED_TO_CLOUD, CLOUD_DISCONNECT_REASON} var _ObservationIDNameToValueMap = map[string]ObservationID{ _ObservationIDName[0:16]: SELF_TEST_RESULT, @@ -393,294 +405,306 @@ var _ObservationIDNameToValueMap = map[string]ObservationID{ _ObservationIDLowerName[352:365]: SITE_IDSTRING, _ObservationIDName[365:379]: SITE_IDNUMERIC, _ObservationIDLowerName[365:379]: SITE_IDNUMERIC, - _ObservationIDName[379:401]: LOCK_CABLE_PERMANENTLY, - _ObservationIDLowerName[379:401]: LOCK_CABLE_PERMANENTLY, - _ObservationIDName[401:411]: IS_ENABLED, - _ObservationIDLowerName[401:411]: IS_ENABLED, - _ObservationIDName[411:434]: CIRCUIT_SEQUENCE_NUMBER, - _ObservationIDLowerName[411:434]: CIRCUIT_SEQUENCE_NUMBER, - _ObservationIDName[434:453]: SINGLE_PHASE_NUMBER, - _ObservationIDLowerName[434:453]: SINGLE_PHASE_NUMBER, - _ObservationIDName[453:478]: ENABLE3_PHASES_DEPRECATED, - _ObservationIDLowerName[453:478]: ENABLE3_PHASES_DEPRECATED, - _ObservationIDName[478:488]: WI_FI_SSID, - _ObservationIDLowerName[478:488]: WI_FI_SSID, - _ObservationIDName[488:507]: ENABLE_IDLE_CURRENT, - _ObservationIDLowerName[488:507]: ENABLE_IDLE_CURRENT, - _ObservationIDName[507:517]: PHASE_MODE, - _ObservationIDLowerName[507:517]: PHASE_MODE, - _ObservationIDName[517:555]: FORCED_THREE_PHASE_ON_ITWITH_GND_FAULT, - _ObservationIDLowerName[517:555]: FORCED_THREE_PHASE_ON_ITWITH_GND_FAULT, - _ObservationIDName[555:575]: LED_STRIP_BRIGHTNESS, - _ObservationIDLowerName[555:575]: LED_STRIP_BRIGHTNESS, - _ObservationIDName[575:603]: LOCAL_AUTHORIZATION_REQUIRED, - _ObservationIDLowerName[575:603]: LOCAL_AUTHORIZATION_REQUIRED, - _ObservationIDName[603:625]: AUTHORIZATION_REQUIRED, - _ObservationIDLowerName[603:625]: AUTHORIZATION_REQUIRED, - _ObservationIDName[625:646]: REMOTE_START_REQUIRED, - _ObservationIDLowerName[625:646]: REMOTE_START_REQUIRED, - _ObservationIDName[646:666]: SMART_BUTTON_ENABLED, - _ObservationIDLowerName[646:666]: SMART_BUTTON_ENABLED, - _ObservationIDName[666:687]: OFFLINE_CHARGING_MODE, - _ObservationIDLowerName[666:687]: OFFLINE_CHARGING_MODE, - _ObservationIDName[687:694]: LEDMODE, - _ObservationIDLowerName[687:694]: LEDMODE, - _ObservationIDName[694:713]: MAX_CHARGER_CURRENT, - _ObservationIDLowerName[694:713]: MAX_CHARGER_CURRENT, - _ObservationIDName[713:736]: DYNAMIC_CHARGER_CURRENT, - _ObservationIDLowerName[713:736]: DYNAMIC_CHARGER_CURRENT, - _ObservationIDName[736:767]: MAX_CURRENT_OFFLINE_FALLBACK_P1, - _ObservationIDLowerName[736:767]: MAX_CURRENT_OFFLINE_FALLBACK_P1, - _ObservationIDName[767:798]: MAX_CURRENT_OFFLINE_FALLBACK_P2, - _ObservationIDLowerName[767:798]: MAX_CURRENT_OFFLINE_FALLBACK_P2, - _ObservationIDName[798:829]: MAX_CURRENT_OFFLINE_FALLBACK_P3, - _ObservationIDLowerName[798:829]: MAX_CURRENT_OFFLINE_FALLBACK_P3, - _ObservationIDName[829:852]: LISTEN_TO_CONTROL_PULSE, - _ObservationIDLowerName[829:852]: LISTEN_TO_CONTROL_PULSE, - _ObservationIDName[852:869]: CONTROL_PULSE_RTT, - _ObservationIDLowerName[852:869]: CONTROL_PULSE_RTT, - _ObservationIDName[869:886]: CHARGING_SCHEDULE, - _ObservationIDLowerName[869:886]: CHARGING_SCHEDULE, - _ObservationIDName[886:902]: PAIRED_EQUALIZER, - _ObservationIDLowerName[886:902]: PAIRED_EQUALIZER, - _ObservationIDName[902:917]: WI_FI_APENABLED, - _ObservationIDLowerName[902:917]: WI_FI_APENABLED, - _ObservationIDName[917:936]: PAIRED_USER_IDTOKEN, - _ObservationIDLowerName[917:936]: PAIRED_USER_IDTOKEN, - _ObservationIDName[936:986]: CIRCUIT_TOTAL_ALLOCATED_PHASE_CONDUCTOR_CURRENT_L1, - _ObservationIDLowerName[936:986]: CIRCUIT_TOTAL_ALLOCATED_PHASE_CONDUCTOR_CURRENT_L1, - _ObservationIDName[986:1036]: CIRCUIT_TOTAL_ALLOCATED_PHASE_CONDUCTOR_CURRENT_L2, - _ObservationIDLowerName[986:1036]: CIRCUIT_TOTAL_ALLOCATED_PHASE_CONDUCTOR_CURRENT_L2, - _ObservationIDName[1036:1086]: CIRCUIT_TOTAL_ALLOCATED_PHASE_CONDUCTOR_CURRENT_L3, - _ObservationIDLowerName[1036:1086]: CIRCUIT_TOTAL_ALLOCATED_PHASE_CONDUCTOR_CURRENT_L3, - _ObservationIDName[1086:1126]: CIRCUIT_TOTAL_PHASE_CONDUCTOR_CURRENT_L1, - _ObservationIDLowerName[1086:1126]: CIRCUIT_TOTAL_PHASE_CONDUCTOR_CURRENT_L1, - _ObservationIDName[1126:1166]: CIRCUIT_TOTAL_PHASE_CONDUCTOR_CURRENT_L2, - _ObservationIDLowerName[1126:1166]: CIRCUIT_TOTAL_PHASE_CONDUCTOR_CURRENT_L2, - _ObservationIDName[1166:1206]: CIRCUIT_TOTAL_PHASE_CONDUCTOR_CURRENT_L3, - _ObservationIDLowerName[1166:1206]: CIRCUIT_TOTAL_PHASE_CONDUCTOR_CURRENT_L3, - _ObservationIDName[1206:1230]: NUMBER_OF_CARS_CONNECTED, - _ObservationIDLowerName[1206:1230]: NUMBER_OF_CARS_CONNECTED, - _ObservationIDName[1230:1253]: NUMBER_OF_CARS_CHARGING, - _ObservationIDLowerName[1230:1253]: NUMBER_OF_CARS_CHARGING, - _ObservationIDName[1253:1276]: NUMBER_OF_CARS_IN_QUEUE, - _ObservationIDLowerName[1253:1276]: NUMBER_OF_CARS_IN_QUEUE, - _ObservationIDName[1276:1304]: NUMBER_OF_CARS_FULLY_CHARGED, - _ObservationIDLowerName[1276:1304]: NUMBER_OF_CARS_FULLY_CHARGED, - _ObservationIDName[1304:1320]: SOFTWARE_RELEASE, - _ObservationIDLowerName[1304:1320]: SOFTWARE_RELEASE, - _ObservationIDName[1320:1325]: ICCID, - _ObservationIDLowerName[1320:1325]: ICCID, - _ObservationIDName[1325:1336]: MODEM_FW_ID, - _ObservationIDLowerName[1325:1336]: MODEM_FW_ID, - _ObservationIDName[1336:1349]: OTAERROR_CODE, - _ObservationIDLowerName[1336:1349]: OTAERROR_CODE, - _ObservationIDName[1349:1372]: MOBILE_NETWORK_OPERATOR, - _ObservationIDLowerName[1349:1372]: MOBILE_NETWORK_OPERATOR, - _ObservationIDName[1372:1385]: REBOOT_REASON, - _ObservationIDLowerName[1372:1385]: REBOOT_REASON, - _ObservationIDName[1385:1401]: POWER_PCBVERSION, - _ObservationIDLowerName[1385:1401]: POWER_PCBVERSION, - _ObservationIDName[1401:1415]: COM_PCBVERSION, - _ObservationIDLowerName[1401:1415]: COM_PCBVERSION, - _ObservationIDName[1415:1436]: REASON_FOR_NO_CURRENT, - _ObservationIDLowerName[1415:1436]: REASON_FOR_NO_CURRENT, - _ObservationIDName[1436:1479]: LOAD_BALANCING_NUMBER_OF_CONNECTED_CHARGERS, - _ObservationIDLowerName[1436:1479]: LOAD_BALANCING_NUMBER_OF_CONNECTED_CHARGERS, - _ObservationIDName[1479:1504]: UDPNUM_OF_CONNECTED_NODES, - _ObservationIDLowerName[1479:1504]: UDPNUM_OF_CONNECTED_NODES, - _ObservationIDName[1504:1520]: LOCAL_CONNECTION, - _ObservationIDLowerName[1504:1520]: LOCAL_CONNECTION, - _ObservationIDName[1520:1530]: PILOT_MODE, - _ObservationIDLowerName[1520:1530]: PILOT_MODE, - _ObservationIDName[1530:1554]: CAR_CONNECTED_DEPRECATED, - _ObservationIDLowerName[1530:1554]: CAR_CONNECTED_DEPRECATED, - _ObservationIDName[1554:1568]: SMART_CHARGING, - _ObservationIDLowerName[1554:1568]: SMART_CHARGING, - _ObservationIDName[1568:1580]: CABLE_LOCKED, - _ObservationIDLowerName[1568:1580]: CABLE_LOCKED, - _ObservationIDName[1580:1592]: CABLE_RATING, - _ObservationIDLowerName[1580:1592]: CABLE_RATING, - _ObservationIDName[1592:1602]: PILOT_HIGH, - _ObservationIDLowerName[1592:1602]: PILOT_HIGH, - _ObservationIDName[1602:1611]: PILOT_LOW, - _ObservationIDLowerName[1602:1611]: PILOT_LOW, - _ObservationIDName[1611:1624]: BACK_PLATE_ID, - _ObservationIDLowerName[1611:1624]: BACK_PLATE_ID, - _ObservationIDName[1624:1645]: USER_IDTOKEN_REVERSED, - _ObservationIDLowerName[1624:1645]: USER_IDTOKEN_REVERSED, - _ObservationIDName[1645:1660]: CHARGER_OP_MODE, - _ObservationIDLowerName[1645:1660]: CHARGER_OP_MODE, - _ObservationIDName[1660:1672]: OUTPUT_PHASE, - _ObservationIDLowerName[1660:1672]: OUTPUT_PHASE, - _ObservationIDName[1672:1698]: DYNAMIC_CIRCUIT_CURRENT_P1, - _ObservationIDLowerName[1672:1698]: DYNAMIC_CIRCUIT_CURRENT_P1, - _ObservationIDName[1698:1724]: DYNAMIC_CIRCUIT_CURRENT_P2, - _ObservationIDLowerName[1698:1724]: DYNAMIC_CIRCUIT_CURRENT_P2, - _ObservationIDName[1724:1750]: DYNAMIC_CIRCUIT_CURRENT_P3, - _ObservationIDLowerName[1724:1750]: DYNAMIC_CIRCUIT_CURRENT_P3, - _ObservationIDName[1750:1764]: OUTPUT_CURRENT, - _ObservationIDLowerName[1750:1764]: OUTPUT_CURRENT, - _ObservationIDName[1764:1779]: DERATED_CURRENT, - _ObservationIDLowerName[1764:1779]: DERATED_CURRENT, - _ObservationIDName[1779:1794]: DERATING_ACTIVE, - _ObservationIDLowerName[1779:1794]: DERATING_ACTIVE, - _ObservationIDName[1794:1806]: DEBUG_STRING, - _ObservationIDLowerName[1794:1806]: DEBUG_STRING, - _ObservationIDName[1806:1818]: ERROR_STRING, - _ObservationIDLowerName[1806:1818]: ERROR_STRING, - _ObservationIDName[1818:1828]: ERROR_CODE, - _ObservationIDLowerName[1818:1828]: ERROR_CODE, - _ObservationIDName[1828:1839]: TOTAL_POWER, - _ObservationIDLowerName[1828:1839]: TOTAL_POWER, - _ObservationIDName[1839:1853]: SESSION_ENERGY, - _ObservationIDLowerName[1839:1853]: SESSION_ENERGY, - _ObservationIDName[1853:1868]: ENERGY_PER_HOUR, - _ObservationIDLowerName[1853:1868]: ENERGY_PER_HOUR, - _ObservationIDName[1868:1884]: LEGACY_EV_STATUS, - _ObservationIDLowerName[1868:1884]: LEGACY_EV_STATUS, - _ObservationIDName[1884:1899]: LIFETIME_ENERGY, - _ObservationIDLowerName[1884:1899]: LIFETIME_ENERGY, - _ObservationIDName[1899:1922]: LIFETIME_RELAY_SWITCHES, - _ObservationIDLowerName[1899:1922]: LIFETIME_RELAY_SWITCHES, - _ObservationIDName[1922:1936]: LIFETIME_HOURS, - _ObservationIDLowerName[1922:1936]: LIFETIME_HOURS, - _ObservationIDName[1936:1979]: DYNAMIC_CURRENT_OFFLINE_FALLBACK_DEPRICATED, - _ObservationIDLowerName[1936:1979]: DYNAMIC_CURRENT_OFFLINE_FALLBACK_DEPRICATED, - _ObservationIDName[1979:1991]: USER_IDTOKEN, - _ObservationIDLowerName[1979:1991]: USER_IDTOKEN, - _ObservationIDName[1991:2007]: CHARGING_SESSION, - _ObservationIDLowerName[1991:2007]: CHARGING_SESSION, - _ObservationIDName[2007:2016]: CELL_RSSI, - _ObservationIDLowerName[2007:2016]: CELL_RSSI, - _ObservationIDName[2016:2024]: CELL_RAT, - _ObservationIDLowerName[2016:2024]: CELL_RAT, - _ObservationIDName[2024:2034]: WI_FI_RSSI, - _ObservationIDLowerName[2024:2034]: WI_FI_RSSI, - _ObservationIDName[2034:2046]: CELL_ADDRESS, - _ObservationIDLowerName[2034:2046]: CELL_ADDRESS, - _ObservationIDName[2046:2059]: WI_FI_ADDRESS, - _ObservationIDLowerName[2046:2059]: WI_FI_ADDRESS, - _ObservationIDName[2059:2069]: WI_FI_TYPE, - _ObservationIDLowerName[2059:2069]: WI_FI_TYPE, - _ObservationIDName[2069:2079]: LOCAL_RSSI, - _ObservationIDLowerName[2069:2079]: LOCAL_RSSI, - _ObservationIDName[2079:2099]: MASTER_BACK_PLATE_ID, - _ObservationIDLowerName[2079:2099]: MASTER_BACK_PLATE_ID, - _ObservationIDName[2099:2113]: LOCAL_TX_POWER, - _ObservationIDLowerName[2099:2113]: LOCAL_TX_POWER, - _ObservationIDName[2113:2124]: LOCAL_STATE, - _ObservationIDLowerName[2113:2124]: LOCAL_STATE, - _ObservationIDName[2124:2135]: FOUND_WI_FI, - _ObservationIDLowerName[2124:2135]: FOUND_WI_FI, - _ObservationIDName[2135:2146]: CHARGER_RAT, - _ObservationIDLowerName[2135:2146]: CHARGER_RAT, - _ObservationIDName[2146:2176]: CELLULAR_INTERFACE_ERROR_COUNT, - _ObservationIDLowerName[2146:2176]: CELLULAR_INTERFACE_ERROR_COUNT, - _ObservationIDName[2176:2206]: CELLULAR_INTERFACE_RESET_COUNT, - _ObservationIDLowerName[2176:2206]: CELLULAR_INTERFACE_RESET_COUNT, - _ObservationIDName[2206:2232]: WIFI_INTERFACE_ERROR_COUNT, - _ObservationIDLowerName[2206:2232]: WIFI_INTERFACE_ERROR_COUNT, - _ObservationIDName[2232:2258]: WIFI_INTERFACE_RESET_COUNT, - _ObservationIDLowerName[2232:2258]: WIFI_INTERFACE_RESET_COUNT, - _ObservationIDName[2258:2273]: LOCAL_NODE_TYPE, - _ObservationIDLowerName[2258:2273]: LOCAL_NODE_TYPE, - _ObservationIDName[2273:2292]: LOCAL_RADIO_CHANNEL, - _ObservationIDLowerName[2273:2292]: LOCAL_RADIO_CHANNEL, - _ObservationIDName[2292:2311]: LOCAL_SHORT_ADDRESS, - _ObservationIDLowerName[2292:2311]: LOCAL_SHORT_ADDRESS, - _ObservationIDName[2311:2344]: LOCAL_PARENT_ADDR_OR_NUM_OF_NODES, - _ObservationIDLowerName[2311:2344]: LOCAL_PARENT_ADDR_OR_NUM_OF_NODES, - _ObservationIDName[2344:2352]: TEMP_MAX, - _ObservationIDLowerName[2344:2352]: TEMP_MAX, - _ObservationIDName[2352:2376]: TEMP_AMBIENT_POWER_BOARD, - _ObservationIDLowerName[2352:2376]: TEMP_AMBIENT_POWER_BOARD, - _ObservationIDName[2376:2389]: TEMP_INPUT_T2, - _ObservationIDLowerName[2376:2389]: TEMP_INPUT_T2, - _ObservationIDName[2389:2402]: TEMP_INPUT_T3, - _ObservationIDLowerName[2389:2402]: TEMP_INPUT_T3, - _ObservationIDName[2402:2415]: TEMP_INPUT_T4, - _ObservationIDLowerName[2402:2415]: TEMP_INPUT_T4, - _ObservationIDName[2415:2428]: TEMP_INPUT_T5, - _ObservationIDLowerName[2415:2428]: TEMP_INPUT_T5, - _ObservationIDName[2428:2441]: TEMP_OUTPUT_N, - _ObservationIDLowerName[2428:2441]: TEMP_OUTPUT_N, - _ObservationIDName[2441:2455]: TEMP_OUTPUT_L1, - _ObservationIDLowerName[2441:2455]: TEMP_OUTPUT_L1, - _ObservationIDName[2455:2469]: TEMP_OUTPUT_L2, - _ObservationIDLowerName[2455:2469]: TEMP_OUTPUT_L2, - _ObservationIDName[2469:2483]: TEMP_OUTPUT_L3, - _ObservationIDLowerName[2469:2483]: TEMP_OUTPUT_L3, - _ObservationIDName[2483:2495]: TEMP_AMBIENT, - _ObservationIDLowerName[2483:2495]: TEMP_AMBIENT, - _ObservationIDName[2495:2508]: LIGHT_AMBIENT, - _ObservationIDLowerName[2495:2508]: LIGHT_AMBIENT, - _ObservationIDName[2508:2524]: INT_REL_HUMIDITY, - _ObservationIDLowerName[2508:2524]: INT_REL_HUMIDITY, - _ObservationIDName[2524:2541]: BACK_PLATE_LOCKED, - _ObservationIDLowerName[2524:2541]: BACK_PLATE_LOCKED, - _ObservationIDName[2541:2554]: CURRENT_MOTOR, - _ObservationIDLowerName[2541:2554]: CURRENT_MOTOR, - _ObservationIDName[2554:2576]: BACK_PLATE_HALL_SENSOR, - _ObservationIDLowerName[2554:2576]: BACK_PLATE_HALL_SENSOR, - _ObservationIDName[2576:2589]: IN_CURRENT_T2, - _ObservationIDLowerName[2576:2589]: IN_CURRENT_T2, - _ObservationIDName[2589:2602]: IN_CURRENT_T3, - _ObservationIDLowerName[2589:2602]: IN_CURRENT_T3, - _ObservationIDName[2602:2615]: IN_CURRENT_T4, - _ObservationIDLowerName[2602:2615]: IN_CURRENT_T4, - _ObservationIDName[2615:2628]: IN_CURRENT_T5, - _ObservationIDLowerName[2615:2628]: IN_CURRENT_T5, - _ObservationIDName[2628:2641]: IN_VOLT_T1_T2, - _ObservationIDLowerName[2628:2641]: IN_VOLT_T1_T2, - _ObservationIDName[2641:2654]: IN_VOLT_T1_T3, - _ObservationIDLowerName[2641:2654]: IN_VOLT_T1_T3, - _ObservationIDName[2654:2667]: IN_VOLT_T1_T4, - _ObservationIDLowerName[2654:2667]: IN_VOLT_T1_T4, - _ObservationIDName[2667:2680]: IN_VOLT_T1_T5, - _ObservationIDLowerName[2667:2680]: IN_VOLT_T1_T5, - _ObservationIDName[2680:2693]: IN_VOLT_T2_T3, - _ObservationIDLowerName[2680:2693]: IN_VOLT_T2_T3, - _ObservationIDName[2693:2706]: IN_VOLT_T2_T4, - _ObservationIDLowerName[2693:2706]: IN_VOLT_T2_T4, - _ObservationIDName[2706:2719]: IN_VOLT_T2_T5, - _ObservationIDLowerName[2706:2719]: IN_VOLT_T2_T5, - _ObservationIDName[2719:2732]: IN_VOLT_T3_T4, - _ObservationIDLowerName[2719:2732]: IN_VOLT_T3_T4, - _ObservationIDName[2732:2745]: IN_VOLT_T3_T5, - _ObservationIDLowerName[2732:2745]: IN_VOLT_T3_T5, - _ObservationIDName[2745:2758]: IN_VOLT_T4_T5, - _ObservationIDLowerName[2745:2758]: IN_VOLT_T4_T5, - _ObservationIDName[2758:2773]: OUT_VOLT_PIN1_2, - _ObservationIDLowerName[2758:2773]: OUT_VOLT_PIN1_2, - _ObservationIDName[2773:2788]: OUT_VOLT_PIN1_3, - _ObservationIDLowerName[2773:2788]: OUT_VOLT_PIN1_3, - _ObservationIDName[2788:2803]: OUT_VOLT_PIN1_4, - _ObservationIDLowerName[2788:2803]: OUT_VOLT_PIN1_4, - _ObservationIDName[2803:2818]: OUT_VOLT_PIN1_5, - _ObservationIDLowerName[2803:2818]: OUT_VOLT_PIN1_5, - _ObservationIDName[2818:2830]: VOLT_LEVEL33, - _ObservationIDLowerName[2818:2830]: VOLT_LEVEL33, - _ObservationIDName[2830:2841]: VOLT_LEVEL5, - _ObservationIDLowerName[2830:2841]: VOLT_LEVEL5, - _ObservationIDName[2841:2853]: VOLT_LEVEL12, - _ObservationIDLowerName[2841:2853]: VOLT_LEVEL12, - _ObservationIDName[2853:2861]: LTE_RSRP, - _ObservationIDLowerName[2853:2861]: LTE_RSRP, - _ObservationIDName[2861:2869]: LTE_SINR, - _ObservationIDLowerName[2861:2869]: LTE_SINR, - _ObservationIDName[2869:2877]: LTE_RSRQ, - _ObservationIDLowerName[2869:2877]: LTE_RSRQ, - _ObservationIDName[2877:2900]: EQ_AVAILABLE_CURRENT_P1, - _ObservationIDLowerName[2877:2900]: EQ_AVAILABLE_CURRENT_P1, - _ObservationIDName[2900:2923]: EQ_AVAILABLE_CURRENT_P2, - _ObservationIDLowerName[2900:2923]: EQ_AVAILABLE_CURRENT_P2, - _ObservationIDName[2923:2946]: EQ_AVAILABLE_CURRENT_P3, - _ObservationIDLowerName[2923:2946]: EQ_AVAILABLE_CURRENT_P3, - _ObservationIDName[2946:2964]: CONNECTED_TO_CLOUD, - _ObservationIDLowerName[2946:2964]: CONNECTED_TO_CLOUD, - _ObservationIDName[2964:2987]: CLOUD_DISCONNECT_REASON, - _ObservationIDLowerName[2964:2987]: CLOUD_DISCONNECT_REASON, + _ObservationIDName[379:396]: RFID_TIMEOUT_AUTH, + _ObservationIDLowerName[379:396]: RFID_TIMEOUT_AUTH, + _ObservationIDName[396:418]: LOCK_CABLE_PERMANENTLY, + _ObservationIDLowerName[396:418]: LOCK_CABLE_PERMANENTLY, + _ObservationIDName[418:428]: IS_ENABLED, + _ObservationIDLowerName[418:428]: IS_ENABLED, + _ObservationIDName[428:453]: TEMPERATURE_MONITOR_STATE, + _ObservationIDLowerName[428:453]: TEMPERATURE_MONITOR_STATE, + _ObservationIDName[453:476]: CIRCUIT_SEQUENCE_NUMBER, + _ObservationIDLowerName[453:476]: CIRCUIT_SEQUENCE_NUMBER, + _ObservationIDName[476:495]: SINGLE_PHASE_NUMBER, + _ObservationIDLowerName[476:495]: SINGLE_PHASE_NUMBER, + _ObservationIDName[495:520]: ENABLE3_PHASES_DEPRECATED, + _ObservationIDLowerName[495:520]: ENABLE3_PHASES_DEPRECATED, + _ObservationIDName[520:530]: WI_FI_SSID, + _ObservationIDLowerName[520:530]: WI_FI_SSID, + _ObservationIDName[530:549]: ENABLE_IDLE_CURRENT, + _ObservationIDLowerName[530:549]: ENABLE_IDLE_CURRENT, + _ObservationIDName[549:559]: PHASE_MODE, + _ObservationIDLowerName[549:559]: PHASE_MODE, + _ObservationIDName[559:597]: FORCED_THREE_PHASE_ON_ITWITH_GND_FAULT, + _ObservationIDLowerName[559:597]: FORCED_THREE_PHASE_ON_ITWITH_GND_FAULT, + _ObservationIDName[597:617]: LED_STRIP_BRIGHTNESS, + _ObservationIDLowerName[597:617]: LED_STRIP_BRIGHTNESS, + _ObservationIDName[617:645]: LOCAL_AUTHORIZATION_REQUIRED, + _ObservationIDLowerName[617:645]: LOCAL_AUTHORIZATION_REQUIRED, + _ObservationIDName[645:667]: AUTHORIZATION_REQUIRED, + _ObservationIDLowerName[645:667]: AUTHORIZATION_REQUIRED, + _ObservationIDName[667:688]: REMOTE_START_REQUIRED, + _ObservationIDLowerName[667:688]: REMOTE_START_REQUIRED, + _ObservationIDName[688:708]: SMART_BUTTON_ENABLED, + _ObservationIDLowerName[688:708]: SMART_BUTTON_ENABLED, + _ObservationIDName[708:729]: OFFLINE_CHARGING_MODE, + _ObservationIDLowerName[708:729]: OFFLINE_CHARGING_MODE, + _ObservationIDName[729:736]: LEDMODE, + _ObservationIDLowerName[729:736]: LEDMODE, + _ObservationIDName[736:755]: MAX_CHARGER_CURRENT, + _ObservationIDLowerName[736:755]: MAX_CHARGER_CURRENT, + _ObservationIDName[755:778]: DYNAMIC_CHARGER_CURRENT, + _ObservationIDLowerName[755:778]: DYNAMIC_CHARGER_CURRENT, + _ObservationIDName[778:809]: MAX_CURRENT_OFFLINE_FALLBACK_P1, + _ObservationIDLowerName[778:809]: MAX_CURRENT_OFFLINE_FALLBACK_P1, + _ObservationIDName[809:840]: MAX_CURRENT_OFFLINE_FALLBACK_P2, + _ObservationIDLowerName[809:840]: MAX_CURRENT_OFFLINE_FALLBACK_P2, + _ObservationIDName[840:871]: MAX_CURRENT_OFFLINE_FALLBACK_P3, + _ObservationIDLowerName[840:871]: MAX_CURRENT_OFFLINE_FALLBACK_P3, + _ObservationIDName[871:897]: RELEASE_CABLE_AT_POWER_OFF, + _ObservationIDLowerName[871:897]: RELEASE_CABLE_AT_POWER_OFF, + _ObservationIDName[897:920]: LISTEN_TO_CONTROL_PULSE, + _ObservationIDLowerName[897:920]: LISTEN_TO_CONTROL_PULSE, + _ObservationIDName[920:937]: CONTROL_PULSE_RTT, + _ObservationIDLowerName[920:937]: CONTROL_PULSE_RTT, + _ObservationIDName[937:960]: CHARGING_SESSION_SIGNED, + _ObservationIDLowerName[937:960]: CHARGING_SESSION_SIGNED, + _ObservationIDName[960:977]: CHARGING_SCHEDULE, + _ObservationIDLowerName[960:977]: CHARGING_SCHEDULE, + _ObservationIDName[977:993]: PAIRED_EQUALIZER, + _ObservationIDLowerName[977:993]: PAIRED_EQUALIZER, + _ObservationIDName[993:1008]: WI_FI_APENABLED, + _ObservationIDLowerName[993:1008]: WI_FI_APENABLED, + _ObservationIDName[1008:1027]: PAIRED_USER_IDTOKEN, + _ObservationIDLowerName[1008:1027]: PAIRED_USER_IDTOKEN, + _ObservationIDName[1027:1077]: CIRCUIT_TOTAL_ALLOCATED_PHASE_CONDUCTOR_CURRENT_L1, + _ObservationIDLowerName[1027:1077]: CIRCUIT_TOTAL_ALLOCATED_PHASE_CONDUCTOR_CURRENT_L1, + _ObservationIDName[1077:1127]: CIRCUIT_TOTAL_ALLOCATED_PHASE_CONDUCTOR_CURRENT_L2, + _ObservationIDLowerName[1077:1127]: CIRCUIT_TOTAL_ALLOCATED_PHASE_CONDUCTOR_CURRENT_L2, + _ObservationIDName[1127:1177]: CIRCUIT_TOTAL_ALLOCATED_PHASE_CONDUCTOR_CURRENT_L3, + _ObservationIDLowerName[1127:1177]: CIRCUIT_TOTAL_ALLOCATED_PHASE_CONDUCTOR_CURRENT_L3, + _ObservationIDName[1177:1217]: CIRCUIT_TOTAL_PHASE_CONDUCTOR_CURRENT_L1, + _ObservationIDLowerName[1177:1217]: CIRCUIT_TOTAL_PHASE_CONDUCTOR_CURRENT_L1, + _ObservationIDName[1217:1257]: CIRCUIT_TOTAL_PHASE_CONDUCTOR_CURRENT_L2, + _ObservationIDLowerName[1217:1257]: CIRCUIT_TOTAL_PHASE_CONDUCTOR_CURRENT_L2, + _ObservationIDName[1257:1297]: CIRCUIT_TOTAL_PHASE_CONDUCTOR_CURRENT_L3, + _ObservationIDLowerName[1257:1297]: CIRCUIT_TOTAL_PHASE_CONDUCTOR_CURRENT_L3, + _ObservationIDName[1297:1321]: NUMBER_OF_CARS_CONNECTED, + _ObservationIDLowerName[1297:1321]: NUMBER_OF_CARS_CONNECTED, + _ObservationIDName[1321:1344]: NUMBER_OF_CARS_CHARGING, + _ObservationIDLowerName[1321:1344]: NUMBER_OF_CARS_CHARGING, + _ObservationIDName[1344:1367]: NUMBER_OF_CARS_IN_QUEUE, + _ObservationIDLowerName[1344:1367]: NUMBER_OF_CARS_IN_QUEUE, + _ObservationIDName[1367:1395]: NUMBER_OF_CARS_FULLY_CHARGED, + _ObservationIDLowerName[1367:1395]: NUMBER_OF_CARS_FULLY_CHARGED, + _ObservationIDName[1395:1411]: SOFTWARE_RELEASE, + _ObservationIDLowerName[1395:1411]: SOFTWARE_RELEASE, + _ObservationIDName[1411:1416]: ICCID, + _ObservationIDLowerName[1411:1416]: ICCID, + _ObservationIDName[1416:1427]: MODEM_FW_ID, + _ObservationIDLowerName[1416:1427]: MODEM_FW_ID, + _ObservationIDName[1427:1440]: OTAERROR_CODE, + _ObservationIDLowerName[1427:1440]: OTAERROR_CODE, + _ObservationIDName[1440:1463]: MOBILE_NETWORK_OPERATOR, + _ObservationIDLowerName[1440:1463]: MOBILE_NETWORK_OPERATOR, + _ObservationIDName[1463:1476]: REBOOT_REASON, + _ObservationIDLowerName[1463:1476]: REBOOT_REASON, + _ObservationIDName[1476:1492]: POWER_PCBVERSION, + _ObservationIDLowerName[1476:1492]: POWER_PCBVERSION, + _ObservationIDName[1492:1506]: COM_PCBVERSION, + _ObservationIDLowerName[1492:1506]: COM_PCBVERSION, + _ObservationIDName[1506:1527]: REASON_FOR_NO_CURRENT, + _ObservationIDLowerName[1506:1527]: REASON_FOR_NO_CURRENT, + _ObservationIDName[1527:1570]: LOAD_BALANCING_NUMBER_OF_CONNECTED_CHARGERS, + _ObservationIDLowerName[1527:1570]: LOAD_BALANCING_NUMBER_OF_CONNECTED_CHARGERS, + _ObservationIDName[1570:1595]: UDPNUM_OF_CONNECTED_NODES, + _ObservationIDLowerName[1570:1595]: UDPNUM_OF_CONNECTED_NODES, + _ObservationIDName[1595:1611]: LOCAL_CONNECTION, + _ObservationIDLowerName[1595:1611]: LOCAL_CONNECTION, + _ObservationIDName[1611:1621]: PILOT_MODE, + _ObservationIDLowerName[1611:1621]: PILOT_MODE, + _ObservationIDName[1621:1645]: CAR_CONNECTED_DEPRECATED, + _ObservationIDLowerName[1621:1645]: CAR_CONNECTED_DEPRECATED, + _ObservationIDName[1645:1659]: SMART_CHARGING, + _ObservationIDLowerName[1645:1659]: SMART_CHARGING, + _ObservationIDName[1659:1671]: CABLE_LOCKED, + _ObservationIDLowerName[1659:1671]: CABLE_LOCKED, + _ObservationIDName[1671:1683]: CABLE_RATING, + _ObservationIDLowerName[1671:1683]: CABLE_RATING, + _ObservationIDName[1683:1693]: PILOT_HIGH, + _ObservationIDLowerName[1683:1693]: PILOT_HIGH, + _ObservationIDName[1693:1702]: PILOT_LOW, + _ObservationIDLowerName[1693:1702]: PILOT_LOW, + _ObservationIDName[1702:1715]: BACK_PLATE_ID, + _ObservationIDLowerName[1702:1715]: BACK_PLATE_ID, + _ObservationIDName[1715:1736]: USER_IDTOKEN_REVERSED, + _ObservationIDLowerName[1715:1736]: USER_IDTOKEN_REVERSED, + _ObservationIDName[1736:1751]: CHARGER_OP_MODE, + _ObservationIDLowerName[1736:1751]: CHARGER_OP_MODE, + _ObservationIDName[1751:1763]: OUTPUT_PHASE, + _ObservationIDLowerName[1751:1763]: OUTPUT_PHASE, + _ObservationIDName[1763:1789]: DYNAMIC_CIRCUIT_CURRENT_P1, + _ObservationIDLowerName[1763:1789]: DYNAMIC_CIRCUIT_CURRENT_P1, + _ObservationIDName[1789:1815]: DYNAMIC_CIRCUIT_CURRENT_P2, + _ObservationIDLowerName[1789:1815]: DYNAMIC_CIRCUIT_CURRENT_P2, + _ObservationIDName[1815:1841]: DYNAMIC_CIRCUIT_CURRENT_P3, + _ObservationIDLowerName[1815:1841]: DYNAMIC_CIRCUIT_CURRENT_P3, + _ObservationIDName[1841:1855]: OUTPUT_CURRENT, + _ObservationIDLowerName[1841:1855]: OUTPUT_CURRENT, + _ObservationIDName[1855:1870]: DERATED_CURRENT, + _ObservationIDLowerName[1855:1870]: DERATED_CURRENT, + _ObservationIDName[1870:1885]: DERATING_ACTIVE, + _ObservationIDLowerName[1870:1885]: DERATING_ACTIVE, + _ObservationIDName[1885:1897]: DEBUG_STRING, + _ObservationIDLowerName[1885:1897]: DEBUG_STRING, + _ObservationIDName[1897:1909]: ERROR_STRING, + _ObservationIDLowerName[1897:1909]: ERROR_STRING, + _ObservationIDName[1909:1919]: ERROR_CODE, + _ObservationIDLowerName[1909:1919]: ERROR_CODE, + _ObservationIDName[1919:1930]: TOTAL_POWER, + _ObservationIDLowerName[1919:1930]: TOTAL_POWER, + _ObservationIDName[1930:1944]: SESSION_ENERGY, + _ObservationIDLowerName[1930:1944]: SESSION_ENERGY, + _ObservationIDName[1944:1959]: ENERGY_PER_HOUR, + _ObservationIDLowerName[1944:1959]: ENERGY_PER_HOUR, + _ObservationIDName[1959:1975]: LEGACY_EV_STATUS, + _ObservationIDLowerName[1959:1975]: LEGACY_EV_STATUS, + _ObservationIDName[1975:1990]: LIFETIME_ENERGY, + _ObservationIDLowerName[1975:1990]: LIFETIME_ENERGY, + _ObservationIDName[1990:2013]: LIFETIME_RELAY_SWITCHES, + _ObservationIDLowerName[1990:2013]: LIFETIME_RELAY_SWITCHES, + _ObservationIDName[2013:2027]: LIFETIME_HOURS, + _ObservationIDLowerName[2013:2027]: LIFETIME_HOURS, + _ObservationIDName[2027:2070]: DYNAMIC_CURRENT_OFFLINE_FALLBACK_DEPRICATED, + _ObservationIDLowerName[2027:2070]: DYNAMIC_CURRENT_OFFLINE_FALLBACK_DEPRICATED, + _ObservationIDName[2070:2082]: USER_IDTOKEN, + _ObservationIDLowerName[2070:2082]: USER_IDTOKEN, + _ObservationIDName[2082:2098]: CHARGING_SESSION, + _ObservationIDLowerName[2082:2098]: CHARGING_SESSION, + _ObservationIDName[2098:2107]: CELL_RSSI, + _ObservationIDLowerName[2098:2107]: CELL_RSSI, + _ObservationIDName[2107:2115]: CELL_RAT, + _ObservationIDLowerName[2107:2115]: CELL_RAT, + _ObservationIDName[2115:2125]: WI_FI_RSSI, + _ObservationIDLowerName[2115:2125]: WI_FI_RSSI, + _ObservationIDName[2125:2137]: CELL_ADDRESS, + _ObservationIDLowerName[2125:2137]: CELL_ADDRESS, + _ObservationIDName[2137:2150]: WI_FI_ADDRESS, + _ObservationIDLowerName[2137:2150]: WI_FI_ADDRESS, + _ObservationIDName[2150:2160]: WI_FI_TYPE, + _ObservationIDLowerName[2150:2160]: WI_FI_TYPE, + _ObservationIDName[2160:2170]: LOCAL_RSSI, + _ObservationIDLowerName[2160:2170]: LOCAL_RSSI, + _ObservationIDName[2170:2190]: MASTER_BACK_PLATE_ID, + _ObservationIDLowerName[2170:2190]: MASTER_BACK_PLATE_ID, + _ObservationIDName[2190:2204]: LOCAL_TX_POWER, + _ObservationIDLowerName[2190:2204]: LOCAL_TX_POWER, + _ObservationIDName[2204:2215]: LOCAL_STATE, + _ObservationIDLowerName[2204:2215]: LOCAL_STATE, + _ObservationIDName[2215:2226]: FOUND_WI_FI, + _ObservationIDLowerName[2215:2226]: FOUND_WI_FI, + _ObservationIDName[2226:2244]: CURRENT_CONNECTION, + _ObservationIDLowerName[2226:2244]: CURRENT_CONNECTION, + _ObservationIDName[2244:2274]: CELLULAR_INTERFACE_ERROR_COUNT, + _ObservationIDLowerName[2244:2274]: CELLULAR_INTERFACE_ERROR_COUNT, + _ObservationIDName[2274:2304]: CELLULAR_INTERFACE_RESET_COUNT, + _ObservationIDLowerName[2274:2304]: CELLULAR_INTERFACE_RESET_COUNT, + _ObservationIDName[2304:2330]: WIFI_INTERFACE_ERROR_COUNT, + _ObservationIDLowerName[2304:2330]: WIFI_INTERFACE_ERROR_COUNT, + _ObservationIDName[2330:2356]: WIFI_INTERFACE_RESET_COUNT, + _ObservationIDLowerName[2330:2356]: WIFI_INTERFACE_RESET_COUNT, + _ObservationIDName[2356:2371]: LOCAL_NODE_TYPE, + _ObservationIDLowerName[2356:2371]: LOCAL_NODE_TYPE, + _ObservationIDName[2371:2390]: LOCAL_RADIO_CHANNEL, + _ObservationIDLowerName[2371:2390]: LOCAL_RADIO_CHANNEL, + _ObservationIDName[2390:2409]: LOCAL_SHORT_ADDRESS, + _ObservationIDLowerName[2390:2409]: LOCAL_SHORT_ADDRESS, + _ObservationIDName[2409:2442]: LOCAL_PARENT_ADDR_OR_NUM_OF_NODES, + _ObservationIDLowerName[2409:2442]: LOCAL_PARENT_ADDR_OR_NUM_OF_NODES, + _ObservationIDName[2442:2450]: TEMP_MAX, + _ObservationIDLowerName[2442:2450]: TEMP_MAX, + _ObservationIDName[2450:2474]: TEMP_AMBIENT_POWER_BOARD, + _ObservationIDLowerName[2450:2474]: TEMP_AMBIENT_POWER_BOARD, + _ObservationIDName[2474:2487]: TEMP_INPUT_T2, + _ObservationIDLowerName[2474:2487]: TEMP_INPUT_T2, + _ObservationIDName[2487:2500]: TEMP_INPUT_T3, + _ObservationIDLowerName[2487:2500]: TEMP_INPUT_T3, + _ObservationIDName[2500:2513]: TEMP_INPUT_T4, + _ObservationIDLowerName[2500:2513]: TEMP_INPUT_T4, + _ObservationIDName[2513:2526]: TEMP_INPUT_T5, + _ObservationIDLowerName[2513:2526]: TEMP_INPUT_T5, + _ObservationIDName[2526:2539]: TEMP_OUTPUT_N, + _ObservationIDLowerName[2526:2539]: TEMP_OUTPUT_N, + _ObservationIDName[2539:2553]: TEMP_OUTPUT_L1, + _ObservationIDLowerName[2539:2553]: TEMP_OUTPUT_L1, + _ObservationIDName[2553:2567]: TEMP_OUTPUT_L2, + _ObservationIDLowerName[2553:2567]: TEMP_OUTPUT_L2, + _ObservationIDName[2567:2581]: TEMP_OUTPUT_L3, + _ObservationIDLowerName[2567:2581]: TEMP_OUTPUT_L3, + _ObservationIDName[2581:2593]: TEMP_AMBIENT, + _ObservationIDLowerName[2581:2593]: TEMP_AMBIENT, + _ObservationIDName[2593:2606]: LIGHT_AMBIENT, + _ObservationIDLowerName[2593:2606]: LIGHT_AMBIENT, + _ObservationIDName[2606:2622]: INT_REL_HUMIDITY, + _ObservationIDLowerName[2606:2622]: INT_REL_HUMIDITY, + _ObservationIDName[2622:2639]: BACK_PLATE_LOCKED, + _ObservationIDLowerName[2622:2639]: BACK_PLATE_LOCKED, + _ObservationIDName[2639:2652]: CURRENT_MOTOR, + _ObservationIDLowerName[2639:2652]: CURRENT_MOTOR, + _ObservationIDName[2652:2674]: BACK_PLATE_HALL_SENSOR, + _ObservationIDLowerName[2652:2674]: BACK_PLATE_HALL_SENSOR, + _ObservationIDName[2674:2688]: INT_CURRENT_T2, + _ObservationIDLowerName[2674:2688]: INT_CURRENT_T2, + _ObservationIDName[2688:2702]: INT_CURRENT_T3, + _ObservationIDLowerName[2688:2702]: INT_CURRENT_T3, + _ObservationIDName[2702:2716]: INT_CURRENT_T4, + _ObservationIDLowerName[2702:2716]: INT_CURRENT_T4, + _ObservationIDName[2716:2730]: INT_CURRENT_T5, + _ObservationIDLowerName[2716:2730]: INT_CURRENT_T5, + _ObservationIDName[2730:2743]: IN_VOLT_T1_T2, + _ObservationIDLowerName[2730:2743]: IN_VOLT_T1_T2, + _ObservationIDName[2743:2756]: IN_VOLT_T1_T3, + _ObservationIDLowerName[2743:2756]: IN_VOLT_T1_T3, + _ObservationIDName[2756:2769]: IN_VOLT_T1_T4, + _ObservationIDLowerName[2756:2769]: IN_VOLT_T1_T4, + _ObservationIDName[2769:2782]: IN_VOLT_T1_T5, + _ObservationIDLowerName[2769:2782]: IN_VOLT_T1_T5, + _ObservationIDName[2782:2795]: IN_VOLT_T2_T3, + _ObservationIDLowerName[2782:2795]: IN_VOLT_T2_T3, + _ObservationIDName[2795:2808]: IN_VOLT_T2_T4, + _ObservationIDLowerName[2795:2808]: IN_VOLT_T2_T4, + _ObservationIDName[2808:2821]: IN_VOLT_T2_T5, + _ObservationIDLowerName[2808:2821]: IN_VOLT_T2_T5, + _ObservationIDName[2821:2834]: IN_VOLT_T3_T4, + _ObservationIDLowerName[2821:2834]: IN_VOLT_T3_T4, + _ObservationIDName[2834:2847]: IN_VOLT_T3_T5, + _ObservationIDLowerName[2834:2847]: IN_VOLT_T3_T5, + _ObservationIDName[2847:2860]: IN_VOLT_T4_T5, + _ObservationIDLowerName[2847:2860]: IN_VOLT_T4_T5, + _ObservationIDName[2860:2875]: OUT_VOLT_PIN1_2, + _ObservationIDLowerName[2860:2875]: OUT_VOLT_PIN1_2, + _ObservationIDName[2875:2890]: OUT_VOLT_PIN1_3, + _ObservationIDLowerName[2875:2890]: OUT_VOLT_PIN1_3, + _ObservationIDName[2890:2905]: OUT_VOLT_PIN1_4, + _ObservationIDLowerName[2890:2905]: OUT_VOLT_PIN1_4, + _ObservationIDName[2905:2920]: OUT_VOLT_PIN1_5, + _ObservationIDLowerName[2905:2920]: OUT_VOLT_PIN1_5, + _ObservationIDName[2920:2935]: OUT_VOLT_PIN2_3, + _ObservationIDLowerName[2920:2935]: OUT_VOLT_PIN2_3, + _ObservationIDName[2935:2947]: VOLT_LEVEL33, + _ObservationIDLowerName[2935:2947]: VOLT_LEVEL33, + _ObservationIDName[2947:2958]: VOLT_LEVEL5, + _ObservationIDLowerName[2947:2958]: VOLT_LEVEL5, + _ObservationIDName[2958:2970]: VOLT_LEVEL12, + _ObservationIDLowerName[2958:2970]: VOLT_LEVEL12, + _ObservationIDName[2970:2978]: LTE_RSRP, + _ObservationIDLowerName[2970:2978]: LTE_RSRP, + _ObservationIDName[2978:2986]: LTE_SINR, + _ObservationIDLowerName[2978:2986]: LTE_SINR, + _ObservationIDName[2986:2994]: LTE_RSRQ, + _ObservationIDLowerName[2986:2994]: LTE_RSRQ, + _ObservationIDName[2994:3014]: CHARGE_SESSION_START, + _ObservationIDLowerName[2994:3014]: CHARGE_SESSION_START, + _ObservationIDName[3014:3037]: EQ_AVAILABLE_CURRENT_P1, + _ObservationIDLowerName[3014:3037]: EQ_AVAILABLE_CURRENT_P1, + _ObservationIDName[3037:3060]: EQ_AVAILABLE_CURRENT_P2, + _ObservationIDLowerName[3037:3060]: EQ_AVAILABLE_CURRENT_P2, + _ObservationIDName[3060:3083]: EQ_AVAILABLE_CURRENT_P3, + _ObservationIDLowerName[3060:3083]: EQ_AVAILABLE_CURRENT_P3, + _ObservationIDName[3083:3101]: CONNECTED_TO_CLOUD, + _ObservationIDLowerName[3083:3101]: CONNECTED_TO_CLOUD, + _ObservationIDName[3101:3124]: CLOUD_DISCONNECT_REASON, + _ObservationIDLowerName[3101:3124]: CLOUD_DISCONNECT_REASON, } var _ObservationIDNames = []string{ @@ -703,150 +727,156 @@ var _ObservationIDNames = []string{ _ObservationIDName[344:352], _ObservationIDName[352:365], _ObservationIDName[365:379], - _ObservationIDName[379:401], - _ObservationIDName[401:411], - _ObservationIDName[411:434], - _ObservationIDName[434:453], - _ObservationIDName[453:478], - _ObservationIDName[478:488], - _ObservationIDName[488:507], - _ObservationIDName[507:517], - _ObservationIDName[517:555], - _ObservationIDName[555:575], - _ObservationIDName[575:603], - _ObservationIDName[603:625], - _ObservationIDName[625:646], - _ObservationIDName[646:666], - _ObservationIDName[666:687], - _ObservationIDName[687:694], - _ObservationIDName[694:713], - _ObservationIDName[713:736], - _ObservationIDName[736:767], - _ObservationIDName[767:798], - _ObservationIDName[798:829], - _ObservationIDName[829:852], - _ObservationIDName[852:869], - _ObservationIDName[869:886], - _ObservationIDName[886:902], - _ObservationIDName[902:917], - _ObservationIDName[917:936], - _ObservationIDName[936:986], - _ObservationIDName[986:1036], - _ObservationIDName[1036:1086], - _ObservationIDName[1086:1126], - _ObservationIDName[1126:1166], - _ObservationIDName[1166:1206], - _ObservationIDName[1206:1230], - _ObservationIDName[1230:1253], - _ObservationIDName[1253:1276], - _ObservationIDName[1276:1304], - _ObservationIDName[1304:1320], - _ObservationIDName[1320:1325], - _ObservationIDName[1325:1336], - _ObservationIDName[1336:1349], - _ObservationIDName[1349:1372], - _ObservationIDName[1372:1385], - _ObservationIDName[1385:1401], - _ObservationIDName[1401:1415], - _ObservationIDName[1415:1436], - _ObservationIDName[1436:1479], - _ObservationIDName[1479:1504], - _ObservationIDName[1504:1520], - _ObservationIDName[1520:1530], - _ObservationIDName[1530:1554], - _ObservationIDName[1554:1568], - _ObservationIDName[1568:1580], - _ObservationIDName[1580:1592], - _ObservationIDName[1592:1602], - _ObservationIDName[1602:1611], - _ObservationIDName[1611:1624], - _ObservationIDName[1624:1645], - _ObservationIDName[1645:1660], - _ObservationIDName[1660:1672], - _ObservationIDName[1672:1698], - _ObservationIDName[1698:1724], - _ObservationIDName[1724:1750], - _ObservationIDName[1750:1764], - _ObservationIDName[1764:1779], - _ObservationIDName[1779:1794], - _ObservationIDName[1794:1806], - _ObservationIDName[1806:1818], - _ObservationIDName[1818:1828], - _ObservationIDName[1828:1839], - _ObservationIDName[1839:1853], - _ObservationIDName[1853:1868], - _ObservationIDName[1868:1884], - _ObservationIDName[1884:1899], - _ObservationIDName[1899:1922], - _ObservationIDName[1922:1936], - _ObservationIDName[1936:1979], - _ObservationIDName[1979:1991], - _ObservationIDName[1991:2007], - _ObservationIDName[2007:2016], - _ObservationIDName[2016:2024], - _ObservationIDName[2024:2034], - _ObservationIDName[2034:2046], - _ObservationIDName[2046:2059], - _ObservationIDName[2059:2069], - _ObservationIDName[2069:2079], - _ObservationIDName[2079:2099], - _ObservationIDName[2099:2113], - _ObservationIDName[2113:2124], - _ObservationIDName[2124:2135], - _ObservationIDName[2135:2146], - _ObservationIDName[2146:2176], - _ObservationIDName[2176:2206], - _ObservationIDName[2206:2232], - _ObservationIDName[2232:2258], - _ObservationIDName[2258:2273], - _ObservationIDName[2273:2292], - _ObservationIDName[2292:2311], - _ObservationIDName[2311:2344], - _ObservationIDName[2344:2352], - _ObservationIDName[2352:2376], - _ObservationIDName[2376:2389], - _ObservationIDName[2389:2402], - _ObservationIDName[2402:2415], - _ObservationIDName[2415:2428], - _ObservationIDName[2428:2441], - _ObservationIDName[2441:2455], - _ObservationIDName[2455:2469], - _ObservationIDName[2469:2483], - _ObservationIDName[2483:2495], - _ObservationIDName[2495:2508], - _ObservationIDName[2508:2524], - _ObservationIDName[2524:2541], - _ObservationIDName[2541:2554], - _ObservationIDName[2554:2576], - _ObservationIDName[2576:2589], - _ObservationIDName[2589:2602], - _ObservationIDName[2602:2615], - _ObservationIDName[2615:2628], - _ObservationIDName[2628:2641], - _ObservationIDName[2641:2654], - _ObservationIDName[2654:2667], - _ObservationIDName[2667:2680], - _ObservationIDName[2680:2693], - _ObservationIDName[2693:2706], - _ObservationIDName[2706:2719], - _ObservationIDName[2719:2732], - _ObservationIDName[2732:2745], - _ObservationIDName[2745:2758], - _ObservationIDName[2758:2773], - _ObservationIDName[2773:2788], - _ObservationIDName[2788:2803], - _ObservationIDName[2803:2818], - _ObservationIDName[2818:2830], - _ObservationIDName[2830:2841], - _ObservationIDName[2841:2853], - _ObservationIDName[2853:2861], - _ObservationIDName[2861:2869], - _ObservationIDName[2869:2877], - _ObservationIDName[2877:2900], - _ObservationIDName[2900:2923], - _ObservationIDName[2923:2946], - _ObservationIDName[2946:2964], - _ObservationIDName[2964:2987], + _ObservationIDName[379:396], + _ObservationIDName[396:418], + _ObservationIDName[418:428], + _ObservationIDName[428:453], + _ObservationIDName[453:476], + _ObservationIDName[476:495], + _ObservationIDName[495:520], + _ObservationIDName[520:530], + _ObservationIDName[530:549], + _ObservationIDName[549:559], + _ObservationIDName[559:597], + _ObservationIDName[597:617], + _ObservationIDName[617:645], + _ObservationIDName[645:667], + _ObservationIDName[667:688], + _ObservationIDName[688:708], + _ObservationIDName[708:729], + _ObservationIDName[729:736], + _ObservationIDName[736:755], + _ObservationIDName[755:778], + _ObservationIDName[778:809], + _ObservationIDName[809:840], + _ObservationIDName[840:871], + _ObservationIDName[871:897], + _ObservationIDName[897:920], + _ObservationIDName[920:937], + _ObservationIDName[937:960], + _ObservationIDName[960:977], + _ObservationIDName[977:993], + _ObservationIDName[993:1008], + _ObservationIDName[1008:1027], + _ObservationIDName[1027:1077], + _ObservationIDName[1077:1127], + _ObservationIDName[1127:1177], + _ObservationIDName[1177:1217], + _ObservationIDName[1217:1257], + _ObservationIDName[1257:1297], + _ObservationIDName[1297:1321], + _ObservationIDName[1321:1344], + _ObservationIDName[1344:1367], + _ObservationIDName[1367:1395], + _ObservationIDName[1395:1411], + _ObservationIDName[1411:1416], + _ObservationIDName[1416:1427], + _ObservationIDName[1427:1440], + _ObservationIDName[1440:1463], + _ObservationIDName[1463:1476], + _ObservationIDName[1476:1492], + _ObservationIDName[1492:1506], + _ObservationIDName[1506:1527], + _ObservationIDName[1527:1570], + _ObservationIDName[1570:1595], + _ObservationIDName[1595:1611], + _ObservationIDName[1611:1621], + _ObservationIDName[1621:1645], + _ObservationIDName[1645:1659], + _ObservationIDName[1659:1671], + _ObservationIDName[1671:1683], + _ObservationIDName[1683:1693], + _ObservationIDName[1693:1702], + _ObservationIDName[1702:1715], + _ObservationIDName[1715:1736], + _ObservationIDName[1736:1751], + _ObservationIDName[1751:1763], + _ObservationIDName[1763:1789], + _ObservationIDName[1789:1815], + _ObservationIDName[1815:1841], + _ObservationIDName[1841:1855], + _ObservationIDName[1855:1870], + _ObservationIDName[1870:1885], + _ObservationIDName[1885:1897], + _ObservationIDName[1897:1909], + _ObservationIDName[1909:1919], + _ObservationIDName[1919:1930], + _ObservationIDName[1930:1944], + _ObservationIDName[1944:1959], + _ObservationIDName[1959:1975], + _ObservationIDName[1975:1990], + _ObservationIDName[1990:2013], + _ObservationIDName[2013:2027], + _ObservationIDName[2027:2070], + _ObservationIDName[2070:2082], + _ObservationIDName[2082:2098], + _ObservationIDName[2098:2107], + _ObservationIDName[2107:2115], + _ObservationIDName[2115:2125], + _ObservationIDName[2125:2137], + _ObservationIDName[2137:2150], + _ObservationIDName[2150:2160], + _ObservationIDName[2160:2170], + _ObservationIDName[2170:2190], + _ObservationIDName[2190:2204], + _ObservationIDName[2204:2215], + _ObservationIDName[2215:2226], + _ObservationIDName[2226:2244], + _ObservationIDName[2244:2274], + _ObservationIDName[2274:2304], + _ObservationIDName[2304:2330], + _ObservationIDName[2330:2356], + _ObservationIDName[2356:2371], + _ObservationIDName[2371:2390], + _ObservationIDName[2390:2409], + _ObservationIDName[2409:2442], + _ObservationIDName[2442:2450], + _ObservationIDName[2450:2474], + _ObservationIDName[2474:2487], + _ObservationIDName[2487:2500], + _ObservationIDName[2500:2513], + _ObservationIDName[2513:2526], + _ObservationIDName[2526:2539], + _ObservationIDName[2539:2553], + _ObservationIDName[2553:2567], + _ObservationIDName[2567:2581], + _ObservationIDName[2581:2593], + _ObservationIDName[2593:2606], + _ObservationIDName[2606:2622], + _ObservationIDName[2622:2639], + _ObservationIDName[2639:2652], + _ObservationIDName[2652:2674], + _ObservationIDName[2674:2688], + _ObservationIDName[2688:2702], + _ObservationIDName[2702:2716], + _ObservationIDName[2716:2730], + _ObservationIDName[2730:2743], + _ObservationIDName[2743:2756], + _ObservationIDName[2756:2769], + _ObservationIDName[2769:2782], + _ObservationIDName[2782:2795], + _ObservationIDName[2795:2808], + _ObservationIDName[2808:2821], + _ObservationIDName[2821:2834], + _ObservationIDName[2834:2847], + _ObservationIDName[2847:2860], + _ObservationIDName[2860:2875], + _ObservationIDName[2875:2890], + _ObservationIDName[2890:2905], + _ObservationIDName[2905:2920], + _ObservationIDName[2920:2935], + _ObservationIDName[2935:2947], + _ObservationIDName[2947:2958], + _ObservationIDName[2958:2970], + _ObservationIDName[2970:2978], + _ObservationIDName[2978:2986], + _ObservationIDName[2986:2994], + _ObservationIDName[2994:3014], + _ObservationIDName[3014:3037], + _ObservationIDName[3037:3060], + _ObservationIDName[3060:3083], + _ObservationIDName[3083:3101], + _ObservationIDName[3101:3124], } // ObservationIDString retrieves an enum value from the enum constants string name. diff --git a/charger/easee/signalr.go b/charger/easee/signalr.go index 545e90df2..8e5d741a8 100644 --- a/charger/easee/signalr.go +++ b/charger/easee/signalr.go @@ -83,8 +83,10 @@ const ( LOCATION ObservationID = 25 // Location coordinate [event] [Position] SITE_IDSTRING ObservationID = 26 // Site ID string [event] [String] SITE_IDNUMERIC ObservationID = 27 // Site ID numeric value [event] [Integer] + RFID_TIMEOUT_AUTH ObservationID = 28 // Timeout set for authentication [Integer] LOCK_CABLE_PERMANENTLY ObservationID = 30 // Lock type2 cable permanently [Boolean] IS_ENABLED ObservationID = 31 // Set true to enable charger, false disables charger [Boolean] + TEMPERATURE_MONITOR_STATE ObservationID = 32 // Retrieves temperature of the charger [Integer] CIRCUIT_SEQUENCE_NUMBER ObservationID = 33 // Charger sequence number on circuit [Integer] SINGLE_PHASE_NUMBER ObservationID = 34 // Phase to use in 1-phase charging [Integer] ENABLE3_PHASES_DEPRECATED ObservationID = 35 // Allow charging using 3-phases [Boolean] @@ -104,6 +106,8 @@ const ( MAX_CURRENT_OFFLINE_FALLBACK_P1 ObservationID = 50 // Maximum circuit current P1 when offline [event] [Integer] MAX_CURRENT_OFFLINE_FALLBACK_P2 ObservationID = 51 // Maximum circuit current P2 when offline [event] [Integer] MAX_CURRENT_OFFLINE_FALLBACK_P3 ObservationID = 52 // Maximum circuit current P3 when offline [event] [Integer] + RELEASE_CABLE_AT_POWER_OFF ObservationID = 54 // Cable release behavior on power loss [String] + CHARGING_SESSION_SIGNED ObservationID = 60 // Signed charging sessions [json] [String] CHARGING_SCHEDULE ObservationID = 62 // Charging schedule [json] [String] PAIRED_EQUALIZER ObservationID = 65 // Paired equalizer details [String] WI_FI_APENABLED ObservationID = 68 // True if WiFi Access Point is enabled, otherwise false [Boolean] @@ -171,7 +175,7 @@ const ( LOCAL_TX_POWER ObservationID = 138 // Local radio transmission power [dBm] [telemetry] [Integer] LOCAL_STATE ObservationID = 139 // Local radio state [event] [String] FOUND_WI_FI ObservationID = 140 // List of found WiFi SSID and RSSI values [event] [String] - CHARGER_RAT ObservationID = 141 // Radio access technology in use: 0 = cellular, 1 = wifi [Integer] + CURRENT_CONNECTION ObservationID = 141 // Radio access technology in use: 0 = cellular, 1 = wifi [Integer] CELLULAR_INTERFACE_ERROR_COUNT ObservationID = 142 // The number of times since boot the system has reported an error on this interface [poll] [Integer] CELLULAR_INTERFACE_RESET_COUNT ObservationID = 143 // The number of times since boot the interface was reset due to high error count [poll] [Integer] WIFI_INTERFACE_ERROR_COUNT ObservationID = 144 // The number of times since boot the system has reported an error on this interface [poll] [Integer] @@ -196,10 +200,10 @@ const ( BACK_PLATE_LOCKED ObservationID = 173 // Back plate confirmed locked [event] [Boolean] CURRENT_MOTOR ObservationID = 174 // Motor current draw [debug] [Double] BACK_PLATE_HALL_SENSOR ObservationID = 175 // Raw sensor value [mV] [Integer] - IN_CURRENT_T2 ObservationID = 182 // Calculated current RMS for input T2 [Amperes] [telemetry] [Double] - IN_CURRENT_T3 ObservationID = 183 // Current RMS for input T3 [Amperes] [telemetry] [Double] - IN_CURRENT_T4 ObservationID = 184 // Current RMS for input T4 [Amperes] [telemetry] [Double] - IN_CURRENT_T5 ObservationID = 185 // Current RMS for input T5 [Amperes] [telemetry] [Double] + INT_CURRENT_T2 ObservationID = 182 // Calculated current RMS for input T2 [Amperes] [telemetry] [Double] + INT_CURRENT_T3 ObservationID = 183 // Current RMS for input T3 [Amperes] [telemetry] [Double] + INT_CURRENT_T4 ObservationID = 184 // Current RMS for input T4 [Amperes] [telemetry] [Double] + INT_CURRENT_T5 ObservationID = 185 // Current RMS for input T5 [Amperes] [telemetry] [Double] IN_VOLT_T1_T2 ObservationID = 190 // Input voltage RMS between T1 and T2 [Volt] [telemetry] [Double] IN_VOLT_T1_T3 ObservationID = 191 // Input voltage RMS between T1 and T3 [Volt] [telemetry] [Double] IN_VOLT_T1_T4 ObservationID = 192 // Input voltage RMS between T1 and T4 [Volt] [telemetry] [Double] @@ -214,12 +218,14 @@ const ( OUT_VOLT_PIN1_3 ObservationID = 203 // Output voltage RMS between type 2 pin 1 and 3 [Volt] [telemetry] [Double] OUT_VOLT_PIN1_4 ObservationID = 204 // Output voltage RMS between type 2 pin 1 and 4 [Volt] [telemetry] [Double] OUT_VOLT_PIN1_5 ObservationID = 205 // Output voltage RMS between type 2 pin 1 and 5 [Volt] [telemetry] [Double] + OUT_VOLT_PIN2_3 ObservationID = 206 // Output voltage RMS between type 2 pin 2 and 3 [Volt] [telemetry] [Double] VOLT_LEVEL33 ObservationID = 210 // 3.3 Volt Level [Volt] [telemetry] [Double] VOLT_LEVEL5 ObservationID = 211 // 5 Volt Level [Volt] [telemetry] [Double] VOLT_LEVEL12 ObservationID = 212 // 12 Volt Level [Volt] [telemetry] [Double] LTE_RSRP ObservationID = 220 // Reference Signal Received Power (LTE) [-144 .. -44 dBm] [Integer] LTE_SINR ObservationID = 221 // Signal to Interference plus Noise Ratio (LTE) [-20 .. +30 dB] [Integer] LTE_RSRQ ObservationID = 222 // Reference Signal Received Quality (LTE) [-19 .. -3 dB] [Integer] + CHARGE_SESSION_START ObservationID = 223 // Last charge session start details [json] [String] EQ_AVAILABLE_CURRENT_P1 ObservationID = 230 // Available current for charging on P1 according to Equalizer [Double] EQ_AVAILABLE_CURRENT_P2 ObservationID = 231 // Available current for charging on P2 according to Equalizer [Double] EQ_AVAILABLE_CURRENT_P3 ObservationID = 232 // Available current for charging on P3 according to Equalizer [Double] From 4d53782b6c07705f111f87d6805569997a9116fa Mon Sep 17 00:00:00 2001 From: andig Date: Sun, 12 Apr 2026 14:42:07 +0200 Subject: [PATCH 0020/1293] chore: add hems-pro platform (#25835) --- go.mod | 2 ++ go.sum | 4 ++++ util/sponsor/auth.go | 15 ++++++++++----- util/sponsor/hemspro.go | 35 +++++++++++++++++++++++++++++++++++ util/sponsor/hemspro_linux.go | 16 ++++++++++++++++ util/sponsor/pulsares.go | 2 +- util/sponsor/victron.go | 2 ++ 7 files changed, 70 insertions(+), 6 deletions(-) create mode 100644 util/sponsor/hemspro.go create mode 100644 util/sponsor/hemspro_linux.go diff --git a/go.mod b/go.mod index 732ea8cc7..478b12dea 100644 --- a/go.mod +++ b/go.mod @@ -25,6 +25,7 @@ require ( github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21 github.com/coder/websocket v1.8.14 github.com/coreos/go-oidc/v3 v3.17.0 + github.com/d2r2/go-i2c v0.0.0-20191123181816-73a8a799d6bc github.com/denisbrodbeck/machineid v1.0.1 github.com/dylanmei/iso8601 v0.1.0 github.com/eclipse/paho.mqtt.golang v1.5.1 @@ -145,6 +146,7 @@ require ( github.com/creack/goselect v0.1.2 // indirect github.com/cronokirby/saferith v0.33.0 // indirect github.com/cstockton/go-conv v1.0.0 // indirect + github.com/d2r2/go-logger v0.0.0-20210606094344-60e9d1233e22 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dmarkham/enumer v1.6.1 // indirect github.com/donovanhide/eventsource v0.0.0-20210830082556-c59027999da0 // indirect diff --git a/go.sum b/go.sum index 3ef475b07..d029cc289 100644 --- a/go.sum +++ b/go.sum @@ -148,6 +148,10 @@ github.com/cronokirby/saferith v0.33.0 h1:TgoQlfsD4LIwx71+ChfRcIpjkw+RPOapDEVxa+ github.com/cronokirby/saferith v0.33.0/go.mod h1:QKJhjoqUtBsXCAVEjw38mFqoi7DebT7kthcD7UzbnoA= github.com/cstockton/go-conv v1.0.0 h1:zj/q/0MpQ/97XfiC9glWiohO8lhgR4TTnHYZifLTv6I= github.com/cstockton/go-conv v1.0.0/go.mod h1:HuiHkkRgOA0IoBNPC7ysG7kNpjDYlgM7Kj62yQPxjy4= +github.com/d2r2/go-i2c v0.0.0-20191123181816-73a8a799d6bc h1:HLRSIWzUGMLCq4ldt0W1GLs3nnAxa5EGoP+9qHgh6j0= +github.com/d2r2/go-i2c v0.0.0-20191123181816-73a8a799d6bc/go.mod h1:AwxDPnsgIpy47jbGXZHA9Rv7pDkOJvQbezPuK1Y+nNk= +github.com/d2r2/go-logger v0.0.0-20210606094344-60e9d1233e22 h1:nO+SY4KOMsF/LsZ5EtbSKhiT3M6sv/igo2PEru/xEHI= +github.com/d2r2/go-logger v0.0.0-20210606094344-60e9d1233e22/go.mod h1:eSx+YfcVy5vCjRZBNIhpIpfCGFMQ6XSOSQkDk7+VCpg= github.com/dave/jennifer v1.7.1 h1:B4jJJDHelWcDhlRQxWeo0Npa/pYKBLrirAQoTN45txo= github.com/dave/jennifer v1.7.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/util/sponsor/auth.go b/util/sponsor/auth.go index eecb0e68e..aa42f0e60 100644 --- a/util/sponsor/auth.go +++ b/util/sponsor/auth.go @@ -20,6 +20,7 @@ package sponsor import ( "context" "fmt" + "os" "strings" "sync" "time" @@ -36,10 +37,7 @@ var ( ExpiresAt time.Time ) -const ( - unavailable = "sponsorship unavailable" - victron = "victron" -) +const unavailable = "sponsorship unavailable" func IsAuthorized() bool { mu.RLock() @@ -64,8 +62,15 @@ func ConfigureSponsorship(token string) error { return nil } + if os.Getenv("HEMSPRO") != "" { + if sub := checkHemsPro(); sub != "" { + Subject = sub + return nil + } + } + var err error - if token, err = readSerial(); token == "" || err != nil { + if token, err = checkPulsares(); token == "" || err != nil { return err } } diff --git a/util/sponsor/hemspro.go b/util/sponsor/hemspro.go new file mode 100644 index 000000000..7e1bdabef --- /dev/null +++ b/util/sponsor/hemspro.go @@ -0,0 +1,35 @@ +//go:build !linux + +package sponsor + +import ( + i2c "github.com/d2r2/go-i2c" +) + +const hemspro = "hemspro" + +// checkHemsPro checks if the hardware is a supported HEMS Pro device and returns sponsor subject +func checkHemsPro() string { + const ( + ADDR = 0b1101000 // 0x68 DS1307 + REG_TIMEDATE = 0x00 + ) + + // Create new connection to I2C bus 1 + i2c, err := i2c.NewI2C(ADDR, 1) + if err != nil { + return "" + } + defer i2c.Close() + + if _, err := i2c.WriteBytes([]byte{REG_TIMEDATE}); err != nil { + return "" + } + + buf := make([]byte, 7) + if n, err := i2c.ReadBytes(buf); err != nil || n != 7 { + return "" + } + + return hemspro +} diff --git a/util/sponsor/hemspro_linux.go b/util/sponsor/hemspro_linux.go new file mode 100644 index 000000000..cfbdf8eed --- /dev/null +++ b/util/sponsor/hemspro_linux.go @@ -0,0 +1,16 @@ +//go:build linux + +package sponsor + +const hemspro = "hemspro" + +// checkHemsPro checks if the hardware is a supported HEMS Pro device and returns sponsor subject +func checkHemsPro() string { + // TODO i2c validation + + if false { + return hemspro + } + + return "" +} diff --git a/util/sponsor/pulsares.go b/util/sponsor/pulsares.go index 0012c76d6..66bf88458 100644 --- a/util/sponsor/pulsares.go +++ b/util/sponsor/pulsares.go @@ -23,7 +23,7 @@ import ( "time" ) -func readSerial() (string, error) { +func checkPulsares() (string, error) { f, err := os.OpenFile("/dev/PulsaresSerial", os.O_RDWR, 0o644) if err != nil { return "", nil diff --git a/util/sponsor/victron.go b/util/sponsor/victron.go index c74bc5e11..51ad01736 100644 --- a/util/sponsor/victron.go +++ b/util/sponsor/victron.go @@ -32,6 +32,8 @@ import ( "google.golang.org/grpc/status" ) +const victron = "victron" + // checkVictron checks if the hardware is a supported victron device and returns sponsor subject func checkVictron() string { vd, err := victronDeviceInfo() From 91f41f77b989da73c87a6b80fd21ee023ab0d7f7 Mon Sep 17 00:00:00 2001 From: andig Date: Sun, 12 Apr 2026 15:16:32 +0200 Subject: [PATCH 0021/1293] chore: fix build tags --- util/sponsor/hemspro.go | 29 +---------------------------- util/sponsor/hemspro_linux.go | 27 +++++++++++++++++++++++---- 2 files changed, 24 insertions(+), 32 deletions(-) diff --git a/util/sponsor/hemspro.go b/util/sponsor/hemspro.go index 7e1bdabef..d6525bcb4 100644 --- a/util/sponsor/hemspro.go +++ b/util/sponsor/hemspro.go @@ -2,34 +2,7 @@ package sponsor -import ( - i2c "github.com/d2r2/go-i2c" -) - -const hemspro = "hemspro" - // checkHemsPro checks if the hardware is a supported HEMS Pro device and returns sponsor subject func checkHemsPro() string { - const ( - ADDR = 0b1101000 // 0x68 DS1307 - REG_TIMEDATE = 0x00 - ) - - // Create new connection to I2C bus 1 - i2c, err := i2c.NewI2C(ADDR, 1) - if err != nil { - return "" - } - defer i2c.Close() - - if _, err := i2c.WriteBytes([]byte{REG_TIMEDATE}); err != nil { - return "" - } - - buf := make([]byte, 7) - if n, err := i2c.ReadBytes(buf); err != nil || n != 7 { - return "" - } - - return hemspro + return "" } diff --git a/util/sponsor/hemspro_linux.go b/util/sponsor/hemspro_linux.go index cfbdf8eed..d7527728d 100644 --- a/util/sponsor/hemspro_linux.go +++ b/util/sponsor/hemspro_linux.go @@ -2,15 +2,34 @@ package sponsor +import ( + i2c "github.com/d2r2/go-i2c" +) + const hemspro = "hemspro" // checkHemsPro checks if the hardware is a supported HEMS Pro device and returns sponsor subject func checkHemsPro() string { - // TODO i2c validation + const ( + ADDR = 0b1101000 // 0x68 DS1307 + REG_TIMEDATE = 0x00 + ) - if false { - return hemspro + // Create new connection to I2C bus 1 + i2c, err := i2c.NewI2C(ADDR, 1) + if err != nil { + return "" + } + defer i2c.Close() + + if _, err := i2c.WriteBytes([]byte{REG_TIMEDATE}); err != nil { + return "" } - return "" + buf := make([]byte, 7) + if n, err := i2c.ReadBytes(buf); err != nil || n != 7 { + return "" + } + + return hemspro } From 79bed17bb45792b7b61d9d18e4fc98abba27ca87 Mon Sep 17 00:00:00 2001 From: andig Date: Sun, 12 Apr 2026 16:23:55 +0200 Subject: [PATCH 0022/1293] chore: ignore node_modules --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index 20159a686..ad18d69a9 100644 --- a/Makefile +++ b/Makefile @@ -63,6 +63,7 @@ lint-ui:: license:: go run github.com/google/go-licenses/v2@latest check \ + --ignore github.com/evcc-io/evcc/node_modules \ --ignore github.com/cespare/xxhash \ --ignore github.com/coder/websocket \ --ignore github.com/cronokirby/saferith \ From 599387194f9841d1b88a48138d3a462cbcb72001 Mon Sep 17 00:00:00 2001 From: andig Date: Sun, 12 Apr 2026 16:29:20 +0200 Subject: [PATCH 0023/1293] chore: upgrade modules --- go.mod | 34 ++++++++++++++--------------- go.sum | 68 +++++++++++++++++++++++++++++----------------------------- 2 files changed, 51 insertions(+), 51 deletions(-) diff --git a/go.mod b/go.mod index 478b12dea..c597feb3e 100644 --- a/go.mod +++ b/go.mod @@ -24,7 +24,7 @@ require ( github.com/cli/browser v1.3.0 github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21 github.com/coder/websocket v1.8.14 - github.com/coreos/go-oidc/v3 v3.17.0 + github.com/coreos/go-oidc/v3 v3.18.0 github.com/d2r2/go-i2c v0.0.0-20191123181816-73a8a799d6bc github.com/denisbrodbeck/machineid v1.0.1 github.com/dylanmei/iso8601 v0.1.0 @@ -33,11 +33,11 @@ require ( github.com/enbility/ship-go v0.6.0 github.com/enbility/spine-go v0.7.0 github.com/evcc-io/openapi-mcp v0.6.0 - github.com/evcc-io/optimizer v0.0.0-20260321154346-f7a5fa8cbe06 + github.com/evcc-io/optimizer v0.0.0-20260411145738-bf13a64d411c github.com/evcc-io/rct v0.2.0 github.com/evcc-io/tesla-proxy-client v0.0.0-20260324063928-151fe10796ae github.com/fatih/structs v1.1.0 - github.com/getkin/kin-openapi v0.134.0 + github.com/getkin/kin-openapi v0.135.0 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.2 @@ -54,7 +54,7 @@ require ( github.com/gregdel/pushover v1.4.0 github.com/grid-x/modbus v0.0.0-20260325140807-cf9e1b9daae0 github.com/hashicorp/go-version v1.9.0 - github.com/hasura/go-graphql-client v0.15.1 + github.com/hasura/go-graphql-client v0.16.0 github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83 github.com/influxdata/influxdb-client-go/v2 v2.14.0 github.com/insomniacslk/tapo v1.0.2 @@ -73,7 +73,7 @@ require ( 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.4.1 + github.com/modelcontextprotocol/go-sdk v1.5.0 github.com/muka/go-bluetooth v0.0.0-20240701044517-04c4f09c514e github.com/nicholas-fedor/shoutrrr v0.14.3 github.com/nicksnyder/go-i18n/v2 v2.6.1 @@ -104,14 +104,14 @@ require ( go.bug.st/serial v1.6.4 go.uber.org/mock v0.6.0 go.yaml.in/yaml/v4 v4.0.0-rc.4 - golang.org/x/crypto v0.49.0 - golang.org/x/crypto/x509roots/fallback v0.0.0-20260323153451-8400f4a93807 - golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 - golang.org/x/net v0.52.0 + golang.org/x/crypto v0.50.0 + golang.org/x/crypto/x509roots/fallback v0.0.0-20260409153322-03ca0dcccbd3 + golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f + golang.org/x/net v0.53.0 golang.org/x/oauth2 v0.36.0 golang.org/x/sync v0.20.0 - golang.org/x/text v0.35.0 - golang.org/x/tools v0.43.0 + golang.org/x/text v0.36.0 + golang.org/x/tools v0.44.0 google.golang.org/grpc v1.80.0 google.golang.org/protobuf v1.36.11 gorm.io/gorm v1.31.1 @@ -203,8 +203,8 @@ require ( github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/ncruces/go-strftime v1.0.0 // indirect github.com/oapi-codegen/runtime v1.1.2 // indirect - github.com/oasdiff/yaml v0.0.0-20260313112342-a3ea61cb4d4c // indirect - github.com/oasdiff/yaml3 v0.0.0-20260224194419-61cd415a242b // indirect + github.com/oasdiff/yaml v0.0.9 // indirect + github.com/oasdiff/yaml3 v0.0.9 // indirect github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect github.com/olekukonko/errors v1.2.0 // indirect github.com/olekukonko/ll v0.1.6 // indirect @@ -239,9 +239,9 @@ require ( gitlab.com/c0b/go-ordered-json v0.0.0-20201030195603-febf46534d5a // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/mod v0.34.0 // indirect - golang.org/x/sys v0.42.0 // indirect - golang.org/x/term v0.41.0 // indirect + golang.org/x/mod v0.35.0 // indirect + golang.org/x/sys v0.43.0 // indirect + golang.org/x/term v0.42.0 // indirect golang.org/x/tools/gopls v0.21.1 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 // indirect gopkg.in/go-playground/validator.v9 v9.30.0 // indirect @@ -250,7 +250,7 @@ require ( modernc.org/libc v1.70.0 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect - modernc.org/sqlite v1.48.1 // indirect + modernc.org/sqlite v1.48.2 // indirect ) tool ( diff --git a/go.sum b/go.sum index d029cc289..405e56c3c 100644 --- a/go.sum +++ b/go.sum @@ -130,8 +130,8 @@ github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:z github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g= github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= -github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc= -github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8= +github.com/coreos/go-oidc/v3 v3.18.0 h1:V9orjXynvu5wiC9SemFTWnG4F45v403aIcjWo0d41+A= +github.com/coreos/go-oidc/v3 v3.18.0/go.mod h1:DYCf24+ncYi+XkIH97GY1+dqoRlbaSI26KVTCI9SrY4= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= @@ -200,8 +200,8 @@ github.com/evcc-io/ocpp-go v0.0.0-20251212212612-b7f92ee0443b h1:5vFr7wOwoi5ylyb github.com/evcc-io/ocpp-go v0.0.0-20251212212612-b7f92ee0443b/go.mod h1:2kcukDdhui4u730VfnYVWuwzDLgw+mBRGDir/QAyBhg= github.com/evcc-io/openapi-mcp v0.6.0 h1:cq8CWG+3gNQ9ID0ZE8MoDHZxgdJmBZ1fFyt5xAfTRfA= github.com/evcc-io/openapi-mcp v0.6.0/go.mod h1:YyOx4zwr6xVUcchAETHERZ+75cZMIrdcjVIZFwBlE1Q= -github.com/evcc-io/optimizer v0.0.0-20260321154346-f7a5fa8cbe06 h1:Yn0zCt586aeYpmWwx5vj3985euSTP7lji5HmZj80U38= -github.com/evcc-io/optimizer v0.0.0-20260321154346-f7a5fa8cbe06/go.mod h1:cHMeq6GfoXQ8LxqY4LH1wn/hrgYEn+ka5RPI1CF37SY= +github.com/evcc-io/optimizer v0.0.0-20260411145738-bf13a64d411c h1:KUJqCEBzEXepnL9mVpIepJF6+Vlx3jki+8y0S+mqpWc= +github.com/evcc-io/optimizer v0.0.0-20260411145738-bf13a64d411c/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-20260324063928-151fe10796ae h1:0ihqBMBjkekSlwD3ZYQIiYite6ZAln9ze60evOE6FkQ= @@ -223,8 +223,8 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= 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.134.0 h1:/L5+1+kfe6dXh8Ot/wqiTgUkjOIEJiC0bbYVziHB8rU= -github.com/getkin/kin-openapi v0.134.0/go.mod h1:wK6ZLG/VgoETO9pcLJ/VmAtIcl/DNlMayNTb716EUxE= +github.com/getkin/kin-openapi v0.135.0 h1:751SjYfbiwqukYuVjwYEIKNfrSwS5YpA7DZnKSwQgtg= +github.com/getkin/kin-openapi v0.135.0/go.mod h1:6dd5FJl6RdX4usBtFBaQhk9q62Yb2J0Mk5IhUO/QqFI= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ= github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc= @@ -391,8 +391,8 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= -github.com/hasura/go-graphql-client v0.15.1 h1:mCb5I+8Bk3FU3GKWvf/zDXkTh7FbGlqJmP3oisBdnN8= -github.com/hasura/go-graphql-client v0.15.1/go.mod h1:jfSZtBER3or+88Q9vFhWHiFMPppfYILRyl+0zsgPIIw= +github.com/hasura/go-graphql-client v0.16.0 h1:DQLfp+djj4j5NPdJkGYym8J55hpm5etML1zqgco78Qc= +github.com/hasura/go-graphql-client v0.16.0/go.mod h1:z/sO2T0zI+HnPNIevQcs+7xA6/gDOc8hgHMrNBzfL2c= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83 h1:B+A58zGFuDrvEZpPN+yS6swJA0nzqgZvDzgl/OPyefU= @@ -521,8 +521,8 @@ github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:F github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/modelcontextprotocol/go-sdk v1.4.1 h1:M4x9GyIPj+HoIlHNGpK2hq5o3BFhC+78PkEaldQRphc= -github.com/modelcontextprotocol/go-sdk v1.4.1/go.mod h1:Bo/mS87hPQqHSRkMv4dQq1XCu6zv4INdXnFZabkNU6s= +github.com/modelcontextprotocol/go-sdk v1.5.0 h1:CHU0FIX9kpueNkxuYtfYQn1Z0slhFzBZuq+x6IiblIU= +github.com/modelcontextprotocol/go-sdk v1.5.0/go.mod h1:gggDIhoemhWs3BGkGwd1umzEXCEMMvAnhTrnbXJKKKA= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -557,10 +557,10 @@ github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= github.com/oapi-codegen/runtime v1.1.2 h1:P2+CubHq8fO4Q6fV1tqDBZHCwpVpvPg7oKiYzQgXIyI= github.com/oapi-codegen/runtime v1.1.2/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg= -github.com/oasdiff/yaml v0.0.0-20260313112342-a3ea61cb4d4c h1:7ACFcSaQsrWtrH4WHHfUqE1C+f8r2uv8KGaW0jTNjus= -github.com/oasdiff/yaml v0.0.0-20260313112342-a3ea61cb4d4c/go.mod h1:JKox4Gszkxt57kj27u7rvi7IFoIULvCZHUsBTUmQM/s= -github.com/oasdiff/yaml3 v0.0.0-20260224194419-61cd415a242b h1:vivRhVUAa9t1q0Db4ZmezBP8pWQWnXHFokZj0AOea2g= -github.com/oasdiff/yaml3 v0.0.0-20260224194419-61cd415a242b/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= +github.com/oasdiff/yaml v0.0.9 h1:zQOvd2UKoozsSsAknnWoDJlSK4lC0mpmjfDsfqNwX48= +github.com/oasdiff/yaml v0.0.9/go.mod h1:8lvhgJG4xiKPj3HN5lDow4jZHPlx1i7dIwzkdAo6oAM= +github.com/oasdiff/yaml3 v0.0.9 h1:rWPrKccrdUm8J0F3sGuU+fuh9+1K/RdJlWF7O/9yw2g= +github.com/oasdiff/yaml3 v0.0.9/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc= @@ -836,13 +836,13 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= -golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= -golang.org/x/crypto/x509roots/fallback v0.0.0-20260323153451-8400f4a93807 h1:sQVhWLXbNsa8CTzHOX3IHc7C4Q2JyxI5AweuMQZ/5H0= -golang.org/x/crypto/x509roots/fallback v0.0.0-20260323153451-8400f4a93807/go.mod h1:+UoQFNBq2p2wO+Q6ddVtYc25GZ6VNdOMyyrd4nrqrKs= +golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= +golang.org/x/crypto/x509roots/fallback v0.0.0-20260409153322-03ca0dcccbd3 h1:oDpwkFFyjnRTqXlggDGNGPdde8tKwKQzxybO7VGHxPo= +golang.org/x/crypto/x509roots/fallback v0.0.0-20260409153322-03ca0dcccbd3/go.mod h1:+UoQFNBq2p2wO+Q6ddVtYc25GZ6VNdOMyyrd4nrqrKs= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 h1:jiDhWWeC7jfWqR9c/uplMOqJ0sbNlNWv0UkzE0vX1MA= -golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:xE1HEv6b+1SCZ5/uscMRjUBKtIxworgEcEi+/n9NQDQ= +golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM= +golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -856,8 +856,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= -golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= +golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= +golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -886,8 +886,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= -golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= -golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= +golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= +golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= @@ -947,8 +947,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= -golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -958,8 +958,8 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= -golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= -golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= +golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= +golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -972,8 +972,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= -golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -997,8 +997,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= -golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= +golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= +golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= golang.org/x/tools/gopls v0.21.1 h1:1/o9z5Brdero4jFm9Jr46Uwj8GU9lQdoSXHMlwRHb/w= golang.org/x/tools/gopls v0.21.1/go.mod h1:eyYJvtYY6UF9WbXilvCkD3fdYMJwG5cVCS4bdOQ2qIs= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1096,8 +1096,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.48.1 h1:S85iToyU6cgeojybE2XJlSbcsvcWkQ6qqNXJHtW5hWA= -modernc.org/sqlite v1.48.1/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig= +modernc.org/sqlite v1.48.2 h1:5CnW4uP8joZtA0LedVqLbZV5GD7F/0x91AXeSyjoh5c= +modernc.org/sqlite v1.48.2/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig= 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 8a3a625cfbb227a4aaad67a565b20842ca055b73 Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Sun, 12 Apr 2026 17:21:13 +0200 Subject: [PATCH 0024/1293] chore: allow ISC licensed dependencies --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index ad18d69a9..fc1168092 100644 --- a/Makefile +++ b/Makefile @@ -70,7 +70,7 @@ license:: --ignore github.com/modern-go/reflect2 \ --ignore github.com/prometheus/client_golang \ --ignore golang.org/x \ - --allowed_licenses=MIT,Apache-2.0,BSD-0-Clause,BSD-2-Clause,BSD-3-Clause,LGPL-2.1,EPL-2.0,MPL-2.0 \ + --allowed_licenses=MIT,Apache-2.0,BSD-0-Clause,BSD-2-Clause,BSD-3-Clause,ISC,LGPL-2.1,EPL-2.0,MPL-2.0 \ ./... license-ui:: From 61c06fa8caa9e3178332ae62195a141002d7bfec Mon Sep 17 00:00:00 2001 From: premultiply <4681172+premultiply@users.noreply.github.com> Date: Sun, 12 Apr 2026 18:47:59 +0200 Subject: [PATCH 0025/1293] Add Stegen Smart Evse (REST API) (#28966) --- charger/smart-evse.go | 432 +++++++++++++++++++ charger/smart-evse_decorators.go | 116 +++++ templates/definition/charger/smart-evse.yaml | 24 ++ 3 files changed, 572 insertions(+) create mode 100644 charger/smart-evse.go create mode 100644 charger/smart-evse_decorators.go create mode 100644 templates/definition/charger/smart-evse.yaml diff --git a/charger/smart-evse.go b/charger/smart-evse.go new file mode 100644 index 000000000..901d26985 --- /dev/null +++ b/charger/smart-evse.go @@ -0,0 +1,432 @@ +package charger + +// LICENSE + +// Copyright (c) evcc.io (andig, naltatis, premultiply) + +// This module is NOT covered by the MIT license. All rights reserved. + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import ( + "fmt" + "net/http" + "strings" + "time" + + "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/util" + "github.com/evcc-io/evcc/util/request" + "github.com/evcc-io/evcc/util/sponsor" +) + +// SmartEVSE-3.5 REST API charger implementation +// https://github.com/dingo35/SmartEVSE-3.5/blob/master/docs/REST_API.md + +// SmartEVSE3 charger implementation +type SmartEVSE3 struct { + *request.Helper + uri string + curr int64 + apiG util.Cacheable[smartEvseRestSettings] +} + +// smartEvseRestSettings represents the JSON returned by GET /settings +type smartEvseRestSettings struct { + Mode string `json:"mode"` + ModeID int `json:"mode_id"` + CarConnected bool `json:"car_connected"` + Evse struct { + Connected int `json:"connected"` + Access int `json:"access"` + Mode int `json:"mode"` + ChargeTimer int `json:"charge_timer"` + State string `json:"state"` + StateID int `json:"state_id"` + Error string `json:"error"` + ErrorID int `json:"error_id"` + RFID string `json:"rfid"` + RFIDReader string `json:"rfidreader"` + RFIDLastRead string `json:"rfid_lastread"` + NrOfPhases int `json:"nrofphases"` + } `json:"evse"` + Settings struct { + ChargeCurrent int `json:"charge_current"` + OverrideCurrent int `json:"override_current"` + CurrentMin int `json:"current_min"` + CurrentMax int `json:"current_max"` + CurrentMain int `json:"current_main"` + EnableC2 string `json:"enable_C2"` + } `json:"settings"` + EvMeter struct { + Description string `json:"description"` + Address int `json:"address"` + ImportActivePower float64 `json:"import_active_power"` + TotalWh float64 `json:"total_wh"` + ChargedWh float64 `json:"charged_wh"` + Currents struct { + Total float64 `json:"TOTAL"` + L1 float64 `json:"L1"` + L2 float64 `json:"L2"` + L3 float64 `json:"L3"` + } `json:"currents"` + ImportActiveEnergy float64 `json:"import_active_energy"` + ExportActiveEnergy float64 `json:"export_active_energy"` + } `json:"ev_meter"` +} + +// SmartEVSE-3.5 operating mode IDs +const ( + smartEvse3ModeOff = 0 + smartEvse3ModeNormal = 1 + smartEvse3ModeSolar = 2 + smartEvse3ModeSmart = 3 + smartEvse3ModePause = 4 +) + +// SmartEVSE-3.5 C2 contactor IDs +const ( + smartEvse3C2NotPresent = 0 + smartEvse3C2AlwaysOff = 1 // single-phase + smartEvse3C2SolarOff = 2 + smartEvse3C2AlwaysOn = 3 // three-phase + smartEvse3C2Auto = 4 +) + +func init() { + registry.Add("smart-evse", NewSmartEVSE3FromConfig) +} + +//go:generate go tool decorate -f decorateSmartEVSE3 -b *SmartEVSE3 -r api.Charger -t api.Meter,api.MeterEnergy,api.PhaseCurrents,api.PhaseSwitcher,api.PhaseGetter,api.Identifier,api.StatusReasoner + +// NewSmartEVSE3FromConfig creates a SmartEVSE-3.5 REST charger from generic config +func NewSmartEVSE3FromConfig(other map[string]any) (api.Charger, error) { + cc := struct { + URI string + Cache time.Duration + }{ + Cache: time.Second, + } + + if err := util.DecodeOther(other, &cc); err != nil { + return nil, err + } + + if cc.URI == "" { + return nil, fmt.Errorf("missing uri") + } + + return NewSmartEVSE3(cc.URI, cc.Cache) +} + +// NewSmartEVSE3 creates a new SmartEVSE-3.5 REST charger +func NewSmartEVSE3(uri string, cache time.Duration) (api.Charger, error) { + log := util.NewLogger("smart-evse") + + wb := &SmartEVSE3{ + Helper: request.NewHelper(log), + uri: strings.TrimRight(util.DefaultScheme(uri, "http"), "/"), + curr: 60, // 6 A in 1/10 A + } + + if !sponsor.IsAuthorized() { + return nil, api.ErrSponsorRequired + } + + wb.apiG = util.ResettableCached(func() (smartEvseRestSettings, error) { + var res smartEvseRestSettings + err := wb.GetJSON(wb.uri+"/settings", &res) + return res, err + }, cache) + + // verify connectivity + res, err := wb.apiG.Get() + if err != nil { + return nil, err + } + + // force NORMAL mode so override_current takes effect + if res.ModeID != smartEvse3ModeNormal { + if err := wb.setMode(smartEvse3ModeNormal); err != nil { + return nil, err + } + } + + // decorate optional EV meter if configured in SmartEVSE + var currentPower, totalEnergy func() (float64, error) + var currents func() (float64, float64, float64, error) + if res.EvMeter.Description != "" && res.EvMeter.Description != "Disabled" { + currentPower = wb.currentPower + totalEnergy = wb.totalEnergy + currents = wb.currents + } + + // decorate optional 1P/3P phase switching via C2 contactor + var phases1p3p func(int) error + var getPhases func() (int, error) + if res.Settings.EnableC2 != "" && res.Settings.EnableC2 != "Not present" { + phases1p3p = wb.phases1p3p + getPhases = wb.getPhases + } + + // decorate optional RFID identification and status reason + var identify func() (string, error) + var statusReason func() (api.Reason, error) + if res.Evse.RFIDReader != "" && res.Evse.RFIDReader != "Disabled" { + identify = wb.identify + statusReason = wb.statusReason + } + + return decorateSmartEVSE3(wb, currentPower, totalEnergy, currents, phases1p3p, getPhases, identify, statusReason), nil +} + +// post issues a POST request to the SmartEVSE with given query parameters. +// Mongoose webserver requires an empty request body to avoid timeout. +func (wb *SmartEVSE3) post(path string, query string) error { + uri := fmt.Sprintf("%s/%s?%s", wb.uri, strings.TrimLeft(path, "/"), query) + + req, err := request.New(http.MethodPost, uri, strings.NewReader(""), request.URLEncoding) + if err != nil { + return err + } + + _, err = wb.DoBody(req) + wb.apiG.Reset() + + return err +} + +func (wb *SmartEVSE3) setMode(mode int) error { + return wb.post("settings", fmt.Sprintf("mode=%d", mode)) +} + +func (wb *SmartEVSE3) setOverrideCurrent(deciAmps int64) error { + return wb.post("settings", fmt.Sprintf("override_current=%d", deciAmps)) +} + +// Status implements the api.Charger interface +func (wb *SmartEVSE3) Status() (api.ChargeStatus, error) { + res, err := wb.apiG.Get() + if err != nil { + return api.StatusNone, err + } + + if !res.CarConnected { + return api.StatusA, nil + } + + // state IDs from SmartEVSE firmware (main_c.h) + switch res.Evse.StateID { + case 0: // STATE_A + return api.StatusA, nil + case 1, 4, 5, 8, 9, 11, 12, 13, 14: + // STATE_B, STATE_COMM_B, STATE_COMM_B_OK, STATE_ACTSTART, STATE_B1, + // STATE_MODEM_REQUEST, STATE_MODEM_WAIT, STATE_MODEM_DONE, STATE_MODEM_DENIED + return api.StatusB, nil + case 2, 3, 6, 7, 10: + // STATE_C, STATE_D, STATE_COMM_C, STATE_COMM_C_OK, STATE_C1 + return api.StatusC, nil + default: + return api.StatusNone, fmt.Errorf("invalid state: %d (%s)", res.Evse.StateID, res.Evse.State) + } +} + +// Enabled implements the api.Charger interface +func (wb *SmartEVSE3) Enabled() (bool, error) { + res, err := wb.apiG.Get() + if err != nil { + return false, err + } + + // STATE_B1 / STATE_C1: EVSE not ready, no PWM signal + if res.Evse.StateID == 9 || res.Evse.StateID == 10 { + return false, nil + } + + if res.ModeID == smartEvse3ModeOff || res.ModeID == smartEvse3ModePause { + return false, nil + } + + return true, nil +} + +// Enable implements the api.Charger interface +func (wb *SmartEVSE3) Enable(enable bool) error { + res, err := wb.apiG.Get() + if err != nil { + return err + } + + if enable { + if res.ModeID != smartEvse3ModeNormal { + return wb.setMode(smartEvse3ModeNormal) + } + return nil + } + + if res.ModeID != smartEvse3ModeOff && res.ModeID != smartEvse3ModePause { + return wb.setMode(smartEvse3ModePause) + } + + return nil +} + +// MaxCurrent implements the api.Charger interface +func (wb *SmartEVSE3) MaxCurrent(current int64) error { + if current < 6 { + return fmt.Errorf("invalid current %d", current) + } + + deciAmps := current * 10 + + if err := wb.setOverrideCurrent(deciAmps); err != nil { + return err + } + + wb.curr = deciAmps + return nil +} + +var _ api.ChargerEx = (*SmartEVSE3)(nil) + +// MaxCurrentMillis implements the api.ChargerEx interface +func (wb *SmartEVSE3) MaxCurrentMillis(current float64) error { + if current < 6 { + return fmt.Errorf("invalid current %.1f", current) + } + + deciAmps := int64(current * 10) + + if err := wb.setOverrideCurrent(deciAmps); err != nil { + return err + } + + wb.curr = deciAmps + return nil +} + +// currentPower implements the api.Meter interface +func (wb *SmartEVSE3) currentPower() (float64, error) { + res, err := wb.apiG.Get() + if err != nil { + return 0, err + } + + // import_active_power is reported in W + return res.EvMeter.ImportActivePower, nil +} + +// totalEnergy implements the api.MeterEnergy interface +func (wb *SmartEVSE3) totalEnergy() (float64, error) { + res, err := wb.apiG.Get() + if err != nil { + return 0, err + } + + // import_active_energy is reported in Wh + return res.EvMeter.ImportActiveEnergy / 1e3, nil +} + +// phases1p3p implements the api.PhaseSwitcher interface +func (wb *SmartEVSE3) phases1p3p(phases int) error { + var c2 int + switch phases { + case 1: + c2 = smartEvse3C2AlwaysOff + case 3: + c2 = smartEvse3C2AlwaysOn + default: + return fmt.Errorf("invalid phases: %d", phases) + } + + // C2 switching takes effect on next state change; disable charger during switch + return whenDisabled(wb, func() error { + return wb.post("settings", fmt.Sprintf("enable_C2=%d", c2)) + }) +} + +// getPhases implements the api.PhaseGetter interface +func (wb *SmartEVSE3) getPhases() (int, error) { + res, err := wb.apiG.Get() + if err != nil { + return 0, err + } + + return res.Evse.NrOfPhases, nil +} + +// currents implements the api.PhaseCurrents interface +func (wb *SmartEVSE3) currents() (float64, float64, float64, error) { + res, err := wb.apiG.Get() + if err != nil { + return 0, 0, 0, err + } + + // phase currents are reported in 1/10 A + return res.EvMeter.Currents.L1 / 10, res.EvMeter.Currents.L2 / 10, res.EvMeter.Currents.L3 / 10, nil +} + +// statusReason implements the api.StatusReasoner interface +func (wb *SmartEVSE3) statusReason() (api.Reason, error) { + res, err := wb.apiG.Get() + if err != nil { + return api.ReasonUnknown, err + } + + // RFID reader enabled, car connected, but access not yet granted + if res.CarConnected && res.Evse.Access == 0 { + return api.ReasonWaitingForAuthorization, nil + } + + return api.ReasonUnknown, nil +} + +// identify implements the api.Identifier interface +func (wb *SmartEVSE3) identify() (string, error) { + res, err := wb.apiG.Get() + if err != nil { + return "", err + } + + return res.Evse.RFIDLastRead, nil +} + +var _ api.Diagnosis = (*SmartEVSE3)(nil) + +// Diagnose implements the api.Diagnosis interface +func (wb *SmartEVSE3) Diagnose() { + res, err := wb.apiG.Get() + if err != nil { + fmt.Printf("\tError: %v\n", err) + return + } + + fmt.Printf("\tMode: %s (%d)\n", res.Mode, res.ModeID) + fmt.Printf("\tCar connected: %t\n", res.CarConnected) + + fmt.Printf("\tEVSE connected: %d\n", res.Evse.Connected) + fmt.Printf("\tEVSE access: %d\n", res.Evse.Access) + fmt.Printf("\tEVSE mode: %d\n", res.Evse.Mode) + fmt.Printf("\tEVSE charge timer: %d\n", res.Evse.ChargeTimer) + fmt.Printf("\tEVSE state: %s (%d)\n", res.Evse.State, res.Evse.StateID) + fmt.Printf("\tEVSE error: %s (%d)\n", res.Evse.Error, res.Evse.ErrorID) + fmt.Printf("\tEVSE RFID: %s\n", res.Evse.RFID) + fmt.Printf("\tEVSE RFID reader: %s\n", res.Evse.RFIDReader) + fmt.Printf("\tEVSE RFID last read: %s\n", res.Evse.RFIDLastRead) + fmt.Printf("\tEVSE nr of phases: %d\n", res.Evse.NrOfPhases) + + fmt.Printf("\tCharge current: %d A\n", res.Settings.ChargeCurrent) + fmt.Printf("\tOverride current: %.1f A\n", float64(res.Settings.OverrideCurrent)/10) + fmt.Printf("\tCurrent min/max: %d/%d A\n", res.Settings.CurrentMin, res.Settings.CurrentMax) + fmt.Printf("\tCurrent main: %d A\n", res.Settings.CurrentMain) + fmt.Printf("\tEnable C2: %s\n", res.Settings.EnableC2) +} diff --git a/charger/smart-evse_decorators.go b/charger/smart-evse_decorators.go new file mode 100644 index 000000000..eab122fb7 --- /dev/null +++ b/charger/smart-evse_decorators.go @@ -0,0 +1,116 @@ +package charger + +// Code generated by github.com/evcc-io/evcc/cmd/tools/decorate.go. DO NOT EDIT. + +import ( + "reflect" + + "github.com/evcc-io/evcc/api" +) + +func decorateSmartEVSE3(base *SmartEVSE3, meter func() (float64, error), meterEnergy func() (float64, error), phaseCurrents func() (float64, float64, float64, error), phaseSwitcher func(int) error, phaseGetter func() (int, error), identifier func() (string, error), statusReasoner func() (api.Reason, error)) api.Charger { + caps := make(map[reflect.Type]any) + + if meter != nil { + caps[reflect.TypeFor[api.Meter]()] = &decorateSmartEVSE3MeterImpl{meter: meter} + } + + if meterEnergy != nil { + caps[reflect.TypeFor[api.MeterEnergy]()] = &decorateSmartEVSE3MeterEnergyImpl{meterEnergy: meterEnergy} + } + + if phaseCurrents != nil { + caps[reflect.TypeFor[api.PhaseCurrents]()] = &decorateSmartEVSE3PhaseCurrentsImpl{phaseCurrents: phaseCurrents} + } + + if phaseSwitcher != nil { + caps[reflect.TypeFor[api.PhaseSwitcher]()] = &decorateSmartEVSE3PhaseSwitcherImpl{phaseSwitcher: phaseSwitcher} + } + + if phaseGetter != nil { + caps[reflect.TypeFor[api.PhaseGetter]()] = &decorateSmartEVSE3PhaseGetterImpl{phaseGetter: phaseGetter} + } + + if identifier != nil { + caps[reflect.TypeFor[api.Identifier]()] = &decorateSmartEVSE3IdentifierImpl{identifier: identifier} + } + + if statusReasoner != nil { + caps[reflect.TypeFor[api.StatusReasoner]()] = &decorateSmartEVSE3StatusReasonerImpl{statusReasoner: statusReasoner} + } + + if len(caps) == 0 { + return base + } + + return &decorateSmartEVSE3Capable{SmartEVSE3: base, caps: caps} +} + +type decorateSmartEVSE3Capable struct { + *SmartEVSE3 + caps map[reflect.Type]any +} + +func (d *decorateSmartEVSE3Capable) Capability(typ reflect.Type) (any, bool) { + c, ok := d.caps[typ] + if !ok && reflect.TypeOf(d).Implements(typ) { + return d, true + } + return c, ok +} + +type decorateSmartEVSE3IdentifierImpl struct { + identifier func() (string, error) +} + +func (impl *decorateSmartEVSE3IdentifierImpl) Identify() (string, error) { + return impl.identifier() +} + +type decorateSmartEVSE3MeterImpl struct { + meter func() (float64, error) +} + +func (impl *decorateSmartEVSE3MeterImpl) CurrentPower() (float64, error) { + return impl.meter() +} + +type decorateSmartEVSE3MeterEnergyImpl struct { + meterEnergy func() (float64, error) +} + +func (impl *decorateSmartEVSE3MeterEnergyImpl) TotalEnergy() (float64, error) { + return impl.meterEnergy() +} + +type decorateSmartEVSE3PhaseCurrentsImpl struct { + phaseCurrents func() (float64, float64, float64, error) +} + +func (impl *decorateSmartEVSE3PhaseCurrentsImpl) Currents() (float64, float64, float64, error) { + return impl.phaseCurrents() +} + +type decorateSmartEVSE3PhaseGetterImpl struct { + phaseGetter func() (int, error) +} + +func (impl *decorateSmartEVSE3PhaseGetterImpl) GetPhases() (int, error) { + return impl.phaseGetter() +} + +type decorateSmartEVSE3PhaseSwitcherImpl struct { + phaseSwitcher func(int) error +} + +func (impl *decorateSmartEVSE3PhaseSwitcherImpl) Phases1p3p(p0 int) error { + return impl.phaseSwitcher(p0) +} + +type decorateSmartEVSE3StatusReasonerImpl struct { + statusReasoner func() (api.Reason, error) +} + +func (impl *decorateSmartEVSE3StatusReasonerImpl) StatusReason() (api.Reason, error) { + return impl.statusReasoner() +} diff --git a/templates/definition/charger/smart-evse.yaml b/templates/definition/charger/smart-evse.yaml new file mode 100644 index 000000000..c15db7708 --- /dev/null +++ b/templates/definition/charger/smart-evse.yaml @@ -0,0 +1,24 @@ +template: smart-evse +products: + - brand: Stegen + description: + generic: Smart Evse v3 / v3.5 +capabilities: ["mA", "rfid", "1p3p"] +requirements: + evcc: ["sponsorship"] + description: + de: | + Anbindung über die REST API der Smart Evse Firmware (FW >= 3.6.0). + evcc setzt das Gerät in den NORMAL Modus und steuert den Ladestrom über `override_current`. + en: | + Uses the REST API of the Smart Evse firmware (FW >= 3.6.0). + evcc puts the device into NORMAL mode and controls the charge current via `override_current`. +params: + - name: host + - name: cache + default: 1s + advanced: true +render: | + type: smart-evse + uri: http://{{ .host }} + cache: {{ .cache }} From acaac1173efa368463a2618a799ef9f23879660d Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Mon, 13 Apr 2026 10:25:20 +0200 Subject: [PATCH 0026/1293] Docker: fix legacy browser assets (#29068) --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index 501c24974..53f9d1a1b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,6 +13,7 @@ RUN --mount=type=cache,target=/root/.npm npm ci COPY Makefile . COPY *.js ./ COPY *.ts *.mts ./ +COPY .browserslistrc . COPY assets assets COPY i18n i18n From 7ffe07af5420875e98425cdbc8f685d6e5a385b9 Mon Sep 17 00:00:00 2001 From: andig Date: Mon, 13 Apr 2026 10:50:09 +0200 Subject: [PATCH 0027/1293] chore: lessen bc --- .../definition/meter/goodwe-wifi-et.yaml | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/templates/definition/meter/goodwe-wifi-et.yaml b/templates/definition/meter/goodwe-wifi-et.yaml index 0fdb7121f..12e32fa0c 100644 --- a/templates/definition/meter/goodwe-wifi-et.yaml +++ b/templates/definition/meter/goodwe-wifi-et.yaml @@ -5,27 +5,25 @@ products: - brand: GoodWe description: generic: ET/EH/BT/BH Hybrid Inverter (WiFi) - params: - name: usage choice: ["grid", "pv", "battery"] - - name: host - example: 192.168.1.26 - + - name: uri + deprecated: true render: | type: custom {{- if eq .usage "grid" }} power: source: aa55udp - host: {{ .host }} + host: {{ or .host .uri }} id: 247 register: 35139 count: 2 decode: int32be energy: source: aa55udp - host: {{ .host }} + host: {{ or .host .uri }} id: 247 register: 36017 count: 2 @@ -35,14 +33,14 @@ render: | {{- if eq .usage "pv" }} power: source: aa55udp - host: {{ .host }} + host: {{ or .host .uri }} id: 247 register: 35137 count: 2 decode: int32be energy: source: aa55udp - host: {{ .host }} + host: {{ or .host .uri }} id: 247 register: 35191 count: 2 @@ -52,14 +50,14 @@ render: | {{- if eq .usage "battery" }} power: source: aa55udp - host: {{ .host }} + host: {{ or .host .uri }} id: 247 register: 35182 count: 2 decode: int32be energy: source: aa55udp - host: {{ .host }} + host: {{ or .host .uri }} id: 247 register: 35209 count: 2 @@ -67,7 +65,7 @@ render: | scale: 0.1 soc: source: aa55udp - host: {{ .host }} + host: {{ or .host .uri }} id: 247 register: 37007 count: 1 From db11f1d7c5ca828a0534634743b203e118450eba Mon Sep 17 00:00:00 2001 From: Maschga <88616799+Maschga@users.noreply.github.com> Date: Mon, 13 Apr 2026 13:08:53 +0200 Subject: [PATCH 0028/1293] Config UI: make circuits configurable (#27842) --- charger/charger.go | 2 +- cmd/setup.go | 75 +++++++++++-- core/circuit/circuit.go | 12 +- core/circuit/config.go | 28 +++++ core/circuit/template.go | 21 ++++ messenger/messenger.go | 2 +- meter/meter.go | 2 +- server/http_config_device_handler.go | 12 +- templates/definition/circuit/static.yaml | 33 ++++++ templates/definition/embed.go | 2 +- tests/config-circuit-device.spec.ts | 133 +++++++++++++++++++++++ util/templates/defaults.yaml | 1 + util/templates/init.go | 2 +- vehicle/vehicle.go | 2 +- 14 files changed, 302 insertions(+), 25 deletions(-) create mode 100644 core/circuit/template.go create mode 100644 templates/definition/circuit/static.yaml create mode 100644 tests/config-circuit-device.spec.ts diff --git a/charger/charger.go b/charger/charger.go index 73ec87914..8452ece0d 100644 --- a/charger/charger.go +++ b/charger/charger.go @@ -27,7 +27,7 @@ func init() { //go:generate go tool decorate -f decorateCustom -b *Charger -r api.Charger -t api.ChargerEx,api.Identifier,api.PhaseSwitcher,api.Resurrector,api.Battery,api.SocLimiter,api.Meter,api.MeterEnergy,api.PhaseCurrents,api.PhaseVoltages -// NewConfigurableFromConfig creates a new configurable charger +// NewConfigurableFromConfig creates a new charger from config func NewConfigurableFromConfig(ctx context.Context, other map[string]any) (api.Charger, error) { var cc struct { embed `mapstructure:",squash"` diff --git a/cmd/setup.go b/cmd/setup.go index e1574da8b..a923e3d39 100644 --- a/cmd/setup.go +++ b/cmd/setup.go @@ -87,6 +87,7 @@ var yamlSource struct { eebus globalconfig.YamlSource tariffs globalconfig.YamlSource messaging globalconfig.YamlSource + circuits globalconfig.YamlSource } var nameRE = regexp.MustCompile(`^[a-zA-Z0-9_.:-]+$`) @@ -157,19 +158,56 @@ func isWritable(filePath string) bool { } func configureCircuits(conf *[]config.Named) error { - // migrate settings + // yaml config from file + if len(*conf) != 0 { + yamlSource.circuits = globalconfig.YamlSourceFile + } + + // yaml config from db (deprecated) if settings.Exists(keys.Circuits) { + if yamlSource.circuits == globalconfig.YamlSourceFile { + // just warn, no error to not break previous behavior + log.WARN.Println("circuits configured via UI yaml; evcc.yaml config will be ignored") + } *conf = []config.Named{} if err := settings.Yaml(keys.Circuits, new([]map[string]any), &conf); err != nil { return err } + yamlSource.circuits = globalconfig.YamlSourceDb } - children := slices.Clone(*conf) + // load configCircuits devices from database + configurable, err := config.ConfigurationsByClass(templates.Circuit) + if err != nil { + return err + } + // device config from db + if yamlSource.circuits != globalconfig.YamlSourceNone && len(configurable) > 0 { + return errors.New("circuits are configured via UI; having an additional yaml config is not allowed") + } + + if err := validateStaticCircuits(slices.Clone(*conf)); err != nil { + return err + } + if err := validateConfigurableCircuits(configurable); err != nil { + return err + } + + for _, c := range configurable { + *conf = append(*conf, c.Named()) + } + + return nil +} + +// validateCircuitConfigs validates circuit configurations with support for both static and configurable types +func validateCircuitConfigs[T any](children []T, getNamedConfig func(T) config.Named, getDevice func(T, api.Circuit) config.Device[api.Circuit]) error { // TODO check for circular references NEXT: - for i, cc := range children { + for i, child := range children { + cc := getNamedConfig(child) + if cc.Name == "" { return fmt.Errorf("cannot create circuit: missing name") } @@ -184,14 +222,14 @@ NEXT: } } - log := util.NewLogger("circuit-" + cc.Name) - props, err := customDevice(cc.Other) if err != nil { return fmt.Errorf("cannot decode custom circuit '%s': %w", cc.Name, err) } - instance, err := circuit.NewFromConfig(context.TODO(), log, props) + ctx := util.WithLogger(context.TODO(), util.NewLogger(cc.Name)) + + instance, err := circuit.NewFromConfig(ctx, cc.Type, props) if err != nil { return fmt.Errorf("cannot create circuit '%s': %w", cc.Name, err) } @@ -202,7 +240,7 @@ NEXT: instance.SetTitle(strings.Title(cc.Name)) } - if err := config.Circuits().Add(config.NewStaticDevice(cc, instance)); err != nil { + if err := config.Circuits().Add(getDevice(child, instance)); err != nil { return err } @@ -211,7 +249,8 @@ NEXT: } if len(children) > 0 { - return fmt.Errorf("circuit is missing parent: %s", children[0].Name) + cn := getNamedConfig(children[0]) + return fmt.Errorf("circuit is missing parent: %s", cn.Name) } var rootFound bool @@ -233,6 +272,26 @@ NEXT: return nil } +func validateStaticCircuits(children []config.Named) error { + return validateCircuitConfigs( + children, + func(cc config.Named) config.Named { return cc }, + func(cc config.Named, instance api.Circuit) config.Device[api.Circuit] { + return config.NewStaticDevice(cc, instance) + }, + ) +} + +func validateConfigurableCircuits(children []config.Config) error { + return validateCircuitConfigs( + children, + func(cc config.Config) config.Named { return cc.Named() }, + func(cc config.Config, instance api.Circuit) config.Device[api.Circuit] { + return config.NewConfigurableDevice(&cc, instance) + }, + ) +} + type newFromConfFunc[T any] func(context.Context, string, map[string]any) (T, error) func staticInstance[T any](typ string, cc config.Named, newFromConf newFromConfFunc[T], h config.Handler[T]) error { diff --git a/core/circuit/circuit.go b/core/circuit/circuit.go index eedd8f187..d31670bdf 100644 --- a/core/circuit/circuit.go +++ b/core/circuit/circuit.go @@ -43,8 +43,12 @@ type Circuit struct { powerUpdated time.Time } -// NewFromConfig creates a new Circuit -func NewFromConfig(ctx context.Context, log *util.Logger, other map[string]any) (api.Circuit, error) { +func init() { + registry.AddCtx(api.Custom, NewConfigurableFromConfig) +} + +// NewConfigurableFromConfig creates a new circuit from config +func NewConfigurableFromConfig(ctx context.Context, other map[string]any) (api.Circuit, error) { cc := struct { Title string // title ParentRef string `mapstructure:"parent"` // parent circuit reference @@ -58,6 +62,9 @@ func NewFromConfig(ctx context.Context, log *util.Logger, other map[string]any) Timeout: time.Minute, } + // drop circuit type- all circuits are custom + delete(other, "type") + if err := util.DecodeOther(other, &cc); err != nil { return nil, err } @@ -74,6 +81,7 @@ func NewFromConfig(ctx context.Context, log *util.Logger, other map[string]any) } } + log := util.ContextLoggerWithDefault(ctx, util.NewLogger("circuit")) circuit, err := New(log, cc.Title, cc.MaxCurrent, cc.MaxPower, meter, cc.Timeout) if err != nil { return nil, err diff --git a/core/circuit/config.go b/core/circuit/config.go index 30af21a74..013f8e686 100644 --- a/core/circuit/config.go +++ b/core/circuit/config.go @@ -1,10 +1,38 @@ package circuit import ( + "context" + "fmt" + "strings" + "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/config" + reg "github.com/evcc-io/evcc/util/registry" ) +var registry = reg.New[api.Circuit]("circuit") + +// NewFromConfig creates api.Circuit from configuration +func NewFromConfig(ctx context.Context, typ string, other map[string]any) (api.Circuit, error) { + // treat any non-template circuit as custom in order for registry lookup to work + if typ == "" { + typ = api.Custom + } + + factory, err := registry.Get(strings.ToLower(typ)) + if err != nil { + return nil, err + } + + v, err := factory(ctx, other) + if err != nil { + err = fmt.Errorf("cannot create circuit type '%s': %w", util.TypeWithTemplateName(typ, other), err) + } + + return v, err +} + func Root() api.Circuit { for _, dev := range config.Circuits().Devices() { if c := dev.Instance(); c.GetParent() == nil { diff --git a/core/circuit/template.go b/core/circuit/template.go new file mode 100644 index 000000000..7f6d68f25 --- /dev/null +++ b/core/circuit/template.go @@ -0,0 +1,21 @@ +package circuit + +import ( + "context" + + "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/util/templates" +) + +func init() { + registry.AddCtx("template", NewFromTemplateConfig) +} + +func NewFromTemplateConfig(ctx context.Context, other map[string]any) (api.Circuit, error) { + instance, err := templates.RenderInstance(templates.Circuit, other) + if err != nil { + return nil, err + } + + return NewConfigurableFromConfig(ctx, instance.Other) +} diff --git a/messenger/messenger.go b/messenger/messenger.go index 59e6dc613..81bbf24e9 100644 --- a/messenger/messenger.go +++ b/messenger/messenger.go @@ -16,7 +16,7 @@ func init() { registry.AddCtx(api.Custom, NewConfigurableFromConfig) } -// NewConfigurableFromConfig creates Messenger from config +// NewConfigurableFromConfig creates a new messenger from config func NewConfigurableFromConfig(ctx context.Context, other map[string]any) (api.Messenger, error) { var cc struct { Send plugin.Config diff --git a/meter/meter.go b/meter/meter.go index 12a5290fd..6194f2018 100644 --- a/meter/meter.go +++ b/meter/meter.go @@ -24,7 +24,7 @@ func init() { //evcc:basetype api.Meter //evcc:types api.MeterEnergy,api.Battery,api.BatteryCapacity,api.BatterySocLimiter,api.BatteryPowerLimiter,api.BatteryController -// NewConfigurableFromConfig creates api.Meter from config +// NewConfigurableFromConfig creates a new meter from config func NewConfigurableFromConfig(ctx context.Context, other map[string]any) (api.Meter, error) { cc := struct { measurement.Energy `mapstructure:",squash"` // energy optional diff --git a/server/http_config_device_handler.go b/server/http_config_device_handler.go index 391c28158..9e0eace53 100644 --- a/server/http_config_device_handler.go +++ b/server/http_config_device_handler.go @@ -11,7 +11,6 @@ import ( "strconv" "dario.cat/mergo" - "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/api/globalconfig" "github.com/evcc-io/evcc/charger" "github.com/evcc-io/evcc/core/circuit" @@ -21,7 +20,6 @@ import ( "github.com/evcc-io/evcc/meter" "github.com/evcc-io/evcc/server/db/settings" "github.com/evcc-io/evcc/tariff" - "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/config" "github.com/evcc-io/evcc/util/templates" "github.com/evcc-io/evcc/vehicle" @@ -327,9 +325,7 @@ func newDeviceHandler(w http.ResponseWriter, r *http.Request) { conf, err = newDevice(ctx, class, req, vehicle.NewFromConfig, config.Vehicles(), force) case templates.Circuit: - conf, err = newDevice(ctx, class, req, func(ctx context.Context, _ string, other map[string]any) (api.Circuit, error) { - return circuit.NewFromConfig(ctx, util.NewLogger("circuit"), other) - }, config.Circuits(), force) + conf, err = newDevice(ctx, class, req, circuit.NewFromConfig, config.Circuits(), force) case templates.Tariff: conf, err = newDevice(ctx, class, req, tariff.NewFromConfig, config.Tariffs(), force) @@ -414,9 +410,7 @@ func updateDeviceHandler(w http.ResponseWriter, r *http.Request) { err = updateDevice(ctx, id, class, req, vehicle.NewFromConfig, config.Vehicles(), force) case templates.Circuit: - err = updateDevice(ctx, id, class, req, func(ctx context.Context, _ string, other map[string]any) (api.Circuit, error) { - return circuit.NewFromConfig(ctx, util.NewLogger("circuit"), other) - }, config.Circuits(), force) + err = updateDevice(ctx, id, class, req, circuit.NewFromConfig, config.Circuits(), force) case templates.Tariff: err = updateDevice(ctx, id, class, req, tariff.NewFromConfig, config.Tariffs(), force) @@ -670,7 +664,7 @@ func testConfigHandler(w http.ResponseWriter, r *http.Request) { instance, err = testConfig(ctx, id, class, req, vehicle.NewFromConfig, config.Vehicles()) case templates.Circuit: - err = api.ErrNotAvailable + instance, err = testConfig(ctx, id, class, req, circuit.NewFromConfig, config.Circuits()) case templates.Tariff: instance, err = testConfig(ctx, id, class, req, tariff.NewFromConfig, config.Tariffs()) diff --git a/templates/definition/circuit/static.yaml b/templates/definition/circuit/static.yaml new file mode 100644 index 000000000..b83f2e594 --- /dev/null +++ b/templates/definition/circuit/static.yaml @@ -0,0 +1,33 @@ +template: static +products: + - description: + en: Static circuit + de: Statischer Stromkreis +params: + - name: title + required: true + example: Main circuit + - name: maxcurrent + help: + de: Vom Lastmanagement maximal erlaubte Stromstärke pro Phase. + en: Maximum current per phase allowed by the load management system. + - name: maxpower + description: + de: Maximale Leistung + en: Maximum power + help: + de: Vom Lastmanagement maximal erlaubte Leistung. Das System verteilt verfügbare Leistung dynamisch auf alle angeschlossenen Ladepunkte und Schaltungen. + en: Maximum power allowed by the load management system. The system dynamically distributes available power across all connected chargers and circuits. + - name: meter + # service: TODO + - name: parent + description: + de: Übergeordneter Stromkreis + en: Parent circuit +render: | + type: custom + title: {{ .title }} + maxcurrent: {{ .maxcurrent }} + maxpower: {{ .maxpower }} + meter: {{ .meter }} + parent: {{ .parent }} diff --git a/templates/definition/embed.go b/templates/definition/embed.go index e7e925872..4a147796b 100644 --- a/templates/definition/embed.go +++ b/templates/definition/embed.go @@ -2,5 +2,5 @@ package definition import "embed" -//go:embed charger/*.yaml meter/*.yaml vehicle/*.yaml tariff/*.yaml messenger/*.yaml +//go:embed charger/*.yaml meter/*.yaml vehicle/*.yaml tariff/*.yaml messenger/*.yaml circuit/*.yaml var YamlTemplates embed.FS diff --git a/tests/config-circuit-device.spec.ts b/tests/config-circuit-device.spec.ts new file mode 100644 index 000000000..2c4ff42f5 --- /dev/null +++ b/tests/config-circuit-device.spec.ts @@ -0,0 +1,133 @@ +// Temporary API-level tests to validate circuit device CRUD endpoints. +// Will be replaced by UI-based tests once the circuit configuration UI is implemented. + +import { test, expect } from "@playwright/test"; +import { start, stop, restart, baseUrl } from "./evcc"; + +test.use({ baseURL: baseUrl() }); +test.describe.configure({ mode: "parallel" }); + +test.beforeEach(async () => { + await start(); +}); + +test.afterEach(async () => { + await stop(); +}); + +test.describe("circuit device api", () => { + test("crud lifecycle and restart persistence", async ({ page }) => { + // create root circuit + const createRoot = await page.request.post("/api/config/devices/circuit", { + data: { + type: "template", + template: "static", + title: "Main", + maxcurrent: 32, + }, + }); + expect(createRoot.status()).toBe(200); + const root = await createRoot.json(); + expect(root).toHaveProperty("id"); + expect(root).toHaveProperty("name"); + + // create child circuit with parent + const createChild = await page.request.post("/api/config/devices/circuit", { + data: { + type: "template", + template: "static", + title: "Garage", + maxcurrent: 16, + parent: root.name, + }, + }); + expect(createChild.status()).toBe(200); + const child = await createChild.json(); + expect(child).toHaveProperty("id"); + + // list circuits + const list = await page.request.get("/api/config/devices/circuit"); + expect(list.status()).toBe(200); + const listBody = await list.json(); + expect(listBody).toHaveLength(2); + + // get single + const get = await page.request.get(`/api/config/devices/circuit/${root.id}`); + expect(get.status()).toBe(200); + const getBody = await get.json(); + expect(getBody).toHaveProperty("type", "template"); + expect(getBody).toHaveProperty("name", root.name); + + // update + const update = await page.request.put(`/api/config/devices/circuit/${root.id}`, { + data: { + type: "template", + template: "static", + title: "Main Updated", + maxcurrent: 48, + }, + }); + expect(update.status()).toBe(200); + + // verify update + const getUpdated = await page.request.get(`/api/config/devices/circuit/${root.id}`); + const updatedBody = await getUpdated.json(); + expect(updatedBody.config.title).toBe("Main Updated"); + expect(updatedBody.config.maxcurrent).toBe(48); + + // delete child + const del = await page.request.delete(`/api/config/devices/circuit/${child.id}`); + expect(del.status()).toBe(200); + + // verify deleted + const listAfter = await page.request.get("/api/config/devices/circuit"); + const listAfterBody = await listAfter.json(); + expect(listAfterBody).toHaveLength(1); + + // restart and verify persistence + await restart(); + + const listRestart = await page.request.get("/api/config/devices/circuit"); + expect(listRestart.status()).toBe(200); + const restartBody = await listRestart.json(); + expect(restartBody).toHaveLength(1); + }); + + test("error handling", async ({ page }) => { + // get non-existent circuit + const get = await page.request.get("/api/config/devices/circuit/99"); + expect(get.status()).toBe(400); + + // delete non-existent circuit + const del = await page.request.delete("/api/config/devices/circuit/99"); + expect(del.status()).toBe(400); + + // create with invalid template + const invalid = await page.request.post("/api/config/devices/circuit", { + data: { + type: "template", + template: "nonexistent", + title: "Bad", + }, + }); + expect(invalid.ok()).toBeFalsy(); + }); + + test("custom type circuit", async ({ page }) => { + // create circuit via custom type with raw YAML + const create = await page.request.post("/api/config/devices/circuit", { + data: { + type: "custom", + yaml: "type: custom\ntitle: Custom Circuit\nmaxCurrent: 24\n", + }, + }); + expect(create.status()).toBe(200); + const result = await create.json(); + expect(result).toHaveProperty("id"); + + // verify it exists + const list = await page.request.get("/api/config/devices/circuit"); + const body = await list.json(); + expect(body).toHaveLength(1); + }); +}); diff --git a/util/templates/defaults.yaml b/util/templates/defaults.yaml index 0900c5bc2..698cd85e5 100644 --- a/util/templates/defaults.yaml +++ b/util/templates/defaults.yaml @@ -182,6 +182,7 @@ params: de: Definiert die maximale Ladeleistung des Fahrzeugs. Hilft, die Ladeplanung zu verbessern, wenn das Fahrzeug üblicherweise weniger Strom nutzt als angeboten oder höhere Ströme bei einphasigem Laden erlaubt als bei dreiphasigem. _Der bereitgestellte Strom des Ladepunktes wird nicht beeinflusst._ en: Defines the maximum charging power of the vehicle. Helps improve charge plan accuracy when the vehicle typically uses less than the offered current or supports higher single-phase current compared to three-phase. _The offered current of the loadpoint is not affected._ type: int + example: 10000 - name: identifiers description: de: Identifikation diff --git a/util/templates/init.go b/util/templates/init.go index aad947df5..9fbfa53bb 100644 --- a/util/templates/init.go +++ b/util/templates/init.go @@ -33,7 +33,7 @@ func init() { baseTmpl = template.Must(FuncMap(template.New("base")).ParseFS(includeFS, "includes/*.tpl")) - for _, class := range []Class{Charger, Meter, Vehicle, Tariff, Messenger} { + for _, class := range []Class{Charger, Meter, Vehicle, Tariff, Messenger, Circuit} { load(class) } } diff --git a/vehicle/vehicle.go b/vehicle/vehicle.go index 37f80a4f2..5da4c7ab9 100644 --- a/vehicle/vehicle.go +++ b/vehicle/vehicle.go @@ -23,7 +23,7 @@ func init() { registry.AddCtx(api.Custom, NewConfigurableFromConfig) } -// NewConfigurableFromConfig creates a new Vehicle +// NewConfigurableFromConfig creates a new vehicle from config func NewConfigurableFromConfig(ctx context.Context, other map[string]any) (api.Vehicle, error) { var cc struct { embed `mapstructure:",squash"` From 4b9474481a28fb64d27d3679541e62736caa5d89 Mon Sep 17 00:00:00 2001 From: andig Date: Mon, 13 Apr 2026 13:55:07 +0200 Subject: [PATCH 0029/1293] Kostal Pico MP: add grid (#29077) --- templates/definition/meter/kostal-piko-mp-plus.yaml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/templates/definition/meter/kostal-piko-mp-plus.yaml b/templates/definition/meter/kostal-piko-mp-plus.yaml index a279ba79b..13be22a84 100644 --- a/templates/definition/meter/kostal-piko-mp-plus.yaml +++ b/templates/definition/meter/kostal-piko-mp-plus.yaml @@ -8,14 +8,19 @@ products: generic: coolcept fleX params: - name: usage - choice: ["pv"] + choice: ["grid", "pv"] - name: host render: | type: custom power: {{- if eq .usage "pv" }} - # PV source: http uri: http://{{ .host }}/measurements.xml jq: .root.Device.Measurements.Measurement[] | select(.attrType == "AC_Power") | if has ("attrValue") then .attrValue else "0" end | tonumber {{- end }} + {{- if eq .usage "grid" }} + source: http + uri: http://{{ .host }}/measurements.xml + jq: .root.Device.Measurements.Measurement[] | select(.attrType == "GridPower") | if has ("attrValue") then .attrValue else "0" end | tonumber + scale: -1 + {{- end }} From ffab2c87212e2d5645cd741025a7f977c76b08c2 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 16:31:52 +0200 Subject: [PATCH 0030/1293] Config UI: don't show "Logout" when started with --disable-auth (#29074) --- assets/js/components/Top/Navigation.vue | 3 ++- tests/auth.spec.ts | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/assets/js/components/Top/Navigation.vue b/assets/js/components/Top/Navigation.vue index 3efc6d1d9..3db3845c3 100644 --- a/assets/js/components/Top/Navigation.vue +++ b/assets/js/components/Top/Navigation.vue @@ -167,6 +167,7 @@ export default defineComponent({ evopt: { type: Object as PropType, required: false }, fatal: { type: Array as PropType, default: () => [] }, experimental: Boolean, + authDisabled: Boolean, }, emits: ["auth-required"], data() { @@ -217,7 +218,7 @@ export default defineComponent({ return !!this.evopt && this.experimental; }, showLogout() { - return isLoggedIn(); + return !this.authDisabled && isLoggedIn(); }, }, mounted() { diff --git a/tests/auth.spec.ts b/tests/auth.spec.ts index 28d8121cd..67edf764d 100644 --- a/tests/auth.spec.ts +++ b/tests/auth.spec.ts @@ -172,6 +172,7 @@ test("disable auth", async ({ page }) => { // configuration page without login await openTopNavigation(page); + await expect(page.getByRole("button", { name: "Logout" })).not.toBeVisible(); await page.getByRole("link", { name: "Configuration" }).click(); await expectTopNavigationClosed(page); await expect(page.getByRole("heading", { name: "Configuration" })).toBeVisible(); From c7e3e486b9a9bd2bd59cdea4334722cd43aaa610 Mon Sep 17 00:00:00 2001 From: andig Date: Mon, 13 Apr 2026 16:36:44 +0200 Subject: [PATCH 0031/1293] chore: fix bottom menu --- assets/js/components/BottomTabs/MoreMenu.vue | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/assets/js/components/BottomTabs/MoreMenu.vue b/assets/js/components/BottomTabs/MoreMenu.vue index d36d00cb8..53b374709 100644 --- a/assets/js/components/BottomTabs/MoreMenu.vue +++ b/assets/js/components/BottomTabs/MoreMenu.vue @@ -77,6 +77,7 @@ export default defineComponent({ sponsor: { type: Object as PropType, default: () => ({}) }, fatal: { type: Array as PropType, default: () => [] }, experimental: Boolean, + authDisabled: Boolean, evopt: { type: Object as PropType, required: false }, installed: String, commit: String, @@ -120,7 +121,7 @@ export default defineComponent({ return !!this.evopt && this.experimental; }, showLogout() { - return isLoggedIn(); + return !this.authDisabled && isLoggedIn(); }, }, mounted() { From 269316887ee63f87d97bcad648ae59de9f61b04e Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 17:09:14 +0200 Subject: [PATCH 0032/1293] Improve config UI dump key ordering: type before config, template first in config (#29081) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: andig <184815+andig@users.noreply.github.com> --- assets/js/components/Issue/format.test.ts | 37 +++++++++++++++++++++++ assets/js/components/Issue/format.ts | 2 +- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/assets/js/components/Issue/format.test.ts b/assets/js/components/Issue/format.test.ts index 70dc81d0b..bb237927f 100644 --- a/assets/js/components/Issue/format.test.ts +++ b/assets/js/components/Issue/format.test.ts @@ -56,6 +56,43 @@ describe("formatJson", () => { }`); }); + it("sorts type before config", () => { + const obj = { + name: "db:9", + config: { host: "192.168.1.1" }, + deviceProduct: "RCT Power", + type: "template", + id: 9, + }; + const result = formatJson(obj); + + expect(result).toBe(`{ + "id": 9, + "name": "db:9", + "type": "template", + "config": {"host":"192.168.1.1"}, + "deviceProduct": "RCT Power" +}`); + }); + + it("sorts template first inside config", () => { + const obj = { + other: { gamma: 3 }, + config: { cache: 37000000000, host: "192.168.1.1", template: "rct-power", usage: "pv" }, + }; + const result = formatJson(obj, ["config"]); + + expect(result).toBe(`{ + "config": { + "template": "rct-power", + "cache": 37000000000, + "host": "192.168.1.1", + "usage": "pv" + }, + "other": {"gamma":3} +}`); + }); + it("keeps nested objects in arrays single-lined with sorted keys", () => { const obj = { items: [ diff --git a/assets/js/components/Issue/format.ts b/assets/js/components/Issue/format.ts index 9024f1df1..fdf168ab3 100644 --- a/assets/js/components/Issue/format.ts +++ b/assets/js/components/Issue/format.ts @@ -1,5 +1,5 @@ function sortedEntries(obj: object): [string, any][] { - const firstKeys = ["id", "name"]; + const firstKeys = ["id", "name", "type", "template"]; return Object.entries(obj).sort(([a], [b]) => { const ai = firstKeys.indexOf(a); const bi = firstKeys.indexOf(b); From 2dbd1a50386bab0331efd721961fbb0f89e302f2 Mon Sep 17 00:00:00 2001 From: andig Date: Mon, 13 Apr 2026 17:30:31 +0200 Subject: [PATCH 0033/1293] Custom devices: add device title to logger name (#29075) --- cmd/setup.go | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/cmd/setup.go b/cmd/setup.go index a923e3d39..401a6057c 100644 --- a/cmd/setup.go +++ b/cmd/setup.go @@ -202,11 +202,11 @@ func configureCircuits(conf *[]config.Named) error { } // validateCircuitConfigs validates circuit configurations with support for both static and configurable types -func validateCircuitConfigs[T any](children []T, getNamedConfig func(T) config.Named, getDevice func(T, api.Circuit) config.Device[api.Circuit]) error { +func validateCircuitConfigs[T any](children []T, getConfigAndLogger func(T) (config.Named, *util.Logger), getDevice func(T, api.Circuit) config.Device[api.Circuit]) error { // TODO check for circular references NEXT: for i, child := range children { - cc := getNamedConfig(child) + cc, log := getConfigAndLogger(child) if cc.Name == "" { return fmt.Errorf("cannot create circuit: missing name") @@ -227,7 +227,7 @@ NEXT: return fmt.Errorf("cannot decode custom circuit '%s': %w", cc.Name, err) } - ctx := util.WithLogger(context.TODO(), util.NewLogger(cc.Name)) + ctx := util.WithLogger(context.TODO(), log) instance, err := circuit.NewFromConfig(ctx, cc.Type, props) if err != nil { @@ -249,7 +249,7 @@ NEXT: } if len(children) > 0 { - cn := getNamedConfig(children[0]) + cn, _ := getConfigAndLogger(children[0]) return fmt.Errorf("circuit is missing parent: %s", cn.Name) } @@ -275,7 +275,7 @@ NEXT: func validateStaticCircuits(children []config.Named) error { return validateCircuitConfigs( children, - func(cc config.Named) config.Named { return cc }, + func(cc config.Named) (config.Named, *util.Logger) { return cc, util.NewLogger(cc.Name) }, func(cc config.Named, instance api.Circuit) config.Device[api.Circuit] { return config.NewStaticDevice(cc, instance) }, @@ -285,7 +285,7 @@ func validateStaticCircuits(children []config.Named) error { func validateConfigurableCircuits(children []config.Config) error { return validateCircuitConfigs( children, - func(cc config.Config) config.Named { return cc.Named() }, + func(cc config.Config) (config.Named, *util.Logger) { return cc.Named(), loggerForConfig(&cc) }, func(cc config.Config, instance api.Circuit) config.Device[api.Circuit] { return config.NewConfigurableDevice(&cc, instance) }, @@ -314,9 +314,18 @@ func staticInstance[T any](typ string, cc config.Named, newFromConf newFromConfF return err //nolint:govet } +// loggerForConfig creates a logger with sensible name for (custom) configurable device +func loggerForConfig(conf *config.Config) *util.Logger { + res := conf.Named().Name + if t := conf.Title; t != "" && t != res { + res += "-" + t + } + return util.NewLogger(res) +} + func configurableInstance[T any](typ string, conf *config.Config, newFromConf newFromConfFunc[T], h config.Handler[T]) error { cc := conf.Named() - ctx, cancel := context.WithCancel(util.WithLogger(context.TODO(), util.NewLogger(cc.Name))) //nolint:govet + ctx, cancel := context.WithCancel(util.WithLogger(context.TODO(), loggerForConfig(conf))) //nolint:govet props, err := customDevice(cc.Other) if err != nil { From 884f1834f9ecbbab8a9866bfd9167e0206f68737 Mon Sep 17 00:00:00 2001 From: MobilettoSoft Date: Mon, 13 Apr 2026 19:48:58 +0200 Subject: [PATCH 0034/1293] PUN tariff: don't fail if next day data is not available (#29092) --- tariff/pun.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tariff/pun.go b/tariff/pun.go index a60f0fd88..85c9baaae 100644 --- a/tariff/pun.go +++ b/tariff/pun.go @@ -91,12 +91,15 @@ func (t *Pun) run(done chan error) { continue } - // get tomorrow data + // get tomorrow data (may not be available before ~13:00 CET) res, err := backoff.RetryWithData(func() (api.Rates, error) { res, err := t.getData(time.Now().AddDate(0, 0, 1)) + if errors.Is(err, ErrPunDataNotAvailable) { + return res, backoff.Permanent(err) + } return res, backoffPermanentError(err) }, bo()) - if err != nil { + if err != nil && !errors.Is(err, ErrPunDataNotAvailable) { once.Do(func() { done <- err }) t.log.ERROR.Println(err) continue From 9f39818386a20242570e8846ad1bc478a126a4c5 Mon Sep 17 00:00:00 2001 From: Sebastian Schrittwieser <10422786+sschritt@users.noreply.github.com> Date: Mon, 13 Apr 2026 22:17:04 +0200 Subject: [PATCH 0035/1293] Config UI: don't show "Logout" when started with --disable-auth (#29096) --- assets/js/components/BottomTabs/Bar.vue | 2 ++ assets/js/components/BottomTabs/MoreItem.vue | 2 ++ 2 files changed, 4 insertions(+) diff --git a/assets/js/components/BottomTabs/Bar.vue b/assets/js/components/BottomTabs/Bar.vue index 2c4bdb468..f29cb21a4 100644 --- a/assets/js/components/BottomTabs/Bar.vue +++ b/assets/js/components/BottomTabs/Bar.vue @@ -32,6 +32,7 @@ :sponsor="sponsor" :fatal="fatal" :experimental="experimental" + :auth-disabled="authDisabled" :evopt="evopt" :installed="installed" :commit="commit" @@ -68,6 +69,7 @@ export default defineComponent({ sponsor: { type: Object as PropType, default: () => ({}) }, fatal: { type: Array as PropType, default: () => [] }, experimental: Boolean, + authDisabled: Boolean, offline: Boolean, startupCompleted: Boolean, evopt: { type: Object as PropType, required: false }, diff --git a/assets/js/components/BottomTabs/MoreItem.vue b/assets/js/components/BottomTabs/MoreItem.vue index 2f93dac30..c4821dab7 100644 --- a/assets/js/components/BottomTabs/MoreItem.vue +++ b/assets/js/components/BottomTabs/MoreItem.vue @@ -15,6 +15,7 @@ :sponsor="sponsor" :fatal="fatal" :experimental="experimental" + :auth-disabled="authDisabled" :evopt="evopt" :installed="installed" :commit="commit" @@ -41,6 +42,7 @@ export default defineComponent({ sponsor: { type: Object as PropType, default: () => ({}) }, fatal: { type: Array as PropType, default: () => [] }, experimental: Boolean, + authDisabled: Boolean, evopt: { type: Object as PropType, required: false }, installed: String, commit: String, From 16f2f89d42aabad68038ec379fe5da999bfb467a Mon Sep 17 00:00:00 2001 From: Sebastian Schrittwieser <10422786+sschritt@users.noreply.github.com> Date: Mon, 13 Apr 2026 22:18:26 +0200 Subject: [PATCH 0036/1293] chore: fix incorrect regex variable (#29089) --- plugin/pipeline/pipeline.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/pipeline/pipeline.go b/plugin/pipeline/pipeline.go index 5503a2960..ffa69bc7c 100644 --- a/plugin/pipeline/pipeline.go +++ b/plugin/pipeline/pipeline.go @@ -68,7 +68,7 @@ func New(log *util.Logger, cc Settings) (*Pipeline, error) { func (p *Pipeline) WithRegex(regex, dflt string) (*Pipeline, error) { re, err := regexp.Compile(regex) if err != nil { - return nil, fmt.Errorf("invalid regex '%s': %w", re, err) + return nil, fmt.Errorf("invalid regex '%s': %w", regex, err) } p.re = re From 9b8ac117779c53bf5d67f401a3ccab3d3e520744 Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Mon, 13 Apr 2026 22:21:33 +0200 Subject: [PATCH 0037/1293] Add remote access (#28688) --- assets/js/components/Config/DeviceTags.vue | 22 +- .../Config/Remote/RemoteClientCreate.vue | 97 +++++++ .../Config/Remote/RemoteClientList.vue | 102 +++++++ .../Config/Remote/RemoteClientReveal.vue | 125 +++++++++ .../components/Config/Remote/RemoteModal.vue | 211 +++++++++++++++ .../components/MaterialIcon/RemoteAccess.vue | 18 ++ assets/js/mixins/formatter.ts | 4 + assets/js/types/evcc.ts | 24 ++ assets/js/utils/remote.ts | 10 + assets/js/views/Config.vue | 44 +++ cmd/root.go | 14 +- core/keys/global.go | 3 + go.mod | 2 + go.sum | 4 + i18n/de.json | 34 +++ i18n/en.json | 35 +++ package-lock.json | 255 +++++++++++++++++- package.json | 2 + server/http.go | 10 +- server/http_remote_handler.go | 62 +++++ server/remote/clients.go | 166 ++++++++++++ server/remote/ratelimit.go | 51 ++++ server/remote/ratelimit_test.go | 70 +++++ server/remote/remote.go | 228 ++++++++++++++++ server/remote/tunnel.go | 199 ++++++++++++++ 25 files changed, 1778 insertions(+), 14 deletions(-) create mode 100644 assets/js/components/Config/Remote/RemoteClientCreate.vue create mode 100644 assets/js/components/Config/Remote/RemoteClientList.vue create mode 100644 assets/js/components/Config/Remote/RemoteClientReveal.vue create mode 100644 assets/js/components/Config/Remote/RemoteModal.vue create mode 100644 assets/js/components/MaterialIcon/RemoteAccess.vue create mode 100644 assets/js/utils/remote.ts create mode 100644 server/http_remote_handler.go create mode 100644 server/remote/clients.go create mode 100644 server/remote/ratelimit.go create mode 100644 server/remote/ratelimit_test.go create mode 100644 server/remote/remote.go create mode 100644 server/remote/tunnel.go diff --git a/assets/js/components/Config/DeviceTags.vue b/assets/js/components/Config/DeviceTags.vue index e99acd7d8..60670cb0d 100644 --- a/assets/js/components/Config/DeviceTags.vue +++ b/assets/js/components/Config/DeviceTags.vue @@ -175,11 +175,21 @@ export default { }, methods: { valueClasses(entry) { - return { - "value--error": !!entry.error, - "value--warning": entry.warning, - "value--muted": entry.muted || entry.value === false, - }; + if (entry.error) { + return "value--error"; + } + if (entry.warning) { + return "value--warning"; + } + if ( + entry.muted || + entry.value === false || + entry.value === null || + entry.value === undefined + ) { + return "value--muted"; + } + return ""; }, fmtDeviceValue(entry) { const { name, value } = entry; @@ -221,7 +231,9 @@ export default { case "singlePhase": case "enabled": case "configured": + case "connected": case "dimmed": + case "loginBlocked": return value ? this.$t("config.deviceValue.yes") : this.$t("config.deviceValue.no"); diff --git a/assets/js/components/Config/Remote/RemoteClientCreate.vue b/assets/js/components/Config/Remote/RemoteClientCreate.vue new file mode 100644 index 000000000..96e915dbf --- /dev/null +++ b/assets/js/components/Config/Remote/RemoteClientCreate.vue @@ -0,0 +1,97 @@ + + + diff --git a/assets/js/components/Config/Remote/RemoteClientList.vue b/assets/js/components/Config/Remote/RemoteClientList.vue new file mode 100644 index 000000000..13ce440eb --- /dev/null +++ b/assets/js/components/Config/Remote/RemoteClientList.vue @@ -0,0 +1,102 @@ + + + diff --git a/assets/js/components/Config/Remote/RemoteClientReveal.vue b/assets/js/components/Config/Remote/RemoteClientReveal.vue new file mode 100644 index 000000000..4e61bc346 --- /dev/null +++ b/assets/js/components/Config/Remote/RemoteClientReveal.vue @@ -0,0 +1,125 @@ + + + + + diff --git a/assets/js/components/Config/Remote/RemoteModal.vue b/assets/js/components/Config/Remote/RemoteModal.vue new file mode 100644 index 000000000..e2b29e8d2 --- /dev/null +++ b/assets/js/components/Config/Remote/RemoteModal.vue @@ -0,0 +1,211 @@ + + + diff --git a/assets/js/components/MaterialIcon/RemoteAccess.vue b/assets/js/components/MaterialIcon/RemoteAccess.vue new file mode 100644 index 000000000..9d1b0401c --- /dev/null +++ b/assets/js/components/MaterialIcon/RemoteAccess.vue @@ -0,0 +1,18 @@ + + + diff --git a/assets/js/mixins/formatter.ts b/assets/js/mixins/formatter.ts index bf946f769..8562f5972 100644 --- a/assets/js/mixins/formatter.ts +++ b/assets/js/mixins/formatter.ts @@ -180,6 +180,10 @@ export default defineComponent({ const formatter = new Intl.DurationFormat(this.$i18n?.locale, { style }); return formatter.format({ minutes, hours }); }, + fmtDurationParts(parts: Record) { + // @ts-expect-error - Intl.DurationFormat is a new API not yet in TS types + return new Intl.DurationFormat(this.$i18n?.locale, { style: "long" }).format(parts); + }, fmtDayString(date: Date) { const YY = `${date.getFullYear()}`; const MM = `${date.getMonth() + 1}`.padStart(2, "0"); diff --git a/assets/js/types/evcc.ts b/assets/js/types/evcc.ts index d53863682..6f613f2d4 100644 --- a/assets/js/types/evcc.ts +++ b/assets/js/types/evcc.ts @@ -93,6 +93,7 @@ export interface State { shm?: ShmConfig; sponsor?: ConfigStatus; eebus?: ConfigStatus; + remote?: Remote; modbusproxy?: ModbusProxy[]; messaging?: ConfigStatus; messagingEvents?: MessagingEvents; @@ -484,6 +485,29 @@ export type Certificate = { private: string; }; +export type Remote = ConfigStatus; + +export type RemoteConfig = { + enabled: boolean; +}; + +export type RemoteStatus = { + connected: boolean; + url?: string; + loginBlocked: boolean; + lastSeen?: Record; +}; + +export type RemoteClient = { + username: string; + createdAt: string; + expiresAt?: string; +}; + +export type RemoteClientCreated = RemoteClient & { + password: string; +}; + export type Eebus = ConfigStatus; export type EebusConfig = { diff --git a/assets/js/utils/remote.ts b/assets/js/utils/remote.ts new file mode 100644 index 000000000..c96617b2a --- /dev/null +++ b/assets/js/utils/remote.ts @@ -0,0 +1,10 @@ +const ACTIVE_THRESHOLD_MS = 5 * 60 * 1000; // 5 minutes + +export function isRemoteClientActive( + lastSeen: Record | undefined, + username: string +): boolean { + const seen = lastSeen?.[username]; + if (!seen) return false; + return Date.now() - new Date(seen).getTime() < ACTIVE_THRESHOLD_MS; +} diff --git a/assets/js/views/Config.vue b/assets/js/views/Config.vue index affb87be4..e6fba841d 100644 --- a/assets/js/views/Config.vue +++ b/assets/js/views/Config.vue @@ -321,6 +321,19 @@ + + + + + @@ -473,6 +487,9 @@ import ModbusProxyIcon from "../components/MaterialIcon/ModbusProxy.vue"; import ModbusProxyModal from "../components/Config/ModbusProxyModal.vue"; import MqttIcon from "../components/MaterialIcon/Mqtt.vue"; import MqttModal from "../components/Config/MqttModal.vue"; +import RemoteAccessIcon from "../components/MaterialIcon/RemoteAccess.vue"; +import RemoteModal from "../components/Config/Remote/RemoteModal.vue"; +import { isRemoteClientActive } from "@/utils/remote"; import NetworkModal from "../components/Config/NetworkModal.vue"; import NotificationIcon from "../components/MaterialIcon/Notification.vue"; import OptimizerIcon from "../components/MaterialIcon/Optimizer.vue"; @@ -504,6 +521,7 @@ import type { SiteConfig, DeviceType, Notification, + Remote, } from "@/types/evcc"; import { CURRENCY, GRID_CONTROL } from "@/types/evcc"; import { circuitTree } from "@/utils/circuits"; @@ -555,6 +573,8 @@ export default defineComponent({ ModbusProxyModal, MqttIcon, MqttModal, + RemoteAccessIcon, + RemoteModal, NetworkModal, NotificationIcon, OptimizerIcon, @@ -763,6 +783,30 @@ export default defineComponent({ return result; }, + remote(): Remote | undefined { + return store.state?.remote; + }, + remoteTags(): DeviceTags { + const remote = this.remote; + if (!remote?.status?.url) { + return { configured: { value: false } }; + } + const tags: DeviceTags = { + enabled: { value: remote.config?.enabled }, + connected: { value: remote.status?.connected }, + }; + if (remote.status?.loginBlocked) { + tags["loginBlocked"] = { value: true, error: true }; + } + if (remote.status?.connected) { + const lastSeen = remote.status?.lastSeen; + const count = lastSeen + ? Object.keys(lastSeen).filter((u) => isRemoteClientActive(lastSeen, u)).length + : 0; + tags["activeClients"] = { value: count }; + } + return tags; + }, sponsor() { return store.state?.sponsor; }, diff --git a/cmd/root.go b/cmd/root.go index a1d10adab..be00a6c64 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -22,6 +22,7 @@ import ( "github.com/evcc-io/evcc/server/eebus" "github.com/evcc-io/evcc/server/mcp" "github.com/evcc-io/evcc/server/network" + "github.com/evcc-io/evcc/server/remote" "github.com/evcc-io/evcc/server/updater" "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/auth" @@ -237,6 +238,12 @@ func runRoot(cmd *cobra.Command, args []string) { // publish to UI go socketHub.Run(pipe.NewDropper(ignoreEmpty).Pipe(tee.Attach()), cache) + // remote access tunnel + var remoteAccess *remote.Remote + if remoteHost := os.Getenv("EVCC_REMOTE_ACCESS"); remoteHost != "" { + remoteAccess = remote.New(remoteHost, httpd.Router(), valueChan) + } + // signal ui listening valueChan <- util.Param{Key: keys.StartupCompleted, Val: false} @@ -385,6 +392,11 @@ func runRoot(cmd *cobra.Command, args []string) { YamlSource: yamlSource.tariffs, }} + // publish remote access status + if remoteAccess != nil { + valueChan <- util.Param{Key: keys.Remote, Val: remoteAccess.ConfigStatus()} + } + // publish system infos valueChan <- util.Param{Key: keys.Version, Val: util.FormattedVersion()} valueChan <- util.Param{Key: keys.Config, Val: viper.ConfigFileUsed()} @@ -429,7 +441,7 @@ func runRoot(cmd *cobra.Command, args []string) { log.INFO.Println("evcc was stopped by user. OS should restart the service. Or restart manually.") err = errors.New("restart required") // https://gokrazy.org/development/process-interface/ once.Do(func() { close(stopC) }) // signal loop to end - }, viper.ConfigFileUsed()) + }, viper.ConfigFileUsed(), remoteAccess) // show and check version, reduce api load during development if util.Version != util.DevVersion { diff --git a/core/keys/global.go b/core/keys/global.go index f8fa5eba1..354ad27bc 100644 --- a/core/keys/global.go +++ b/core/keys/global.go @@ -30,6 +30,9 @@ const ( Telemetry = "telemetry" Optimizer = "optimizer" DemoMode = "demoMode" + Remote = "remote" + RemoteClients = "remoteClients" + RemoteLastSeen = "remoteLastSeen" AuthDisabled = "authDisabled" AuthProviders = "authProviders" ) diff --git a/go.mod b/go.mod index c597feb3e..27a5a776b 100644 --- a/go.mod +++ b/go.mod @@ -54,6 +54,7 @@ require ( github.com/gregdel/pushover v1.4.0 github.com/grid-x/modbus v0.0.0-20260325140807-cf9e1b9daae0 github.com/hashicorp/go-version v1.9.0 + github.com/hashicorp/yamux v0.1.2 github.com/hasura/go-graphql-client v0.16.0 github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83 github.com/influxdata/influxdb-client-go/v2 v2.14.0 @@ -85,6 +86,7 @@ require ( github.com/robertkrimen/otto v0.5.1 github.com/samber/lo v1.53.0 github.com/sandrolain/httpcache v1.4.0 + github.com/sethvargo/go-password v0.3.1 github.com/sirupsen/logrus v1.9.4 github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 github.com/smallnest/chanx v1.2.0 diff --git a/go.sum b/go.sum index 405e56c3c..427b45073 100644 --- a/go.sum +++ b/go.sum @@ -391,6 +391,8 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= +github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= github.com/hasura/go-graphql-client v0.16.0 h1:DQLfp+djj4j5NPdJkGYym8J55hpm5etML1zqgco78Qc= github.com/hasura/go-graphql-client v0.16.0/go.mod h1:z/sO2T0zI+HnPNIevQcs+7xA6/gDOc8hgHMrNBzfL2c= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= @@ -686,6 +688,8 @@ github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc= github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg= github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfvNt0= github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0= +github.com/sethvargo/go-password v0.3.1 h1:WqrLTjo7X6AcVYfC6R7GtSyuUQR9hGyAj/f1PYQZCJU= +github.com/sethvargo/go-password v0.3.1/go.mod h1:rXofC1zT54N7R8K/h1WDUdkf9BOx5OptoxrMBcrXzvs= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= diff --git a/i18n/de.json b/i18n/de.json index 4e34cb620..3e24344f7 100644 --- a/i18n/de.json +++ b/i18n/de.json @@ -113,6 +113,7 @@ "title": "Währung" }, "deviceValue": { + "activeClients": "Aktive Clients", "amount": "Anzahl", "broker": "Broker", "bucket": "Bucket", @@ -144,6 +145,7 @@ "hemsActiveLimit": "Aktives Limit", "hemsType": "Kommunikation", "identifier": "RFID-Kennung", + "loginBlocked": "Login-Limit erreicht", "max": "max", "messengers": "Dienste", "no": "Nein", @@ -631,6 +633,38 @@ "titleAdd": "PV-Anlage hinzufügen", "titleEdit": "PV-Zähler bearbeiten" }, + "remote": { + "active": "Aktiv", + "addClient": "Client hinzufügen", + "addClientDescription": "Zugangsdaten werden ausschließlich lokal auf deiner evcc-Instanz gespeichert und überprüft.", + "addClientTitle": "Remote-Client hinzufügen", + "clientCreated": "Client erstellt", + "clients": "Clients", + "confirmDelete": "Client löschen?", + "connected": "Verbunden", + "createClient": "Client erstellen", + "description": "Externer Zugriff auf die evcc-Installation über die evcc-App. Ohne Portfreigabe oder VPN.", + "deviceName": "Gerätename", + "disconnected": "Nicht verbunden", + "done": "Fertig", + "enableLabel": "Remotezugriff aktivieren", + "expiration": "Ablauf", + "expirationNone": "Nie", + "expired": "abgelaufen", + "expiresIn": "läuft {time} ab", + "lastActive": "aktiv {time}", + "loginBlocked": "Remote-Logins sind wegen zu vieler fehlgeschlagener Anmeldeversuche für eine Minute gesperrt.", + "manualLogin": "Oder melde dich manuell unter {url} in deinem Browser mit diesen Zugangsdaten an:", + "noClients": "Noch keine Clients. Niemand kann sich verbinden.", + "password": "Passwort", + "passwordOnce": "Dieses Passwort wird nur einmal angezeigt. QR-Code scannen oder jetzt kopieren. Es wird später nicht mehr angezeigt.", + "qrInstall": "Installiere die evcc-App für {ios} oder {android}.", + "qrScan": "Scanne den Code mit der Kamera deines Smartphones, um dich zu verbinden. Klicke ihn an, wenn du bereits dein Handy nutzt.", + "removeClient": "Client entfernen", + "title": "Remotezugriff", + "url": "Öffentliche URL", + "username": "Benutzername" + }, "section": { "additionalMeter": "Zusätzliche Zähler", "general": "Allgemein", diff --git a/i18n/en.json b/i18n/en.json index f87439e35..b17667740 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -113,6 +113,7 @@ "title": "Currency" }, "deviceValue": { + "activeClients": "Active clients", "amount": "Amount", "broker": "Broker", "bucket": "Bucket", @@ -126,6 +127,7 @@ "chargedEnergy": "Charged", "co2": "Grid CO₂", "configured": "Configured", + "connected": "Connected", "connections": "Connections", "controllable": "Controllable", "currency": "Currency", @@ -144,6 +146,7 @@ "hemsActiveLimit": "Active limit", "hemsType": "Communication", "identifier": "RFID-Identifier", + "loginBlocked": "Login limit reached", "max": "max", "messengers": "Services", "no": "no", @@ -631,6 +634,38 @@ "titleAdd": "Add Solar Meter", "titleEdit": "Edit Solar Meter" }, + "remote": { + "active": "Active", + "addClient": "Add client", + "addClientDescription": "Credentials are stored and verified only locally on your evcc instance.", + "addClientTitle": "Add Remote Client", + "clientCreated": "Client created", + "clients": "Clients", + "confirmDelete": "Delete client?", + "connected": "Connected", + "createClient": "Create client", + "description": "Access your evcc installation from anywhere using the evcc mobile app. No port forwarding or VPN required.", + "deviceName": "Device name", + "disconnected": "Disconnected", + "done": "Done", + "enableLabel": "Enable remote access", + "expiration": "Expiration", + "expirationNone": "Never", + "expired": "expired", + "expiresIn": "expires {time}", + "lastActive": "active {time}", + "loginBlocked": "Remote logins are blocked for one minute due to too many failed login attempts.", + "manualLogin": "Or sign in manually at {url} in your browser using these credentials:", + "noClients": "No clients yet. No one can connect yet.", + "password": "Password", + "passwordOnce": "This password is shown only once. Scan the QR code or copy it now. You won't be able to see it again.", + "qrInstall": "Install the evcc app for {ios} or {android}.", + "qrScan": "Scan the code with your phone's camera to connect. Click it, if you're already using your phone.", + "removeClient": "Remove client", + "title": "Remote Access", + "url": "Public URL", + "username": "Username" + }, "section": { "additionalMeter": "Additional meters", "general": "General", diff --git a/package-lock.json b/package-lock.json index 829f0c83a..55d5578b7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "dayjs": "^1.11.20", "echarts": "^6.0.0", "monaco-editor": "0.52.2", + "qrcode": "^1.5.4", "smoothscroll-polyfill": "^0.4.4", "snarkdown": "^2.0.0", "vue": "^3.5.31", @@ -40,6 +41,7 @@ "@types/bootstrap": "^5.2.10", "@types/canvas-confetti": "^1.9.0", "@types/kill-port": "^2.0.3", + "@types/qrcode": "^1.5.6", "@types/smoothscroll-polyfill": "^0.3.4", "@types/wait-on": "^5.3.4", "@types/ws": "^8.18.1", @@ -3435,6 +3437,16 @@ "undici-types": "~7.18.0" } }, + "node_modules/@types/qrcode": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz", + "integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/readable-stream": { "version": "4.0.23", "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.23.tgz", @@ -4439,7 +4451,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -4449,7 +4460,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -5115,6 +5125,15 @@ "node": ">=6" } }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001780", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001780.tgz", @@ -5324,7 +5343,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -5337,7 +5355,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, "node_modules/combined-stream": { @@ -5655,6 +5672,15 @@ } } }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/deep-eql": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", @@ -5790,6 +5816,12 @@ "node": ">=8" } }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", + "license": "MIT" + }, "node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -6993,7 +7025,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" @@ -7686,7 +7717,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -9165,6 +9195,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -9215,7 +9254,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -9346,6 +9384,15 @@ "node": ">=18" } }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -9702,6 +9749,173 @@ "node": ">=6" } }, + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "license": "MIT", + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/qrcode/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/qrcode/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/qrcode/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/qrcode/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "license": "ISC" + }, + "node_modules/qrcode/node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/qs": { "version": "6.15.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", @@ -9997,6 +10211,21 @@ "regjsparser": "bin/parser" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "license": "ISC" + }, "node_modules/resolve": { "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", @@ -10298,6 +10527,12 @@ "node": ">=10" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -12470,6 +12705,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "license": "ISC" + }, "node_modules/which-typed-array": { "version": "1.1.20", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", diff --git a/package.json b/package.json index 17d725fde..ee0fe7d6f 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "dayjs": "^1.11.20", "echarts": "^6.0.0", "monaco-editor": "0.52.2", + "qrcode": "^1.5.4", "smoothscroll-polyfill": "^0.4.4", "snarkdown": "^2.0.0", "vue": "^3.5.31", @@ -62,6 +63,7 @@ "@types/bootstrap": "^5.2.10", "@types/canvas-confetti": "^1.9.0", "@types/kill-port": "^2.0.3", + "@types/qrcode": "^1.5.6", "@types/smoothscroll-polyfill": "^0.3.4", "@types/wait-on": "^5.3.4", "@types/ws": "^8.18.1", diff --git a/server/http.go b/server/http.go index a0c4cfe4f..a39d2d45a 100644 --- a/server/http.go +++ b/server/http.go @@ -15,6 +15,7 @@ import ( "github.com/evcc-io/evcc/hems/shm" "github.com/evcc-io/evcc/server/assets" "github.com/evcc-io/evcc/server/eebus" + "github.com/evcc-io/evcc/server/remote" "github.com/evcc-io/evcc/server/service" "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/auth" @@ -225,7 +226,7 @@ func (s *HTTPd) RegisterSiteHandlers(site site.API) { } // RegisterSystemHandler provides system level handlers -func (s *HTTPd) RegisterSystemHandler(site *core.Site, pub publisher, cache *util.ParamCache, auth auth.Auth, shutdown func(), configFile string) { +func (s *HTTPd) RegisterSystemHandler(site *core.Site, pub publisher, cache *util.ParamCache, auth auth.Auth, shutdown func(), configFile string, remoteAccess *remote.Remote) { router := s.Server.Handler.(*mux.Router) // api @@ -299,6 +300,13 @@ func (s *HTTPd) RegisterSystemHandler(site *core.Site, pub publisher, cache *uti "optimizer": {"POST", "/optimizer/{value:[01truefalse]+}", boolHandler(setOptimizer(pub), getOptimizer)}, } + if remoteAccess != nil { + routes["remote"] = route{"POST", "/remote/{value:[01truefalse]+}", boolHandler(remoteAccess.Enable, remoteAccess.Enabled)} + routes["remoteclients"] = route{"GET", "/remote/clients", remoteClientsHandler(remoteAccess)} + routes["createremoteclient"] = route{"POST", "/remote/clients", createRemoteClientHandler(remoteAccess)} + routes["deleteremoteclient"] = route{"DELETE", "/remote/clients", deleteRemoteClientHandler(remoteAccess)} + } + // yaml handlers for key, fun := range map[string]func() (any, any){ keys.Hems: func() (any, any) { return map[string]any{}, config.Typed{} }, diff --git a/server/http_remote_handler.go b/server/http_remote_handler.go new file mode 100644 index 000000000..5b7b417a2 --- /dev/null +++ b/server/http_remote_handler.go @@ -0,0 +1,62 @@ +package server + +import ( + "encoding/json" + "net/http" + "time" + + "github.com/evcc-io/evcc/server/remote" +) + +// remoteClientsHandler returns the list of remote tunnel clients. +func remoteClientsHandler(r *remote.Remote) http.HandlerFunc { + return func(w http.ResponseWriter, _ *http.Request) { + jsonWrite(w, r.Clients()) + } +} + +// createRemoteClientHandler creates a new tunnel client and returns the +// cleartext password (shown to the user only once). +func createRemoteClientHandler(r *remote.Remote) http.HandlerFunc { + return func(w http.ResponseWriter, req *http.Request) { + var body struct { + Username string `json:"username"` + ExpiresIn int64 `json:"expiresIn"` // seconds; 0 = never + } + if err := json.NewDecoder(req.Body).Decode(&body); err != nil { + jsonError(w, http.StatusBadRequest, err) + return + } + + client, password, err := r.CreateClient(body.Username, time.Duration(body.ExpiresIn)*time.Second) + if err != nil { + jsonError(w, http.StatusBadRequest, err) + return + } + + jsonWrite(w, struct { + Username string `json:"username"` + Password string `json:"password"` + CreatedAt time.Time `json:"createdAt"` + ExpiresAt *time.Time `json:"expiresAt,omitempty"` + }{ + Username: client.Username, + Password: password, + CreatedAt: client.CreatedAt, + ExpiresAt: client.ExpiresAt, + }) + } +} + +// deleteRemoteClientHandler removes a tunnel client by username. +// Username is passed as a query parameter to allow arbitrary characters. +func deleteRemoteClientHandler(r *remote.Remote) http.HandlerFunc { + return func(w http.ResponseWriter, req *http.Request) { + username := req.URL.Query().Get("username") + if err := r.DeleteClient(username); err != nil { + jsonError(w, http.StatusNotFound, err) + return + } + jsonWrite(w, true) + } +} diff --git a/server/remote/clients.go b/server/remote/clients.go new file mode 100644 index 000000000..f4907aec6 --- /dev/null +++ b/server/remote/clients.go @@ -0,0 +1,166 @@ +package remote + +import ( + "crypto/rand" + "errors" + "fmt" + "slices" + "strings" + "time" + + "github.com/evcc-io/evcc/core/keys" + "github.com/evcc-io/evcc/server/db/settings" + "github.com/samber/lo" + "github.com/sethvargo/go-password/password" + "golang.org/x/crypto/bcrypt" +) + +// dummyHash is a bcrypt hash of a random value, used to make the +// "unknown user" path take the same time as a real password check and +// prevent username enumeration via timing side channels. +var dummyHash []byte + +func init() { + buf := make([]byte, 16) + if _, err := rand.Read(buf); err != nil { + panic(err) + } + h, err := bcrypt.GenerateFromPassword(buf, bcrypt.DefaultCost) + if err != nil { + panic(err) + } + dummyHash = h +} + +// Client is a single tunnel basic-auth credential used by a remote client. +type Client struct { + Username string `json:"username"` + CreatedAt time.Time `json:"createdAt"` + ExpiresAt *time.Time `json:"expiresAt,omitempty"` +} + +type persistedClient struct { + Client + Hash string `json:"hash"` +} + +// loadClients reads the persisted client list. +func loadClients() []persistedClient { + var res []persistedClient + _ = settings.Json(keys.RemoteClients, &res) + return res +} + +// saveClients persists the given client list. +func saveClients(list []persistedClient) error { + return settings.SetJson(keys.RemoteClients, list) +} + +// generatePassword returns a crypto-random alphanumeric password +// with 20 characters including 4 digits (~96 bits of entropy). +func generatePassword() (string, error) { + return password.Generate(20, 4, 0, false, false) +} + +// Clients returns the list of configured clients (without password hashes). +func (r *Remote) Clients() []Client { + return lo.Map(loadClients(), func(c persistedClient, _ int) Client { + return c.Client + }) +} + +// CreateClient creates a new client with an auto-generated password. +// expiresIn <= 0 means the client never expires. +// Returns the cleartext password (shown to the user only once). +func (r *Remote) CreateClient(username string, expiresIn time.Duration) (Client, string, error) { + username = strings.TrimSpace(username) + if username == "" { + return Client{}, "", errors.New("username required") + } + // RFC 7617: ":" is the basic-auth separator; reject control chars too. + for _, r := range username { + if r == ':' || r < 0x20 || r == 0x7f { + return Client{}, "", errors.New("username contains invalid characters") + } + } + + var expires *time.Time + if expiresIn > 0 { + expires = new(time.Now().Add(expiresIn)) + } + + r.mu.Lock() + defer r.mu.Unlock() + + list := loadClients() + for _, c := range list { + if c.Username == username { + return Client{}, "", fmt.Errorf("client %q already exists", username) + } + } + + password, err := generatePassword() + if err != nil { + return Client{}, "", err + } + + hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + return Client{}, "", err + } + + c := persistedClient{ + Client: Client{ + Username: username, + CreatedAt: time.Now(), + ExpiresAt: expires, + }, + Hash: string(hash), + } + list = append(list, c) + if err := saveClients(list); err != nil { + return Client{}, "", err + } + + return c.Client, password, nil +} + +// DeleteClient removes a client by username. +func (r *Remote) DeleteClient(username string) error { + r.mu.Lock() + defer r.mu.Unlock() + + list := loadClients() + idx := slices.IndexFunc(list, func(c persistedClient) bool { + return c.Username == username + }) + + if idx == -1 { + return fmt.Errorf("client %s not found", username) + } + + return saveClients(slices.Delete(list, idx, idx+1)) +} + +// Authenticate validates basic-auth credentials. Always runs bcrypt +// (against a dummy hash on miss) to prevent username enumeration via timing. +func (r *Remote) Authenticate(username, password string) bool { + hash := dummyHash + var found *persistedClient + for _, c := range loadClients() { + if c.Username == username { + found = &c + hash = []byte(c.Hash) + break + } + } + + valid := bcrypt.CompareHashAndPassword(hash, []byte(password)) == nil + if !valid || found == nil { + return false + } + if found.ExpiresAt != nil && time.Now().After(*found.ExpiresAt) { + return false + } + return true +} diff --git a/server/remote/ratelimit.go b/server/remote/ratelimit.go new file mode 100644 index 000000000..76c43930b --- /dev/null +++ b/server/remote/ratelimit.go @@ -0,0 +1,51 @@ +package remote + +import ( + "sync" + "time" +) + +// authRateLimiter tracks failed authentication attempts in a sliding window. +// When the failure count exceeds the threshold, further attempts are blocked +// to prevent brute-force attacks. +type authRateLimiter struct { + mu sync.Mutex + failures []time.Time + window time.Duration + max int + now func() time.Time +} + +func newAuthRateLimiter() *authRateLimiter { + return &authRateLimiter{ + window: time.Minute, + max: 10, + now: time.Now, + } +} + +// allow checks whether an authentication attempt should proceed. +func (rl *authRateLimiter) allow() bool { + rl.mu.Lock() + defer rl.mu.Unlock() + + cutoff := rl.now().Add(-rl.window) + + // prune old entries + valid := rl.failures[:0] + for _, t := range rl.failures { + if t.After(cutoff) { + valid = append(valid, t) + } + } + rl.failures = valid + + return len(rl.failures) < rl.max +} + +// fail records a failed authentication attempt. +func (rl *authRateLimiter) fail() { + rl.mu.Lock() + defer rl.mu.Unlock() + rl.failures = append(rl.failures, rl.now()) +} diff --git a/server/remote/ratelimit_test.go b/server/remote/ratelimit_test.go new file mode 100644 index 000000000..f0f1a7077 --- /dev/null +++ b/server/remote/ratelimit_test.go @@ -0,0 +1,70 @@ +package remote + +import ( + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestAuthRateLimiter(t *testing.T) { + t.Run("allows requests under threshold", func(t *testing.T) { + rl := newAuthRateLimiter() + + for range rl.max { + assert.True(t, rl.allow()) + rl.fail() + } + }) + + t.Run("blocks after threshold", func(t *testing.T) { + rl := newAuthRateLimiter() + + for range rl.max { + rl.fail() + } + + assert.False(t, rl.allow()) + }) + + t.Run("recovers after window expires", func(t *testing.T) { + now := time.Now() + var mu sync.Mutex + + rl := newAuthRateLimiter() + rl.now = func() time.Time { + mu.Lock() + defer mu.Unlock() + return now + } + + for range rl.max { + rl.fail() + } + + assert.False(t, rl.allow()) + + // advance past window + mu.Lock() + now = now.Add(rl.window + time.Second) + mu.Unlock() + + assert.True(t, rl.allow()) + }) + + t.Run("successful auth does not count as failure", func(t *testing.T) { + rl := newAuthRateLimiter() + + // fill up to max-1 failures + for range rl.max - 1 { + rl.fail() + } + + // allow should still work (no fail() call = successful auth) + assert.True(t, rl.allow()) + + // still under threshold + assert.True(t, rl.allow()) + }) +} diff --git a/server/remote/remote.go b/server/remote/remote.go new file mode 100644 index 000000000..80158dc10 --- /dev/null +++ b/server/remote/remote.go @@ -0,0 +1,228 @@ +package remote + +import ( + "fmt" + "net/http" + "sync" + "time" + + "github.com/evcc-io/evcc/api/globalconfig" + "github.com/evcc-io/evcc/cmd/shutdown" + "github.com/evcc-io/evcc/core/keys" + "github.com/evcc-io/evcc/server/db/settings" + "github.com/evcc-io/evcc/util" + "github.com/evcc-io/evcc/util/request" + "github.com/evcc-io/evcc/util/sponsor" +) + +// Settings is the persisted remote access configuration. +type Settings struct { + Enabled bool `json:"enabled"` + URL string `json:"url,omitempty"` + Token string `json:"token,omitempty"` + TunnelURL string `json:"tunnelUrl,omitempty"` +} + +// Remote manages the remote access tunnel lifecycle. +type Remote struct { + mu sync.Mutex + cloudHost string + settings Settings + tunnel *Tunnel + httpHandler http.Handler + log *util.Logger + publisher chan<- util.Param + lastSeen map[string]time.Time // persisted: username → last activity + connected map[string]int // in-memory: active connection count per user +} + +// New creates a new Remote manager, loads persisted settings, and connects if enabled. +func New(cloudHost string, httpHandler http.Handler, valueChan chan<- util.Param) *Remote { + r := &Remote{ + cloudHost: cloudHost, + httpHandler: httpHandler, + log: util.NewLogger("remote"), + publisher: valueChan, + lastSeen: make(map[string]time.Time), + connected: make(map[string]int), + } + + // load saved settings + _ = settings.Json(keys.Remote, &r.settings) + _ = settings.Json(keys.RemoteLastSeen, &r.lastSeen) + + if r.settings.Enabled && r.settings.Token != "" { + go r.connect() + } + + shutdown.Register(r.disconnect) + + go func() { + for range time.Tick(time.Minute) { + r.publish() + } + }() + + return r +} + +// Enable enables or disables remote access. When enabling for the first time, +// it registers with the cloud to obtain a URL and token. +func (r *Remote) Enable(enable bool) error { + r.mu.Lock() + r.settings.Enabled = enable + r.saveSettings() + r.mu.Unlock() + + if enable { + // TODO why do we need a go routine for this? + go r.connect() + } else { + r.disconnect() + } + + r.publish() + return nil +} + +// Enabled returns whether remote access is enabled. +func (r *Remote) Enabled() bool { + r.mu.Lock() + defer r.mu.Unlock() + return r.settings.Enabled +} + +func (r *Remote) connect() { + r.mu.Lock() + token := r.settings.Token + r.mu.Unlock() + + if token == "" { + if err := r.register(); err != nil { + r.log.ERROR.Printf("registration failed: %v", err) + return + } + } + + r.log.INFO.Printf("remote access via %s", r.settings.URL) + + tunnel := NewTunnel(r.settings.TunnelURL, r.settings.Token, r.httpHandler, r.Authenticate, r.TrackActivity, r.log, r.publish) + + r.mu.Lock() + r.tunnel = tunnel + r.mu.Unlock() + + // blocks until disconnected + tunnel.run() +} + +func (r *Remote) disconnect() { + r.mu.Lock() + defer r.mu.Unlock() + + if r.tunnel != nil { + r.tunnel.Close() + r.tunnel = nil + } +} + +type registerRequest struct { + SponsorToken string `json:"sponsorToken"` +} + +type registerResponse struct { + URL string `json:"url"` + Token string `json:"token"` + TunnelURL string `json:"tunnelUrl"` +} + +// register calls the cloud registration endpoint and persists the result. +func (r *Remote) register() error { + uri := fmt.Sprintf("https://%s/api/register", r.cloudHost) + data := registerRequest{SponsorToken: sponsor.Token} + req, _ := request.New(http.MethodPost, uri, request.MarshalJSON(data), request.JSONEncoding) + + var res registerResponse + + client := request.NewHelper(r.log) + if err := client.DoJSON(req, &res); err != nil { + return err + } + + r.mu.Lock() + r.settings.URL = res.URL + r.settings.Token = res.Token + r.settings.TunnelURL = res.TunnelURL + r.saveSettings() + r.mu.Unlock() + + r.log.INFO.Printf("registered as %s", res.URL) + return nil +} + +// TrackActivity tracks remote client connections and disconnections. +func (r *Remote) TrackActivity(username string, active bool) { + r.mu.Lock() + defer r.mu.Unlock() + if active { + r.lastSeen[username] = time.Now() + r.connected[username]++ + } else if r.connected[username] > 0 { + r.connected[username]-- + } +} + +// saveSettings persists the current settings. Must be called with mu held. +func (r *Remote) saveSettings() { + if err := settings.SetJson(keys.Remote, r.settings); err != nil { + r.log.ERROR.Println(err) + } +} + +// ConfigStatus returns the current remote access config and status. +func (r *Remote) ConfigStatus() globalconfig.ConfigStatus { + r.mu.Lock() + defer r.mu.Unlock() + + connected := r.tunnel != nil && r.tunnel.IsConnected() + loginBlocked := r.tunnel != nil && r.tunnel.LoginBlocked() + + return globalconfig.ConfigStatus{ + Config: struct { + Enabled bool `json:"enabled"` + }{ + Enabled: r.settings.Enabled, + }, + Status: struct { + Connected bool `json:"connected"` + URL string `json:"url,omitempty"` + LoginBlocked bool `json:"loginBlocked"` + LastSeen map[string]time.Time `json:"lastSeen,omitempty"` + }{ + Connected: connected, + URL: r.settings.URL, + LoginBlocked: loginBlocked, + LastSeen: r.lastSeen, + }, + } +} + +// publish sends the current status to the UI via the value channel. +func (r *Remote) publish() { + if r.publisher == nil { + return + } + + // refresh lastSeen for open connections (auth only fires once) + r.mu.Lock() + now := time.Now() + for user, count := range r.connected { + if count > 0 { + r.lastSeen[user] = now + } + } + _ = settings.SetJson(keys.RemoteLastSeen, r.lastSeen) + r.mu.Unlock() + + r.publisher <- util.Param{Key: keys.Remote, Val: r.ConfigStatus()} +} diff --git a/server/remote/tunnel.go b/server/remote/tunnel.go new file mode 100644 index 000000000..303acf92b --- /dev/null +++ b/server/remote/tunnel.go @@ -0,0 +1,199 @@ +package remote + +import ( + "context" + "errors" + "fmt" + "io" + "net/http" + "sync" + "time" + + "github.com/cenkalti/backoff/v4" + "github.com/coder/websocket" + "github.com/evcc-io/evcc/util" + "github.com/evcc-io/evcc/util/sponsor" + "github.com/hashicorp/yamux" +) + +// Tunnel manages a WebSocket+yamux tunnel to the cloud proxy. +type Tunnel struct { + tunnelURL string + token string + httpHandler http.Handler + authenticate func(user, pass string) bool + trackActivity func(username string, active bool) + log *util.Logger + cancel func() + onStateChange func() + rateLimiter *authRateLimiter + + mu sync.Mutex + session *yamux.Session +} + +// NewTunnel creates a new tunnel client. +func NewTunnel(tunnelURL, token string, httpHandler http.Handler, authenticate func(user, pass string) bool, trackActivity func(string, bool), log *util.Logger, onStateChange func()) *Tunnel { + return &Tunnel{ + tunnelURL: tunnelURL, + token: token, + httpHandler: httpHandler, + authenticate: authenticate, + trackActivity: trackActivity, + log: log, + onStateChange: onStateChange, + rateLimiter: newAuthRateLimiter(), + } +} + +// run establishes the tunnel and reconnects on failure. +func (t *Tunnel) run() { + bo := backoff.NewExponentialBackOff( + backoff.WithInitialInterval(time.Second), + backoff.WithMaxInterval(60*time.Second), + backoff.WithMaxElapsedTime(0), + ) + + ctx, cancel := context.WithCancel(context.Background()) + t.cancel = cancel + + for { + ok, err := t.connect(ctx) + if err != nil && !errors.Is(err, context.Canceled) { + t.log.ERROR.Printf("tunnel: %v", err) + } + + // reset backoff after successful connection + if ok { + bo.Reset() + } + + select { + case <-ctx.Done(): + return + case <-time.After(bo.NextBackOff()): + } + } +} + +func (t *Tunnel) connect(ctx context.Context) (bool, error) { + conn, _, err := websocket.Dial(ctx, t.tunnelURL, &websocket.DialOptions{ + HTTPHeader: http.Header{ + "Authorization": []string{"Bearer " + t.token}, + "X-Sponsor-Token": []string{sponsor.Token}, + }, + }) + if err != nil { + return false, fmt.Errorf("websocket dial: %w", err) + } + + netConn := websocket.NetConn(ctx, conn, websocket.MessageBinary) + + config := yamux.DefaultConfig() + config.LogOutput = t.log.TRACE.Writer() + + session, err := yamux.Client(netConn, config) + if err != nil { + netConn.Close() // closes the underlying socket connection + return false, fmt.Errorf("yamux client: %w", err) + } + + t.changeState(session, nil) + + // accept streams from the proxy + srv := &http.Server{ + Handler: t.basicAuthMiddleware(t.httpHandler), + } + + if err := srv.Serve(session); err != nil { + t.changeState(nil, err) + } + + return true, nil +} + +func (t *Tunnel) changeState(session *yamux.Session, err error) { + t.mu.Lock() + t.session = session + t.mu.Unlock() + + if t.onStateChange != nil { + t.onStateChange() + } + + if session != nil { + t.log.INFO.Println("tunnel connected") + } else { + if errors.Is(err, context.Canceled) || errors.Is(err, io.EOF) { + t.log.INFO.Println("tunnel disconnected") + } else { + t.log.INFO.Println("tunnel disconnected:", err) + } + } +} + +// IsConnected returns whether the tunnel is currently connected. +func (t *Tunnel) IsConnected() bool { + t.mu.Lock() + defer t.mu.Unlock() + return t.session != nil +} + +// LoginBlocked returns whether login attempts are currently blocked by the rate limiter. +func (t *Tunnel) LoginBlocked() bool { + return !t.rateLimiter.allow() +} + +// Close tears down the tunnel. +func (t *Tunnel) Close() { + t.mu.Lock() + defer t.mu.Unlock() + + // close websocket; produces io.EOF in yamux which it handles silently + if t.session != nil { + t.session.Close() // closes the underlying socket connection + } + + if t.cancel != nil { + t.cancel() + t.cancel = nil + } +} + +// basicAuthMiddleware wraps a handler with HTTP basic auth, validating +// credentials against the given authenticate function per request. +// It rate-limits failed attempts to prevent brute-force attacks. +func (t *Tunnel) basicAuthMiddleware(next http.Handler) http.Handler { + rejectAuth := func(w http.ResponseWriter) { + w.Header().Set("WWW-Authenticate", `Basic realm="evcc"`) + http.Error(w, "Unauthorized", http.StatusUnauthorized) + } + + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + user, pass, ok := r.BasicAuth() + if !ok || t.authenticate == nil { + rejectAuth(w) + return + } + + if !t.rateLimiter.allow() { + t.log.INFO.Printf("login blocked for %q (rate limited)", user) + http.Error(w, "Too many failed login attempts. Try again in 1 minute.", http.StatusTooManyRequests) + return + } + + if !t.authenticate(user, pass) { + t.rateLimiter.fail() + t.log.INFO.Printf("failed login attempt for %q", user) + rejectAuth(w) + return + } + + if t.trackActivity != nil { + t.trackActivity(user, true) + defer t.trackActivity(user, false) // long-running requests (ws) + } + + next.ServeHTTP(w, r) + }) +} From 4c44ebc5d5e1fe07d9a35282e22b1f4aa9b372da Mon Sep 17 00:00:00 2001 From: andig Date: Tue, 14 Apr 2026 10:51:30 +0200 Subject: [PATCH 0038/1293] chore: fix connected --- charger/smart-evse.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/charger/smart-evse.go b/charger/smart-evse.go index 901d26985..ac01f3755 100644 --- a/charger/smart-evse.go +++ b/charger/smart-evse.go @@ -46,7 +46,7 @@ type smartEvseRestSettings struct { ModeID int `json:"mode_id"` CarConnected bool `json:"car_connected"` Evse struct { - Connected int `json:"connected"` + Connected bool `json:"connected"` Access int `json:"access"` Mode int `json:"mode"` ChargeTimer int `json:"charge_timer"` @@ -413,7 +413,7 @@ func (wb *SmartEVSE3) Diagnose() { fmt.Printf("\tMode: %s (%d)\n", res.Mode, res.ModeID) fmt.Printf("\tCar connected: %t\n", res.CarConnected) - fmt.Printf("\tEVSE connected: %d\n", res.Evse.Connected) + fmt.Printf("\tEVSE connected: %t\n", res.Evse.Connected) fmt.Printf("\tEVSE access: %d\n", res.Evse.Access) fmt.Printf("\tEVSE mode: %d\n", res.Evse.Mode) fmt.Printf("\tEVSE charge timer: %d\n", res.Evse.ChargeTimer) From b45edd218784482179e10fd685b37a0f31b42f9d Mon Sep 17 00:00:00 2001 From: andig Date: Tue, 14 Apr 2026 10:58:23 +0200 Subject: [PATCH 0039/1293] Mqtt: reset full/empty times (#29050) --- core/site_optimizer.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/core/site_optimizer.go b/core/site_optimizer.go index 8ce7eb9b0..ab6da5d6d 100644 --- a/core/site_optimizer.go +++ b/core/site_optimizer.go @@ -70,8 +70,8 @@ type batteryDetail struct { type batteryResult struct { batteryDetail - Full *time.Time `json:"full"` - Empty *time.Time `json:"empty"` + Full time.Time `json:"full,omitzero"` + Empty time.Time `json:"empty,omitzero"` } // func (br batteryResult) MarshalJSON() ([]byte, error) { @@ -471,15 +471,15 @@ func (site *Site) batteryRequest(dev config.Device[api.Meter], b types.Measureme return bat, detail } -func matchSoc(ts []float32, fun func(float32) bool) *time.Time { +func matchSoc(ts []float32, fun func(float32) bool) time.Time { for i, soc := range ts { if fun(soc) { // TODO first slot - return new(time.Now().Add(time.Duration(i+1) * tariff.SlotDuration)) + return time.Now().Add(time.Duration(i+1) * tariff.SlotDuration) } } - return nil + return time.Time{} } // continuousDemand creates a slice of power demands depending on loadpoint mode From 4b528cd355ee7a4d32503382d8ce429d421ab2ac Mon Sep 17 00:00:00 2001 From: andig Date: Tue, 14 Apr 2026 11:39:43 +0200 Subject: [PATCH 0040/1293] EEBus: handle negative production limits (#29104) --- hems/eebus/eebus.go | 29 ++++++++++++++++++----------- hems/eebus/events.go | 2 +- meter/eebus.go | 4 ++-- 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/hems/eebus/eebus.go b/hems/eebus/eebus.go index a9ef56405..ccbe35fbf 100644 --- a/hems/eebus/eebus.go +++ b/hems/eebus/eebus.go @@ -38,7 +38,7 @@ type EEBus struct { smartgridProductionId uint productionLimit ucapi.LoadLimit productionLimitActivated time.Time - failsafeProductionLimit float64 + failsafeProductionLimit *float64 heartbeat *util.Value[struct{}] interval time.Duration @@ -49,7 +49,7 @@ type Limits struct { FailsafeConsumptionActivePowerLimit float64 ProductionNominalMax float64 - FailsafeProductionActivePowerLimit float64 + FailsafeProductionActivePowerLimit *float64 FailsafeDurationMinimum time.Duration } @@ -67,7 +67,7 @@ func NewFromConfig(ctx context.Context, other map[string]any, site site.API) (*E FailsafeConsumptionActivePowerLimit: 4200, ProductionNominalMax: 0, - FailsafeProductionActivePowerLimit: 0, + FailsafeProductionActivePowerLimit: nil, // 0 is a valid limit FailsafeDurationMinimum: 2 * time.Hour, }, @@ -144,8 +144,8 @@ func NewEEBus(ctx context.Context, ski string, limits Limits, passthrough func(b if err := c.cs.CsLPPInterface.SetProductionNominalMax(limits.ProductionNominalMax); err != nil { c.log.ERROR.Println("CS LPP SetProductionNominalMax:", err) } - if c.failsafeProductionLimit > 0 { - if err := c.cs.CsLPPInterface.SetFailsafeProductionActivePowerLimit(c.failsafeProductionLimit, true); err != nil { + if c.failsafeProductionLimit != nil && *c.failsafeProductionLimit >= 0 { + if err := c.cs.CsLPPInterface.SetFailsafeProductionActivePowerLimit(*c.failsafeProductionLimit, true); err != nil { c.log.ERROR.Println("CS LPP SetFailsafeProductionActivePowerLimit:", err) } } @@ -181,7 +181,14 @@ func (c *EEBus) run() error { if heartbeatErr != nil && c.status != StatusFailsafe { // LPC-914/2 c.log.WARN.Println("missing heartbeat- entering failsafe mode") - c.setStatusAndLimit(StatusFailsafe, c.failsafeConsumptionLimit, c.failsafeProductionLimit) + c.setStatus(StatusFailsafe) + + c.setConsumptionLimit(c.failsafeConsumptionLimit) + + if c.failsafeProductionLimit != nil { + // production limit is negative, failsafe limits are always positive + c.setProductionLimit(-*c.failsafeProductionLimit, true) + } return nil } @@ -193,7 +200,10 @@ func (c *EEBus) run() error { } c.log.DEBUG.Println("heartbeat returned or failsafe duration exceeded- leaving failsafe mode") - c.setStatusAndLimit(StatusNormal, 0, 0) + c.setStatus(StatusNormal) + + c.setConsumptionLimit(0) + c.setProductionLimit(0, false) } // LPC-914/1 @@ -227,12 +237,9 @@ func (c *EEBus) run() error { return nil } -func (c *EEBus) setStatusAndLimit(status status, consumption, production float64) { +func (c *EEBus) setStatus(status status) { c.status = status c.statusUpdated = time.Now() - - c.setConsumptionLimit(consumption) - c.setProductionLimit(production, true) } func (c *EEBus) setConsumptionLimit(limit float64) { diff --git a/hems/eebus/events.go b/hems/eebus/events.go index 3dbd6bfda..beca70ba8 100644 --- a/hems/eebus/events.go +++ b/hems/eebus/events.go @@ -183,7 +183,7 @@ func (c *EEBus) updateFailsafeProductionActivePowerLimit() { c.mux.Lock() defer c.mux.Unlock() - c.failsafeProductionLimit = limit + c.failsafeProductionLimit = new(limit) } func (c *EEBus) updateFailsafeConsumptionDurationMinimum() { diff --git a/meter/eebus.go b/meter/eebus.go index 6b2cfaf3c..f2e4310c3 100644 --- a/meter/eebus.go +++ b/meter/eebus.go @@ -249,8 +249,8 @@ func (c *EEBus) Curtailed() (bool, error) { return false, err } - // Check if limit is active and has a valid power value - return limit.IsActive && limit.Value > 0, nil + // Check if limit is active and has a valid power value (valid is zero or negative) + return limit.IsActive && limit.Value <= 0, nil } // Curtail implements the api.Curtailer interface From 794a26d9166ebb23fe25deaa96a7305e3325fb05 Mon Sep 17 00:00:00 2001 From: andig Date: Tue, 14 Apr 2026 13:10:42 +0200 Subject: [PATCH 0041/1293] chore: fix naming (#29106) --- i18n/de.json | 2 +- i18n/en.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/i18n/de.json b/i18n/de.json index 3e24344f7..bbf0a02a9 100644 --- a/i18n/de.json +++ b/i18n/de.json @@ -341,7 +341,7 @@ "defaultsHintLink": "Einstellungen anpassen", "delete": "Löschen", "electricalSubtitle": "Im Zweifelsfall Elektriker fragen.", - "electricalTitle": "Elektrisch", + "electricalTitle": "Elektrik", "energyMeterHelp": "Zusätzlicher Zähler, wenn die Wallbox keinen integrierten hat.", "energyMeterLabel": "Energiezähler", "estimateLabel": "Ladestand zwischen API-Updates interpolieren", diff --git a/i18n/en.json b/i18n/en.json index b17667740..e4e669582 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -342,7 +342,7 @@ "defaultsHintLink": "Adjust settings", "delete": "Delete", "electricalSubtitle": "When in doubt, ask your electrician.", - "electricalTitle": "Electrical", + "electricalTitle": "Electrics", "energyMeterHelp": "Additional meter if the charger doesn't have an integrated one.", "energyMeterLabel": "Energy meter", "estimateLabel": "Interpolate charge level between API updates", From 812ffa47b8d9a36a6d46aedf5edbaffab8bfd86a Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Tue, 14 Apr 2026 16:46:59 +0200 Subject: [PATCH 0042/1293] Forecast UI: add feed-in, edge-to-edge layout, design improvements (#29090) --- assets/css/app.css | 9 +- assets/js/colors.ts | 3 + assets/js/components/Forecast/Co2Chart.vue | 2 +- assets/js/components/Forecast/Co2Details.vue | 57 ++++++ assets/js/components/Forecast/GridDetails.vue | 107 ++++++++++ assets/js/components/Forecast/PriceChart.vue | 141 +++++++------ assets/js/components/Forecast/SolarChart.vue | 2 +- .../js/components/Forecast/SolarDetails.vue | 79 ++++++++ assets/js/components/Forecast/chartStyles.css | 1 + assets/js/components/Forecast/echarts.ts | 10 +- assets/js/mixins/formatter.ts | 5 + assets/js/settings.ts | 4 + assets/js/utils/forecast.test.ts | 30 ++- assets/js/utils/forecast.ts | 8 +- assets/js/views/Forecast.vue | 191 ++++++++---------- i18n/en.json | 11 +- tests/config-grid.spec.ts | 2 +- 17 files changed, 484 insertions(+), 178 deletions(-) create mode 100644 assets/js/components/Forecast/Co2Details.vue create mode 100644 assets/js/components/Forecast/GridDetails.vue create mode 100644 assets/js/components/Forecast/SolarDetails.vue diff --git a/assets/css/app.css b/assets/css/app.css index cc748436c..15171e613 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -21,7 +21,7 @@ --evcc-darkest-green: #076f20ff; --evcc-darkest-green-rgb: 7, 111, 32; --evcc-yellow: #faf000; - --evcc-dark-yellow: #bbb400; + --evcc-dark-yellow: #f6bb0f; --evcc-orange: #ff9000; --evcc-orange-rgb: 255, 144, 0; --evcc-red: #fc440f; @@ -39,6 +39,7 @@ --evcc-pv: var(--evcc-dark-green); --evcc-battery: var(--evcc-darker-green); --evcc-export: var(--evcc-yellow); + --evcc-export-contrast: var(--evcc-dark-yellow); --evcc-price: #ff912fff; --evcc-co2: #00916eff; --evcc-background: var(--bs-gray-bright); @@ -100,6 +101,8 @@ :root.dark { --evcc-grid: var(--bs-gray-medium); + --evcc-export: var(--evcc-yellow); + --evcc-export-contrast: var(--evcc-yellow); --evcc-background: var(--bs-gray-deep); --evcc-box: var(--bs-gray-dark); --evcc-box-border: var(--bs-gray-darker); @@ -186,6 +189,10 @@ h5 { color: var(--evcc-co2) !important; } +.text-export { + color: var(--evcc-export-contrast) !important; +} + a { color: var(--bs-primary); } diff --git a/assets/js/colors.ts b/assets/js/colors.ts index f47242ec5..a98765010 100644 --- a/assets/js/colors.ts +++ b/assets/js/colors.ts @@ -17,6 +17,7 @@ const colors: { pricePerKWh: string | null; price: string | null; co2: string | null; + export: string | null; background: string | null; light: string | null; selfPalette: string[]; @@ -31,6 +32,7 @@ const colors: { pricePerKWh: null, price: null, co2: null, + export: null, background: null, light: null, selfPalette: ["#0FDE41FF", "#FFBD2FFF", "#FD6158FF", "#03C1EFFF", "#0F662DFF", "#FF922EFF"], @@ -75,6 +77,7 @@ export function updateCssColors() { colors.grid = style.getPropertyValue("--evcc-grid"); colors.price = style.getPropertyValue("--evcc-price"); colors.co2 = style.getPropertyValue("--evcc-co2"); + colors.export = style.getPropertyValue("--evcc-export-contrast"); colors.background = style.getPropertyValue("--evcc-background"); colors.pricePerKWh = style.getPropertyValue("--bs-gray-medium"); colors.co2PerKWh = style.getPropertyValue("--bs-gray-medium"); diff --git a/assets/js/components/Forecast/Co2Chart.vue b/assets/js/components/Forecast/Co2Chart.vue index ea688deea..5a3d15d3d 100644 --- a/assets/js/components/Forecast/Co2Chart.vue +++ b/assets/js/components/Forecast/Co2Chart.vue @@ -95,7 +95,7 @@ export default defineComponent({ { type: "line", data: this.slots.map((s) => [s.start, s.value]), - smooth: 0.05, + smooth: true, symbol: "circle", symbolSize: 6, showSymbol: false, diff --git a/assets/js/components/Forecast/Co2Details.vue b/assets/js/components/Forecast/Co2Details.vue new file mode 100644 index 000000000..3358f0b5d --- /dev/null +++ b/assets/js/components/Forecast/Co2Details.vue @@ -0,0 +1,57 @@ + + + diff --git a/assets/js/components/Forecast/GridDetails.vue b/assets/js/components/Forecast/GridDetails.vue new file mode 100644 index 000000000..70d5cecb0 --- /dev/null +++ b/assets/js/components/Forecast/GridDetails.vue @@ -0,0 +1,107 @@ + + + diff --git a/assets/js/components/Forecast/PriceChart.vue b/assets/js/components/Forecast/PriceChart.vue index 00298fa81..6a57b9019 100644 --- a/assets/js/components/Forecast/PriceChart.vue +++ b/assets/js/components/Forecast/PriceChart.vue @@ -7,6 +7,7 @@ diff --git a/assets/js/components/Forecast/SolarChart.vue b/assets/js/components/Forecast/SolarChart.vue index 528279bef..ba97ee967 100644 --- a/assets/js/components/Forecast/SolarChart.vue +++ b/assets/js/components/Forecast/SolarChart.vue @@ -103,7 +103,7 @@ export default defineComponent({ { type: "line", data, - smooth: 0.05, + smooth: true, symbol: "circle", symbolSize: 6, showSymbol: false, diff --git a/assets/js/components/Forecast/SolarDetails.vue b/assets/js/components/Forecast/SolarDetails.vue new file mode 100644 index 000000000..32497c190 --- /dev/null +++ b/assets/js/components/Forecast/SolarDetails.vue @@ -0,0 +1,79 @@ + + + diff --git a/assets/js/components/Forecast/chartStyles.css b/assets/js/components/Forecast/chartStyles.css index 5b9f3e23e..10f4a53a4 100644 --- a/assets/js/components/Forecast/chartStyles.css +++ b/assets/js/components/Forecast/chartStyles.css @@ -1,6 +1,7 @@ .forecast-chart-scroll { overflow-x: auto; padding-bottom: 4px; + margin-bottom: 0.5rem; user-select: none; -webkit-user-select: none; -webkit-touch-callout: none; diff --git a/assets/js/components/Forecast/echarts.ts b/assets/js/components/Forecast/echarts.ts index d56556b54..9fde86ba1 100644 --- a/assets/js/components/Forecast/echarts.ts +++ b/assets/js/components/Forecast/echarts.ts @@ -53,7 +53,7 @@ export function markPointLabel( animation: true, clip: true, symbol: "rect", - symbolSize: 1, + symbolSize: 0, label: { show: true, position: "top", @@ -63,7 +63,7 @@ export function markPointLabel( color: colors.background, backgroundColor: color, borderRadius: 4, - padding: [5, 10], + padding: [5, 15], offset: [0, -2], formatter: (p: { data: { value: string } }) => p.data.value, }, @@ -111,7 +111,7 @@ export function tooltipStyle( } export function forecastGrid() { - return { top: 36, right: 16, bottom: 4, left: 34, borderWidth: 0 }; + return { top: 36, right: 16, bottom: 16, left: 24, borderWidth: 0 }; } export function forecastXAxes(startDate: Date, endDate: Date, weekdayShort: (d: Date) => string) { @@ -167,13 +167,13 @@ export function forecastYAxis(overrides: Record = {}) { axisLine: { show: false }, axisTick: { show: false }, splitLine: { - showMinLine: false, + showMinLine: true, showMaxLine: false, lineStyle: { color: colors.border || "" }, }, axisLabel: { fontSize: 10, - opacity: 0.5, + opacity: 0.75, ...(axisLabel as Record), }, ...rest, diff --git a/assets/js/mixins/formatter.ts b/assets/js/mixins/formatter.ts index 8562f5972..9489dd492 100644 --- a/assets/js/mixins/formatter.ts +++ b/assets/js/mixins/formatter.ts @@ -221,6 +221,11 @@ export default defineComponent({ weekday: "short", }).format(date); }, + weekdayLong(date: Date) { + return new Intl.DateTimeFormat(this.$i18n?.locale, { + weekday: "long", + }).format(date); + }, fmtAbsoluteDate(date: Date) { const weekday = this.weekdayPrefix(date); const hour = new Intl.DateTimeFormat(this.$i18n?.locale, { diff --git a/assets/js/settings.ts b/assets/js/settings.ts index fcd200779..9ea7fcff8 100644 --- a/assets/js/settings.ts +++ b/assets/js/settings.ts @@ -21,6 +21,7 @@ const SESSIONS_GROUP = "sessions_group"; const SESSIONS_TYPE = "sessions_type"; const SETTINGS_SOLAR_ADJUSTED = "settings_solar_adjusted"; const SETTINGS_PRICE_ZOOM = "settings_price_zoom"; +const SETTINGS_HIDE_FEEDIN = "settings_hide_feedin"; const LAST_BATTERY_SMART_COST_LIMIT = "last_battery_smart_cost_limit"; const LAST_TARGET_TIME = "last_target_time"; const LAST_SOC_GOAL = "last_soc_goal"; @@ -124,6 +125,7 @@ export interface Settings { sessionsType: string; solarAdjusted: boolean; priceZoom: boolean; + hideFeedin: boolean; loadpoints: Record; lastBatterySmartCostLimit: number | undefined; lastTargetTime: string | null; @@ -151,6 +153,7 @@ const settings: Settings = reactive({ sessionsType: read(SESSIONS_TYPE), solarAdjusted: readBool(SETTINGS_SOLAR_ADJUSTED), priceZoom: readBool(SETTINGS_PRICE_ZOOM), + hideFeedin: readBool(SETTINGS_HIDE_FEEDIN), loadpoints: readJSON(LOADPOINTS), lastBatterySmartCostLimit: readNumber(LAST_BATTERY_SMART_COST_LIMIT), lastTargetTime: read(LAST_TARGET_TIME), @@ -177,6 +180,7 @@ watch(() => settings.sessionsGroup, save(SESSIONS_GROUP)); watch(() => settings.sessionsType, save(SESSIONS_TYPE)); watch(() => settings.solarAdjusted, saveBool(SETTINGS_SOLAR_ADJUSTED)); watch(() => settings.priceZoom, saveBool(SETTINGS_PRICE_ZOOM)); +watch(() => settings.hideFeedin, saveBool(SETTINGS_HIDE_FEEDIN)); watch(() => settings.loadpoints, saveJSON(LOADPOINTS), { deep: true }); watch(() => settings.lastBatterySmartCostLimit, saveNumber(LAST_BATTERY_SMART_COST_LIMIT)); watch(() => settings.lastTargetTime, save(LAST_TARGET_TIME)); diff --git a/assets/js/utils/forecast.test.ts b/assets/js/utils/forecast.test.ts index 7a1ba0265..f6f1b6b74 100644 --- a/assets/js/utils/forecast.test.ts +++ b/assets/js/utils/forecast.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "vitest"; -import { findLowestSumSlotIndex } from "./forecast"; +import { findLowestSumSlotIndex, isStaticTariff } from "./forecast"; describe("findLowestSumSlotIndex", () => { test("finds lowest sum with span of 4", () => { @@ -55,3 +55,31 @@ describe("findLowestSumSlotIndex", () => { expect(findLowestSumSlotIndex(slots, 2)).toBe(0); }); }); + +describe("isStaticTariff", () => { + test("returns false for undefined", () => { + expect(isStaticTariff(undefined)).toBe(false); + }); + + test("returns false for empty array", () => { + expect(isStaticTariff([])).toBe(false); + }); + + test("returns true when all values are identical", () => { + const slots = [ + { start: "2025-01-01T00:00:00Z", end: "2025-01-01T00:15:00Z", value: 0.25 }, + { start: "2025-01-01T00:15:00Z", end: "2025-01-01T00:30:00Z", value: 0.25 }, + { start: "2025-01-01T00:30:00Z", end: "2025-01-01T00:45:00Z", value: 0.25 }, + ]; + expect(isStaticTariff(slots)).toBe(true); + }); + + test("returns false when values differ", () => { + const slots = [ + { start: "2025-01-01T00:00:00Z", end: "2025-01-01T00:15:00Z", value: 0.25 }, + { start: "2025-01-01T00:15:00Z", end: "2025-01-01T00:30:00Z", value: 0.3 }, + { start: "2025-01-01T00:30:00Z", end: "2025-01-01T00:45:00Z", value: 0.25 }, + ]; + expect(isStaticTariff(slots)).toBe(false); + }); +}); diff --git a/assets/js/utils/forecast.ts b/assets/js/utils/forecast.ts index 66314158e..e18719493 100644 --- a/assets/js/utils/forecast.ts +++ b/assets/js/utils/forecast.ts @@ -1,4 +1,4 @@ -import type { TimeseriesEntry, SolarDetails } from "../components/Forecast/types"; +import type { ForecastSlot, TimeseriesEntry, SolarDetails } from "../components/Forecast/types"; import deepCopy from "./deepClone"; export enum ForecastType { @@ -65,6 +65,12 @@ export function adjustedSolar(solar?: SolarDetails): SolarDetails | undefined { return result; } +export function isStaticTariff(slots?: ForecastSlot[]): boolean { + if (!Array.isArray(slots) || slots.length === 0) return false; + const firstValue = slots[0].value; + return slots.every((slot) => slot.value === firstValue); +} + export interface SlotWithValue { start: string | Date; value: number; diff --git a/assets/js/views/Forecast.vue b/assets/js/views/Forecast.vue index 2410839d4..c1cd0b682 100644 --- a/assets/js/views/Forecast.vue +++ b/assets/js/views/Forecast.vue @@ -41,19 +41,10 @@
-
+
-
-

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

+
+

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

- +
+ +
+
-
-
-

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

+
+
+

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

- + +
+
-
-

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

- +
+

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

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

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

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