Compare commits
8 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b4b4a333c | ||
|
|
4ca64eaf49 | ||
|
|
58938bd0a2 | ||
|
|
07f3982d1e | ||
|
|
e4ca549ea6 | ||
|
|
0990bd9784 | ||
|
|
fff6479115 | ||
|
|
58c4b446d6 |
19 changed files with 114 additions and 52 deletions
|
|
@ -27,6 +27,7 @@ const colors: {
|
||||||
temperature: string | null;
|
temperature: string | null;
|
||||||
export: string | null;
|
export: string | null;
|
||||||
background: string | null;
|
background: string | null;
|
||||||
|
box: string | null;
|
||||||
light: string | null;
|
light: string | null;
|
||||||
selfPalette: string[];
|
selfPalette: string[];
|
||||||
palette: string[];
|
palette: string[];
|
||||||
|
|
@ -44,6 +45,7 @@ const colors: {
|
||||||
temperature: null,
|
temperature: null,
|
||||||
export: null,
|
export: null,
|
||||||
background: null,
|
background: null,
|
||||||
|
box: null,
|
||||||
light: null,
|
light: null,
|
||||||
selfPalette: ["#0FDE41", "#FFBD2F", "#FD6158", "#03C1EF", "#0F662D", "#FF922E"],
|
selfPalette: ["#0FDE41", "#FFBD2F", "#FD6158", "#03C1EF", "#0F662D", "#FF922E"],
|
||||||
palette: [
|
palette: [
|
||||||
|
|
@ -152,6 +154,7 @@ export function updateCssColors() {
|
||||||
colors.temperature = style.getPropertyValue("--evcc-temperature");
|
colors.temperature = style.getPropertyValue("--evcc-temperature");
|
||||||
colors.export = style.getPropertyValue("--evcc-export-contrast");
|
colors.export = style.getPropertyValue("--evcc-export-contrast");
|
||||||
colors.background = style.getPropertyValue("--evcc-background");
|
colors.background = style.getPropertyValue("--evcc-background");
|
||||||
|
colors.box = style.getPropertyValue("--evcc-box");
|
||||||
colors.pricePerKWh = style.getPropertyValue("--bs-gray-medium");
|
colors.pricePerKWh = style.getPropertyValue("--bs-gray-medium");
|
||||||
colors.co2PerKWh = style.getPropertyValue("--bs-gray-medium");
|
colors.co2PerKWh = style.getPropertyValue("--bs-gray-medium");
|
||||||
colors.light = style.getPropertyValue("--bs-gray-light");
|
colors.light = style.getPropertyValue("--bs-gray-light");
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-if="chartData.labels.length > 1" class="row">
|
<div v-if="chartData.labels.length > 1" class="row">
|
||||||
<div class="col-12 col-md-6 mb-3">
|
<div class="col-12 col-md-6 col-lg-12 col-xxl-6 mb-3">
|
||||||
<PolarArea :data="chartData" :options="options" />
|
<PolarArea :data="chartData" :options="options" />
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-md-6 d-flex align-items-center py-0 py-md-3">
|
<div class="col-12 col-md-6 col-lg-12 col-xxl-6 d-flex align-items-center">
|
||||||
<LegendList :legends="legends" :device-colors="deviceColors" grid />
|
<LegendList :legends="legends" :device-colors="deviceColors" grid />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -130,7 +130,7 @@ export default defineComponent({
|
||||||
beginAtZero: false,
|
beginAtZero: false,
|
||||||
ticks: {
|
ticks: {
|
||||||
color: colors.muted || "",
|
color: colors.muted || "",
|
||||||
backdropColor: colors.background || "",
|
backdropColor: colors.box || "",
|
||||||
font: { size: 10 },
|
font: { size: 10 },
|
||||||
callback: this.formatValue,
|
callback: this.formatValue,
|
||||||
maxTicksLimit: 6,
|
maxTicksLimit: 6,
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-if="chartData.labels.length > 1" class="row">
|
<div v-if="chartData.labels.length > 1" class="row">
|
||||||
<div class="col-12 col-md-6 mb-3">
|
<div class="col-12 col-md-6 col-lg-12 col-xxl-6 mb-3">
|
||||||
<Doughnut :data="chartData" :options="options" />
|
<Doughnut :data="chartData" :options="options" />
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-md-6 d-flex align-items-center">
|
<div class="col-12 col-md-6 col-lg-12 col-xxl-6 d-flex align-items-center">
|
||||||
<LegendList :legends="legends" :device-colors="deviceColors" grid />
|
<LegendList :legends="legends" :device-colors="deviceColors" grid />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -94,7 +94,7 @@ export default defineComponent({
|
||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
color: colors.text || "",
|
color: colors.text || "",
|
||||||
borderWidth: 3,
|
borderWidth: 3,
|
||||||
borderColor: colors.background || "",
|
borderColor: colors.box || "",
|
||||||
cutout: "70%",
|
cutout: "70%",
|
||||||
radius: "95%",
|
radius: "95%",
|
||||||
animation: { duration: 250 },
|
animation: { duration: 250 },
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-if="chartData.labels.length > 1" class="row">
|
<div v-if="chartData.labels.length > 1" class="row">
|
||||||
<div class="col-12 col-md-6 mb-3">
|
<div class="col-12 col-md-6 col-lg-12 col-xxl-6 mb-3">
|
||||||
<Doughnut :data="chartData" :options="options" />
|
<Doughnut :data="chartData" :options="options" />
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-md-6 d-flex align-items-center">
|
<div class="col-12 col-md-6 col-lg-12 col-xxl-6 d-flex align-items-center">
|
||||||
<LegendList :legends="legends" :device-colors="deviceColors" grid />
|
<LegendList :legends="legends" :device-colors="deviceColors" grid />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -107,7 +107,7 @@ export default defineComponent({
|
||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
color: colors.text,
|
color: colors.text,
|
||||||
borderWidth: 3,
|
borderWidth: 3,
|
||||||
borderColor: colors.background,
|
borderColor: colors.box,
|
||||||
cutout: "70%",
|
cutout: "70%",
|
||||||
radius: "95%",
|
radius: "95%",
|
||||||
animation: { duration: 250 },
|
animation: { duration: 250 },
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<ul
|
<ul
|
||||||
class="root p-0 d-flex flex-wrap column-gap-4 row-gap-2"
|
class="root p-0 m-0 d-flex flex-wrap column-gap-4 row-gap-2"
|
||||||
:class="{
|
:class="{
|
||||||
'root--small-equal-widths': smallEqualWidths,
|
'root--small-equal-widths': smallEqualWidths,
|
||||||
'root--grid': grid,
|
'root--grid': grid,
|
||||||
|
|
@ -50,7 +50,7 @@
|
||||||
<div
|
<div
|
||||||
v-for="value in valueList(legend.value)"
|
v-for="value in valueList(legend.value)"
|
||||||
:key="value"
|
:key="value"
|
||||||
class="text-muted text-nowrap legend-value text-end"
|
class="text-muted text-nowrap legend-value text-end tabular"
|
||||||
>
|
>
|
||||||
{{ value }}
|
{{ value }}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -174,7 +174,7 @@ const COLUMNS_PER_BREAKPOINT = {
|
||||||
sm: 3,
|
sm: 3,
|
||||||
md: 4,
|
md: 4,
|
||||||
lg: 7,
|
lg: 7,
|
||||||
xl: 8,
|
xl: 7,
|
||||||
xxl: 9,
|
xxl: 9,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-if="chartData.labels.length > 1" class="row">
|
<div v-if="chartData.labels.length > 1" class="row">
|
||||||
<div class="col-12 col-md-6 mb-3">
|
<div class="col-12 col-md-6 col-lg-12 col-xxl-6 mb-3">
|
||||||
<PolarArea :data="chartData" :options="options" />
|
<PolarArea :data="chartData" :options="options" />
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-md-6 d-flex align-items-center py-0 py-md-3">
|
<div class="col-12 col-md-6 col-lg-12 col-xxl-6 d-flex align-items-center">
|
||||||
<LegendList :legends="legends" :device-colors="deviceColors" grid />
|
<LegendList :legends="legends" :device-colors="deviceColors" grid />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -123,7 +123,7 @@ export default defineComponent({
|
||||||
ticks: {
|
ticks: {
|
||||||
stepSize: 25,
|
stepSize: 25,
|
||||||
color: colors.muted,
|
color: colors.muted,
|
||||||
backdropColor: colors.background,
|
backdropColor: colors.box,
|
||||||
},
|
},
|
||||||
grid: { color: colors.border },
|
grid: { color: colors.border },
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -180,7 +180,7 @@ export default defineComponent({
|
||||||
ticks: {
|
ticks: {
|
||||||
stepSize: 20,
|
stepSize: 20,
|
||||||
color: colors.muted,
|
color: colors.muted,
|
||||||
backdropColor: colors.background,
|
backdropColor: colors.box,
|
||||||
font: { size: 10 },
|
font: { size: 10 },
|
||||||
callback: (value: number) => this.fmtPercentage(value, 0),
|
callback: (value: number) => this.fmtPercentage(value, 0),
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -86,9 +86,9 @@
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
<div v-if="showExtraCharts">
|
<div v-if="showExtraCharts">
|
||||||
<div class="row align-items-start">
|
<div class="row">
|
||||||
<div class="col-12 col-lg-6 mb-4">
|
<div class="col-12 col-lg-6 mb-4">
|
||||||
<Card :title="firstExtraTitle" edge-to-edge>
|
<Card :title="firstExtraTitle" edge-to-edge class="h-100">
|
||||||
<div v-if="activeType === types.SOLAR">
|
<div v-if="activeType === types.SOLAR">
|
||||||
<SolarYearChart
|
<SolarYearChart
|
||||||
v-if="showSolarYearChart"
|
v-if="showSolarYearChart"
|
||||||
|
|
@ -116,7 +116,7 @@
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-lg-6 mb-4">
|
<div class="col-12 col-lg-6 mb-4">
|
||||||
<Card :title="secondExtraTitle" edge-to-edge>
|
<Card :title="secondExtraTitle" edge-to-edge class="h-100">
|
||||||
<EnergyGroupedChart
|
<EnergyGroupedChart
|
||||||
v-if="activeType === types.SOLAR"
|
v-if="activeType === types.SOLAR"
|
||||||
:sessions="currentSessions"
|
:sessions="currentSessions"
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,12 @@
|
||||||
"npm": ">=10.0.0",
|
"npm": ">=10.0.0",
|
||||||
"node": ">=26.0.0"
|
"node": ">=26.0.0"
|
||||||
},
|
},
|
||||||
|
"devEngines": {
|
||||||
|
"packageManager": {
|
||||||
|
"name": "npm",
|
||||||
|
"version": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": "github:evcc-io/evcc",
|
"repository": "github:evcc-io/evcc",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
||||||
|
|
@ -226,22 +226,25 @@ func (p *HTTP) request(url string, body string) ([]byte, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// warn on uncached GET polling: a repeated roundtrip means neither a configured
|
|
||||||
// cache nor the device's own response headers spared it. cache hits are exempt.
|
|
||||||
if p.method == http.MethodGet && p.mu == nil && resp.Header.Get(httpcache.XFromCache) == "" {
|
|
||||||
if key := stripQuery(url); repeatedGet(key, time.Now()) {
|
|
||||||
p.log.WARN.Printf("uncached request repeated within 1s, please report at https://github.com/evcc-io/evcc/issues: %s", key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val, err := request.ReadBody(resp)
|
val, err := request.ReadBody(resp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err2 := knownErrors(val); err2 != nil {
|
if err2 := knownErrors(val); err2 != nil {
|
||||||
err = err2
|
err = err2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return val, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return val, err
|
// warn on uncached GET polling: a repeated roundtrip means neither a configured
|
||||||
|
// cache nor the device's own response headers spared it. cache hits are exempt.
|
||||||
|
// only successful responses count, a failed one is retried by the caller
|
||||||
|
if p.method == http.MethodGet && p.mu == nil && resp.Header.Get(httpcache.XFromCache) == "" {
|
||||||
|
if repeatedGet(url, time.Now()) {
|
||||||
|
p.log.WARN.Printf("uncached request repeated within 1s, please report at https://github.com/evcc-io/evcc/issues: %s", url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return val, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type httpAccess struct {
|
type httpAccess struct {
|
||||||
|
|
@ -254,15 +257,6 @@ var (
|
||||||
httpSeen = make(map[string]httpAccess)
|
httpSeen = make(map[string]httpAccess)
|
||||||
)
|
)
|
||||||
|
|
||||||
// stripQuery drops the query and fragment so cache-busting params do not make
|
|
||||||
// each poll look like a distinct url.
|
|
||||||
func stripQuery(url string) string {
|
|
||||||
if i := strings.IndexAny(url, "?#"); i >= 0 {
|
|
||||||
return url[:i]
|
|
||||||
}
|
|
||||||
return url
|
|
||||||
}
|
|
||||||
|
|
||||||
// repeatedGet reports the first time url is fetched again within a second, a sign
|
// repeatedGet reports the first time url is fetched again within a second, a sign
|
||||||
// the response should be cached. It fires once per url to avoid log spam.
|
// the response should be cached. It fires once per url to avoid log spam.
|
||||||
func repeatedGet(url string, now time.Time) bool {
|
func repeatedGet(url string, now time.Time) bool {
|
||||||
|
|
|
||||||
|
|
@ -164,6 +164,27 @@ func (suite *httpTestSuite) TestSetPath() {
|
||||||
suite.Require().Equal("/foo/bar/4711", suite.h.req.URL.String())
|
suite.Require().Equal("/foo/bar/4711", suite.h.req.URL.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRepeatedGetFailed(t *testing.T) {
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
http.Error(w, "nope", http.StatusInternalServerError)
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
p := NewHTTP(util.NewLogger("foo"), http.MethodGet, srv.URL, false, 0)
|
||||||
|
g, err := p.StringGetter()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// a failing request is retried by the caller, it must not count as a sighting
|
||||||
|
for range 2 {
|
||||||
|
_, err := g()
|
||||||
|
require.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
httpSeenMu.Lock()
|
||||||
|
defer httpSeenMu.Unlock()
|
||||||
|
require.NotContains(t, httpSeen, srv.URL)
|
||||||
|
}
|
||||||
|
|
||||||
func TestRepeatedGet(t *testing.T) {
|
func TestRepeatedGet(t *testing.T) {
|
||||||
url := "http://repeated.test/uncached"
|
url := "http://repeated.test/uncached"
|
||||||
t0 := time.Now()
|
t0 := time.Now()
|
||||||
|
|
@ -176,8 +197,7 @@ func TestRepeatedGet(t *testing.T) {
|
||||||
require.False(t, repeatedGet(spaced, t0))
|
require.False(t, repeatedGet(spaced, t0))
|
||||||
require.False(t, repeatedGet(spaced, t0.Add(2*time.Second))) // >1s apart: no warn
|
require.False(t, repeatedGet(spaced, t0.Add(2*time.Second))) // >1s apart: no warn
|
||||||
|
|
||||||
// query params are stripped before keying, so cache-busting still counts as a repeat
|
// query params are part of the key, so cache-busting urls are distinct requests
|
||||||
require.Equal(t, "http://q.test/path", stripQuery("http://q.test/path?ts=1&x=2#frag"))
|
require.False(t, repeatedGet("http://q.test/path?ts=1", t0))
|
||||||
require.False(t, repeatedGet(stripQuery("http://q.test/path?ts=1"), t0))
|
require.False(t, repeatedGet("http://q.test/path?ts=2", t0.Add(300*time.Millisecond)))
|
||||||
require.True(t, repeatedGet(stripQuery("http://q.test/path?ts=2"), t0.Add(300*time.Millisecond)))
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -425,7 +425,7 @@ render: |
|
||||||
address: 60324 # Grid Maximum Export Power (kW * 1000)
|
address: 60324 # Grid Maximum Export Power (kW * 1000)
|
||||||
type: writemultiple
|
type: writemultiple
|
||||||
encoding: uint32
|
encoding: uint32
|
||||||
curtailed:
|
curtailed: # the power limit is expressed as percent of nominal discharge power
|
||||||
source: go
|
source: go
|
||||||
in:
|
in:
|
||||||
- name: limit
|
- name: limit
|
||||||
|
|
@ -437,7 +437,18 @@ render: |
|
||||||
address: 60324 # Grid Maximum Export Power (kW * 1000)
|
address: 60324 # Grid Maximum Export Power (kW * 1000)
|
||||||
type: holding
|
type: holding
|
||||||
decode: uint32
|
decode: uint32
|
||||||
script: limit != 0xFFFFFFFF
|
- name: maxdischargepower
|
||||||
|
type: int
|
||||||
|
config:
|
||||||
|
source: const
|
||||||
|
value: {{ .maxdischargepower }}
|
||||||
|
script: |
|
||||||
|
percent := 100
|
||||||
|
if limit != 0xFFFFFFFF && maxdischargepower > 0 {
|
||||||
|
// round, the watt conversion does not reproduce the written percent exactly
|
||||||
|
percent = (limit*100 + maxdischargepower/2) / maxdischargepower
|
||||||
|
}
|
||||||
|
percent
|
||||||
dim:
|
dim:
|
||||||
source: ifelse
|
source: ifelse
|
||||||
if:
|
if:
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,10 @@ function chartSection(page: Page, heading: string): Locator {
|
||||||
}
|
}
|
||||||
|
|
||||||
test("device colors: autoassign, override, persistence", async ({ page }) => {
|
test("device colors: autoassign, override, persistence", async ({ page }) => {
|
||||||
|
// fixture sessions are dated 2026-05. Sessions.vue ranks loadpoints by energy
|
||||||
|
// over the last three months relative to now, so pin the clock to keep them in range
|
||||||
|
await page.clock.setFixedTime(new Date("2026-05-15T12:00:00Z"));
|
||||||
|
|
||||||
// ---------- Step 1 — Sessions, by-vehicle view ----------
|
// ---------- Step 1 — Sessions, by-vehicle view ----------
|
||||||
await page.goto("/#/sessions?year=2026&month=5");
|
await page.goto("/#/sessions?year=2026&month=5");
|
||||||
await expect(page.getByRole("heading", { name: "Charging Sessions" })).toBeVisible();
|
await expect(page.getByRole("heading", { name: "Charging Sessions" })).toBeVisible();
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ import (
|
||||||
"github.com/evcc-io/evcc/util/cloud"
|
"github.com/evcc-io/evcc/util/cloud"
|
||||||
"github.com/evcc-io/evcc/util/machine"
|
"github.com/evcc-io/evcc/util/machine"
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
"google.golang.org/grpc"
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
)
|
)
|
||||||
|
|
@ -46,6 +47,9 @@ func machineID() string {
|
||||||
|
|
||||||
const unavailable = "sponsorship unavailable"
|
const unavailable = "sponsorship unavailable"
|
||||||
|
|
||||||
|
// startupTimeout leaves the network time to settle at boot; grpc retries dialing with backoff until deadline
|
||||||
|
const startupTimeout = 30 * time.Second
|
||||||
|
|
||||||
func IsAuthorized() bool {
|
func IsAuthorized() bool {
|
||||||
mu.RLock()
|
mu.RLock()
|
||||||
defer mu.RUnlock()
|
defer mu.RUnlock()
|
||||||
|
|
@ -98,10 +102,10 @@ func ConfigureSponsorship(token string) error {
|
||||||
|
|
||||||
client := pb.NewAuthClient(conn)
|
client := pb.NewAuthClient(conn)
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), startupTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
res, err := client.IsAuthorized(ctx, &pb.AuthRequest{Token: token, MachineId: machineID()})
|
res, err := client.IsAuthorized(ctx, &pb.AuthRequest{Token: token, MachineId: machineID()}, grpc.WaitForReady(true))
|
||||||
if err == nil && res.Authorized {
|
if err == nil && res.Authorized {
|
||||||
Subject = res.Subject
|
Subject = res.Subject
|
||||||
ExpiresAt = res.ExpiresAt.AsTime()
|
ExpiresAt = res.ExpiresAt.AsTime()
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ import (
|
||||||
|
|
||||||
"github.com/evcc-io/evcc/api/proto/pb"
|
"github.com/evcc-io/evcc/api/proto/pb"
|
||||||
"github.com/evcc-io/evcc/util/cloud"
|
"github.com/evcc-io/evcc/util/cloud"
|
||||||
"github.com/evcc-io/evcc/util/request"
|
"google.golang.org/grpc"
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
)
|
)
|
||||||
|
|
@ -34,7 +34,7 @@ func checkHardware(vendor string, metadata map[string]string) string {
|
||||||
return unavailable
|
return unavailable
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), request.Timeout)
|
ctx, cancel := context.WithTimeout(context.Background(), startupTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
client := pb.NewAuthClient(conn)
|
client := pb.NewAuthClient(conn)
|
||||||
|
|
@ -42,7 +42,7 @@ func checkHardware(vendor string, metadata map[string]string) string {
|
||||||
MachineId: machineID(),
|
MachineId: machineID(),
|
||||||
Vendor: vendor,
|
Vendor: vendor,
|
||||||
Metadata: metadata,
|
Metadata: metadata,
|
||||||
})
|
}, grpc.WaitForReady(true))
|
||||||
|
|
||||||
if err == nil && res.Authorized {
|
if err == nil && res.Authorized {
|
||||||
return res.Subject
|
return res.Subject
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,10 @@ var requiredKeys = []string{
|
||||||
"vehicle.vehicle.travelledDistance",
|
"vehicle.vehicle.travelledDistance",
|
||||||
}
|
}
|
||||||
|
|
||||||
const requiredVersion = "v6"
|
const (
|
||||||
|
containerName = "evcc.io"
|
||||||
|
requiredVersion = "v6"
|
||||||
|
)
|
||||||
|
|
||||||
type API struct {
|
type API struct {
|
||||||
*request.Helper
|
*request.Helper
|
||||||
|
|
|
||||||
|
|
@ -205,7 +205,7 @@ func (v *MqttConnector) handler(_ mqtt.Client, m mqtt.Message) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
v.log.TRACE.Println("recv: " + string(m.Payload()))
|
v.log.TRACE.Printf("recv %s: %s", m.Topic(), string(m.Payload()))
|
||||||
|
|
||||||
v.mu.RLock()
|
v.mu.RLock()
|
||||||
defer v.mu.RUnlock()
|
defer v.mu.RUnlock()
|
||||||
|
|
|
||||||
|
|
@ -71,14 +71,17 @@ func (v *Provider) findOrCreateContainer() (string, error) {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// obsolete containers keep streaming, resulting in duplicate messages
|
||||||
|
defer v.deleteObsoleteContainers(containers)
|
||||||
|
|
||||||
if i := slices.IndexFunc(containers, func(c Container) bool {
|
if i := slices.IndexFunc(containers, func(c Container) bool {
|
||||||
return c.Name == "evcc.io" && c.Purpose == requiredVersion
|
return c.Name == containerName && c.Purpose == requiredVersion
|
||||||
}); i >= 0 {
|
}); i >= 0 {
|
||||||
return containers[i].ContainerId, nil
|
return containers[i].ContainerId, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := v.api.CreateContainer(CreateContainer{
|
res, err := v.api.CreateContainer(CreateContainer{
|
||||||
Name: "evcc.io",
|
Name: containerName,
|
||||||
Purpose: requiredVersion,
|
Purpose: requiredVersion,
|
||||||
TechnicalDescriptors: requiredKeys,
|
TechnicalDescriptors: requiredKeys,
|
||||||
})
|
})
|
||||||
|
|
@ -86,6 +89,20 @@ func (v *Provider) findOrCreateContainer() (string, error) {
|
||||||
return res.ContainerId, err
|
return res.ContainerId, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (v *Provider) deleteObsoleteContainers(containers []Container) {
|
||||||
|
for _, c := range containers {
|
||||||
|
if c.Name != containerName || c.Purpose == requiredVersion {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
v.log.DEBUG.Printf("deleting obsolete container %s (%s)", c.ContainerId, c.Purpose)
|
||||||
|
|
||||||
|
if err := v.api.DeleteContainer(c.ContainerId); err != nil {
|
||||||
|
v.log.WARN.Printf("delete container %s: %v", c.ContainerId, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (v *Provider) setupContainer() error {
|
func (v *Provider) setupContainer() error {
|
||||||
container, err := v.findOrCreateContainer()
|
container, err := v.findOrCreateContainer()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue