From ce97c635d65ae7772185348d2fb966afa45443a3 Mon Sep 17 00:00:00 2001 From: andig Date: Sat, 27 Apr 2024 10:28:49 +0200 Subject: [PATCH 001/168] Add E3DC native implementation (#13413) --- go.mod | 6 +- go.sum | 10 + meter/e3dc.go | 301 ++++++++++++++++++ meter/e3dc_decorators.go | 137 ++++++++ .../meter/{e3dc.yaml => e3dc-modbus.yaml} | 1 + templates/definition/meter/e3dc-rscp.yaml | 34 ++ util/templates/types.go | 11 - util/templates/usage.go | 12 + util/templates/usage_enumer.go | 14 +- 9 files changed, 513 insertions(+), 13 deletions(-) create mode 100644 meter/e3dc.go create mode 100644 meter/e3dc_decorators.go rename templates/definition/meter/{e3dc.yaml => e3dc-modbus.yaml} (98%) create mode 100644 templates/definition/meter/e3dc-rscp.yaml create mode 100644 util/templates/usage.go diff --git a/go.mod b/go.mod index ad37684d99..ed19251166 100644 --- a/go.mod +++ b/go.mod @@ -77,6 +77,7 @@ require ( github.com/samber/lo v1.39.0 github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 github.com/smallnest/chanx v1.2.0 + github.com/spali/go-rscp v0.2.0-beta4 github.com/spf13/cast v1.6.0 github.com/spf13/cobra v1.8.0 github.com/spf13/jwalterweatherman v1.1.0 @@ -108,9 +109,11 @@ require ( github.com/ahmetb/go-linq/v3 v3.2.0 // indirect github.com/andybalholm/cascadia v1.3.2 // indirect github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect + github.com/azihsoyn/rijndael256 v0.0.0-20200316065338-d14eefa2b66b // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bitly/go-simplejson v0.5.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cstockton/go-conv v1.0.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/fatih/color v1.16.0 // indirect @@ -172,8 +175,9 @@ require ( github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/shopspring/decimal v1.4.0 // indirect - github.com/sirupsen/logrus v1.9.3 // indirect + github.com/sirupsen/logrus v1.9.3 github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spali/go-slicereader v0.0.0-20201122145524-8e262e1a5127 // indirect github.com/spf13/afero v1.11.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/teivah/onecontext v1.3.0 // indirect diff --git a/go.sum b/go.sum index 0d73a3caf1..1eaddfabe0 100644 --- a/go.sum +++ b/go.sum @@ -52,6 +52,8 @@ github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN github.com/aws/aws-sdk-go v1.51.21 h1:UrT6JC9R9PkYYXDZBV0qDKTualMr+bfK2eboTknMgbs= github.com/aws/aws-sdk-go v1.51.21/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= +github.com/azihsoyn/rijndael256 v0.0.0-20200316065338-d14eefa2b66b h1:/2dABok/UswXOj5rjbR5bZ411ApGBq1pAEZdy5rvFrY= +github.com/azihsoyn/rijndael256 v0.0.0-20200316065338-d14eefa2b66b/go.mod h1:ef+2vMUkiKcy2Tz7HykB01KbgUnkK4gQKq4ZeR4RYVs= github.com/basgys/goxml2json v1.1.0 h1:4ln5i4rseYfXNd86lGEB+Vi652IsIXIvggKM/BhUKVw= github.com/basgys/goxml2json v1.1.0/go.mod h1:wH7a5Np/Q4QoECFIU8zTQlZwZkrilY0itPfecMw41Dw= github.com/basvdlei/gotsmart v0.0.3 h1:7hrI6btBc8dmFzzHRDkr9Xl87PvrrOT43DzbsTFqMk8= @@ -94,6 +96,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI= github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +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/dave/jennifer v1.6.1 h1:T4T/67t6RAA5AIV6+NP8Uk/BIsXgDoqEowgycdQQLuk= github.com/dave/jennifer v1.6.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -186,6 +190,8 @@ github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEe github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc= github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8= +github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg= +github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/goburrow/serial v0.1.0 h1:v2T1SQa/dlUqQiYIT8+Cu7YolfqAi3K96UmhwYyuSrA= github.com/goburrow/serial v0.1.0/go.mod h1:sAiqG0nRVswsm1C97xsttiYCzSLBmUZ/VSlVLZJ8haA= github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= @@ -606,6 +612,10 @@ github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4k github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spali/go-rscp v0.2.0-beta4 h1:ct9YZTCmTW2IMg74O16nJu0QntGF26dxY5ZejRvl280= +github.com/spali/go-rscp v0.2.0-beta4/go.mod h1:yPHx7clunJmpCLFDc60XL04/lp8p/DrrhfeBqM3J8cc= +github.com/spali/go-slicereader v0.0.0-20201122145524-8e262e1a5127 h1:YDqvwAH/l3S4ZULmKlUYszPyLBjHq73CLuUPU+2jJeE= +github.com/spali/go-slicereader v0.0.0-20201122145524-8e262e1a5127/go.mod h1:nf5bOq6n8UugtmQiD3l0BzkE5VP4NvyngFZVkH3ZzgM= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= diff --git a/meter/e3dc.go b/meter/e3dc.go new file mode 100644 index 0000000000..016cf453f9 --- /dev/null +++ b/meter/e3dc.go @@ -0,0 +1,301 @@ +package meter + +import ( + "errors" + "net" + "slices" + "strconv" + "sync" + "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/templates" + "github.com/samber/lo" + "github.com/sirupsen/logrus" + "github.com/spali/go-rscp/rscp" + "github.com/spf13/cast" +) + +type E3dc struct { + capacity float64 + dischargeLimit uint32 + usage templates.Usage // TODO check if we really want to depend on templates + conn *rscp.Client +} + +func init() { + registry.Add("e3dc-rscp", NewE3dcFromConfig) +} + +//go:generate go run ../cmd/tools/decorate.go -f decorateE3dc -b *E3dc -r api.Meter -t "api.BatteryCapacity,Capacity,func() float64" -t "api.Battery,Soc,func() (float64, error)" -t "api.BatteryController,SetBatteryMode,func(api.BatteryMode) error" + +func NewE3dcFromConfig(other map[string]interface{}) (api.Meter, error) { + cc := struct { + Usage templates.Usage + Uri string + User string + Password string + Key string + Battery uint16 // battery id + DischargeLimit uint32 + Timeout time.Duration + }{ + Timeout: request.Timeout, + } + + if err := util.DecodeOther(other, &cc); err != nil { + return nil, err + } + + host, port_, err := net.SplitHostPort(util.DefaultPort(cc.Uri, 5033)) + if err != nil { + return nil, err + } + + port, _ := strconv.Atoi(port_) + + cfg := rscp.ClientConfig{ + Address: host, + Port: uint16(port), + Username: cc.User, + Password: cc.Password, + Key: cc.Key, + ConnectionTimeout: cc.Timeout, + SendTimeout: cc.Timeout, + ReceiveTimeout: cc.Timeout, + } + + return NewE3dc(cfg, cc.Usage, cc.Battery, cc.DischargeLimit) +} + +var e3dcOnce sync.Once + +func NewE3dc(cfg rscp.ClientConfig, usage templates.Usage, batteryId uint16, dischargeLimit uint32) (api.Meter, error) { + e3dcOnce.Do(func() { + log := util.NewLogger("e3dc") + rscp.Log.SetLevel(logrus.DebugLevel) + rscp.Log.SetOutput(log.TRACE.Writer()) + }) + + conn, err := rscp.NewClient(cfg) + if err != nil { + return nil, err + } + + m := &E3dc{ + usage: usage, + conn: conn, + dischargeLimit: dischargeLimit, + } + + // decorate api.BatterySoc + var ( + batterySoc func() (float64, error) + batteryCapacity func() float64 + batteryMode func(api.BatteryMode) error + ) + + if usage == templates.UsageBattery { + batterySoc = m.batterySoc + batteryCapacity = m.batteryCapacity + batteryMode = m.setBatteryMode + + res, err := m.conn.Send(rscp.Message{ + Tag: rscp.BAT_REQ_DATA, + DataType: rscp.Container, + Value: []rscp.Message{ + { + Tag: rscp.BAT_INDEX, + DataType: rscp.UInt16, + Value: batteryId, + }, + { + Tag: rscp.BAT_REQ_SPECIFICATION, + DataType: rscp.None, + }, + }, + }) + if err != nil { + return nil, err + } + + batSpec, err := rscpContains(res, rscp.BAT_SPECIFICATION) + if err != nil { + return nil, err + } + + batCap, err := rscpContains(&batSpec, rscp.BAT_SPECIFIED_CAPACITY) + if err != nil { + return nil, err + } + + cap, err := rscpValue(batCap, cast.ToFloat64E) + if err != nil { + return nil, err + } + + m.capacity = cap / 1e3 + } + + return decorateE3dc(m, batteryCapacity, batterySoc, batteryMode), nil +} + +func (m *E3dc) CurrentPower() (float64, error) { + switch m.usage { + case templates.UsageGrid: + res, err := m.conn.Send(*rscp.NewMessage(rscp.EMS_REQ_POWER_GRID, nil)) + if err != nil { + return 0, err + } + return rscpValue(*res, cast.ToFloat64E) + + case templates.UsagePV: + res, err := m.conn.SendMultiple([]rscp.Message{ + *rscp.NewMessage(rscp.EMS_REQ_POWER_PV, nil), + *rscp.NewMessage(rscp.EMS_REQ_POWER_ADD, nil), + }) + if err != nil { + return 0, err + } + + values, err := rscpValues(res, cast.ToFloat64E) + if err != nil { + return 0, err + } + + return lo.Sum(values), nil + + case templates.UsageBattery: + res, err := m.conn.Send(*rscp.NewMessage(rscp.EMS_REQ_POWER_BAT, nil)) + if err != nil { + return 0, err + } + pwr, err := rscpValue(*res, cast.ToFloat64E) + if err != nil { + return 0, err + } + + return -pwr, nil + + default: + return 0, api.ErrNotAvailable + } +} + +func (m *E3dc) batteryCapacity() float64 { + return m.capacity +} + +func (m *E3dc) batterySoc() (float64, error) { + res, err := m.conn.Send(*rscp.NewMessage(rscp.EMS_REQ_BAT_SOC, nil)) + if err != nil { + return 0, err + } + return rscpValue(*res, cast.ToFloat64E) +} + +func (m *E3dc) setBatteryMode(mode api.BatteryMode) error { + var ( + res []rscp.Message + err error + ) + + switch mode { + case api.BatteryNormal: + res, err = m.conn.SendMultiple([]rscp.Message{ + e3dcDischargeBatteryLimit(false, 0), + e3dcBatteryCharge(0), + }) + + case api.BatteryHold: + res, err = m.conn.SendMultiple([]rscp.Message{ + e3dcDischargeBatteryLimit(true, m.dischargeLimit), + e3dcBatteryCharge(0), + }) + + case api.BatteryCharge: + res, err = m.conn.SendMultiple([]rscp.Message{ + e3dcDischargeBatteryLimit(false, 0), + e3dcBatteryCharge(10000), // 10kWh + }) + + default: + return api.ErrNotAvailable + } + + if err == nil { + err = rscpError(res...) + } + return err +} + +func e3dcDischargeBatteryLimit(active bool, limit uint32) rscp.Message { + contents := []rscp.Message{ + *rscp.NewMessage(rscp.EMS_POWER_LIMITS_USED, active), + } + + if active { + contents = append(contents, *rscp.NewMessage(rscp.EMS_MAX_DISCHARGE_POWER, limit)) + } + + return *rscp.NewMessage(rscp.EMS_REQ_SET_POWER_SETTINGS, contents) +} + +func e3dcBatteryCharge(amount uint32) rscp.Message { + return *rscp.NewMessage(rscp.EMS_REQ_START_MANUAL_CHARGE, amount) +} + +func rscpError(msg ...rscp.Message) error { + var errs []error + for _, m := range msg { + if m.DataType == rscp.Error { + errs = append(errs, errors.New(rscp.RscpError(cast.ToUint32(m.Value)).String())) + } + } + return errors.Join(errs...) +} + +func rscpContains(msg *rscp.Message, tag rscp.Tag) (rscp.Message, error) { + var zero rscp.Message + + slice, ok := msg.Value.([]rscp.Message) + if !ok { + return zero, errors.New("not a slice looking for " + tag.String()) + } + + idx := slices.IndexFunc(slice, func(m rscp.Message) bool { + return m.Tag == tag + }) + if idx < 0 { + return zero, errors.New("missing " + tag.String()) + } + + res := slice[idx] + return res, rscpError(res) +} + +func rscpValue[T any](msg rscp.Message, fun func(any) (T, error)) (T, error) { + var zero T + if err := rscpError(msg); err != nil { + return zero, err + } + + return fun(msg.Value) +} + +func rscpValues[T any](msg []rscp.Message, fun func(any) (T, error)) ([]T, error) { + res := make([]T, 0, len(msg)) + + for _, m := range msg { + v, err := rscpValue(m, fun) + if err != nil { + return nil, err + } + + res = append(res, v) + } + + return res, nil +} diff --git a/meter/e3dc_decorators.go b/meter/e3dc_decorators.go new file mode 100644 index 0000000000..5e7e3e478d --- /dev/null +++ b/meter/e3dc_decorators.go @@ -0,0 +1,137 @@ +package meter + +// Code generated by github.com/evcc-io/evcc/cmd/tools/decorate.go. DO NOT EDIT. + +import ( + "github.com/evcc-io/evcc/api" +) + +func decorateE3dc(base *E3dc, batteryCapacity func() float64, battery func() (float64, error), batteryController func(api.BatteryMode) error) api.Meter { + switch { + case battery == nil && batteryCapacity == nil && batteryController == nil: + return base + + case battery == nil && batteryCapacity != nil && batteryController == nil: + return &struct { + *E3dc + api.BatteryCapacity + }{ + E3dc: base, + BatteryCapacity: &decorateE3dcBatteryCapacityImpl{ + batteryCapacity: batteryCapacity, + }, + } + + case battery != nil && batteryCapacity == nil && batteryController == nil: + return &struct { + *E3dc + api.Battery + }{ + E3dc: base, + Battery: &decorateE3dcBatteryImpl{ + battery: battery, + }, + } + + case battery != nil && batteryCapacity != nil && batteryController == nil: + return &struct { + *E3dc + api.Battery + api.BatteryCapacity + }{ + E3dc: base, + Battery: &decorateE3dcBatteryImpl{ + battery: battery, + }, + BatteryCapacity: &decorateE3dcBatteryCapacityImpl{ + batteryCapacity: batteryCapacity, + }, + } + + case battery == nil && batteryCapacity == nil && batteryController != nil: + return &struct { + *E3dc + api.BatteryController + }{ + E3dc: base, + BatteryController: &decorateE3dcBatteryControllerImpl{ + batteryController: batteryController, + }, + } + + case battery == nil && batteryCapacity != nil && batteryController != nil: + return &struct { + *E3dc + api.BatteryCapacity + api.BatteryController + }{ + E3dc: base, + BatteryCapacity: &decorateE3dcBatteryCapacityImpl{ + batteryCapacity: batteryCapacity, + }, + BatteryController: &decorateE3dcBatteryControllerImpl{ + batteryController: batteryController, + }, + } + + case battery != nil && batteryCapacity == nil && batteryController != nil: + return &struct { + *E3dc + api.Battery + api.BatteryController + }{ + E3dc: base, + Battery: &decorateE3dcBatteryImpl{ + battery: battery, + }, + BatteryController: &decorateE3dcBatteryControllerImpl{ + batteryController: batteryController, + }, + } + + case battery != nil && batteryCapacity != nil && batteryController != nil: + return &struct { + *E3dc + api.Battery + api.BatteryCapacity + api.BatteryController + }{ + E3dc: base, + Battery: &decorateE3dcBatteryImpl{ + battery: battery, + }, + BatteryCapacity: &decorateE3dcBatteryCapacityImpl{ + batteryCapacity: batteryCapacity, + }, + BatteryController: &decorateE3dcBatteryControllerImpl{ + batteryController: batteryController, + }, + } + } + + return nil +} + +type decorateE3dcBatteryImpl struct { + battery func() (float64, error) +} + +func (impl *decorateE3dcBatteryImpl) Soc() (float64, error) { + return impl.battery() +} + +type decorateE3dcBatteryCapacityImpl struct { + batteryCapacity func() float64 +} + +func (impl *decorateE3dcBatteryCapacityImpl) Capacity() float64 { + return impl.batteryCapacity() +} + +type decorateE3dcBatteryControllerImpl struct { + batteryController func(api.BatteryMode) error +} + +func (impl *decorateE3dcBatteryControllerImpl) SetBatteryMode(p0 api.BatteryMode) error { + return impl.batteryController(p0) +} diff --git a/templates/definition/meter/e3dc.yaml b/templates/definition/meter/e3dc-modbus.yaml similarity index 98% rename from templates/definition/meter/e3dc.yaml rename to templates/definition/meter/e3dc-modbus.yaml index 80614fdee1..8cccec9baf 100644 --- a/templates/definition/meter/e3dc.yaml +++ b/templates/definition/meter/e3dc-modbus.yaml @@ -1,4 +1,5 @@ template: e3dc +deprecated: true products: - brand: E3/DC params: diff --git a/templates/definition/meter/e3dc-rscp.yaml b/templates/definition/meter/e3dc-rscp.yaml new file mode 100644 index 0000000000..c855653f2f --- /dev/null +++ b/templates/definition/meter/e3dc-rscp.yaml @@ -0,0 +1,34 @@ +template: e3dc-rscp +products: + - brand: E3/DC +params: + - name: usage + choice: ["grid", "pv", "battery"] + allinone: true + - name: host + - name: port + default: 5033 + - name: user + - name: password + - name: key + - name: battery + type: number + advanced: true + - name: dischargelimit + description: + de: Entladelimit in W + en: Discharge limit in W + help: + de: Limitiert die Entladeleistung im 'Halten' Batteriemodus + en: Limits discharge power in 'Hold' battery mode + type: number + advanced: true +render: | + type: e3dc-rscp + usage: {{ .usage }} + uri: {{ .host }}:{{ .port }} + user: {{ .user }} + password: {{ .password }} + key: {{ .key }} + battery: {{ .battery }} + dischargelimit: {{ .dischargelimit }} diff --git a/util/templates/types.go b/util/templates/types.go index 42c7158e78..52510e1c47 100644 --- a/util/templates/types.go +++ b/util/templates/types.go @@ -9,17 +9,6 @@ import ( "dario.cat/mergo" ) -type Usage int - -//go:generate enumer -type Usage -trimprefix Usage -transform=lower -const ( - UsageGrid Usage = iota - UsagePV - UsageBattery - UsageCharge - UsageAux -) - const ( ParamUsage = "usage" ParamModbus = "modbus" diff --git a/util/templates/usage.go b/util/templates/usage.go new file mode 100644 index 0000000000..1981429959 --- /dev/null +++ b/util/templates/usage.go @@ -0,0 +1,12 @@ +package templates + +type Usage int + +//go:generate enumer -type Usage -trimprefix Usage -transform=lower -text +const ( + UsageGrid Usage = iota + UsagePV + UsageBattery + UsageCharge + UsageAux +) diff --git a/util/templates/usage_enumer.go b/util/templates/usage_enumer.go index 8655c6581f..c6c4b605b9 100644 --- a/util/templates/usage_enumer.go +++ b/util/templates/usage_enumer.go @@ -1,4 +1,4 @@ -// Code generated by "enumer -type Usage -trimprefix Usage -transform=lower"; DO NOT EDIT. +// Code generated by "enumer -type Usage -trimprefix Usage -transform=lower -text"; DO NOT EDIT. package templates @@ -88,3 +88,15 @@ func (i Usage) IsAUsage() bool { } return false } + +// MarshalText implements the encoding.TextMarshaler interface for Usage +func (i Usage) MarshalText() ([]byte, error) { + return []byte(i.String()), nil +} + +// UnmarshalText implements the encoding.TextUnmarshaler interface for Usage +func (i *Usage) UnmarshalText(text []byte) error { + var err error + *i, err = UsageString(string(text)) + return err +} From 652cab3192b57df3a98d5f13cfe787c5fd67e2f8 Mon Sep 17 00:00:00 2001 From: andig Date: Sat, 27 Apr 2024 10:36:48 +0200 Subject: [PATCH 002/168] Fix phase powers not applied for signed currents --- core/site.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/site.go b/core/site.go index bec18a0e76..21558801bf 100644 --- a/core/site.go +++ b/core/site.go @@ -553,7 +553,8 @@ func (site *Site) updateGridMeter() error { // grid phase powers var p1, p2, p3 float64 if phaseMeter, ok := site.gridMeter.(api.PhasePowers); ok { - if p1, p2, p3, err := phaseMeter.Powers(); err == nil { + var err error // phases needed for signed currents + if p1, p2, p3, err = phaseMeter.Powers(); err == nil { phases := []float64{p1, p2, p3} site.log.DEBUG.Printf("grid powers: %.0fW", phases) site.publish(keys.GridPowers, phases) From 26973a0d2c98a90b504d85dd31162b89b30d5bf3 Mon Sep 17 00:00:00 2001 From: andig Date: Sat, 27 Apr 2024 10:50:35 +0200 Subject: [PATCH 003/168] Load Management and Peak Shaving (#13207) --- api/api.go | 31 ++- api/mock.go | 255 +++++++++++++++++++++- cmd/setup.go | 164 +++++++++++++- cmd/setup_circuits_test.go | 140 ++++++++++++ cmd/setup_test.go | 9 +- core/circuit.go | 308 +++++++++++++++++++++++++++ core/circuit_test.go | 123 +++++++++++ core/helper.go | 8 + core/keys/site.go | 1 + core/loadpoint.go | 61 ++++-- core/loadpoint/api.go | 5 + core/loadpoint/mock.go | 28 +++ core/loadpoint_api.go | 23 ++ core/site.go | 33 ++- core/site/api.go | 3 + core/site_api.go | 23 +- core/site_circuits.go | 38 ++++ lm.md | 43 ++++ server/http_config_device_handler.go | 28 +++ util/config/instance.go | 22 +- util/config/types.go | 14 ++ util/ptr.go | 15 ++ util/templates/class.go | 1 + util/templates/class_enumer.go | 12 +- 24 files changed, 1339 insertions(+), 49 deletions(-) create mode 100644 cmd/setup_circuits_test.go create mode 100644 core/circuit.go create mode 100644 core/circuit_test.go create mode 100644 core/site_circuits.go create mode 100644 lm.md create mode 100644 util/ptr.go diff --git a/api/api.go b/api/api.go index 77500df088..cc6a9ea7ff 100644 --- a/api/api.go +++ b/api/api.go @@ -7,7 +7,7 @@ import ( "time" ) -//go:generate mockgen -package api -destination mock.go github.com/evcc-io/evcc/api Charger,ChargeState,CurrentLimiter,PhaseSwitcher,Identifier,Meter,MeterEnergy,Vehicle,ChargeRater,Battery,Tariff,BatteryController +//go:generate mockgen -package api -destination mock.go github.com/evcc-io/evcc/api Charger,ChargeState,CurrentLimiter,PhaseSwitcher,Identifier,Meter,MeterEnergy,PhaseCurrents,Vehicle,ChargeRater,Battery,Tariff,BatteryController,Circuit // Meter provides total active power in W type Meter interface { @@ -202,3 +202,32 @@ type FeatureDescriber interface { type CsvWriter interface { WriteCsv(context.Context, io.Writer) error } + +// CircuitMeasurements is the measurements a circuit or load must deliver +type CircuitMeasurements interface { + GetChargePower() float64 + GetMaxPhaseCurrent() float64 +} + +// CircuitLoad represents a loadpoint attached to a circuit +type CircuitLoad interface { + CircuitMeasurements + GetCircuit() Circuit +} + +// Circuit defines the load control domain +type Circuit interface { + CircuitMeasurements + GetTitle() string + SetTitle(string) + GetParent() Circuit + RegisterChild(child Circuit) + HasMeter() bool + GetMaxPower() float64 + GetMaxCurrent() float64 + SetMaxPower(float64) + SetMaxCurrent(float64) + Update([]CircuitLoad) error + ValidateCurrent(old, new float64) float64 + ValidatePower(old, new float64) float64 +} diff --git a/api/mock.go b/api/mock.go index c3f79d5258..9b81c990ad 100644 --- a/api/mock.go +++ b/api/mock.go @@ -1,9 +1,9 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/evcc-io/evcc/api (interfaces: Charger,ChargeState,CurrentLimiter,PhaseSwitcher,Identifier,Meter,MeterEnergy,Vehicle,ChargeRater,Battery,Tariff,BatteryController) +// Source: github.com/evcc-io/evcc/api (interfaces: Charger,ChargeState,CurrentLimiter,PhaseSwitcher,Identifier,Meter,MeterEnergy,PhaseCurrents,Vehicle,ChargeRater,Battery,Tariff,BatteryController,Circuit) // // Generated by this command: // -// mockgen -package api -destination mock.go github.com/evcc-io/evcc/api Charger,ChargeState,CurrentLimiter,PhaseSwitcher,Identifier,Meter,MeterEnergy,Vehicle,ChargeRater,Battery,Tariff,BatteryController +// mockgen -package api -destination mock.go github.com/evcc-io/evcc/api Charger,ChargeState,CurrentLimiter,PhaseSwitcher,Identifier,Meter,MeterEnergy,PhaseCurrents,Vehicle,ChargeRater,Battery,Tariff,BatteryController,Circuit // // Package api is a generated GoMock package. @@ -324,6 +324,46 @@ func (mr *MockMeterEnergyMockRecorder) TotalEnergy() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TotalEnergy", reflect.TypeOf((*MockMeterEnergy)(nil).TotalEnergy)) } +// MockPhaseCurrents is a mock of PhaseCurrents interface. +type MockPhaseCurrents struct { + ctrl *gomock.Controller + recorder *MockPhaseCurrentsMockRecorder +} + +// MockPhaseCurrentsMockRecorder is the mock recorder for MockPhaseCurrents. +type MockPhaseCurrentsMockRecorder struct { + mock *MockPhaseCurrents +} + +// NewMockPhaseCurrents creates a new mock instance. +func NewMockPhaseCurrents(ctrl *gomock.Controller) *MockPhaseCurrents { + mock := &MockPhaseCurrents{ctrl: ctrl} + mock.recorder = &MockPhaseCurrentsMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockPhaseCurrents) EXPECT() *MockPhaseCurrentsMockRecorder { + return m.recorder +} + +// Currents mocks base method. +func (m *MockPhaseCurrents) Currents() (float64, float64, float64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Currents") + ret0, _ := ret[0].(float64) + ret1, _ := ret[1].(float64) + ret2, _ := ret[2].(float64) + ret3, _ := ret[3].(error) + return ret0, ret1, ret2, ret3 +} + +// Currents indicates an expected call of Currents. +func (mr *MockPhaseCurrentsMockRecorder) Currents() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Currents", reflect.TypeOf((*MockPhaseCurrents)(nil).Currents)) +} + // MockVehicle is a mock of Vehicle interface. type MockVehicle struct { ctrl *gomock.Controller @@ -636,3 +676,214 @@ func (mr *MockBatteryControllerMockRecorder) SetBatteryMode(arg0 any) *gomock.Ca mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetBatteryMode", reflect.TypeOf((*MockBatteryController)(nil).SetBatteryMode), arg0) } + +// MockCircuit is a mock of Circuit interface. +type MockCircuit struct { + ctrl *gomock.Controller + recorder *MockCircuitMockRecorder +} + +// MockCircuitMockRecorder is the mock recorder for MockCircuit. +type MockCircuitMockRecorder struct { + mock *MockCircuit +} + +// NewMockCircuit creates a new mock instance. +func NewMockCircuit(ctrl *gomock.Controller) *MockCircuit { + mock := &MockCircuit{ctrl: ctrl} + mock.recorder = &MockCircuitMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockCircuit) EXPECT() *MockCircuitMockRecorder { + return m.recorder +} + +// GetChargePower mocks base method. +func (m *MockCircuit) GetChargePower() float64 { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetChargePower") + ret0, _ := ret[0].(float64) + return ret0 +} + +// GetChargePower indicates an expected call of GetChargePower. +func (mr *MockCircuitMockRecorder) GetChargePower() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChargePower", reflect.TypeOf((*MockCircuit)(nil).GetChargePower)) +} + +// GetMaxCurrent mocks base method. +func (m *MockCircuit) GetMaxCurrent() float64 { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetMaxCurrent") + ret0, _ := ret[0].(float64) + return ret0 +} + +// GetMaxCurrent indicates an expected call of GetMaxCurrent. +func (mr *MockCircuitMockRecorder) GetMaxCurrent() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMaxCurrent", reflect.TypeOf((*MockCircuit)(nil).GetMaxCurrent)) +} + +// GetMaxPhaseCurrent mocks base method. +func (m *MockCircuit) GetMaxPhaseCurrent() float64 { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetMaxPhaseCurrent") + ret0, _ := ret[0].(float64) + return ret0 +} + +// GetMaxPhaseCurrent indicates an expected call of GetMaxPhaseCurrent. +func (mr *MockCircuitMockRecorder) GetMaxPhaseCurrent() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMaxPhaseCurrent", reflect.TypeOf((*MockCircuit)(nil).GetMaxPhaseCurrent)) +} + +// GetMaxPower mocks base method. +func (m *MockCircuit) GetMaxPower() float64 { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetMaxPower") + ret0, _ := ret[0].(float64) + return ret0 +} + +// GetMaxPower indicates an expected call of GetMaxPower. +func (mr *MockCircuitMockRecorder) GetMaxPower() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMaxPower", reflect.TypeOf((*MockCircuit)(nil).GetMaxPower)) +} + +// GetParent mocks base method. +func (m *MockCircuit) GetParent() Circuit { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetParent") + ret0, _ := ret[0].(Circuit) + return ret0 +} + +// GetParent indicates an expected call of GetParent. +func (mr *MockCircuitMockRecorder) GetParent() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetParent", reflect.TypeOf((*MockCircuit)(nil).GetParent)) +} + +// GetTitle mocks base method. +func (m *MockCircuit) GetTitle() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetTitle") + ret0, _ := ret[0].(string) + return ret0 +} + +// GetTitle indicates an expected call of GetTitle. +func (mr *MockCircuitMockRecorder) GetTitle() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTitle", reflect.TypeOf((*MockCircuit)(nil).GetTitle)) +} + +// HasMeter mocks base method. +func (m *MockCircuit) HasMeter() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "HasMeter") + ret0, _ := ret[0].(bool) + return ret0 +} + +// HasMeter indicates an expected call of HasMeter. +func (mr *MockCircuitMockRecorder) HasMeter() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasMeter", reflect.TypeOf((*MockCircuit)(nil).HasMeter)) +} + +// RegisterChild mocks base method. +func (m *MockCircuit) RegisterChild(arg0 Circuit) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "RegisterChild", arg0) +} + +// RegisterChild indicates an expected call of RegisterChild. +func (mr *MockCircuitMockRecorder) RegisterChild(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RegisterChild", reflect.TypeOf((*MockCircuit)(nil).RegisterChild), arg0) +} + +// SetMaxCurrent mocks base method. +func (m *MockCircuit) SetMaxCurrent(arg0 float64) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetMaxCurrent", arg0) +} + +// SetMaxCurrent indicates an expected call of SetMaxCurrent. +func (mr *MockCircuitMockRecorder) SetMaxCurrent(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetMaxCurrent", reflect.TypeOf((*MockCircuit)(nil).SetMaxCurrent), arg0) +} + +// SetMaxPower mocks base method. +func (m *MockCircuit) SetMaxPower(arg0 float64) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetMaxPower", arg0) +} + +// SetMaxPower indicates an expected call of SetMaxPower. +func (mr *MockCircuitMockRecorder) SetMaxPower(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetMaxPower", reflect.TypeOf((*MockCircuit)(nil).SetMaxPower), arg0) +} + +// SetTitle mocks base method. +func (m *MockCircuit) SetTitle(arg0 string) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetTitle", arg0) +} + +// SetTitle indicates an expected call of SetTitle. +func (mr *MockCircuitMockRecorder) SetTitle(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetTitle", reflect.TypeOf((*MockCircuit)(nil).SetTitle), arg0) +} + +// Update mocks base method. +func (m *MockCircuit) Update(arg0 []CircuitLoad) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Update", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// Update indicates an expected call of Update. +func (mr *MockCircuitMockRecorder) Update(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockCircuit)(nil).Update), arg0) +} + +// ValidateCurrent mocks base method. +func (m *MockCircuit) ValidateCurrent(arg0, arg1 float64) float64 { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ValidateCurrent", arg0, arg1) + ret0, _ := ret[0].(float64) + return ret0 +} + +// ValidateCurrent indicates an expected call of ValidateCurrent. +func (mr *MockCircuitMockRecorder) ValidateCurrent(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateCurrent", reflect.TypeOf((*MockCircuit)(nil).ValidateCurrent), arg0, arg1) +} + +// ValidatePower mocks base method. +func (m *MockCircuit) ValidatePower(arg0, arg1 float64) float64 { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ValidatePower", arg0, arg1) + ret0, _ := ret[0].(float64) + return ret0 +} + +// ValidatePower indicates an expected call of ValidatePower. +func (mr *MockCircuitMockRecorder) ValidatePower(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidatePower", reflect.TypeOf((*MockCircuit)(nil).ValidatePower), arg0, arg1) +} diff --git a/cmd/setup.go b/cmd/setup.go index 1447020049..0fa77e7624 100644 --- a/cmd/setup.go +++ b/cmd/setup.go @@ -45,6 +45,7 @@ import ( "github.com/gorilla/handlers" "github.com/gorilla/mux" "github.com/libp2p/zeroconf/v2" + "github.com/spf13/cast" "github.com/spf13/cobra" "github.com/spf13/viper" "golang.org/x/sync/errgroup" @@ -95,6 +96,7 @@ type globalConfig struct { Tariffs tariffConfig Site map[string]interface{} Loadpoints []map[string]interface{} + Circuits []config.Named } type mqttConfig struct { @@ -183,6 +185,115 @@ func loadConfigFile(conf *globalConfig) error { return err } +func configureCircuits(static []config.Named, names ...string) error { + children := slices.Clone(static) + + // TODO: check for circular references +NEXT: + for i, cc := range children { + if cc.Name == "" { + return fmt.Errorf("cannot create circuit: missing name") + } + + if err := nameValid(cc.Name); err != nil { + return fmt.Errorf("cannot create circuit: duplicate name: %s", cc.Name) + } + + if parent := cast.ToString(cc.Property("parent")); parent != "" { + if _, err := config.Circuits().ByName(parent); err != nil { + continue + } + } + + log := util.NewLogger("circuit-" + cc.Name) + instance, err := core.NewCircuitFromConfig(log, cc.Other) + if err != nil { + return fmt.Errorf("cannot create circuit '%s': %w", cc.Name, err) + } + + // ensure config has title + if instance.GetTitle() == "" { + //lint:ignore SA1019 as Title is safe on ascii + instance.SetTitle(strings.Title(cc.Name)) + } + + if err := config.Circuits().Add(config.NewStaticDevice(cc, instance)); err != nil { + return err + } + + children = slices.Delete(children, i, i+1) + goto NEXT + } + + if len(children) > 0 { + return fmt.Errorf("circuit is missing parent: %s", children[0].Name) + } + + // append devices from database + configurable, err := config.ConfigurationsByClass(templates.Circuit) + if err != nil { + return err + } + + children2 := slices.Clone(configurable) + +NEXT2: + for i, conf := range children2 { + cc := conf.Named() + + if len(names) > 0 && !slices.Contains(names, cc.Name) { + return nil + } + + if parent := cast.ToString(cc.Property("parent")); parent != "" { + if _, err := config.Circuits().ByName(parent); err != nil { + continue + } + } + + log := util.NewLogger("circuit-" + cc.Name) + instance, err := core.NewCircuitFromConfig(log, cc.Other) + if err != nil { + return fmt.Errorf("cannot create circuit '%s': %w", cc.Name, err) + } + + // ensure config has title + if instance.GetTitle() == "" { + //lint:ignore SA1019 as Title is safe on ascii + instance.SetTitle(strings.Title(cc.Name)) + } + + if err := config.Circuits().Add(config.NewConfigurableDevice(conf, instance)); err != nil { + return err + } + + children2 = slices.Delete(children2, i, i+1) + goto NEXT2 + } + + if len(children2) > 0 { + return fmt.Errorf("missing parent circuit: %s", children2[0].Named().Name) + } + + var rootFound bool + for _, dev := range config.Circuits().Devices() { + c := dev.Instance() + + if c.GetParent() == nil { + if rootFound { + return errors.New("cannot have multiple root circuits") + } + rootFound = true + } + } + + if !rootFound && len(config.Circuits().Devices()) > 0 { + return errors.New("root circuit required") + } + + return nil +} + func configureMeters(static []config.Named, names ...string) error { for i, cc := range static { if cc.Name == "" { @@ -647,6 +758,9 @@ func configureDevices(conf globalConfig) error { if err := configureChargers(conf.Chargers); err != nil { return err } + if err := configureCircuits(conf.Circuits); err != nil { + return err + } return configureVehicles(conf.Vehicles) } @@ -665,7 +779,43 @@ func configureSiteAndLoadpoints(conf globalConfig) (*core.Site, error) { return nil, err } - return configureSite(conf.Site, loadpoints, tariffs) + site, err := configureSite(conf.Site, loadpoints, tariffs) + if err != nil { + return nil, err + } + + if len(config.Circuits().Devices()) > 0 { + if err := validateCircuits(site, loadpoints); err != nil { + return nil, err + } + } + + return site, nil +} + +func validateCircuits(site site.API, loadpoints []*core.Loadpoint) error { +CONTINUE: + for _, dev := range config.Circuits().Devices() { + instance := dev.Instance() + + if instance.HasMeter() || site.GetCircuit() == instance { + continue + } + + for _, lp := range loadpoints { + if lp.GetCircuit() == instance { + continue CONTINUE + } + } + + return fmt.Errorf("circuit %s has no meter or loadpoint assigned", dev.Config().Name) + } + + if site.GetCircuit() == nil { + return errors.New("site has no circuit") + } + + return nil } func configureSite(conf map[string]interface{}, loadpoints []*core.Loadpoint, tariffs *tariff.Tariffs) (*core.Site, error) { @@ -674,19 +824,25 @@ func configureSite(conf map[string]interface{}, loadpoints []*core.Loadpoint, ta return nil, fmt.Errorf("failed configuring site: %w", err) } + if len(config.Circuits().Devices()) > 0 && site.GetCircuit() == nil { + return nil, errors.New("site has no circuit") + } + return site, nil } -func configureLoadpoints(conf globalConfig) (loadpoints []*core.Loadpoint, err error) { +func configureLoadpoints(conf globalConfig) ([]*core.Loadpoint, error) { if len(conf.Loadpoints) == 0 { return nil, errors.New("missing loadpoints") } - for id, lpc := range conf.Loadpoints { + var loadpoints []*core.Loadpoint + + for id, cfg := range conf.Loadpoints { log := util.NewLoggerWithLoadpoint("lp-"+strconv.Itoa(id+1), id+1) settings := &core.Settings{Key: "lp" + strconv.Itoa(id+1) + "."} - lp, err := core.NewLoadpointFromConfig(log, settings, lpc) + lp, err := core.NewLoadpointFromConfig(log, settings, cfg) if err != nil { return nil, fmt.Errorf("failed configuring loadpoint: %w", err) } diff --git a/cmd/setup_circuits_test.go b/cmd/setup_circuits_test.go new file mode 100644 index 0000000000..ab48fa1922 --- /dev/null +++ b/cmd/setup_circuits_test.go @@ -0,0 +1,140 @@ +package cmd + +import ( + "strings" + "testing" + + "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/core" + "github.com/evcc-io/evcc/meter" + "github.com/evcc-io/evcc/server/db" + "github.com/evcc-io/evcc/tariff" + "github.com/evcc-io/evcc/util/config" + "github.com/spf13/viper" + "github.com/stretchr/testify/suite" + "go.uber.org/mock/gomock" +) + +func TestSetupCircuits(t *testing.T) { + suite.Run(t, new(circuitsTestSuite)) +} + +type circuitsTestSuite struct { + suite.Suite +} + +func (suite *circuitsTestSuite) SetupSuite() { + db, err := db.New("sqlite", ":memory:") + if err != nil { + suite.T().Fatal(err) + } + config.Init(db) +} + +func (suite *circuitsTestSuite) SetupTest() { + config.Reset() +} + +func (suite *circuitsTestSuite) TestCircuitConf() { + var conf globalConfig + viper.SetConfigType("yaml") + + suite.Require().NoError(viper.ReadConfig(strings.NewReader(`circuits: +- name: master + maxPower: 10000 +- name: slave + parent: master + maxPower: 10000 +loadpoints: +- charger: test + circuit: slave +`))) + + suite.Require().NoError(viper.UnmarshalExact(&conf)) + + suite.Require().NoError(configureCircuits(conf.Circuits)) + suite.Require().Len(config.Circuits().Devices(), 2) + suite.Require().False(config.Circuits().Devices()[0].Instance().HasMeter()) + + // empty charger + suite.Require().NoError(config.Chargers().Add(config.NewStaticDevice(config.Named{ + Name: "test", + }, api.Charger(nil)))) + + lps, err := configureLoadpoints(conf) + suite.Require().NoError(err) + suite.Require().Len(lps, 1) + suite.Require().NotNil(lps[0].GetCircuit()) +} + +func (suite *circuitsTestSuite) TestLoadpointMissingCircuitError() { + var conf globalConfig + viper.SetConfigType("yaml") + + suite.Require().NoError(viper.ReadConfig(strings.NewReader(` +loadpoints: +- charger: test +`))) + + suite.Require().NoError(viper.UnmarshalExact(&conf)) + + ctrl := gomock.NewController(suite.T()) + circuit := api.NewMockCircuit(ctrl) + + // mock circuit + suite.Require().NoError(config.Circuits().Add(config.NewStaticDevice(config.Named{ + Name: "test", + }, api.Circuit(circuit)))) + + // mock charger + suite.Require().NoError(config.Chargers().Add(config.NewStaticDevice(config.Named{ + Name: "test", + }, api.Charger(nil)))) + + lps, err := configureLoadpoints(conf) + suite.Require().NoError(err) + + site := core.NewSite() + circuit.EXPECT().HasMeter().Return(false) + suite.Require().Error(validateCircuits(site, lps)) +} + +func (suite *circuitsTestSuite) TestSiteMissingCircuitError() { + var conf globalConfig + viper.SetConfigType("yaml") + + suite.Require().NoError(viper.ReadConfig(strings.NewReader(` +loadpoints: +- charger: test +site: + meters: + grid: grid +`))) + + suite.Require().NoError(viper.UnmarshalExact(&conf)) + + lps := []*core.Loadpoint{ + new(core.Loadpoint), + } + + // mock circuit + suite.Require().NoError(config.Circuits().Add(config.NewStaticDevice(config.Named{ + Name: "test", + }, api.Circuit(nil)))) + + // mock meter + m, _ := meter.NewConfigurable(func() (float64, error) { + return 0, nil + }) + suite.Require().NoError(config.Meters().Add(config.NewStaticDevice(config.Named{ + Name: "grid", + }, api.Meter(m)))) + + // mock charger + suite.Require().NoError(config.Chargers().Add(config.NewStaticDevice(config.Named{ + Name: "test", + }, api.Charger(nil)))) + + _, err := configureSite(conf.Site, lps, new(tariff.Tariffs)) + suite.Require().Error(err) +} diff --git a/cmd/setup_test.go b/cmd/setup_test.go index f00f8f5bb2..068b7883cb 100644 --- a/cmd/setup_test.go +++ b/cmd/setup_test.go @@ -10,15 +10,12 @@ import ( "github.com/spf13/viper" ) -const sample = ` -loadpoints: -- mode: off -` - func TestYamlOff(t *testing.T) { var conf globalConfig viper.SetConfigType("yaml") - if err := viper.ReadConfig(strings.NewReader(sample)); err != nil { + if err := viper.ReadConfig(strings.NewReader(`loadpoints: +- mode: off +`)); err != nil { t.Error(err) } diff --git a/core/circuit.go b/core/circuit.go new file mode 100644 index 0000000000..9c4ee28bff --- /dev/null +++ b/core/circuit.go @@ -0,0 +1,308 @@ +package core + +import ( + "fmt" + "sync" + + "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/util" + "github.com/evcc-io/evcc/util/config" +) + +var _ api.Circuit = (*Circuit)(nil) + +// the circuit instances to control the load +type Circuit struct { + mu sync.RWMutex + log *util.Logger + + title string + parent api.Circuit // parent circuit + children []api.Circuit // child circuits + meter api.Meter // meter to determine current power + + maxCurrent float64 // max allowed current + maxPower float64 // max allowed power + + current float64 + power float64 +} + +// NewCircuitFromConfig creates a new Circuit +func NewCircuitFromConfig(log *util.Logger, other map[string]interface{}) (api.Circuit, error) { + var cc struct { + Title string `mapstructure:"title"` // title + ParentRef string `mapstructure:"parent"` // parent circuit reference + MeterRef string `mapstructure:"meter"` // meter reference + MaxCurrent float64 `mapstructure:"maxCurrent"` // the max allowed current + MaxPower float64 `mapstructure:"maxPower"` // the max allowed power + } + + if err := util.DecodeOther(other, &cc); err != nil { + return nil, err + } + + var meter api.Meter + if cc.MeterRef != "" { + dev, err := config.Meters().ByName(cc.MeterRef) + if err != nil { + return nil, err + } + meter = dev.Instance() + } + + circuit, err := NewCircuit(log, cc.Title, cc.MaxCurrent, cc.MaxPower, meter) + if err != nil { + return nil, err + } + + if cc.ParentRef != "" { + dev, err := config.Circuits().ByName(cc.ParentRef) + if err != nil { + return nil, err + } + circuit.SetParent(dev.Instance()) + } + + return circuit, err +} + +// NewCircuit creates a circuit +func NewCircuit(log *util.Logger, title string, maxCurrent, maxPower float64, meter api.Meter) (*Circuit, error) { + c := &Circuit{ + log: log, + title: title, + maxCurrent: maxCurrent, + maxPower: maxPower, + meter: meter, + } + + if maxPower == 0 { + c.log.DEBUG.Printf("validation of max power disabled") + } + + if maxCurrent == 0 { + c.log.DEBUG.Printf("validation of max phase current disabled") + } else if _, ok := meter.(api.PhaseCurrents); meter != nil && !ok { + return nil, fmt.Errorf("meter does not support phase currents") + } + + return c, nil +} + +func (c *Circuit) GetTitle() string { + c.mu.RLock() + defer c.mu.RUnlock() + return c.title +} + +func (c *Circuit) SetTitle(title string) { + c.mu.Lock() + defer c.mu.Unlock() + c.title = title +} + +// GetParent returns the parent circuit +func (c *Circuit) GetParent() api.Circuit { + c.mu.RLock() + defer c.mu.RUnlock() + return c.parent +} + +// SetParent set parent circuit +func (c *Circuit) SetParent(parent api.Circuit) { + c.mu.Lock() + defer c.mu.Unlock() + c.parent = parent + if parent != nil { + parent.RegisterChild(c) + } +} + +// HasMeter returns the max power setting +func (c *Circuit) HasMeter() bool { + c.mu.RLock() + defer c.mu.RUnlock() + return c.meter != nil +} + +// GetMaxPower returns the max power setting +func (c *Circuit) GetMaxPower() float64 { + c.mu.RLock() + defer c.mu.RUnlock() + return c.maxPower +} + +// SetMaxPower sets the max power +func (c *Circuit) SetMaxPower(power float64) { + c.mu.Lock() + defer c.mu.Unlock() + c.maxPower = power +} + +// GetMaxCurrent returns the max current setting +func (c *Circuit) GetMaxCurrent() float64 { + c.mu.RLock() + defer c.mu.RUnlock() + return c.maxCurrent +} + +// SetMaxCurrent sets the max current +func (c *Circuit) SetMaxCurrent(current float64) { + c.mu.Lock() + defer c.mu.Unlock() + c.maxCurrent = current +} + +// RegisterChild registers child circuit +func (c *Circuit) RegisterChild(child api.Circuit) { + c.children = append(c.children, child) +} + +func (c *Circuit) updateLoadpoints(loadpoints []api.CircuitLoad) { + c.power = 0 + c.current = 0 + + for _, lp := range loadpoints { + if lp.GetCircuit() != c { + continue + } + + c.power += lp.GetChargePower() + c.current += lp.GetMaxPhaseCurrent() + } +} + +func (c *Circuit) updateMeters() error { + if f, err := c.meter.CurrentPower(); err == nil { + // TODO handle negative powers + c.power = f + } else { + return fmt.Errorf("circuit power: %w", err) + } + + if phaseMeter, ok := c.meter.(api.PhaseCurrents); ok { + if l1, l2, l3, err := phaseMeter.Currents(); err == nil { + // TODO handle negative currents + c.current = max(l1, l2, l3) + } else { + return fmt.Errorf("circuit currents: %w", err) + } + } + + return nil +} + +func (c *Circuit) Update(loadpoints []api.CircuitLoad) (err error) { + defer func() { + if c.maxPower != 0 && c.power > c.maxPower { + c.log.WARN.Printf("over power detected: %gW > %gW", c.power, c.maxPower) + } else { + c.log.DEBUG.Printf("power: %gW", c.power) + } + + if c.maxCurrent != 0 && c.current > c.maxCurrent { + c.log.WARN.Printf("over current detected: %gA > %gA", c.current, c.maxCurrent) + } else { + c.log.DEBUG.Printf("current: %gA", c.current) + } + }() + + // update children depth-first + for _, ch := range c.children { + if err := ch.Update(loadpoints); err != nil { + return err + } + } + + // meter available + if c.meter != nil { + return c.updateMeters() + } + + // no meter available + c.updateLoadpoints(loadpoints) + for _, ch := range c.children { + c.power += ch.GetChargePower() + c.current += ch.GetMaxPhaseCurrent() + } + + return nil +} + +// GetChargePower returns the actual power +func (c *Circuit) GetChargePower() float64 { + return c.power +} + +// GetMaxPhaseCurrent returns the actual current +func (c *Circuit) GetMaxPhaseCurrent() float64 { + return c.current +} + +// ValidatePower validates power request +func (c *Circuit) ValidatePower(old, new float64) float64 { + delta := max(0, new-old) + + if c.maxPower != 0 { + if c.power+delta > c.maxPower { + new = max(0, c.maxPower-c.power) + c.log.DEBUG.Printf("validate power: %gW -> %gW <= %gW at %gW: capped at %gW", old, new, c.maxPower, c.power, new) + } else { + c.log.TRACE.Printf("validate power: %gW -> %gW <= %gW at %gW: ok", old, new, c.maxPower, c.power) + } + } + + if c.parent != nil { + res := c.parent.ValidatePower(c.power, new) + if res != new { + c.log.TRACE.Printf("validate power: %gW -> %gW at %gW: capped at %gW", old, new, c.power, new) + } + return res + } + + return new +} + +// ValidateCurrent validates current request +func (c *Circuit) ValidateCurrent(old, new float64) (res float64) { + delta := max(0, new-old) + + if c.maxCurrent != 0 { + if c.current+delta > c.maxCurrent { + new = max(0, c.maxCurrent-c.current) + c.log.DEBUG.Printf("validate current: %gA -> %gA <= %gA at %gA: capped at %gA", old, new, c.maxCurrent, c.current, new) + } else { + c.log.TRACE.Printf("validate current: %gA -> %gA <= %gA at %gA: ok", old, new, c.maxCurrent, c.current) + } + } + + if c.parent != nil { + res := c.parent.ValidateCurrent(c.current, new) + if res != new { + c.log.TRACE.Printf("validate current: %gA -> %gA at %gA: capped by parent at %gA", old, new, c.current, res) + } + return res + } + + return new +} + +// func (c *Circuit) validate(typ string, current, old, new float64, parentFunc func(o, n float64) float64) float64 { +// delta := max(0, new-old) + +// if c.maxPower != 0 { +// if c.power+delta > c.maxPower { +// new = max(0, c.maxPower-c.power) +// c.log.TRACE.Printf("validate power: %g -> %g <= %g at %g: capped at %g", old, new, c.maxPower, c.power, new) +// } else { +// c.log.TRACE.Printf("validate power: %g -> %g <= %g at %g: ok", old, new, c.maxPower, c.power) +// } +// } + +// if c.parent != nil { +// return c.parent.ValidatePower(c.power, new) +// } + +// return new +// } diff --git a/core/circuit_test.go b/core/circuit_test.go new file mode 100644 index 0000000000..f29e67d74b --- /dev/null +++ b/core/circuit_test.go @@ -0,0 +1,123 @@ +package core + +import ( + "testing" + + "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/util" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" +) + +func TestCircuitPower(t *testing.T) { + log := util.NewLogger("foo") + + circ := func(t *testing.T, ctrl *gomock.Controller, maxP float64) (*Circuit, *api.MockMeter) { + m := api.NewMockMeter(ctrl) + c, err := NewCircuit(log, "foo", 0, maxP, m) + require.NoError(t, err) + return c, m + } + + for _, tc := range []struct { + pm, cm1, cm2 float64 + req, res float64 + }{ + // no load + {0, 0, 0, 0, 0}, + {0, 0, 0, 1, 1}, + {0, 0, 0, 2, 1}, + + // c1 loaded + {0, 1, 0, 0, 0}, + {0, 1, 0, 1, 0}, + {0, 1, 0, 2, 0}, + + // pc loaded + {1, 0, 0, 0, 0}, + {1, 0, 0, 1, 0}, + {1, 0, 0, 2, 0}, + } { + ctrl := gomock.NewController(t) + + pc, pm := circ(t, ctrl, 1) + c1, cm1 := circ(t, ctrl, 1) + c2, cm2 := circ(t, ctrl, 1) + + c1.SetParent(pc) + c2.SetParent(pc) + + // update meters + pm.EXPECT().CurrentPower().Return(tc.pm, nil) + cm1.EXPECT().CurrentPower().Return(tc.cm1, nil) + cm2.EXPECT().CurrentPower().Return(tc.cm2, nil) + require.NoError(t, pc.Update(nil)) + + require.Equal(t, tc.res, c1.ValidatePower(0, tc.req)) + + ctrl.Finish() + } +} + +// func TestCircuitCurrents(t *testing.T) { +// log := util.NewLogger("foo") + +// type mockMeter struct { +// *api.MockMeter +// *api.MockPhaseCurrents +// } + +// circ := func(t *testing.T, ctrl *gomock.Controller, maxP float64) (*Circuit, *mockMeter) { +// m := api.NewMockMeter(ctrl) +// mc := api.NewMockPhaseCurrents(ctrl) +// mm := &mockMeter{m, mc} +// c, err := NewCircuit(log, 0, maxP, mm) +// require.NoError(t, err) +// return c, mm +// } + +// for _, tc := range []struct { +// pm, cm1, cm2 float64 +// req, res float64 +// }{ +// // no load +// {0, 0, 0, 0, 0}, +// {0, 0, 0, 1, 1}, +// {0, 0, 0, 2, 1}, + +// // c1 loaded +// {0, 1, 0, 0, 0}, +// {0, 1, 0, 1, 0}, +// {0, 1, 0, 2, 0}, + +// // pc loaded +// {1, 0, 0, 0, 0}, +// {1, 0, 0, 1, 0}, +// {1, 0, 0, 2, 0}, +// } { +// ctrl := gomock.NewController(t) + +// pc, pm := circ(t, ctrl, 1) +// c1, cm1 := circ(t, ctrl, 1) +// c2, cm2 := circ(t, ctrl, 1) + +// c1.SetParent(pc) +// c2.SetParent(pc) + +// // update meters +// pm.MockMeter.EXPECT().CurrentPower().Return(tc.pm, nil) +// cm1.MockMeter.EXPECT().CurrentPower().Return(tc.cm1, nil) +// cm2.MockMeter.EXPECT().CurrentPower().Return(tc.cm2, nil) + +// // update meters +// pm.MockPhaseCurrents.EXPECT().Currents().Return(tc.pm, tc.pm, tc.pm, nil) +// cm1.MockPhaseCurrents.EXPECT().Currents().Return(tc.cm1, tc.cm1, tc.cm1, nil) +// cm2.MockPhaseCurrents.EXPECT().Currents().Return(tc.cm2, tc.cm2, tc.cm2, nil) +// require.NoError(t, pc.Update(nil)) + +// require.Equal(t, tc.res, c1.ValidatePower(0, tc.req)) +// require.Equal(t, tc.res, c1.ValidateCurrent(0, tc.req)) + +// ctrl.Finish() +// } +// } diff --git a/core/helper.go b/core/helper.go index 471523d2a9..5110d384b7 100644 --- a/core/helper.go +++ b/core/helper.go @@ -30,6 +30,14 @@ func powerToCurrent(power float64, phases int) float64 { return power / (float64(phases) * Voltage) } +// currentToPower is a helper function to convert current to sum power +func currentToPower(current float64, phases int) float64 { + if Voltage == 0 { + panic("Voltage is not set") + } + return current * float64(phases) * Voltage +} + // sitePower returns the available delta power that the charger might additionally consume // negative value: available power (grid export), positive value: grid import func sitePower(log *util.Logger, maxGrid, grid, battery, residual float64) float64 { diff --git a/core/keys/site.go b/core/keys/site.go index e4be858f8f..5c88f9cdf9 100644 --- a/core/keys/site.go +++ b/core/keys/site.go @@ -28,6 +28,7 @@ const ( TariffPriceHome = "tariffPriceHome" TariffPriceLoadpoints = "tariffPriceLoadpoints" Vehicles = "vehicles" + Circuits = "circuits" PasswordConfigured = "passwordConfigured" Interval = "interval" diff --git a/core/loadpoint.go b/core/loadpoint.go index b5c76e86db..bdb173fe96 100644 --- a/core/loadpoint.go +++ b/core/loadpoint.go @@ -108,6 +108,7 @@ type Loadpoint struct { Title_ string `mapstructure:"title"` // UI title Priority_ int `mapstructure:"priority"` // Priority + CircuitRef string `mapstructure:"circuit"` // Circuit reference ChargerRef string `mapstructure:"charger"` // Charger reference VehicleRef string `mapstructure:"vehicle"` // Vehicle reference MeterRef string `mapstructure:"meter"` // Charge meter reference @@ -144,6 +145,7 @@ type Loadpoint struct { chargeRater api.ChargeRater chargedAtStartup float64 // session energy at startup + circuit api.Circuit // Circuit chargeMeter api.Meter // Charger usage meter vehicle api.Vehicle // Currently active vehicle defaultVehicle api.Vehicle // Default vehicle (disables detection) @@ -203,6 +205,14 @@ func NewLoadpointFromConfig(log *util.Logger, settings *Settings, other map[stri lp.Soc.Poll.Mode = pollCharging } + if lp.CircuitRef != "" { + dev, err := config.Circuits().ByName(lp.CircuitRef) + if err != nil { + return nil, err + } + lp.circuit = dev.Instance() + } + if lp.MeterRef != "" { dev, err := config.Meters().ByName(lp.MeterRef) if err != nil { @@ -761,6 +771,17 @@ func (lp *Loadpoint) setLimit(chargeCurrent float64) error { chargeCurrent = math.Trunc(chargeCurrent) } + // apply circuit limits + if lp.circuit != nil { + currentLimit := lp.circuit.ValidateCurrent(lp.chargeCurrent, chargeCurrent) + + activePhases := lp.ActivePhases() + powerLimit := lp.circuit.ValidatePower(lp.chargePower, currentToPower(chargeCurrent, activePhases)) + currentLimitViaPower := powerToCurrent(powerLimit, activePhases) + + chargeCurrent = min(currentLimit, currentLimitViaPower) + } + // set current if chargeCurrent != lp.chargeCurrent && chargeCurrent >= lp.effectiveMinCurrent() { var err error @@ -1293,8 +1314,8 @@ func (lp *Loadpoint) pvMaxCurrent(mode api.ChargeMode, sitePower float64, batter return targetCurrent } -// UpdateChargePower updates charge meter power -func (lp *Loadpoint) UpdateChargePower() { +// UpdateChargePowerAndCurrents updates charge meter power and currents for load management +func (lp *Loadpoint) UpdateChargePowerAndCurrents() { bo := backoff.NewExponentialBackOff() bo.MaxElapsedTime = time.Second @@ -1313,12 +1334,10 @@ func (lp *Loadpoint) UpdateChargePower() { lp.log.WARN.Printf("charge power must not be negative: %.0f", power) } } else { - lp.log.ERROR.Printf("charge meter: %v", err) + lp.log.ERROR.Printf("charge power: %v", err) } -} -// updateChargeCurrents uses PhaseCurrents interface to count phases with current >=1A -func (lp *Loadpoint) updateChargeCurrents() { + // update charge currents lp.chargeCurrents = nil phaseMeter, ok := lp.chargeMeter.(api.PhaseCurrents) @@ -1326,15 +1345,27 @@ func (lp *Loadpoint) updateChargeCurrents() { return // don't guess } - i1, i2, i3, err := phaseMeter.Currents() - if err != nil { - lp.log.ERROR.Printf("charge meter: %v", err) - return + if err := backoff.Retry(func() error { + i1, i2, i3, err := phaseMeter.Currents() + if err != nil { + return err + } + + lp.chargeCurrents = []float64{i1, i2, i3} + lp.log.DEBUG.Printf("charge currents: %.3gA", lp.chargeCurrents) + lp.publish(keys.ChargeCurrents, lp.chargeCurrents) + + return nil + }, bo); err != nil { + lp.log.ERROR.Printf("charge currents: %v", err) } +} - lp.chargeCurrents = []float64{i1, i2, i3} - lp.log.DEBUG.Printf("charge currents: %.3gA", lp.chargeCurrents) - lp.publish(keys.ChargeCurrents, lp.chargeCurrents) +// phasesFromChargeCurrents uses PhaseCurrents interface to count phases with current >=1A +func (lp *Loadpoint) phasesFromChargeCurrents() { + if lp.chargeCurrents == nil { + return + } if lp.charging() && lp.phaseSwitchCompleted() { var phases int @@ -1555,9 +1586,9 @@ func (lp *Loadpoint) Update(sitePower float64, autoCharge, batteryBuffered, batt lp.publish(keys.SmartCostActive, autoCharge) lp.processTasks() - // read and publish meters first- charge power has already been updated by the site + // read and publish meters first- charge power and currents have already been updated by the site lp.updateChargeVoltages() - lp.updateChargeCurrents() + lp.phasesFromChargeCurrents() lp.sessionEnergy.SetEnvironment(greenShare, effPrice, effCo2) diff --git a/core/loadpoint/api.go b/core/loadpoint/api.go index 567b88d9ed..9eb97505aa 100644 --- a/core/loadpoint/api.go +++ b/core/loadpoint/api.go @@ -124,6 +124,8 @@ type API interface { GetChargePower() float64 // GetChargePowerFlexibility returns the flexible amount of current charging power GetChargePowerFlexibility() float64 + // GetMaxPhaseCurrent returns max phase current + GetMaxPhaseCurrent() float64 // // charge progress @@ -146,4 +148,7 @@ type API interface { SetVehicle(vehicle api.Vehicle) // StartVehicleDetection allows triggering vehicle detection for debugging purposes StartVehicleDetection() + + // GetCircuit gets the assigned circuit + GetCircuit() api.Circuit } diff --git a/core/loadpoint/mock.go b/core/loadpoint/mock.go index 0fd00c5b4a..cf7578bba7 100644 --- a/core/loadpoint/mock.go +++ b/core/loadpoint/mock.go @@ -138,6 +138,20 @@ func (mr *MockAPIMockRecorder) GetChargePowerFlexibility() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChargePowerFlexibility", reflect.TypeOf((*MockAPI)(nil).GetChargePowerFlexibility)) } +// GetCircuit mocks base method. +func (m *MockAPI) GetCircuit() api.Circuit { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCircuit") + ret0, _ := ret[0].(api.Circuit) + return ret0 +} + +// GetCircuit indicates an expected call of GetCircuit. +func (mr *MockAPIMockRecorder) GetCircuit() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCircuit", reflect.TypeOf((*MockAPI)(nil).GetCircuit)) +} + // GetDisableThreshold mocks base method. func (m *MockAPI) GetDisableThreshold() float64 { m.ctrl.T.Helper() @@ -208,6 +222,20 @@ func (mr *MockAPIMockRecorder) GetMaxCurrent() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMaxCurrent", reflect.TypeOf((*MockAPI)(nil).GetMaxCurrent)) } +// GetMaxPhaseCurrent mocks base method. +func (m *MockAPI) GetMaxPhaseCurrent() float64 { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetMaxPhaseCurrent") + ret0, _ := ret[0].(float64) + return ret0 +} + +// GetMaxPhaseCurrent indicates an expected call of GetMaxPhaseCurrent. +func (mr *MockAPIMockRecorder) GetMaxPhaseCurrent() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMaxPhaseCurrent", reflect.TypeOf((*MockAPI)(nil).GetMaxPhaseCurrent)) +} + // GetMinCurrent mocks base method. func (m *MockAPI) GetMinCurrent() float64 { m.ctrl.T.Helper() diff --git a/core/loadpoint_api.go b/core/loadpoint_api.go index e828215bff..738dc3021c 100644 --- a/core/loadpoint_api.go +++ b/core/loadpoint_api.go @@ -323,6 +323,16 @@ func (lp *Loadpoint) GetChargePowerFlexibility() float64 { return max(0, lp.GetChargePower()-lp.EffectiveMinPower()) } +// GetMaxPhaseCurrent returns the current charge power +func (lp *Loadpoint) GetMaxPhaseCurrent() float64 { + lp.RLock() + defer lp.RUnlock() + if lp.chargeCurrents == nil { + return lp.chargeCurrent + } + return max(lp.chargeCurrents[0], lp.chargeCurrents[1], lp.chargeCurrents[2]) +} + // GetMinCurrent returns the min loadpoint current func (lp *Loadpoint) GetMinCurrent() float64 { lp.RLock() @@ -498,3 +508,16 @@ func (lp *Loadpoint) SetSmartCostLimit(val float64) { lp.publish(keys.SmartCostLimit, lp.smartCostLimit) } } + +// GetCircuit returns the assigned circuit +func (lp *Loadpoint) GetCircuit() api.Circuit { + lp.RLock() + defer lp.RUnlock() + + // return untyped nil + if lp.circuit == nil { + return nil + } + + return lp.circuit +} diff --git a/core/site.go b/core/site.go index 21558801bf..2b282eac9d 100644 --- a/core/site.go +++ b/core/site.go @@ -65,13 +65,16 @@ type Site struct { log *util.Logger // configuration - Title string `mapstructure:"title"` // UI title - Voltage float64 `mapstructure:"voltage"` // Operating voltage. 230V for Germany. - ResidualPower float64 `mapstructure:"residualPower"` // PV meter only: household usage. Grid meter: household safety margin - Meters MetersConfig // Meter references - MaxGridSupplyWhileBatteryCharging float64 `mapstructure:"maxGridSupplyWhileBatteryCharging"` // ignore battery charging if AC consumption is above this value + Title string `mapstructure:"title"` // UI title + Voltage float64 `mapstructure:"voltage"` // Operating voltage. 230V for Germany. + ResidualPower float64 `mapstructure:"residualPower"` // PV meter only: household usage. Grid meter: household safety margin + Meters MetersConfig `mapstructure:"meters"` // Meter references + CircuitRef string `mapstructure:"circuit"` // Circuit reference + + MaxGridSupplyWhileBatteryCharging float64 `mapstructure:"maxGridSupplyWhileBatteryCharging"` // ignore battery charging if AC consumption is above this value // meters + circuit api.Circuit // Circuit gridMeter api.Meter // Grid usage meter pvMeters []api.Meter // PV generation meters batteryMeters []api.Meter // Battery charging meters @@ -162,6 +165,15 @@ func NewSiteFromConfig( // add meters from config site.restoreMeters() + // circuit + if site.CircuitRef != "" { + dev, err := config.Circuits().ByName(site.CircuitRef) + if err != nil { + return nil, err + } + site.circuit = dev.Instance() + } + // grid meter if site.Meters.GridMeterRef != "" { dev, err := config.Meters().ByName(site.Meters.GridMeterRef) @@ -753,12 +765,21 @@ func (site *Site) update(lp updater) { // update all loadpoint's charge power var totalChargePower float64 for _, lp := range site.loadpoints { - lp.UpdateChargePower() + lp.UpdateChargePowerAndCurrents() totalChargePower += lp.GetChargePower() site.prioritizer.UpdateChargePowerFlexibility(lp) } + // update all circuits' power and currents + if site.circuit != nil { + if err := site.circuit.Update(site.loadpointsAsCircuitDevices()); err != nil { + site.log.ERROR.Println(err) + } + + site.publishCircuits() + } + // prioritize if possible var flexiblePower float64 if lp.GetMode() == api.ModePV { diff --git a/core/site/api.go b/core/site/api.go index 86ce5b3f55..7ea5b476ff 100644 --- a/core/site/api.go +++ b/core/site/api.go @@ -11,6 +11,9 @@ type API interface { Loadpoints() []loadpoint.API Vehicles() Vehicles + // GetCircuit returns the assigned circuit + GetCircuit() api.Circuit + // Meta GetTitle() string SetTitle(string) diff --git a/core/site_api.go b/core/site_api.go index d22d3afd66..7d216998e3 100644 --- a/core/site_api.go +++ b/core/site_api.go @@ -10,6 +10,7 @@ import ( "github.com/evcc-io/evcc/core/site" "github.com/evcc-io/evcc/server/db/settings" "github.com/evcc-io/evcc/util/config" + "github.com/samber/lo" ) var _ site.API = (*Site)(nil) @@ -121,13 +122,14 @@ func (site *Site) SetAuxMeterRefs(ref []string) { settings.SetString(keys.AuxMeters, strings.Join(filterConfigurable(ref), ",")) } -// Loadpoints returns the list loadpoints +// Loadpoints returns the loadpoints as api interfaces func (site *Site) Loadpoints() []loadpoint.API { - res := make([]loadpoint.API, len(site.loadpoints)) - for id, lp := range site.loadpoints { - res[id] = lp - } - return res + return lo.Map(site.loadpoints, func(lp *Loadpoint, _ int) loadpoint.API { return lp }) +} + +// loadpointsAsCircuitDevices returns the loadpoints as circuit devices +func (site *Site) loadpointsAsCircuitDevices() []api.CircuitLoad { + return lo.Map(site.loadpoints, func(lp *Loadpoint, _ int) api.CircuitLoad { return lp }) } // Vehicles returns the site vehicles @@ -135,6 +137,15 @@ func (site *Site) Vehicles() site.Vehicles { return &vehicles{log: site.log} } +// GetCircuit returns the circuit +func (site *Site) GetCircuit() api.Circuit { + if site.circuit == nil { + // return untyped nil + return nil + } + return site.circuit +} + // GetPrioritySoc returns the PrioritySoc func (site *Site) GetPrioritySoc() float64 { site.RLock() diff --git a/core/site_circuits.go b/core/site_circuits.go new file mode 100644 index 0000000000..1e73380096 --- /dev/null +++ b/core/site_circuits.go @@ -0,0 +1,38 @@ +package core + +import ( + "github.com/evcc-io/evcc/core/keys" + "github.com/evcc-io/evcc/util" + "github.com/evcc-io/evcc/util/config" +) + +type circuitStruct struct { + Power float64 `json:"power"` + Current *float64 `json:"current,omitempty"` + MaxPower float64 `json:"maxPower,omitempty"` + MaxCurrent float64 `json:"maxCurrent,omitempty"` +} + +// publishCircuits returns a list of circuit titles +func (site *Site) publishCircuits() { + cc := config.Circuits().Devices() + res := make(map[string]circuitStruct, len(cc)) + + for _, c := range cc { + instance := c.Instance() + + data := circuitStruct{ + Power: instance.GetChargePower(), + MaxPower: instance.GetMaxPower(), + MaxCurrent: instance.GetMaxCurrent(), + } + + if instance.GetMaxCurrent() > 0 { + data.Current = util.PtrTo(instance.GetMaxPhaseCurrent()) + } + + res[c.Config().Name] = data + } + + site.publish(keys.Circuits, res) +} diff --git a/lm.md b/lm.md new file mode 100644 index 0000000000..f7ea066e4f --- /dev/null +++ b/lm.md @@ -0,0 +1,43 @@ +## Heute (ohne LM) + +je Ladepunkt: + +- Leistung Site aktualisieren +- Leistung alle Ladepunkte aktualisieren +- Gesamtbudget berechnen +- Aktuellen Ladepunkt steuern + +## Lastmanagement #8427 + +Setup: + +- alle Circuits hierarchisch dem Parent vMeter zurodnen + +je Ladepunkt: + +- Leistung Site aktualisieren +- Leistung aller Ladepunkte aktualisieren +- Gesamtbudget berechnen +- Aktuellen Ladepunkt steuern + - dabei Strom/Leistung durch Circuit begrenzen + - Circuit aktualisieren oder aggregierte Strom/Leistung aus vMeter verwenden + -> u.U. mehrere Circuit-Zähler auszulesen + - eigenen Strom/Leistung an LM zurück melden + +## Vorschlag zur Vereinfachung der vMeter + +Setup: + +- ENTFÄLLT: alle Circuits hierarchisch dem Parent vMeter zuordnen + +je Ladepunkt: + +- Leistung Site aktualisieren +- Leistung alle Ladepunkte aktualisieren +- NEU: Ströme alle Ladepunkte aktualisieren (falls vorhanden) +- NEU: Leistung aller Circuits depth-first aktualisieren + - dafür Werte der Ladepunkte verwenden wo kein Circuit Meter vorhanden +- Gesamtbudget berechnen +- Aktuellen Ladepunkt steuern + - dabei Strom/Leistung durch Circuit begrenzen + - ENTFÄLLT: Circuit aktualisieren oder aggregierte Strom/Leistung aus vMeter verwenden diff --git a/server/http_config_device_handler.go b/server/http_config_device_handler.go index 74e09c9b91..a28cc5502c 100644 --- a/server/http_config_device_handler.go +++ b/server/http_config_device_handler.go @@ -6,8 +6,11 @@ import ( "net/http" "strconv" + "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/charger" + "github.com/evcc-io/evcc/core" "github.com/evcc-io/evcc/meter" + "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" @@ -50,6 +53,9 @@ func devicesHandler(w http.ResponseWriter, r *http.Request) { case templates.Vehicle: res, err = devicesConfig(class, config.Vehicles()) + + case templates.Circuit: + res, err = devicesConfig(class, config.Circuits()) } if err != nil { @@ -123,6 +129,9 @@ func deviceConfigHandler(w http.ResponseWriter, r *http.Request) { case templates.Vehicle: res, err = deviceConfig(class, id, config.Vehicles()) + + case templates.Circuit: + res, err = deviceConfig(class, id, config.Circuits()) } if err != nil { @@ -166,6 +175,9 @@ func deviceStatusHandler(w http.ResponseWriter, r *http.Request) { case templates.Vehicle: instance, err = deviceStatus(name, config.Vehicles()) + + case templates.Circuit: + instance, err = deviceStatus(name, config.Circuits()) } if err != nil { @@ -218,6 +230,11 @@ func newDeviceHandler(w http.ResponseWriter, r *http.Request) { case templates.Vehicle: conf, err = newDevice(class, req, vehicle.NewFromConfig, config.Vehicles()) + + case templates.Circuit: + conf, err = newDevice(class, req, func(_ string, other map[string]interface{}) (api.Circuit, error) { + return core.NewCircuitFromConfig(util.NewLogger("circuit"), other) + }, config.Circuits()) } if err != nil { @@ -284,6 +301,11 @@ func updateDeviceHandler(w http.ResponseWriter, r *http.Request) { case templates.Vehicle: err = updateDevice(id, class, req, vehicle.NewFromConfig, config.Vehicles()) + + case templates.Circuit: + err = updateDevice(id, class, req, func(_ string, other map[string]interface{}) (api.Circuit, error) { + return core.NewCircuitFromConfig(util.NewLogger("circuit"), other) + }, config.Circuits()) } setConfigDirty() @@ -347,6 +369,9 @@ func deleteDeviceHandler(w http.ResponseWriter, r *http.Request) { case templates.Vehicle: err = deleteDevice(id, config.Vehicles()) + + case templates.Circuit: + err = deleteDevice(id, config.Circuits()) } setConfigDirty() @@ -413,6 +438,9 @@ func testConfigHandler(w http.ResponseWriter, r *http.Request) { case templates.Vehicle: instance, err = testConfig(id, class, req, vehicle.NewFromConfig, config.Vehicles()) + + case templates.Circuit: + err = api.ErrNotAvailable } if err != nil { diff --git a/util/config/instance.go b/util/config/instance.go index 2fd2dcfacd..d6fbee1b4b 100644 --- a/util/config/instance.go +++ b/util/config/instance.go @@ -7,14 +7,22 @@ import ( var bus = evbus.New() -var instance = struct { +var instance struct { meters *handler[api.Meter] chargers *handler[api.Charger] vehicles *handler[api.Vehicle] -}{ - meters: &handler[api.Meter]{topic: "meter"}, - chargers: &handler[api.Charger]{topic: "charger"}, - vehicles: &handler[api.Vehicle]{topic: "vehicle"}, + circuits *handler[api.Circuit] +} + +func init() { + Reset() +} + +func Reset() { + instance.meters = &handler[api.Meter]{topic: "meter"} + instance.chargers = &handler[api.Charger]{topic: "charger"} + instance.vehicles = &handler[api.Vehicle]{topic: "vehicle"} + instance.circuits = &handler[api.Circuit]{topic: "circuit"} } type Handler[T any] interface { @@ -37,6 +45,10 @@ func Vehicles() Handler[api.Vehicle] { return instance.vehicles } +func Circuits() Handler[api.Circuit] { + return instance.circuits +} + // Instances returns the instances of the given devices func Instances[T any](devices []Device[T]) []T { res := make([]T, 0, len(devices)) diff --git a/util/config/types.go b/util/config/types.go index af37a20c5f..9e7334b9cd 100644 --- a/util/config/types.go +++ b/util/config/types.go @@ -1,5 +1,9 @@ package config +import ( + "strings" +) + type Typed struct { Type string `json:"type"` Other map[string]interface{} `mapstructure:",remain"` @@ -10,3 +14,13 @@ type Named struct { Type string `json:"type"` Other map[string]interface{} `mapstructure:",remain"` } + +// Property returns the value of the named property +func (n Named) Property(key string) any { + for k, v := range n.Other { + if strings.EqualFold(k, key) { + return v + } + } + return nil +} diff --git a/util/ptr.go b/util/ptr.go new file mode 100644 index 0000000000..5525b75452 --- /dev/null +++ b/util/ptr.go @@ -0,0 +1,15 @@ +package util + +// PtrTo returns a pointer to the value passed as argument. The zero is returned as nil. +func PtrTo[T comparable](v T) *T { + var zero T + if v == zero { + return nil + } + return &v +} + +// PtrToWithZero returns a pointer to the value passed as argument, including its zero value. +func PtrToWithZero[T any](v T) *T { + return &v +} diff --git a/util/templates/class.go b/util/templates/class.go index faa734e41e..8e970ec687 100644 --- a/util/templates/class.go +++ b/util/templates/class.go @@ -9,4 +9,5 @@ const ( Meter Vehicle Tariff + Circuit ) diff --git a/util/templates/class_enumer.go b/util/templates/class_enumer.go index b21ff61dee..0891e5c019 100644 --- a/util/templates/class_enumer.go +++ b/util/templates/class_enumer.go @@ -7,11 +7,11 @@ import ( "strings" ) -const _ClassName = "ChargerMeterVehicleTariff" +const _ClassName = "ChargerMeterVehicleTariffCircuit" -var _ClassIndex = [...]uint8{0, 7, 12, 19, 25} +var _ClassIndex = [...]uint8{0, 7, 12, 19, 25, 32} -const _ClassLowerName = "chargermetervehicletariff" +const _ClassLowerName = "chargermetervehicletariffcircuit" func (i Class) String() string { i -= 1 @@ -29,9 +29,10 @@ func _ClassNoOp() { _ = x[Meter-(2)] _ = x[Vehicle-(3)] _ = x[Tariff-(4)] + _ = x[Circuit-(5)] } -var _ClassValues = []Class{Charger, Meter, Vehicle, Tariff} +var _ClassValues = []Class{Charger, Meter, Vehicle, Tariff, Circuit} var _ClassNameToValueMap = map[string]Class{ _ClassName[0:7]: Charger, @@ -42,6 +43,8 @@ var _ClassNameToValueMap = map[string]Class{ _ClassLowerName[12:19]: Vehicle, _ClassName[19:25]: Tariff, _ClassLowerName[19:25]: Tariff, + _ClassName[25:32]: Circuit, + _ClassLowerName[25:32]: Circuit, } var _ClassNames = []string{ @@ -49,6 +52,7 @@ var _ClassNames = []string{ _ClassName[7:12], _ClassName[12:19], _ClassName[19:25], + _ClassName[25:32], } // ClassString retrieves an enum value from the enum constants string name. From 07e7dedc009802f04e8f0faa37ec1f9f66046634 Mon Sep 17 00:00:00 2001 From: andig Date: Sun, 28 Apr 2024 09:17:07 +0200 Subject: [PATCH 004/168] Warp: fix error handling --- charger/warp2.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/charger/warp2.go b/charger/warp2.go index c64383d5c8..5a346f61f3 100644 --- a/charger/warp2.go +++ b/charger/warp2.go @@ -258,10 +258,10 @@ func (wb *Warp2) meterValues() ([]float64, error) { err := wb.meterDetailsG(&res) if err == nil && len(res) <= 5 { - return nil, errors.New("invalid length") + err = errors.New("invalid length") } - return res, nil + return res, err } // currents implements the api.MeterCurrrents interface From 539f453a6c6ff10adb6e934376c1eda4ba587415 Mon Sep 17 00:00:00 2001 From: andig Date: Sun, 28 Apr 2024 11:54:49 +0200 Subject: [PATCH 005/168] TWC3/Bender: remove api.ChargeDuration (#13615) --- charger/bender.go | 23 ++--------------------- charger/twc3.go | 9 ++------- 2 files changed, 4 insertions(+), 28 deletions(-) diff --git a/charger/bender.go b/charger/bender.go index 1267103afd..0c1f24bccd 100644 --- a/charger/bender.go +++ b/charger/bender.go @@ -27,7 +27,6 @@ import ( "encoding/binary" "fmt" "math" - "time" "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/util" @@ -201,26 +200,8 @@ func (wb *BenderCC) MaxCurrent(current int64) error { return err } -var _ api.ChargeTimer = (*BenderCC)(nil) - -// ChargeDuration implements the api.ChargeTimer interface -func (wb *BenderCC) ChargeDuration() (time.Duration, error) { - if wb.legacy { - b, err := wb.conn.ReadHoldingRegisters(bendRegChargingDurationLegacy, 1) - if err != nil { - return 0, err - } - - return time.Duration(binary.BigEndian.Uint16(b)) * time.Second, nil - } - - b, err := wb.conn.ReadHoldingRegisters(bendRegChargingDuration, 2) - if err != nil { - return 0, err - } - - return time.Duration(binary.BigEndian.Uint32(b)) * time.Second, nil -} +// removed: https://github.com/evcc-io/evcc/issues/13555 +// var _ api.ChargeTimer = (*BenderCC)(nil) // CurrentPower implements the api.Meter interface func (wb *BenderCC) currentPower() (float64, error) { diff --git a/charger/twc3.go b/charger/twc3.go index 9cb49ae3c3..9c34a4cf5a 100644 --- a/charger/twc3.go +++ b/charger/twc3.go @@ -166,13 +166,8 @@ func (v *Twc3) ChargedEnergy() (float64, error) { return res.SessionEnergyWh / 1e3, err } -var _ api.ChargeTimer = (*Twc3)(nil) - -// ChargeDuration implements the api.ChargeTimer interface -func (v *Twc3) ChargeDuration() (time.Duration, error) { - res, err := v.vitalsG() - return time.Duration(res.SessionS) * time.Second, err -} +// removed: https://github.com/evcc-io/evcc/issues/13555 +// var _ api.ChargeTimer = (*Twc3)(nil) // Use workaround if voltageC_v is approximately half of grid_v // From e8dc709f983d78d25b48698355ed4bf62f0e68ec Mon Sep 17 00:00:00 2001 From: Andy4OS <89736280+Andy4OS@users.noreply.github.com> Date: Sun, 28 Apr 2024 17:44:41 +0200 Subject: [PATCH 006/168] Keba: fixes to api.PhaseGetter (#13624) - Fixes register read length of kebaRegPhaseState to 32 bits. - This register holds the state of the external contactor ( 2 x normally open), which is used to switch between 1 phase and 3 phases. If this register holds a zero, only one phase is active. If it holds a one, three phases are active. --- charger/keba-modbus.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/charger/keba-modbus.go b/charger/keba-modbus.go index 4aef317455..d868b26a99 100644 --- a/charger/keba-modbus.go +++ b/charger/keba-modbus.go @@ -287,12 +287,14 @@ func (wb *Keba) phases1p3p(phases int) error { // getPhases implements the api.PhaseGetter interface func (wb *Keba) getPhases() (int, error) { - b, err := wb.conn.ReadHoldingRegisters(kebaRegPhaseState, 1) + b, err := wb.conn.ReadHoldingRegisters(kebaRegPhaseState, 2) if err != nil { return 0, err } - - return int(binary.BigEndian.Uint16(b)), nil + if binary.BigEndian.Uint32(b) == 0 { + return 1, nil + } + return 3, nil } var _ api.Diagnosis = (*Keba)(nil) From a6056e27d787ec85e4d39296187e4b2fdcfd3bd0 Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Sun, 28 Apr 2024 23:11:23 +0200 Subject: [PATCH 007/168] UI: adaptive power digits (#13619) --- .../js/components/Energyflow/Energyflow.vue | 51 ++++++++++++++----- .../components/Energyflow/EnergyflowEntry.vue | 9 +++- .../components/Energyflow/Visualization.vue | 5 +- assets/js/components/Loadpoint.vue | 6 +-- .../js/components/LoadpointSettingsModal.vue | 8 +-- assets/js/mixins/formatter.js | 8 ++- assets/js/mixins/formatter.test.js | 12 ++--- assets/js/views/ChargingSessions.vue | 2 +- tests/basics.spec.js | 2 +- tests/config.spec.js | 6 +-- 10 files changed, 74 insertions(+), 35 deletions(-) diff --git a/assets/js/components/Energyflow/Energyflow.vue b/assets/js/components/Energyflow/Energyflow.vue index 857044bf9d..c34a56cedb 100644 --- a/assets/js/components/Energyflow/Energyflow.vue +++ b/assets/js/components/Energyflow/Energyflow.vue @@ -18,7 +18,8 @@ :pvProduction="pvProduction" :homePower="homePower" :batterySoc="batterySoc" - :powerInKw="powerInKw" + :powerInKw="visualizationFormat.kw" + :powerDigits="visualizationFormat.digits" :vehicleIcons="vehicleIcons" /> @@ -74,14 +75,16 @@ icon="sun" :power="pvProduction" :powerTooltip="pvTooltip" - :powerInKw="powerInKw" + :powerInKw="entryFormat.kw" + :powerDigits="entryFormat.digits" /> = 1000; + visualizationFormat: function () { + const max = Math.max(this.gridImport, this.selfPv, this.selfBattery, this.pvExport); + const kw = this.inKw(max); + const digits = kw ? this.digitsKw(max) : 0; + return { kw, digits }; + }, + entryFormat: function () { + const max = Math.max( + this.gridImport, + this.pvProduction, + this.batteryDischarge, + this.homePower, + this.loadpointsPower, + this.pvExport, + this.batteryCharge + ); + const kw = this.inKw(max); + const digits = kw ? this.digitsKw(max) : 0; + return { kw, digits }; }, inPower: function () { return this.gridImport + this.pvProduction + this.batteryDischarge; @@ -285,7 +310,9 @@ export default { if (!Array.isArray(this.pv) || this.pv.length <= 1) { return; } - return this.pv.map(({ power }) => this.fmtKw(power, this.powerInKw)); + return this.pv.map(({ power }) => + this.fmtKw(power, this.entryFormat.kw, true, this.entryFormat.digits) + ); }, batteryFmt() { return (soc) => `${Math.round(soc)}%`; @@ -328,7 +355,7 @@ export default { return this.fmtPricePerKWh(value, this.currency, true); }, kw: function (watt) { - return this.fmtKw(watt, this.powerInKw); + return this.fmtKw(watt, this.entryFormat.kw, true, this.entryFormat.digits); }, toggleDetails: function () { this.updateHeight(); diff --git a/assets/js/components/Energyflow/EnergyflowEntry.vue b/assets/js/components/Energyflow/EnergyflowEntry.vue index 0a055aef4a..4dcf96ad2e 100644 --- a/assets/js/components/Energyflow/EnergyflowEntry.vue +++ b/assets/js/components/Energyflow/EnergyflowEntry.vue @@ -48,6 +48,7 @@ export default { power: { type: Number }, powerTooltip: { type: Array }, powerInKw: { type: Boolean }, + powerDigits: { type: Number }, soc: { type: Number }, details: { type: Number }, detailsFmt: { type: Function }, @@ -86,6 +87,12 @@ export default { this.$refs.powerNumber.forceUpdate(); } }, + powerDigits(newVal, oldVal) { + // force update if digits change but not the value + if (newVal !== oldVal) { + this.$refs.powerNumber.forceUpdate(); + } + }, }, mounted: function () { this.updatePowerTooltip(); @@ -93,7 +100,7 @@ export default { }, methods: { kw: function (watt) { - return this.fmtKw(watt, this.powerInKw); + return this.fmtKw(watt, this.powerInKw, true, this.powerDigits); }, updatePowerTooltip() { this.powerTooltipInstance = this.updateTooltip( diff --git a/assets/js/components/Energyflow/Visualization.vue b/assets/js/components/Energyflow/Visualization.vue index a5f90fab64..aa8ffbe978 100644 --- a/assets/js/components/Energyflow/Visualization.vue +++ b/assets/js/components/Energyflow/Visualization.vue @@ -116,6 +116,7 @@ export default { homePower: { type: Number, default: 0 }, batterySoc: { type: Number, default: 0 }, powerInKw: { type: Boolean, default: false }, + powerDigits: { type: Number, default: 0 }, }, data: function () { return { width: 0 }; @@ -171,7 +172,7 @@ export default { return ""; } const withUnit = this.enoughSpaceForUnit(watt); - return this.fmtKw(watt, this.powerInKw, withUnit); + return this.fmtKw(watt, this.powerInKw, withUnit, this.powerDigits); }, powerLabelAvailableSpace(power) { if (this.totalAdjusted === 0) return 0; @@ -182,7 +183,7 @@ export default { return this.powerLabelAvailableSpace(power) > 40; }, enoughSpaceForUnit(power) { - return this.powerLabelAvailableSpace(power) > 60; + return this.powerLabelAvailableSpace(power) > 80; }, hideLabelIcon(power, minWidth = 32) { if (this.totalAdjusted === 0) return true; diff --git a/assets/js/components/Loadpoint.vue b/assets/js/components/Loadpoint.vue index e8374af872..7bdbb7f477 100644 --- a/assets/js/components/Loadpoint.vue +++ b/assets/js/components/Loadpoint.vue @@ -315,12 +315,10 @@ export default { api.delete(this.apiPath("vehicle")); }, fmtPower(value) { - const inKw = value == 0 || value >= 1000; - return this.fmtKw(value, inKw); + return this.fmtKw(value, this.inKw(value)); }, fmtEnergy(value) { - const inKw = value == 0 || value >= 1000; - return this.fmtKWh(value, inKw); + return this.fmtKWh(value, this.inKw(value)); }, }, }; diff --git a/assets/js/components/LoadpointSettingsModal.vue b/assets/js/components/LoadpointSettingsModal.vue index 2789a6eb15..7f1733659a 100644 --- a/assets/js/components/LoadpointSettingsModal.vue +++ b/assets/js/components/LoadpointSettingsModal.vue @@ -220,7 +220,7 @@ export default { return this.maxPowerPhases(this.phasesConfigured); } } - return this.fmtKw(this.maxCurrent * V * this.phasesActive); + return this.fmtKw(this.maxCurrent * V * this.phasesActive, true, true, 1); }, minPower: function () { if (this.chargerPhases1p3p) { @@ -231,7 +231,7 @@ export default { return this.minPowerPhases(this.phasesConfigured); } } - return this.fmtKw(this.minCurrent * V * this.phasesActive); + return this.fmtKw(this.minCurrent * V * this.phasesActive, true, true, 1); }, minCurrentOptions: function () { const opt1 = [...range(Math.floor(this.maxCurrent), 1), 0.5, 0.25, 0.125]; @@ -279,10 +279,10 @@ export default { }, methods: { maxPowerPhases: function (phases) { - return this.fmtKw(this.maxCurrent * V * phases); + return this.fmtKw(this.maxCurrent * V * phases, true, true, 1); }, minPowerPhases: function (phases) { - return this.fmtKw(this.minCurrent * V * phases); + return this.fmtKw(this.minCurrent * V * phases, true, true, 1); }, formId: function (name) { return `loadpoint_${this.id}_${name}`; diff --git a/assets/js/mixins/formatter.js b/assets/js/mixins/formatter.js index df8c57eeb9..89bc454a88 100644 --- a/assets/js/mixins/formatter.js +++ b/assets/js/mixins/formatter.js @@ -33,9 +33,15 @@ export default { val = Math.abs(val); return val >= this.fmtLimit ? this.round(val / 1e3, this.fmtDigits) : this.round(val, 0); }, + inKw: function (watt) { + return watt === 0 || watt >= 1000; + }, + digitsKw: function (watt) { + return watt < 10000 ? 2 : 1; + }, fmtKw: function (watt = 0, kw = true, withUnit = true, digits) { if (digits === undefined) { - digits = kw ? 1 : 0; + digits = kw ? this.digitsKw(watt) : 0; } const value = kw ? watt / 1000 : watt; let unit = ""; diff --git a/assets/js/mixins/formatter.test.js b/assets/js/mixins/formatter.test.js index 8ebfefe881..bb983152c2 100644 --- a/assets/js/mixins/formatter.test.js +++ b/assets/js/mixins/formatter.test.js @@ -12,14 +12,14 @@ const fmt = mount({ describe("fmtkW", () => { test("should format kW and W", () => { - expect(fmt.fmtKw(0, true)).eq("0,0 kW"); - expect(fmt.fmtKw(1200, true)).eq("1,2 kW"); + expect(fmt.fmtKw(0, true)).eq("0,00 kW"); + expect(fmt.fmtKw(1200, true)).eq("1,20 kW"); expect(fmt.fmtKw(0, false)).eq("0 W"); expect(fmt.fmtKw(1200, false)).eq("1.200 W"); }); test("should format without unit", () => { - expect(fmt.fmtKw(0, true, false)).eq("0,0"); - expect(fmt.fmtKw(1200, true, false)).eq("1,2"); + expect(fmt.fmtKw(0, true, false)).eq("0,00"); + expect(fmt.fmtKw(1200, true, false)).eq("1,20"); expect(fmt.fmtKw(0, false, false)).eq("0"); expect(fmt.fmtKw(1200, false, false)).eq("1.200"); }); @@ -32,8 +32,8 @@ describe("fmtkW", () => { describe("fmtKWh", () => { test("should format with units", () => { - expect(fmt.fmtKWh(1200)).eq("1,2 kWh"); - expect(fmt.fmtKWh(1200, true)).eq("1,2 kWh"); + expect(fmt.fmtKWh(1200)).eq("1,20 kWh"); + expect(fmt.fmtKWh(1200, true)).eq("1,20 kWh"); expect(fmt.fmtKWh(1200, false)).eq("1.200 Wh"); expect(fmt.fmtKWh(1200, false, false)).eq("1.200"); }); diff --git a/assets/js/views/ChargingSessions.vue b/assets/js/views/ChargingSessions.vue index c842193755..be63e3fa01 100644 --- a/assets/js/views/ChargingSessions.vue +++ b/assets/js/views/ChargingSessions.vue @@ -299,7 +299,7 @@ export default { unit: "kWh", total: this.chargedEnergy, value: (session) => session.chargedEnergy, - format: (value) => this.fmtKWh(value * 1e3, true, false), + format: (value) => this.fmtKWh(value * 1e3, true, false, 2), }, { name: "solar", diff --git a/tests/basics.spec.js b/tests/basics.spec.js index 490d7c795e..6511fad449 100644 --- a/tests/basics.spec.js +++ b/tests/basics.spec.js @@ -20,7 +20,7 @@ test.describe("main screen", async () => { test("visualization", async ({ page }) => { const locator = page.getByTestId("visualization"); await expect(locator).toBeVisible(); - await expect(locator).toContainText("1.0 kW"); + await expect(locator).toContainText("1.00 kW"); }); test("one loadpoint", async ({ page }) => { diff --git a/tests/config.spec.js b/tests/config.spec.js index 6edeccb0f6..f563df2999 100644 --- a/tests/config.spec.js +++ b/tests/config.spec.js @@ -177,7 +177,7 @@ test.describe("meters", async () => { await expect(meterModal.getByRole("button", { name: "Validate & save" })).toBeVisible(); await meterModal.getByRole("link", { name: "validate" }).click(); await expect(meterModal.getByText("SoC: 75.0%")).toBeVisible(); - await expect(meterModal.getByText("Power: -2.5 kW")).toBeVisible(); + await expect(meterModal.getByText("Power: -2.50 kW")).toBeVisible(); await meterModal.getByRole("button", { name: "Save" }).click(); await expect(page.getByTestId("battery")).toBeVisible(1); await expect(page.getByTestId("battery")).toContainText("openems"); @@ -190,14 +190,14 @@ test.describe("meters", async () => { await expect(page.getByTestId("battery")).toBeVisible(1); await expect(page.getByTestId("battery")).toContainText("openems"); await expect(page.getByTestId("battery").getByText("SoC: 75.0%")).toBeVisible(); - await expect(page.getByTestId("battery").getByText("Power: -2.5 kW")).toBeVisible(); + await expect(page.getByTestId("battery").getByText("Power: -2.50 kW")).toBeVisible(); await expect(page.getByTestId("battery").getByText("Capacity: 20.0 kWh")).toBeVisible(); // restart and check in main ui await restart(CONFIG_EMPTY); await page.goto("/"); await page.getByTestId("visualization").click(); - await expect(page.getByTestId("energyflow")).toContainText("Battery charging75%2.5 kW"); + await expect(page.getByTestId("energyflow")).toContainText("Battery charging75%2.50 kW"); // delete #1 await page.goto("/#/config"); From 090b0a700e4f5b2142df8774855b47e15a621a83 Mon Sep 17 00:00:00 2001 From: andig Date: Mon, 29 Apr 2024 09:03:55 +0200 Subject: [PATCH 008/168] Revert "Loadpoint: add welcomecharge feature (#13534)" This reverts commit 1d4e69591f3f09c7d237fc7685eb6580be370320. --- api/feature.go | 1 - api/feature_enumer.go | 12 ++++-------- core/coordinator/adapter.go | 4 ++-- core/coordinator/api.go | 4 ++-- core/coordinator/coordinator.go | 12 +++--------- core/coordinator/dummy.go | 2 +- core/loadpoint.go | 16 ---------------- core/loadpoint_vehicle.go | 10 +--------- 8 files changed, 13 insertions(+), 48 deletions(-) diff --git a/api/feature.go b/api/feature.go index 44b5240c5a..15d7e200f4 100644 --- a/api/feature.go +++ b/api/feature.go @@ -10,5 +10,4 @@ const ( IntegratedDevice Heating Retryable - WelcomeCharge ) diff --git a/api/feature_enumer.go b/api/feature_enumer.go index 736284087b..858b77ac91 100644 --- a/api/feature_enumer.go +++ b/api/feature_enumer.go @@ -7,11 +7,11 @@ import ( "strings" ) -const _FeatureName = "OfflineCoarseCurrentIntegratedDeviceHeatingRetryableWelcomeCharge" +const _FeatureName = "OfflineCoarseCurrentIntegratedDeviceHeatingRetryable" -var _FeatureIndex = [...]uint8{0, 7, 20, 36, 43, 52, 65} +var _FeatureIndex = [...]uint8{0, 7, 20, 36, 43, 52} -const _FeatureLowerName = "offlinecoarsecurrentintegrateddeviceheatingretryablewelcomecharge" +const _FeatureLowerName = "offlinecoarsecurrentintegrateddeviceheatingretryable" func (i Feature) String() string { i -= 1 @@ -30,10 +30,9 @@ func _FeatureNoOp() { _ = x[IntegratedDevice-(3)] _ = x[Heating-(4)] _ = x[Retryable-(5)] - _ = x[WelcomeCharge-(6)] } -var _FeatureValues = []Feature{Offline, CoarseCurrent, IntegratedDevice, Heating, Retryable, WelcomeCharge} +var _FeatureValues = []Feature{Offline, CoarseCurrent, IntegratedDevice, Heating, Retryable} var _FeatureNameToValueMap = map[string]Feature{ _FeatureName[0:7]: Offline, @@ -46,8 +45,6 @@ var _FeatureNameToValueMap = map[string]Feature{ _FeatureLowerName[36:43]: Heating, _FeatureName[43:52]: Retryable, _FeatureLowerName[43:52]: Retryable, - _FeatureName[52:65]: WelcomeCharge, - _FeatureLowerName[52:65]: WelcomeCharge, } var _FeatureNames = []string{ @@ -56,7 +53,6 @@ var _FeatureNames = []string{ _FeatureName[20:36], _FeatureName[36:43], _FeatureName[43:52], - _FeatureName[52:65], } // FeatureString retrieves an enum value from the enum constants string name. diff --git a/core/coordinator/adapter.go b/core/coordinator/adapter.go index e700828168..1642e750b9 100644 --- a/core/coordinator/adapter.go +++ b/core/coordinator/adapter.go @@ -19,8 +19,8 @@ func NewAdapter(lp loadpoint.API, c *Coordinator) API { } } -func (a *adapter) GetVehicles(availableOnly bool) []api.Vehicle { - return a.c.GetVehicles(availableOnly) +func (a *adapter) GetVehicles() []api.Vehicle { + return a.c.GetVehicles() } func (a *adapter) Owner(v api.Vehicle) loadpoint.API { diff --git a/core/coordinator/api.go b/core/coordinator/api.go index 5e2cc91332..6bb1963271 100644 --- a/core/coordinator/api.go +++ b/core/coordinator/api.go @@ -7,8 +7,8 @@ import ( // API is the coordinator API type API interface { - // GetVehicles returns the list of all vehicles, filtered by availability - GetVehicles(availableOnly bool) []api.Vehicle + // GetVehicles returns the list of all vehicles + GetVehicles() []api.Vehicle // Owner returns the loadpoint that currently owns the vehicle Owner(api.Vehicle) loadpoint.API diff --git a/core/coordinator/coordinator.go b/core/coordinator/coordinator.go index 01b4e4f3c2..3eee8dadb1 100644 --- a/core/coordinator/coordinator.go +++ b/core/coordinator/coordinator.go @@ -1,6 +1,7 @@ package coordinator import ( + "slices" "sync" "github.com/evcc-io/evcc/api" @@ -26,18 +27,11 @@ func New(log *util.Logger, vehicles []api.Vehicle) *Coordinator { } // GetVehicles returns the list of all vehicles -func (c *Coordinator) GetVehicles(availableOnly bool) []api.Vehicle { +func (c *Coordinator) GetVehicles() []api.Vehicle { c.mu.RLock() defer c.mu.RUnlock() - res := make([]api.Vehicle, 0, len(c.vehicles)) - for _, v := range c.vehicles { - if _, tracked := c.tracked[v]; !availableOnly || availableOnly && !tracked { - res = append(res, v) - } - } - - return res + return slices.Clone(c.vehicles) } // Owner returns the loadpoint that currently owns the vehicle diff --git a/core/coordinator/dummy.go b/core/coordinator/dummy.go index fddb037ab9..07eaee8099 100644 --- a/core/coordinator/dummy.go +++ b/core/coordinator/dummy.go @@ -12,7 +12,7 @@ func NewDummy() API { return new(dummy) } -func (a *dummy) GetVehicles(_ bool) []api.Vehicle { +func (a *dummy) GetVehicles() []api.Vehicle { return nil } diff --git a/core/loadpoint.go b/core/loadpoint.go index bdb173fe96..200caf8ac1 100644 --- a/core/loadpoint.go +++ b/core/loadpoint.go @@ -5,7 +5,6 @@ import ( "fmt" "math" "reflect" - "slices" "strings" "sync" "testing" @@ -481,9 +480,6 @@ func (lp *Loadpoint) evVehicleConnectHandler() { lp.socEstimator.Reset() } - // get pv mode before vehicle defaults are applied - pvMode := lp.GetMode() == api.ModePV || lp.GetMode() == api.ModeMinPV - // set default or start detection if !lp.chargerHasFeature(api.IntegratedDevice) { lp.vehicleDefaultOrDetect() @@ -492,18 +488,6 @@ func (lp *Loadpoint) evVehicleConnectHandler() { // immediately allow pv mode activity lp.elapsePVTimer() - // Enable charging on connect if any available vehicle requires it. We're using the PV timer - // to disable after the welcome, hence this must be placed after elapsePVTimer. - // TODO check is this doesn't conflict with vehicle defaults like mode: off - if pvMode { - for _, v := range lp.availableVehicles() { - if slices.Contains(v.Features(), api.WelcomeCharge) { - lp.setLimit(lp.effectiveMinCurrent()) - break - } - } - } - // create charging session lp.createSession() } diff --git a/core/loadpoint_vehicle.go b/core/loadpoint_vehicle.go index c2afa4caf8..a8b5b502fb 100644 --- a/core/loadpoint_vehicle.go +++ b/core/loadpoint_vehicle.go @@ -20,20 +20,12 @@ const ( vehicleDetectDuration = 10 * time.Minute ) -// availableVehicles is the slice of vehicles from the coordinator that are available -func (lp *Loadpoint) availableVehicles() []api.Vehicle { - if lp.coordinator == nil { - return nil - } - return lp.coordinator.GetVehicles(true) -} - // coordinatedVehicles is the slice of vehicles from the coordinator func (lp *Loadpoint) coordinatedVehicles() []api.Vehicle { if lp.coordinator == nil { return nil } - return lp.coordinator.GetVehicles(false) + return lp.coordinator.GetVehicles() } // setVehicleIdentifier updated the vehicle id as read from the charger From 33aa8841bb18b008e0b7e5697c2fcb71241bc69a Mon Sep 17 00:00:00 2001 From: duck Date: Tue, 30 Apr 2024 07:05:01 +0100 Subject: [PATCH 009/168] Octopusenergy: support API keys for tariff data lookup (#13637) --- tariff/octopus.go | 83 +++++++--- tariff/octopus/api.go | 35 ----- tariff/octopus/graphql/api.go | 147 ++++++++++++++++++ tariff/octopus/graphql/types.go | 79 ++++++++++ tariff/octopus/rest/api.go | 48 ++++++ templates/definition/tariff/octopus-api.yaml | 14 ++ .../tariff/octopus-productcode.yaml | 20 +++ 7 files changed, 373 insertions(+), 53 deletions(-) delete mode 100644 tariff/octopus/api.go create mode 100644 tariff/octopus/graphql/api.go create mode 100644 tariff/octopus/graphql/types.go create mode 100644 tariff/octopus/rest/api.go create mode 100644 templates/definition/tariff/octopus-api.yaml create mode 100644 templates/definition/tariff/octopus-productcode.yaml diff --git a/tariff/octopus.go b/tariff/octopus.go index b79bf947d6..1a496573ad 100644 --- a/tariff/octopus.go +++ b/tariff/octopus.go @@ -3,21 +3,24 @@ package tariff import ( "errors" "slices" + "strings" "sync" "time" "github.com/cenkalti/backoff/v4" "github.com/evcc-io/evcc/api" - "github.com/evcc-io/evcc/tariff/octopus" + octoGql "github.com/evcc-io/evcc/tariff/octopus/graphql" + octoRest "github.com/evcc-io/evcc/tariff/octopus/rest" "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/request" ) type Octopus struct { - log *util.Logger - uri string - region string - data *util.Monitor[api.Rates] + log *util.Logger + region string + productCode string + apikey string + data *util.Monitor[api.Rates] } var _ api.Tariff = (*Octopus)(nil) @@ -28,26 +31,47 @@ func init() { func NewOctopusFromConfig(other map[string]interface{}) (api.Tariff, error) { var cc struct { - Region string - Tariff string + Region string + Tariff string // DEPRECATED: use ProductCode + ProductCode string + ApiKey string } + logger := util.NewLogger("octopus") + if err := util.DecodeOther(other, &cc); err != nil { return nil, err } - if cc.Region == "" { - return nil, errors.New("missing region") - } - if cc.Tariff == "" { - return nil, errors.New("missing tariff code") + // Allow ApiKey to be missing only if Region and Tariff are not. + if cc.ApiKey == "" { + if cc.Region == "" { + return nil, errors.New("missing region") + } + if cc.Tariff == "" { + // deprecated - copy to correct slot and WARN + logger.WARN.Print("'tariff' is deprecated and will break in a future version - use 'productCode' instead") + cc.ProductCode = cc.Tariff + } + if cc.ProductCode == "" { + return nil, errors.New("missing product code") + } + } else { + // ApiKey validators + if cc.Region != "" || cc.Tariff != "" { + return nil, errors.New("cannot use apikey at same time as product code") + } + if len(cc.ApiKey) != 32 || !strings.HasPrefix(cc.ApiKey, "sk_live_") { + return nil, errors.New("invalid apikey format") + } } t := &Octopus{ - log: util.NewLogger("octopus"), - uri: octopus.ConstructRatesAPI(cc.Tariff, cc.Region), - region: cc.Tariff, - data: util.NewMonitor[api.Rates](2 * time.Hour), + log: logger, + region: cc.Region, + productCode: cc.ProductCode, + apikey: cc.ApiKey, + data: util.NewMonitor[api.Rates](2 * time.Hour), } done := make(chan error) @@ -62,12 +86,35 @@ func (t *Octopus) run(done chan error) { client := request.NewHelper(t.log) bo := newBackoff() + var restQueryUri string + + // If ApiKey is available, use GraphQL to get appropriate tariff code before entering execution loop. + if t.apikey != "" { + gqlCli, err := octoGql.NewClient(t.log, t.apikey) + if err != nil { + once.Do(func() { done <- err }) + t.log.ERROR.Println(err) + return + } + tariffCode, err := gqlCli.TariffCode() + if err != nil { + once.Do(func() { done <- err }) + t.log.ERROR.Println(err) + return + } + restQueryUri = octoRest.ConstructRatesAPIFromTariffCode(tariffCode) + } else { + // Construct Rest Query URI using tariff and region codes. + restQueryUri = octoRest.ConstructRatesAPIFromProductAndRegionCode(t.productCode, t.region) + } + + // TODO tick every 15 minutes if GraphQL is available to poll for Intelligent slots. tick := time.NewTicker(time.Hour) for ; true; <-tick.C { - var res octopus.UnitRates + var res octoRest.UnitRates if err := backoff.Retry(func() error { - return backoffPermanentError(client.GetJSON(t.uri, &res)) + return backoffPermanentError(client.GetJSON(restQueryUri, &res)) }, bo); err != nil { once.Do(func() { done <- err }) diff --git a/tariff/octopus/api.go b/tariff/octopus/api.go deleted file mode 100644 index 1f96453a95..0000000000 --- a/tariff/octopus/api.go +++ /dev/null @@ -1,35 +0,0 @@ -package octopus - -import ( - "fmt" - "strings" - "time" -) - -// ProductURI defines the location of the tariff information page. Substitute %s with tariff name. -const ProductURI = "https://api.octopus.energy/v1/products/%s/" - -// RatesURI defines the location of the full tariff rates page, including speculation. -// Substitute first %s with tariff name, second with region code. -const RatesURI = ProductURI + "electricity-tariffs/E-1R-%s-%s/standard-unit-rates/" - -// ConstructRatesAPI returns a validly formatted, fully qualified URI to the unit rate information. -func ConstructRatesAPI(tariff string, region string) string { - t := strings.ToUpper(tariff) - r := strings.ToUpper(region) - return fmt.Sprintf(RatesURI, t, t, r) -} - -type UnitRates struct { - Count uint64 `json:"count"` - Next string `json:"next"` - Previous string `json:"previous"` - Results []Rate `json:"results"` -} - -type Rate struct { - ValidityStart time.Time `json:"valid_from"` - ValidityEnd time.Time `json:"valid_to"` - PriceInclusiveTax float64 `json:"value_inc_vat"` - PriceExclusiveTax float64 `json:"value_exc_vat"` -} diff --git a/tariff/octopus/graphql/api.go b/tariff/octopus/graphql/api.go new file mode 100644 index 0000000000..0bb24e8725 --- /dev/null +++ b/tariff/octopus/graphql/api.go @@ -0,0 +1,147 @@ +package graphql + +import ( + "context" + "errors" + "net/http" + "sync" + "time" + + "github.com/evcc-io/evcc/util" + "github.com/evcc-io/evcc/util/request" + "github.com/hasura/go-graphql-client" +) + +// BaseURI is Octopus Energy's core API root. +const BaseURI = "https://api.octopus.energy" + +// URI is the GraphQL query endpoint for Octopus Energy. +const URI = BaseURI + "/v1/graphql/" + +// OctopusGraphQLClient provides an interface for communicating with Octopus Energy's Kraken platform. +type OctopusGraphQLClient struct { + *graphql.Client + + // apikey is the Octopus Energy API key (provided by user) + apikey string + + // token is the GraphQL token used for communication with kraken (we get this ourselves with the apikey) + token *string + // tokenExpiration tracks the expiry of the acquired token. A new Token should be obtained if this time is passed. + tokenExpiration time.Time + // tokenMtx should be held when requesting a new token. + tokenMtx sync.Mutex + + // accountNumber is the Octopus Energy account number associated with the given API key (queried ourselves via GraphQL) + accountNumber string +} + +// NewClient returns a new, unauthenticated instance of OctopusGraphQLClient. +func NewClient(log *util.Logger, apikey string) (*OctopusGraphQLClient, error) { + cli := request.NewClient(log) + + gq := &OctopusGraphQLClient{ + Client: graphql.NewClient(URI, cli), + apikey: apikey, + } + + if err := gq.refreshToken(); err != nil { + return nil, err + } + + // Future requests must have the appropriate Authorization header set. + gq.Client = gq.Client.WithRequestModifier(func(r *http.Request) { + gq.tokenMtx.Lock() + defer gq.tokenMtx.Unlock() + r.Header.Add("Authorization", *gq.token) + }) + + return gq, nil +} + +// refreshToken updates the GraphQL token from the set apikey. +// Basic caching is provided - it will not update the token if it hasn't expired yet. +func (c *OctopusGraphQLClient) refreshToken() error { + // take a lock against the token mutex for the refresh + c.tokenMtx.Lock() + defer c.tokenMtx.Unlock() + + if time.Until(c.tokenExpiration) > 5*time.Minute { + return nil + } + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + + var q krakenTokenAuthentication + if err := c.Client.Mutate(ctx, &q, map[string]interface{}{"apiKey": c.apikey}); err != nil { + return err + } + + c.token = &q.ObtainKrakenToken.Token + c.tokenExpiration = time.Now().Add(time.Hour) + return nil +} + +// AccountNumber queries the Account Number assigned to the associated API key. +// Caching is provided. +func (c *OctopusGraphQLClient) AccountNumber() (string, error) { + // Check cache + if c.accountNumber != "" { + return c.accountNumber, nil + } + + // Update refresh token (if necessary) + if err := c.refreshToken(); err != nil { + return "", err + } + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + + var q krakenAccountLookup + if err := c.Client.Query(ctx, &q, nil); err != nil { + return "", err + } + + if len(q.Viewer.Accounts) == 0 { + return "", errors.New("no account associated with given octopus api key") + } + if len(q.Viewer.Accounts) > 1 { + return "", errors.New("more than one octopus account on this api key not supported") + } + c.accountNumber = q.Viewer.Accounts[0].Number + return c.accountNumber, nil +} + +// TariffCode queries the Tariff Code of the first Electricity Agreement active on the account. +func (c *OctopusGraphQLClient) TariffCode() (string, error) { + // Update refresh token (if necessary) + if err := c.refreshToken(); err != nil { + return "", err + } + + // Get Account Number + acc, err := c.AccountNumber() + if err != nil { + return "", nil + } + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + + var q krakenAccountElectricityAgreements + if err := c.Client.Query(ctx, &q, map[string]interface{}{"accountNumber": acc}); err != nil { + return "", err + } + + if len(q.Account.ElectricityAgreements) == 0 { + return "", errors.New("no electricity agreements found") + } + + // check type + //switch t := q.Account.ElectricityAgreements[0].Tariff.(type) { + // + //} + return q.Account.ElectricityAgreements[0].Tariff.TariffCode(), nil +} diff --git a/tariff/octopus/graphql/types.go b/tariff/octopus/graphql/types.go new file mode 100644 index 0000000000..0a9b2f129c --- /dev/null +++ b/tariff/octopus/graphql/types.go @@ -0,0 +1,79 @@ +package graphql + +// krakenTokenAuthentication is a representation of a GraphQL query for obtaining a Kraken API token. +type krakenTokenAuthentication struct { + ObtainKrakenToken struct { + Token string + } `graphql:"obtainKrakenToken(input: {APIKey: $apiKey})"` +} + +// krakenAccountLookup is a representation of a GraphQL query for obtaining the Account Number associated with the +// credentials used to authorize the request. +type krakenAccountLookup struct { + Viewer struct { + Accounts []struct { + Number string + } + } +} + +type tariffData struct { + // yukky but the best way I can think of to handle this + // access via any relevant tariff data entry (i.e. standardTariff) + standardTariff `graphql:"... on StandardTariff"` + dayNightTariff `graphql:"... on DayNightTariff"` + threeRateTariff `graphql:"... on ThreeRateTariff"` + halfHourlyTariff `graphql:"... on HalfHourlyTariff"` + prepayTariff `graphql:"... on PrepayTariff"` +} + +// TariffCode is a shortcut function to obtaining the Tariff Code of the given tariff, regardless of tariff type. +// Developer Note: GraphQL query returns the same element keys regardless of type, +// so it should always be decoded as standardTariff at least. +// We are unlikely to use the other Tariff types for data access (?). +func (d *tariffData) TariffCode() string { + return d.standardTariff.TariffCode +} + +type tariffType struct { + Id string + DisplayName string + FullName string + ProductCode string + StandingCharge float32 + PreVatStandingCharge float32 +} + +type tariffTypeWithTariffCode struct { + tariffType + TariffCode string +} + +type standardTariff struct { + tariffTypeWithTariffCode +} +type dayNightTariff struct { + tariffTypeWithTariffCode +} +type threeRateTariff struct { + tariffTypeWithTariffCode +} +type halfHourlyTariff struct { + tariffTypeWithTariffCode +} +type prepayTariff struct { + tariffTypeWithTariffCode +} + +type krakenAccountElectricityAgreements struct { + Account struct { + ElectricityAgreements []struct { + Id int + Tariff tariffData + MeterPoint struct { + // Mpan is the serial number of the meter that this ElectricityAgreement is bound to. + Mpan string + } + } `graphql:"electricityAgreements(active: true)"` + } `graphql:"account(accountNumber: $accountNumber)"` +} diff --git a/tariff/octopus/rest/api.go b/tariff/octopus/rest/api.go new file mode 100644 index 0000000000..a1308edd3a --- /dev/null +++ b/tariff/octopus/rest/api.go @@ -0,0 +1,48 @@ +package rest + +import ( + "fmt" + "strings" + "time" +) + +// ProductURI defines the location of the tariff information page. Substitute %s with tariff name. +const ProductURI = "https://api.octopus.energy/v1/products/%s/" + +// RatesURI defines the location of the full tariff rates page, including speculation. +// Substitute first %s with product code, second with tariff code. +const RatesURI = ProductURI + "electricity-tariffs/%s/standard-unit-rates/" + +// ConstructRatesAPIFromProductAndRegionCode returns a validly formatted, fully qualified URI to the unit rate information +// derived from the given product code and region. +func ConstructRatesAPIFromProductAndRegionCode(product string, region string) string { + tCode := strings.ToUpper(fmt.Sprintf("E-1R-%s-%s", product, region)) + return fmt.Sprintf(RatesURI, product, tCode) +} + +// ConstructRatesAPIFromTariffCode returns a validly formatted, fully qualified URI to the unit rate information +// derived from the given Tariff Code. +func ConstructRatesAPIFromTariffCode(tariff string) string { + // Hacky bullshit, saves handling both the product and tariff codes in GQL mode. + // Hopefully Octopus don't change how this works otherwise we might have to do this properly :( + if len(tariff) < 7 { + // OOB check + return "" + } + pCode := tariff[5 : len(tariff)-2] + return fmt.Sprintf(RatesURI, pCode, tariff) +} + +type UnitRates struct { + Count uint64 `json:"count"` + Next string `json:"next"` + Previous string `json:"previous"` + Results []Rate `json:"results"` +} + +type Rate struct { + ValidityStart time.Time `json:"valid_from"` + ValidityEnd time.Time `json:"valid_to"` + PriceInclusiveTax float64 `json:"value_inc_vat"` + PriceExclusiveTax float64 `json:"value_exc_vat"` +} diff --git a/templates/definition/tariff/octopus-api.yaml b/templates/definition/tariff/octopus-api.yaml new file mode 100644 index 0000000000..6121f7b8af --- /dev/null +++ b/templates/definition/tariff/octopus-api.yaml @@ -0,0 +1,14 @@ +template: octopus-api +products: + - brand: Octopus Energy + description: + en: Octopus Energy - API +params: + - name: apiKey + type: string + required: true + description: + en: "Your Octopus Energy API Key. You can find it here: https://octopus.energy/dashboard/new/accounts/personal-details/api-access" +render: | + type: octopusenergy + apikey: {{ .apikey }} diff --git a/templates/definition/tariff/octopus-productcode.yaml b/templates/definition/tariff/octopus-productcode.yaml new file mode 100644 index 0000000000..0dfcd3edcd --- /dev/null +++ b/templates/definition/tariff/octopus-productcode.yaml @@ -0,0 +1,20 @@ +template: octopus-productcode +products: + - brand: Octopus Energy + description: + en: Octopus Energy - Product Code +params: + - name: productCode + type: string + required: true + description: + en: "The tariff code for your energy contract. Make sure this is set to your import tariff code. It'll look something like this: AGILE-FLEX-22-11-25" + - name: region + type: string + required: true + description: + en: "The DNO region you are located in. More information: https://www.energy-stats.uk/dno-region-codes-explained/" +render: | + type: octopusenergy + productCode: {{ .productCode }} + region: {{ .region }} From da4af01d736503b77901648d8a03f45516a99591 Mon Sep 17 00:00:00 2001 From: andig Date: Tue, 30 Apr 2024 09:24:30 +0200 Subject: [PATCH 010/168] Innogy: add api.MeterEnergy --- charger/innogy.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/charger/innogy.go b/charger/innogy.go index 7ef73a68b0..dcbe429c03 100644 --- a/charger/innogy.go +++ b/charger/innogy.go @@ -35,6 +35,7 @@ const ( igyRegManufacturer = 100 // Input igyRegFirmware = 200 // Input igyRegStatus = 275 // Input + igyRegEnergy = 307 // Input igyRegCurrents = 1006 // current readings per phase ) @@ -163,6 +164,18 @@ func (wb *Innogy) CurrentPower() (float64, error) { return 230 * (l1 + l2 + l3), err } +var _ api.MeterEnergy = (*Innogy)(nil) + +// TotalEnergy implements the api.MeterEnergy interface +func (wb *Innogy) TotalEnergy() (float64, error) { + b, err := wb.conn.ReadInputRegisters(igyRegEnergy, 2) + if err != nil { + return 0, err + } + + return float64(math.Float32frombits(binary.BigEndian.Uint32(b))), nil +} + var _ api.PhaseCurrents = (*Innogy)(nil) // Currents implements the api.PhaseCurrents interface From b0a599027bfece903272c85e469679f28414fc99 Mon Sep 17 00:00:00 2001 From: Corneels de Waard <18464324+corneels@users.noreply.github.com> Date: Tue, 30 Apr 2024 22:19:44 +1000 Subject: [PATCH 011/168] chore: minor (#13647) Update `HomeAssistant` to correct style `Home Assistant` --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5e63a3bac0..69d7afb4db 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ evcc is an extensible EV Charge Controller and home energy management system. Fe - logging using [InfluxDB](https://www.influxdata.com) and [Grafana](https://grafana.com/grafana/) - granular charge power control down to mA steps with supported chargers (labeled by e.g. smartWB as [OLC](https://board.evse-wifi.de/viewtopic.php?f=16&t=187)) - REST and MQTT [APIs](https://docs.evcc.io/docs/reference/api) for integration with home automation systems -- Add-ons for [HomeAssistant](https://github.com/evcc-io/evcc-hassio-addon) and [OpenHAB](https://www.openhab.org/addons/bindings/evcc) (not maintained by the evcc core team) +- Add-ons for [Home Assistant](https://github.com/evcc-io/evcc-hassio-addon) and [OpenHAB](https://www.openhab.org/addons/bindings/evcc) (not maintained by the evcc core team) ## Getting Started From 4ed241e9f0fd72489ddbb5b568b76b1b82474e7a Mon Sep 17 00:00:00 2001 From: premultiply <4681172+premultiply@users.noreply.github.com> Date: Tue, 30 Apr 2024 14:00:36 +0000 Subject: [PATCH 012/168] sungrow-charger: add more diagnostics --- charger/sungrow.go | 70 ++++++++++++++++++++++++++++------------------ 1 file changed, 43 insertions(+), 27 deletions(-) diff --git a/charger/sungrow.go b/charger/sungrow.go index 6e959493c7..61e43dd13f 100644 --- a/charger/sungrow.go +++ b/charger/sungrow.go @@ -35,20 +35,24 @@ type Sungrow struct { } const ( - // holding - sgRegEnable = 21210 // uint16 - sgRegMaxCurrent = 21202 // uint16 0.01A - sgRegPhases = 21203 // uint16 - sgRegWorkingMode = 21262 // uint16 [Network=0, Plug&Play=2, EMS=6] - - // input - sgRegPhasesPower = 21224 // uint16 + // input (read only) + sgRegPhase = 21224 // uint16 [1: Single-phase, 3: Three-phase] + sgRegWorkMode = 21262 // uint16 [0: Network, 2: Plug&Play, 6: EMS] + sgRegRemCtrlStatus = 21267 // uint16 sgRegPhasesState = 21269 // uint16 sgRegTotalEnergy = 21299 // uint32s 1Wh sgRegActivePower = 21307 // uint32s 1W sgRegChargedEnergy = 21309 // uint32s 1Wh - sgRegStartMode = 21313 // uint16 [EMS=1, Swiping=2] + sgRegStartMode = 21313 // uint16 [1: Started by EMS, 2: Started by swiping card] + sgRegPowerRequest = 21314 // uint16 [0: Enable, 1: Close] + sgRegPowerFlag = 21315 // uint16 [0: Charging or power regulation is not allowed; 1: Charging or power regulation is allowed] sgRegState = 21316 // uint16 + + // holding + sgRegSetOutI = 21202 // uint16 0.01A + sgRegPhaseSwitch = 21203 // uint16 + sgRegUnavailable = 21210 // uint16 + sgRegRemoteControl = 21211 // uint16 ) var ( @@ -140,7 +144,7 @@ func (wb *Sungrow) Status() (api.ChargeStatus, error) { // Enabled implements the api.Charger interface func (wb *Sungrow) Enabled() (bool, error) { - b, err := wb.conn.ReadHoldingRegisters(sgRegEnable, 1) + b, err := wb.conn.ReadInputRegisters(sgRegRemCtrlStatus, 1) if err != nil { return false, err } @@ -151,11 +155,11 @@ func (wb *Sungrow) Enabled() (bool, error) { // Enable implements the api.Charger interface func (wb *Sungrow) Enable(enable bool) error { var u uint16 - if enable { + if !enable { u = 1 } - _, err := wb.conn.WriteSingleRegister(sgRegEnable, u) + _, err := wb.conn.WriteSingleRegister(sgRegRemoteControl, u) return err } @@ -173,7 +177,7 @@ func (wb *Sungrow) MaxCurrentMillis(current float64) error { return fmt.Errorf("invalid current %.1f", current) } - _, err := wb.conn.WriteSingleRegister(sgRegMaxCurrent, uint16(current*10)) + _, err := wb.conn.WriteSingleRegister(sgRegSetOutI, uint16(current*10)) return err } @@ -182,7 +186,7 @@ var _ api.Meter = (*Sungrow)(nil) // CurrentPower implements the api.Meter interface func (wb *Sungrow) CurrentPower() (float64, error) { - b, err := wb.conn.ReadHoldingRegisters(sgRegActivePower, 2) + b, err := wb.conn.ReadInputRegisters(sgRegActivePower, 2) if err != nil { return 0, err } @@ -208,7 +212,7 @@ var _ api.ChargeRater = (*Sungrow)(nil) // ChargedEnergy implements the api.MeterEnergy interface func (wb *Sungrow) ChargedEnergy() (float64, error) { - b, err := wb.conn.ReadHoldingRegisters(sgRegChargedEnergy, 2) + b, err := wb.conn.ReadInputRegisters(sgRegChargedEnergy, 2) if err != nil { return 0, err } @@ -220,7 +224,7 @@ var _ api.MeterEnergy = (*Sungrow)(nil) // TotalEnergy implements the api.MeterEnergy interface func (wb *Sungrow) TotalEnergy() (float64, error) { - b, err := wb.conn.ReadHoldingRegisters(sgRegTotalEnergy, 2) + b, err := wb.conn.ReadInputRegisters(sgRegTotalEnergy, 2) if err != nil { return 0, err } @@ -238,7 +242,7 @@ func (wb *Sungrow) Phases1p3p(phases int) error { u = 1 } - _, err := wb.conn.WriteSingleRegister(sgRegPhases, u) + _, err := wb.conn.WriteSingleRegister(sgRegPhaseSwitch, u) return err } @@ -247,20 +251,23 @@ var _ api.Diagnosis = (*Sungrow)(nil) // Diagnose implements the api.Diagnosis interface func (wb *Sungrow) Diagnose() { - if b, err := wb.conn.ReadHoldingRegisters(sgRegMaxCurrent, 1); err == nil { - fmt.Printf("\tMaxCurrent:\t%d\n", binary.BigEndian.Uint16(b)) + if b, err := wb.conn.ReadHoldingRegisters(sgRegSetOutI, 1); err == nil { + fmt.Printf("\tSetOutI:\t%d\n", binary.BigEndian.Uint16(b)) } - if b, err := wb.conn.ReadHoldingRegisters(sgRegPhases, 1); err == nil { - fmt.Printf("\tPhases:\t%d\n", binary.BigEndian.Uint16(b)) + if b, err := wb.conn.ReadHoldingRegisters(sgRegPhaseSwitch, 1); err == nil { + fmt.Printf("\tPhaseSwitch:\t%d\n", binary.BigEndian.Uint16(b)) } - if b, err := wb.conn.ReadHoldingRegisters(sgRegEnable, 1); err == nil { - fmt.Printf("\tEnable:\t%d\n", binary.BigEndian.Uint16(b)) + if b, err := wb.conn.ReadHoldingRegisters(sgRegUnavailable, 1); err == nil { + fmt.Printf("\tUnavailable:\t%d\n", binary.BigEndian.Uint16(b)) } - if b, err := wb.conn.ReadHoldingRegisters(sgRegWorkingMode, 1); err == nil { - fmt.Printf("\tWorkingMode:\t%d\n", binary.BigEndian.Uint16(b)) + if b, err := wb.conn.ReadHoldingRegisters(sgRegRemoteControl, 1); err == nil { + fmt.Printf("\tRemoteControl:\t%d\n", binary.BigEndian.Uint16(b)) } - if b, err := wb.conn.ReadInputRegisters(sgRegPhasesPower, 1); err == nil { - fmt.Printf("\tPhasesPower:\t%d\n", binary.BigEndian.Uint16(b)) + if b, err := wb.conn.ReadInputRegisters(sgRegWorkMode, 1); err == nil { + fmt.Printf("\tWorkMode:\t%d\n", binary.BigEndian.Uint16(b)) + } + if b, err := wb.conn.ReadInputRegisters(sgRegPhase, 1); err == nil { + fmt.Printf("\tPhase:\t%d\n", binary.BigEndian.Uint16(b)) } if b, err := wb.conn.ReadInputRegisters(sgRegPhasesState, 1); err == nil { fmt.Printf("\tPhasesState:\t%d\n", binary.BigEndian.Uint16(b)) @@ -271,4 +278,13 @@ func (wb *Sungrow) Diagnose() { if b, err := wb.conn.ReadInputRegisters(sgRegState, 1); err == nil { fmt.Printf("\tState:\t%d\n", binary.BigEndian.Uint16(b)) } + if b, err := wb.conn.ReadInputRegisters(sgRegRemCtrlStatus, 1); err == nil { + fmt.Printf("\tRemCtrlStatus:\t%d\n", binary.BigEndian.Uint16(b)) + } + if b, err := wb.conn.ReadInputRegisters(sgRegPowerRequest, 1); err == nil { + fmt.Printf("\tPowerRequest:\t%d\n", binary.BigEndian.Uint16(b)) + } + if b, err := wb.conn.ReadInputRegisters(sgRegPowerFlag, 1); err == nil { + fmt.Printf("\tPowerFlag:\t%d\n", binary.BigEndian.Uint16(b)) + } } From 4b6ff7e7240c35e85824485ef834d5f74831fae3 Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Tue, 30 Apr 2024 16:36:01 +0200 Subject: [PATCH 013/168] Revert "UI: adaptive power digits (#13619)" (#13653) This reverts commit a6056e27d787ec85e4d39296187e4b2fdcfd3bd0. --- .../js/components/Energyflow/Energyflow.vue | 51 +++++-------------- .../components/Energyflow/EnergyflowEntry.vue | 9 +--- .../components/Energyflow/Visualization.vue | 5 +- assets/js/components/Loadpoint.vue | 6 ++- .../js/components/LoadpointSettingsModal.vue | 8 +-- assets/js/mixins/formatter.js | 8 +-- assets/js/mixins/formatter.test.js | 12 ++--- assets/js/views/ChargingSessions.vue | 2 +- tests/basics.spec.js | 2 +- tests/config.spec.js | 6 +-- 10 files changed, 35 insertions(+), 74 deletions(-) diff --git a/assets/js/components/Energyflow/Energyflow.vue b/assets/js/components/Energyflow/Energyflow.vue index c34a56cedb..857044bf9d 100644 --- a/assets/js/components/Energyflow/Energyflow.vue +++ b/assets/js/components/Energyflow/Energyflow.vue @@ -18,8 +18,7 @@ :pvProduction="pvProduction" :homePower="homePower" :batterySoc="batterySoc" - :powerInKw="visualizationFormat.kw" - :powerDigits="visualizationFormat.digits" + :powerInKw="powerInKw" :vehicleIcons="vehicleIcons" /> @@ -75,16 +74,14 @@ icon="sun" :power="pvProduction" :powerTooltip="pvTooltip" - :powerInKw="entryFormat.kw" - :powerDigits="entryFormat.digits" + :powerInKw="powerInKw" /> = 1000; }, inPower: function () { return this.gridImport + this.pvProduction + this.batteryDischarge; @@ -310,9 +285,7 @@ export default { if (!Array.isArray(this.pv) || this.pv.length <= 1) { return; } - return this.pv.map(({ power }) => - this.fmtKw(power, this.entryFormat.kw, true, this.entryFormat.digits) - ); + return this.pv.map(({ power }) => this.fmtKw(power, this.powerInKw)); }, batteryFmt() { return (soc) => `${Math.round(soc)}%`; @@ -355,7 +328,7 @@ export default { return this.fmtPricePerKWh(value, this.currency, true); }, kw: function (watt) { - return this.fmtKw(watt, this.entryFormat.kw, true, this.entryFormat.digits); + return this.fmtKw(watt, this.powerInKw); }, toggleDetails: function () { this.updateHeight(); diff --git a/assets/js/components/Energyflow/EnergyflowEntry.vue b/assets/js/components/Energyflow/EnergyflowEntry.vue index 4dcf96ad2e..0a055aef4a 100644 --- a/assets/js/components/Energyflow/EnergyflowEntry.vue +++ b/assets/js/components/Energyflow/EnergyflowEntry.vue @@ -48,7 +48,6 @@ export default { power: { type: Number }, powerTooltip: { type: Array }, powerInKw: { type: Boolean }, - powerDigits: { type: Number }, soc: { type: Number }, details: { type: Number }, detailsFmt: { type: Function }, @@ -87,12 +86,6 @@ export default { this.$refs.powerNumber.forceUpdate(); } }, - powerDigits(newVal, oldVal) { - // force update if digits change but not the value - if (newVal !== oldVal) { - this.$refs.powerNumber.forceUpdate(); - } - }, }, mounted: function () { this.updatePowerTooltip(); @@ -100,7 +93,7 @@ export default { }, methods: { kw: function (watt) { - return this.fmtKw(watt, this.powerInKw, true, this.powerDigits); + return this.fmtKw(watt, this.powerInKw); }, updatePowerTooltip() { this.powerTooltipInstance = this.updateTooltip( diff --git a/assets/js/components/Energyflow/Visualization.vue b/assets/js/components/Energyflow/Visualization.vue index aa8ffbe978..a5f90fab64 100644 --- a/assets/js/components/Energyflow/Visualization.vue +++ b/assets/js/components/Energyflow/Visualization.vue @@ -116,7 +116,6 @@ export default { homePower: { type: Number, default: 0 }, batterySoc: { type: Number, default: 0 }, powerInKw: { type: Boolean, default: false }, - powerDigits: { type: Number, default: 0 }, }, data: function () { return { width: 0 }; @@ -172,7 +171,7 @@ export default { return ""; } const withUnit = this.enoughSpaceForUnit(watt); - return this.fmtKw(watt, this.powerInKw, withUnit, this.powerDigits); + return this.fmtKw(watt, this.powerInKw, withUnit); }, powerLabelAvailableSpace(power) { if (this.totalAdjusted === 0) return 0; @@ -183,7 +182,7 @@ export default { return this.powerLabelAvailableSpace(power) > 40; }, enoughSpaceForUnit(power) { - return this.powerLabelAvailableSpace(power) > 80; + return this.powerLabelAvailableSpace(power) > 60; }, hideLabelIcon(power, minWidth = 32) { if (this.totalAdjusted === 0) return true; diff --git a/assets/js/components/Loadpoint.vue b/assets/js/components/Loadpoint.vue index 7bdbb7f477..e8374af872 100644 --- a/assets/js/components/Loadpoint.vue +++ b/assets/js/components/Loadpoint.vue @@ -315,10 +315,12 @@ export default { api.delete(this.apiPath("vehicle")); }, fmtPower(value) { - return this.fmtKw(value, this.inKw(value)); + const inKw = value == 0 || value >= 1000; + return this.fmtKw(value, inKw); }, fmtEnergy(value) { - return this.fmtKWh(value, this.inKw(value)); + const inKw = value == 0 || value >= 1000; + return this.fmtKWh(value, inKw); }, }, }; diff --git a/assets/js/components/LoadpointSettingsModal.vue b/assets/js/components/LoadpointSettingsModal.vue index 7f1733659a..2789a6eb15 100644 --- a/assets/js/components/LoadpointSettingsModal.vue +++ b/assets/js/components/LoadpointSettingsModal.vue @@ -220,7 +220,7 @@ export default { return this.maxPowerPhases(this.phasesConfigured); } } - return this.fmtKw(this.maxCurrent * V * this.phasesActive, true, true, 1); + return this.fmtKw(this.maxCurrent * V * this.phasesActive); }, minPower: function () { if (this.chargerPhases1p3p) { @@ -231,7 +231,7 @@ export default { return this.minPowerPhases(this.phasesConfigured); } } - return this.fmtKw(this.minCurrent * V * this.phasesActive, true, true, 1); + return this.fmtKw(this.minCurrent * V * this.phasesActive); }, minCurrentOptions: function () { const opt1 = [...range(Math.floor(this.maxCurrent), 1), 0.5, 0.25, 0.125]; @@ -279,10 +279,10 @@ export default { }, methods: { maxPowerPhases: function (phases) { - return this.fmtKw(this.maxCurrent * V * phases, true, true, 1); + return this.fmtKw(this.maxCurrent * V * phases); }, minPowerPhases: function (phases) { - return this.fmtKw(this.minCurrent * V * phases, true, true, 1); + return this.fmtKw(this.minCurrent * V * phases); }, formId: function (name) { return `loadpoint_${this.id}_${name}`; diff --git a/assets/js/mixins/formatter.js b/assets/js/mixins/formatter.js index 89bc454a88..df8c57eeb9 100644 --- a/assets/js/mixins/formatter.js +++ b/assets/js/mixins/formatter.js @@ -33,15 +33,9 @@ export default { val = Math.abs(val); return val >= this.fmtLimit ? this.round(val / 1e3, this.fmtDigits) : this.round(val, 0); }, - inKw: function (watt) { - return watt === 0 || watt >= 1000; - }, - digitsKw: function (watt) { - return watt < 10000 ? 2 : 1; - }, fmtKw: function (watt = 0, kw = true, withUnit = true, digits) { if (digits === undefined) { - digits = kw ? this.digitsKw(watt) : 0; + digits = kw ? 1 : 0; } const value = kw ? watt / 1000 : watt; let unit = ""; diff --git a/assets/js/mixins/formatter.test.js b/assets/js/mixins/formatter.test.js index bb983152c2..8ebfefe881 100644 --- a/assets/js/mixins/formatter.test.js +++ b/assets/js/mixins/formatter.test.js @@ -12,14 +12,14 @@ const fmt = mount({ describe("fmtkW", () => { test("should format kW and W", () => { - expect(fmt.fmtKw(0, true)).eq("0,00 kW"); - expect(fmt.fmtKw(1200, true)).eq("1,20 kW"); + expect(fmt.fmtKw(0, true)).eq("0,0 kW"); + expect(fmt.fmtKw(1200, true)).eq("1,2 kW"); expect(fmt.fmtKw(0, false)).eq("0 W"); expect(fmt.fmtKw(1200, false)).eq("1.200 W"); }); test("should format without unit", () => { - expect(fmt.fmtKw(0, true, false)).eq("0,00"); - expect(fmt.fmtKw(1200, true, false)).eq("1,20"); + expect(fmt.fmtKw(0, true, false)).eq("0,0"); + expect(fmt.fmtKw(1200, true, false)).eq("1,2"); expect(fmt.fmtKw(0, false, false)).eq("0"); expect(fmt.fmtKw(1200, false, false)).eq("1.200"); }); @@ -32,8 +32,8 @@ describe("fmtkW", () => { describe("fmtKWh", () => { test("should format with units", () => { - expect(fmt.fmtKWh(1200)).eq("1,20 kWh"); - expect(fmt.fmtKWh(1200, true)).eq("1,20 kWh"); + expect(fmt.fmtKWh(1200)).eq("1,2 kWh"); + expect(fmt.fmtKWh(1200, true)).eq("1,2 kWh"); expect(fmt.fmtKWh(1200, false)).eq("1.200 Wh"); expect(fmt.fmtKWh(1200, false, false)).eq("1.200"); }); diff --git a/assets/js/views/ChargingSessions.vue b/assets/js/views/ChargingSessions.vue index be63e3fa01..c842193755 100644 --- a/assets/js/views/ChargingSessions.vue +++ b/assets/js/views/ChargingSessions.vue @@ -299,7 +299,7 @@ export default { unit: "kWh", total: this.chargedEnergy, value: (session) => session.chargedEnergy, - format: (value) => this.fmtKWh(value * 1e3, true, false, 2), + format: (value) => this.fmtKWh(value * 1e3, true, false), }, { name: "solar", diff --git a/tests/basics.spec.js b/tests/basics.spec.js index 6511fad449..490d7c795e 100644 --- a/tests/basics.spec.js +++ b/tests/basics.spec.js @@ -20,7 +20,7 @@ test.describe("main screen", async () => { test("visualization", async ({ page }) => { const locator = page.getByTestId("visualization"); await expect(locator).toBeVisible(); - await expect(locator).toContainText("1.00 kW"); + await expect(locator).toContainText("1.0 kW"); }); test("one loadpoint", async ({ page }) => { diff --git a/tests/config.spec.js b/tests/config.spec.js index f563df2999..6edeccb0f6 100644 --- a/tests/config.spec.js +++ b/tests/config.spec.js @@ -177,7 +177,7 @@ test.describe("meters", async () => { await expect(meterModal.getByRole("button", { name: "Validate & save" })).toBeVisible(); await meterModal.getByRole("link", { name: "validate" }).click(); await expect(meterModal.getByText("SoC: 75.0%")).toBeVisible(); - await expect(meterModal.getByText("Power: -2.50 kW")).toBeVisible(); + await expect(meterModal.getByText("Power: -2.5 kW")).toBeVisible(); await meterModal.getByRole("button", { name: "Save" }).click(); await expect(page.getByTestId("battery")).toBeVisible(1); await expect(page.getByTestId("battery")).toContainText("openems"); @@ -190,14 +190,14 @@ test.describe("meters", async () => { await expect(page.getByTestId("battery")).toBeVisible(1); await expect(page.getByTestId("battery")).toContainText("openems"); await expect(page.getByTestId("battery").getByText("SoC: 75.0%")).toBeVisible(); - await expect(page.getByTestId("battery").getByText("Power: -2.50 kW")).toBeVisible(); + await expect(page.getByTestId("battery").getByText("Power: -2.5 kW")).toBeVisible(); await expect(page.getByTestId("battery").getByText("Capacity: 20.0 kWh")).toBeVisible(); // restart and check in main ui await restart(CONFIG_EMPTY); await page.goto("/"); await page.getByTestId("visualization").click(); - await expect(page.getByTestId("energyflow")).toContainText("Battery charging75%2.50 kW"); + await expect(page.getByTestId("energyflow")).toContainText("Battery charging75%2.5 kW"); // delete #1 await page.goto("/#/config"); From 942a4391c584e360ece29bd891fb07bad004269a Mon Sep 17 00:00:00 2001 From: andig Date: Wed, 1 May 2024 13:15:50 +0200 Subject: [PATCH 014/168] Fix log ui accidentally depending on console log level (#13669) --- util/log.go | 15 ++++++++------- util/{redactor.go => log_redactor.go} | 24 +++++++++++++++++------- util/log_test.go | 16 ++++++++++++++++ 3 files changed, 41 insertions(+), 14 deletions(-) rename util/{redactor.go => log_redactor.go} (74%) create mode 100644 util/log_test.go diff --git a/util/log.go b/util/log.go index c70297a2df..f9458f4464 100644 --- a/util/log.go +++ b/util/log.go @@ -3,6 +3,7 @@ package util import ( "io" "log" + "os" "regexp" "strconv" "strings" @@ -19,10 +20,7 @@ var ( loggersMux sync.Mutex // OutThreshold is the default console log level - OutThreshold = jww.LevelWarn - - // LogThreshold is the default log file level - LogThreshold = jww.LevelTrace + OutThreshold = jww.LevelInfo ) // LogAreaPadding of log areas @@ -60,7 +58,10 @@ func newLogger(area string, lp int) *Logger { level := logLevelForArea(area) redactor := new(Redactor) - notepad := jww.NewNotepad(level, level, redactor, logstash.DefaultHandler, padded, log.Ldate|log.Ltime) + notepad := jww.NewNotepad( + level, jww.LevelTrace, + &redactWriter{os.Stdout, redactor}, &redactWriter{logstash.DefaultHandler, redactor}, + padded, log.Ldate|log.Ltime) logger := &Logger{ Notepad: notepad, @@ -95,7 +96,7 @@ func Loggers(cb func(string, *Logger)) { func logLevelForArea(area string) jww.Threshold { level, ok := levels[strings.ToLower(area)] if !ok { - level = LogThreshold + level = OutThreshold } return level } @@ -103,7 +104,7 @@ func logLevelForArea(area string) jww.Threshold { // LogLevel sets log level for all loggers func LogLevel(defaultLevel string, areaLevels map[string]string) { // default level - LogThreshold = logstash.LogLevelToThreshold(defaultLevel) + OutThreshold = logstash.LogLevelToThreshold(defaultLevel) // area levels for area, level := range areaLevels { diff --git a/util/redactor.go b/util/log_redactor.go similarity index 74% rename from util/redactor.go rename to util/log_redactor.go index 13cb49c43d..3b27df44b7 100644 --- a/util/redactor.go +++ b/util/log_redactor.go @@ -2,8 +2,8 @@ package util import ( "bytes" + "io" "net/url" - "os" "sync" ) @@ -16,7 +16,12 @@ var ( RedactHook = RedactDefaultHook ) -// Redactor implements a redacting io.Writer +// RedactDefaultHook expands a redaction item to include URL encoding +func RedactDefaultHook(s string) []string { + return []string{s, url.QueryEscape(s)} +} + +// Redactor implements log redaction type Redactor struct { mu sync.Mutex redact []string @@ -34,16 +39,21 @@ func (l *Redactor) Redact(redact ...string) { } } -func (l *Redactor) Write(p []byte) (n int, err error) { +func (l *Redactor) redacted(p []byte) []byte { l.mu.Lock() for _, s := range l.redact { p = bytes.ReplaceAll(p, []byte(s), []byte(RedactReplacement)) } l.mu.Unlock() - return os.Stdout.Write(p) + return p } -// RedactDefaultHook expands a redaction item to include URL encoding -func RedactDefaultHook(s string) []string { - return []string{s, url.QueryEscape(s)} +// redactWriter implements a redacting io.Writer +type redactWriter struct { + w io.Writer + r *Redactor +} + +func (w *redactWriter) Write(p []byte) (n int, err error) { + return w.w.Write(w.r.redacted(p)) } diff --git a/util/log_test.go b/util/log_test.go new file mode 100644 index 0000000000..f458ae960a --- /dev/null +++ b/util/log_test.go @@ -0,0 +1,16 @@ +package util + +import ( + "testing" + + "github.com/evcc-io/evcc/util/logstash" + jww "github.com/spf13/jwalterweatherman" + "github.com/stretchr/testify/require" +) + +func TestLogger(t *testing.T) { + log := NewLogger("test") + log.TRACE.Print("foo") + + require.Len(t, logstash.All(nil, jww.LevelTrace, 0), 1) +} From fd653ec838eeedfa40ed26835184649fd705e51a Mon Sep 17 00:00:00 2001 From: andig Date: Wed, 1 May 2024 13:19:37 +0200 Subject: [PATCH 015/168] chore: upgrade github actions --- .github/workflows/default.yml | 4 +--- .github/workflows/documentation.yml | 4 ++-- .github/workflows/website.yml | 4 ++-- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/.github/workflows/default.yml b/.github/workflows/default.yml index d783620729..95dcaa5a8b 100644 --- a/.github/workflows/default.yml +++ b/.github/workflows/default.yml @@ -134,11 +134,9 @@ jobs: - run: mkdir dist && touch dist/empty - name: Lint - uses: golangci/golangci-lint-action@v4 + uses: golangci/golangci-lint-action@v5 with: version: latest - skip-pkg-cache: true - skip-build-cache: true args: --out-format=colored-line-number --timeout 5m ui: diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 97d81fc69e..d929665501 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -27,7 +27,7 @@ jobs: run: make install docs - name: Deploy to docs repo - uses: peaceiris/actions-gh-pages@v3 + uses: peaceiris/actions-gh-pages@v4 with: personal_token: ${{ secrets.GH_TOKEN }} publish_dir: ./templates/docs @@ -36,4 +36,4 @@ jobs: destination_dir: ${{ github.event_name == 'release' && 'templates/release' || github.event_name == 'schedule' && 'templates/nightly' || 'templates/unknown_trigger' }} allow_empty_commit: false commit_message: ${{ github.event_name == 'release' && 'Templates update for release' || github.event_name == 'schedule' && 'Templates update for nightly' || 'Templates update unknown trigger' }} - if: ${{ success() }} + if: success() diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index da93ba98b5..07536caa32 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -28,7 +28,7 @@ jobs: run: rm templates/evcc.io/.gitignore - name: Deploy to evcc.io repo - uses: peaceiris/actions-gh-pages@v3 + uses: peaceiris/actions-gh-pages@v4 with: personal_token: ${{ secrets.GH_TOKEN }} publish_dir: ./templates/evcc.io/ @@ -37,4 +37,4 @@ jobs: destination_dir: data allow_empty_commit: false commit_message: Brand data update - if: ${{ success() }} + if: success() From db8808627a835cb3169b835ac0e3b7a2825cf83b Mon Sep 17 00:00:00 2001 From: hurzhurz <16383058+hurzhurz@users.noreply.github.com> Date: Wed, 1 May 2024 16:33:31 +0200 Subject: [PATCH 016/168] PSA: change authentication from user/password to token (#13612) --- cmd/token.go | 10 ++- cmd/token_psa.go | 44 +++++++++++ templates/definition/vehicle/citroen.yaml | 38 +++++++++- templates/definition/vehicle/ds.yaml | 38 +++++++++- templates/definition/vehicle/opel.yaml | 40 +++++++++- templates/definition/vehicle/peugeot.yaml | 38 +++++++++- vehicle/psa.go | 91 +++++++++-------------- vehicle/psa/helper.go | 20 +++++ vehicle/psa/identity.go | 88 +++++++++++++++------- vehicle/psa/oauth2.go | 66 ++++++++++++++++ 10 files changed, 378 insertions(+), 95 deletions(-) create mode 100644 cmd/token_psa.go create mode 100644 vehicle/psa/helper.go create mode 100644 vehicle/psa/oauth2.go diff --git a/cmd/token.go b/cmd/token.go index 4df3900539..05e785a0ab 100644 --- a/cmd/token.go +++ b/cmd/token.go @@ -51,11 +51,19 @@ func runToken(cmd *cobra.Command, args []string) { var token *oauth2.Token var err error - switch strings.ToLower(vehicleConf.Type) { + typ := strings.ToLower(vehicleConf.Type) + if typ == "template" { + typ = strings.ToLower(vehicleConf.Other["template"].(string)) + } + + switch typ { case "mercedes": token, err = mercedesToken() case "tronity": token, err = tronityToken(conf, vehicleConf) + case "citroen", "ds", "opel", "peugeot": + token, err = psaToken(typ) + default: log.FATAL.Fatalf("vehicle type '%s' does not support token authentication", vehicleConf.Type) } diff --git a/cmd/token_psa.go b/cmd/token_psa.go new file mode 100644 index 0000000000..62e82c7fcd --- /dev/null +++ b/cmd/token_psa.go @@ -0,0 +1,44 @@ +package cmd + +import ( + "context" + "fmt" + "strings" + + "github.com/AlecAivazis/survey/v2" + "github.com/evcc-io/evcc/util" + "github.com/evcc-io/evcc/util/request" + "github.com/evcc-io/evcc/vehicle/psa" + "golang.org/x/oauth2" +) + +func psaToken(brand string) (*oauth2.Token, error) { + var country string + prompt_country := &survey.Input{ + Message: "Please enter your country code:", + } + if err := survey.AskOne(prompt_country, &country, survey.WithValidator(survey.Required)); err != nil { + return nil, err + } + + cv := oauth2.GenerateVerifier() + oc := psa.Oauth2Config(brand, strings.ToLower(country)) + + authorize_url := oc.AuthCodeURL("", oauth2.S256ChallengeOption(cv)) + + fmt.Println("Please visit: ", authorize_url) + fmt.Println("And grab the authorization code like described here: https://github.com/flobz/psa_car_controller/discussions/779") + + var code string + prompt_code := &survey.Input{ + Message: "Please enter your authorization code:", + } + if err := survey.AskOne(prompt_code, &code, survey.WithValidator(survey.Required)); err != nil { + return nil, err + } + + client := request.NewClient(util.NewLogger(brand)) + ctx := context.WithValue(context.Background(), oauth2.HTTPClient, client) + + return oc.Exchange(ctx, code, oauth2.VerifierOption(cv)) +} diff --git a/templates/definition/vehicle/citroen.yaml b/templates/definition/vehicle/citroen.yaml index 421456e8ff..59fff56e44 100644 --- a/templates/definition/vehicle/citroen.yaml +++ b/templates/definition/vehicle/citroen.yaml @@ -1,10 +1,44 @@ template: citroen products: - brand: Citroën +requirements: + description: + de: | + Benötigt `access` und `refresh` Tokens. Diese können über den Befehl "evcc token [name]" generiert werden. + en: | + Citroën `access` and `refresh` tokens are required. These can be generated with command "evcc token [name]". params: - - preset: vehicle-base + - name: title + - name: icon + default: car + advanced: true + - name: user + required: true + - name: password + deprecated: true + - name: tokens + required: true + mask: true + help: + en: "See https://docs.evcc.io/en/docs/devices/vehicles#citroen" + de: "Siehe https://docs.evcc.io/docs/devices/vehicles#citroen" + - name: vin + example: V... + - name: capacity + - name: phases + advanced: true + - name: password + deprecated: true - preset: vehicle-identify render: | type: citroen - {{ include "vehicle-base" . }} + title: {{ .title }} + icon: {{ .icon }} + user: {{ .user }} + tokens: + access: {{ .accessToken }} + refresh: {{ .refreshToken }} + capacity: {{ .capacity }} + phases: {{ .phases }} + vin: {{ .vin }} {{ include "vehicle-identify" . }} diff --git a/templates/definition/vehicle/ds.yaml b/templates/definition/vehicle/ds.yaml index 11982df4ee..e1c5f5775c 100644 --- a/templates/definition/vehicle/ds.yaml +++ b/templates/definition/vehicle/ds.yaml @@ -1,10 +1,44 @@ template: ds products: - brand: DS +requirements: + description: + de: | + Benötigt `access` und `refresh` Tokens. Diese können über den Befehl "evcc token [name]" generiert werden. + en: | + DS `access` and `refresh` tokens are required. These can be generated with command "evcc token [name]". params: - - preset: vehicle-base + - name: title + - name: icon + default: car + advanced: true + - name: user + required: true + - name: password + deprecated: true + - name: tokens + required: true + mask: true + help: + en: "See https://docs.evcc.io/en/docs/devices/vehicles#ds" + de: "Siehe https://docs.evcc.io/docs/devices/vehicles#ds" + - name: vin + example: V... + - name: capacity + - name: phases + advanced: true + - name: password + deprecated: true - preset: vehicle-identify render: | type: ds - {{ include "vehicle-base" . }} + title: {{ .title }} + icon: {{ .icon }} + user: {{ .user }} + tokens: + access: {{ .accessToken }} + refresh: {{ .refreshToken }} + capacity: {{ .capacity }} + phases: {{ .phases }} + vin: {{ .vin }} {{ include "vehicle-identify" . }} diff --git a/templates/definition/vehicle/opel.yaml b/templates/definition/vehicle/opel.yaml index f7370cf4e8..7c3401661a 100644 --- a/templates/definition/vehicle/opel.yaml +++ b/templates/definition/vehicle/opel.yaml @@ -1,12 +1,44 @@ template: opel products: - brand: Opel +requirements: + description: + de: | + Benötigt `access` und `refresh` Tokens. Diese können über den Befehl "evcc token [name]" generiert werden. + en: | + Opel `access` and `refresh` tokens are required. These can be generated with command "evcc token [name]". params: - - preset: vehicle-base - - preset: vehicle-identify + - name: title + - name: icon + default: car + advanced: true + - name: user + required: true + - name: password + deprecated: true + - name: tokens + required: true + mask: true + help: + en: "See https://docs.evcc.io/en/docs/devices/vehicles#opel" + de: "Siehe https://docs.evcc.io/docs/devices/vehicles#opel" - name: vin - example: WP0... + example: V... + - name: capacity + - name: phases + advanced: true + - name: password + deprecated: true + - preset: vehicle-identify render: | type: opel - {{ include "vehicle-base" . }} + title: {{ .title }} + icon: {{ .icon }} + user: {{ .user }} + tokens: + access: {{ .accessToken }} + refresh: {{ .refreshToken }} + capacity: {{ .capacity }} + phases: {{ .phases }} + vin: {{ .vin }} {{ include "vehicle-identify" . }} diff --git a/templates/definition/vehicle/peugeot.yaml b/templates/definition/vehicle/peugeot.yaml index 4bf8dfb896..c79645e71d 100644 --- a/templates/definition/vehicle/peugeot.yaml +++ b/templates/definition/vehicle/peugeot.yaml @@ -1,10 +1,44 @@ template: peugeot products: - brand: Peugeot +requirements: + description: + de: | + Benötigt `access` und `refresh` Tokens. Diese können über den Befehl "evcc token [name]" generiert werden. + en: | + Peugeot `access` and `refresh` tokens are required. These can be generated with command "evcc token [name]". params: - - preset: vehicle-base + - name: title + - name: icon + default: car + advanced: true + - name: user + required: true + - name: password + deprecated: true + - name: tokens + required: true + mask: true + help: + en: "See https://docs.evcc.io/en/docs/devices/vehicles#peugeot" + de: "Siehe https://docs.evcc.io/docs/devices/vehicles#peugeot" + - name: vin + example: V... + - name: capacity + - name: phases + advanced: true + - name: password + deprecated: true - preset: vehicle-identify render: | type: peugeot - {{ include "vehicle-base" . }} + title: {{ .title }} + icon: {{ .icon }} + user: {{ .user }} + tokens: + access: {{ .accessToken }} + refresh: {{ .refreshToken }} + capacity: {{ .capacity }} + phases: {{ .phases }} + vin: {{ .vin }} {{ include "vehicle-identify" . }} diff --git a/vehicle/psa.go b/vehicle/psa.go index e0ddfd4888..36d0ea9b8a 100644 --- a/vehicle/psa.go +++ b/vehicle/psa.go @@ -1,7 +1,7 @@ package vehicle import ( - "fmt" + "strings" "time" "github.com/evcc-io/evcc/api" @@ -12,46 +12,18 @@ import ( // https://github.com/TA2k/ioBroker.psa func init() { - registry.Add("citroen", NewCitroenFromConfig) - registry.Add("ds", NewDSFromConfig) - registry.Add("opel", NewOpelFromConfig) - registry.Add("peugeot", NewPeugeotFromConfig) -} - -// NewCitroenFromConfig creates a new vehicle -func NewCitroenFromConfig(other map[string]interface{}) (api.Vehicle, error) { - log := util.NewLogger("citroen") - return newPSA(log, - "citroen.com", "clientsB2CCitroen", - "5364defc-80e6-447b-bec6-4af8d1542cae", "iE0cD8bB0yJ0dS6rO3nN1hI2wU7uA5xR4gP7lD6vM0oH0nS8dN", - other) -} - -// NewDSFromConfig creates a new vehicle -func NewDSFromConfig(other map[string]interface{}) (api.Vehicle, error) { - log := util.NewLogger("ds") - return newPSA(log, - "driveds.com", "clientsB2CDS", - "cbf74ee7-a303-4c3d-aba3-29f5994e2dfa", "X6bE6yQ3tH1cG5oA6aW4fS6hK0cR0aK5yN2wE4hP8vL8oW5gU3", - other) -} - -// NewOpelFromConfig creates a new vehicle -func NewOpelFromConfig(other map[string]interface{}) (api.Vehicle, error) { - log := util.NewLogger("opel") - return newPSA(log, - "opel.com", "clientsB2COpel", - "07364655-93cb-4194-8158-6b035ac2c24c", "F2kK7lC5kF5qN7tM0wT8kE3cW1dP0wC5pI6vC0sQ5iP5cN8cJ8", - other) -} - -// NewPeugeotFromConfig creates a new vehicle -func NewPeugeotFromConfig(other map[string]interface{}) (api.Vehicle, error) { - log := util.NewLogger("peugeot") - return newPSA(log, - "peugeot.com", "clientsB2CPeugeot", - "1eebc2d5-5df3-459b-a624-20abfcf82530", "T5tP7iS0cO8sC0lA2iE2aR7gK6uE5rF3lJ8pC3nO1pR7tL8vU1", - other) + registry.Add("citroen", func(other map[string]any) (api.Vehicle, error) { + return newPSA("citroen", "clientsB2CCitroen", other) + }) + registry.Add("ds", func(other map[string]any) (api.Vehicle, error) { + return newPSA("ds", "clientsB2CDS", other) + }) + registry.Add("opel", func(other map[string]any) (api.Vehicle, error) { + return newPSA("opel", "clientsB2COpel", other) + }) + registry.Add("peugeot", func(other map[string]any) (api.Vehicle, error) { + return newPSA("peugeot", "clientsB2CPeugeot", other) + }) } // PSA is an api.Vehicle implementation for PSA cars @@ -61,17 +33,16 @@ type PSA struct { } // newPSA creates a new vehicle -func newPSA(log *util.Logger, brand, realm, id, secret string, other map[string]interface{}) (api.Vehicle, error) { +func newPSA(brand, realm string, other map[string]interface{}) (api.Vehicle, error) { cc := struct { - embed `mapstructure:",squash"` - Credentials ClientCredentials - User, Password, VIN string - Cache time.Duration + embed `mapstructure:",squash"` + VIN string + User string + Password string `mapstructure:"password"` + Country string + Tokens Tokens + Cache time.Duration }{ - Credentials: ClientCredentials{ - ID: id, - Secret: secret, - }, Cache: interval, } @@ -79,22 +50,30 @@ func newPSA(log *util.Logger, brand, realm, id, secret string, other map[string] return nil, err } - if cc.User == "" || cc.Password == "" { + if cc.User == "" { return nil, api.ErrMissingCredentials } + token, err := cc.Tokens.Token() + if err != nil { + return nil, err + } + v := &PSA{ embed: &cc.embed, } - log.Redact(cc.User, cc.Password, cc.VIN) - identity := psa.NewIdentity(log, brand, cc.Credentials.ID, cc.Credentials.Secret) + log := util.NewLogger(brand) + log.Redact(cc.User, cc.Tokens.Access, cc.Tokens.Refresh) - if err := identity.Login(cc.User, cc.Password); err != nil { - return v, fmt.Errorf("login failed: %w", err) + oc := psa.Oauth2Config(brand, strings.ToLower(cc.Country)) + identity, err := psa.NewIdentity(log, brand, cc.User, oc, token) + if err != nil { + return nil, err } - api := psa.NewAPI(log, identity, realm, cc.Credentials.ID) + // TODO still needed? + api := psa.NewAPI(log, identity, realm, oc.ClientID) vehicle, err := ensureVehicleEx( cc.VIN, api.Vehicles, diff --git a/vehicle/psa/helper.go b/vehicle/psa/helper.go new file mode 100644 index 0000000000..c086042576 --- /dev/null +++ b/vehicle/psa/helper.go @@ -0,0 +1,20 @@ +package psa + +import ( + "sync" + + "golang.org/x/oauth2" +) + +var ( + mu sync.Mutex + identities = make(map[string]oauth2.TokenSource) +) + +func getInstance(subject string) oauth2.TokenSource { + return identities[subject] +} + +func addInstance(subject string, identity oauth2.TokenSource) { + identities[subject] = identity +} diff --git a/vehicle/psa/identity.go b/vehicle/psa/identity.go index f239401cf8..3437f15996 100644 --- a/vehicle/psa/identity.go +++ b/vehicle/psa/identity.go @@ -2,48 +2,80 @@ package psa import ( "context" - "fmt" + "errors" + "strings" + "sync" + "github.com/evcc-io/evcc/server/db/settings" "github.com/evcc-io/evcc/util" + "github.com/evcc-io/evcc/util/oauth" "github.com/evcc-io/evcc/util/request" "golang.org/x/oauth2" ) type Identity struct { - *request.Helper - oc *oauth2.Config oauth2.TokenSource + mu sync.Mutex + oc *oauth2.Config + log *util.Logger + subject string } // NewIdentity creates PSA identity -func NewIdentity(log *util.Logger, brand, id, secret string) *Identity { - return &Identity{ - Helper: request.NewHelper(log), - oc: &oauth2.Config{ - ClientID: id, - ClientSecret: secret, - Endpoint: oauth2.Endpoint{ - AuthURL: "https://api.mpsa.com/api/connectedcar/v2/oauth/authorize", - TokenURL: fmt.Sprintf("https://idpcvs.%s/am/oauth2/access_token", brand), - AuthStyle: oauth2.AuthStyleInHeader, - }, - Scopes: []string{"openid profile"}, - }, +func NewIdentity(log *util.Logger, brand, user string, oc *oauth2.Config, token *oauth2.Token) (oauth2.TokenSource, error) { + // serialise instance handling + mu.Lock() + defer mu.Unlock() + + // reuse identity instance + subject := "psa." + strings.ToLower(brand) + "." + strings.ToLower(user) + if instance := getInstance(subject); instance != nil { + return instance, nil + } + + v := &Identity{ + log: log, + oc: oc, + subject: subject, + } + + var tok oauth2.Token + if err := settings.Json(v.subject, &tok); err == nil { + token = &tok + } + + if !token.Valid() { + if tok, err := v.RefreshToken(token); err == nil { + token = tok + } } + + if !token.Valid() { + return nil, errors.New("token expired") + } + + v.TokenSource = oauth.RefreshTokenSource(token, v) + + // add instance + addInstance(v.subject, v) + + return v, nil } -func (v *Identity) Login(user, password string) error { - ctx := context.WithValue( - context.Background(), - oauth2.HTTPClient, - v.Client, - ) - - // replace client with authenticated oauth client - token, err := v.oc.PasswordCredentialsToken(ctx, user, password) - if err == nil { - v.TokenSource = v.oc.TokenSource(ctx, token) +func (v *Identity) RefreshToken(token *oauth2.Token) (*oauth2.Token, error) { + v.mu.Lock() + defer v.mu.Unlock() + + client := request.NewClient(v.log) + ctx := context.WithValue(context.Background(), oauth2.HTTPClient, client) + + tok, err := v.oc.TokenSource(ctx, token).Token() + if err != nil { + return nil, err } - return err + v.TokenSource = oauth.RefreshTokenSource(tok, v) + err = settings.SetJson(v.subject, tok) + + return tok, err } diff --git a/vehicle/psa/oauth2.go b/vehicle/psa/oauth2.go new file mode 100644 index 0000000000..6758d2587b --- /dev/null +++ b/vehicle/psa/oauth2.go @@ -0,0 +1,66 @@ +package psa + +import ( + "fmt" + + "golang.org/x/oauth2" +) + +func Oauth2Config(brand, country string) *oauth2.Config { + switch brand { + case "citroen": + return &oauth2.Config{ + ClientID: "5364defc-80e6-447b-bec6-4af8d1542cae", + ClientSecret: "iE0cD8bB0yJ0dS6rO3nN1hI2wU7uA5xR4gP7lD6vM0oH0nS8dN", + Endpoint: oauth2.Endpoint{ + AuthURL: "https://idpcvs.citroen.com/am/oauth2/authorize", + TokenURL: "https://idpcvs.citroen.com/am/oauth2/access_token", + AuthStyle: oauth2.AuthStyleInHeader, + }, + RedirectURL: fmt.Sprintf("mymacsdk://oauth2redirect/%s", country), + Scopes: []string{"openid", "profile"}, + } + + case "ds": + return &oauth2.Config{ + ClientID: "cbf74ee7-a303-4c3d-aba3-29f5994e2dfa", + ClientSecret: "X6bE6yQ3tH1cG5oA6aW4fS6hK0cR0aK5yN2wE4hP8vL8oW5gU3", + Endpoint: oauth2.Endpoint{ + AuthURL: "https://idpcvs.driveds.com/am/oauth2/authorize", + TokenURL: "https://idpcvs.driveds.com/am/oauth2/access_token", + AuthStyle: oauth2.AuthStyleInHeader, + }, + RedirectURL: fmt.Sprintf("mymdssdk://oauth2redirect/%s", country), + Scopes: []string{"openid", "profile"}, + } + + case "opel": + return &oauth2.Config{ + ClientID: "07364655-93cb-4194-8158-6b035ac2c24c", + ClientSecret: "F2kK7lC5kF5qN7tM0wT8kE3cW1dP0wC5pI6vC0sQ5iP5cN8cJ8", + Endpoint: oauth2.Endpoint{ + AuthURL: "https://idpcvs.opel.com/am/oauth2/authorize", + TokenURL: "https://idpcvs.opel.com/am/oauth2/access_token", + AuthStyle: oauth2.AuthStyleInHeader, + }, + RedirectURL: fmt.Sprintf("mymopsdk://oauth2redirect/%s", country), + Scopes: []string{"openid", "profile"}, + } + + case "peugeot": + return &oauth2.Config{ + ClientID: "1eebc2d5-5df3-459b-a624-20abfcf82530", + ClientSecret: "T5tP7iS0cO8sC0lA2iE2aR7gK6uE5rF3lJ8pC3nO1pR7tL8vU1", + Endpoint: oauth2.Endpoint{ + AuthURL: "https://idpcvs.peugeot.com/am/oauth2/authorize", + TokenURL: "https://idpcvs.peugeot.com/am/oauth2/access_token", + AuthStyle: oauth2.AuthStyleInHeader, + }, + RedirectURL: fmt.Sprintf("mymap://oauth2redirect/%s", country), + Scopes: []string{"openid", "profile"}, + } + + default: + panic("invalid brand: " + brand) + } +} From 8bbf193781f11b4c0ccc434da61b5419e676c154 Mon Sep 17 00:00:00 2001 From: andig Date: Wed, 1 May 2024 16:43:40 +0200 Subject: [PATCH 017/168] Add Pulsar Max --- templates/definition/charger/ocpp-pulsarplus.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/definition/charger/ocpp-pulsarplus.yaml b/templates/definition/charger/ocpp-pulsarplus.yaml index bcd5539af9..9df311b40f 100644 --- a/templates/definition/charger/ocpp-pulsarplus.yaml +++ b/templates/definition/charger/ocpp-pulsarplus.yaml @@ -2,7 +2,7 @@ template: pulsarplus products: - brand: wallbox description: - generic: Pulsar Plus, Commander 2, Copper SB + generic: Pulsar Plus, Pulsar Max, Commander 2, Copper SB requirements: description: de: | From b234f5f5b5d70be20a24243a16ee98776ef5ec4f Mon Sep 17 00:00:00 2001 From: andig Date: Wed, 1 May 2024 16:51:21 +0200 Subject: [PATCH 018/168] chore: remove duplicate param --- templates/definition/vehicle/citroen.yaml | 2 -- templates/definition/vehicle/ds.yaml | 2 -- templates/definition/vehicle/opel.yaml | 2 -- templates/definition/vehicle/peugeot.yaml | 2 -- 4 files changed, 8 deletions(-) diff --git a/templates/definition/vehicle/citroen.yaml b/templates/definition/vehicle/citroen.yaml index 59fff56e44..924bc1cbc7 100644 --- a/templates/definition/vehicle/citroen.yaml +++ b/templates/definition/vehicle/citroen.yaml @@ -27,8 +27,6 @@ params: - name: capacity - name: phases advanced: true - - name: password - deprecated: true - preset: vehicle-identify render: | type: citroen diff --git a/templates/definition/vehicle/ds.yaml b/templates/definition/vehicle/ds.yaml index e1c5f5775c..c022ae5121 100644 --- a/templates/definition/vehicle/ds.yaml +++ b/templates/definition/vehicle/ds.yaml @@ -27,8 +27,6 @@ params: - name: capacity - name: phases advanced: true - - name: password - deprecated: true - preset: vehicle-identify render: | type: ds diff --git a/templates/definition/vehicle/opel.yaml b/templates/definition/vehicle/opel.yaml index 7c3401661a..031f2433ee 100644 --- a/templates/definition/vehicle/opel.yaml +++ b/templates/definition/vehicle/opel.yaml @@ -27,8 +27,6 @@ params: - name: capacity - name: phases advanced: true - - name: password - deprecated: true - preset: vehicle-identify render: | type: opel diff --git a/templates/definition/vehicle/peugeot.yaml b/templates/definition/vehicle/peugeot.yaml index c79645e71d..734f1b161e 100644 --- a/templates/definition/vehicle/peugeot.yaml +++ b/templates/definition/vehicle/peugeot.yaml @@ -27,8 +27,6 @@ params: - name: capacity - name: phases advanced: true - - name: password - deprecated: true - preset: vehicle-identify render: | type: peugeot From 1a9ed42fdc903df83b464d05c4cf9c759f1f87dc Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Wed, 1 May 2024 22:24:20 +0200 Subject: [PATCH 019/168] Translations update from Hosted Weblate (#13449) * Translated using Weblate (Swedish) Currently translated at 100.0% (412 of 412 strings) Translated using Weblate (Swedish) Currently translated at 98.3% (405 of 412 strings) Translated using Weblate (Swedish) Currently translated at 98.2% (402 of 409 strings) Translated using Weblate (Swedish) Currently translated at 98.0% (401 of 409 strings) Co-authored-by: Jonas Gate Translate-URL: https://hosted.weblate.org/projects/evcc/evcc/sv/ Translation: evcc/evcc * Translated using Weblate (Dutch) Currently translated at 99.7% (408 of 409 strings) Translated using Weblate (Dutch) Currently translated at 98.5% (403 of 409 strings) Co-authored-by: Bart Translate-URL: https://hosted.weblate.org/projects/evcc/evcc/nl/ Translation: evcc/evcc * Translated using Weblate (Dutch) Currently translated at 98.5% (403 of 409 strings) Co-authored-by: Ewout Translate-URL: https://hosted.weblate.org/projects/evcc/evcc/nl/ Translation: evcc/evcc * Translated using Weblate (Dutch) Currently translated at 99.2% (406 of 409 strings) Co-authored-by: Renatus Ro Translate-URL: https://hosted.weblate.org/projects/evcc/evcc/nl/ Translation: evcc/evcc * Translated using Weblate (Spanish) Currently translated at 100.0% (412 of 412 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (409 of 409 strings) Co-authored-by: gallegonovato Translate-URL: https://hosted.weblate.org/projects/evcc/evcc/es/ Translation: evcc/evcc * Translated using Weblate (Lithuanian) Currently translated at 100.0% (412 of 412 strings) Translated using Weblate (Lithuanian) Currently translated at 100.0% (412 of 412 strings) Co-authored-by: RTTTC <94727758+RTTTC@users.noreply.github.com> Translate-URL: https://hosted.weblate.org/projects/evcc/evcc/lt/ Translation: evcc/evcc * Translated using Weblate (Danish) Currently translated at 100.0% (412 of 412 strings) Co-authored-by: amtssp Translate-URL: https://hosted.weblate.org/projects/evcc/evcc/da/ Translation: evcc/evcc * Translated using Weblate (French) Currently translated at 99.5% (410 of 412 strings) Co-authored-by: FraBoCH Translate-URL: https://hosted.weblate.org/projects/evcc/evcc/fr/ Translation: evcc/evcc * Translated using Weblate (Dutch) Currently translated at 99.7% (411 of 412 strings) Co-authored-by: luccie007 Translate-URL: https://hosted.weblate.org/projects/evcc/evcc/nl/ Translation: evcc/evcc * fix toml --------- Co-authored-by: Jonas Gate Co-authored-by: Bart Co-authored-by: Ewout Co-authored-by: Renatus Ro Co-authored-by: gallegonovato Co-authored-by: RTTTC <94727758+RTTTC@users.noreply.github.com> Co-authored-by: amtssp Co-authored-by: FraBoCH Co-authored-by: luccie007 Co-authored-by: premultiply <4681172+premultiply@users.noreply.github.com> --- i18n/da.toml | 36 +++++++++++++++++++++++++++++-- i18n/es.toml | 36 +++++++++++++++++++++++++++++-- i18n/fr.toml | 32 +++++++++++++++++++++++++++ i18n/lt.toml | 7 +++++- i18n/nl.toml | 61 ++++++++++++++++++++++++++++++++++++++++++++++++---- i18n/sv.toml | 36 +++++++++++++++++++++++++++++-- 6 files changed, 197 insertions(+), 11 deletions(-) diff --git a/i18n/da.toml b/i18n/da.toml index 0a051091bb..15add36079 100644 --- a/i18n/da.toml +++ b/i18n/da.toml @@ -13,7 +13,7 @@ legendTitle = "Hvordan skal solenergi benyttes?" legendTopAutostart = "starter automatisk" legendTopName = "batteri understøttet opladning" legendTopSubline = "uden afbrydelser" -modalTitle = "Batteri indstillinger" +modalTitle = "Hus batteri" [batterySettings.bufferStart] above = "når den er over {soc}" @@ -78,10 +78,22 @@ validateSave = "Valider og gem" titleAdd = "Tilføj solmåler" titleEdit = "Rediger solmåler" +[config.section] +general = "Generelle" +system = "System" + [config.site] cancel = "Afbryd" save = "Gem" +[config.system] +logs = "Log" +restart = "Genstart" +restartRequiredDescription = "Genstart for at se effekten" +restartRequiredMessage = "Konfiguration er ændret" +restartingDescription = "Vent venligst..." +restartingMessage = "Genstarter evcc." + [config.title] description = "Vises på hovedskærm og på browserfane" label = "Titel" @@ -195,6 +207,8 @@ discussionsButton = "GitHub diskussioner" documentationButton = "Dokumentation" issueButton = "Meld en fejl" issueDescription = "Fundet en mærkelig eller forkert adfærd?" +logsButton = "Se logfil" +logsDescription = "Check log for fejl" modalTitle = "Brug for hjælp?" primaryActions = "Noget fungerer ikke, som det skulle? Disse er gode steder at få hjælp." restartButton = "Genstart" @@ -208,12 +222,24 @@ description = "Under normale omstændigheder bør genstart ikke være nødvendig disclaimer = "Bemærk: evcc afsluttes og har brug for at operativsystemet genstarter tjenesten." modalTitle = "Er du sikker på, at du vil genstarte?" +[log] +areaLabel = "Filtrer per område" +areas = "Alle områder" +download = "Hent hele log filen" +levelLabel = "Filtrer på log niveau" +noResults = "Ingen matchende logposter" +search = "Søg" +showAll = "Vis alle poster" +title = "Log filer" +update = "Opdater automatisk" + [loginModal] cancel = "Afbryd" error = "Login mislykkedes" invalid = "Ugyldig adgangskode" login = "Log på" password = "Adgangskode" +reset = "Nulstil adgangskode" title = "Godkendelse" [main] @@ -397,6 +423,7 @@ waitForVehicle = "Parat. Venter på køretøj ..." [notifications] dismissAll = "Afvis alle" +logs = "Se hele log filen" modalTitle = "Underretninger" [offline] @@ -466,7 +493,12 @@ allVehicles = "alle køretøjer" filter = "Filter" [settings] -title = "Generelle indstillinger" +title = "Brugergrænseflade" + +[settings.fullscreen] +enter = "Gå til fuldskærmsvisning" +exit = "Afslut fuldskærm" +label = "Fuldskærm" [settings.hiddenFeatures] label = "Experimentalt" diff --git a/i18n/es.toml b/i18n/es.toml index f6205119d7..22a10c58e3 100644 --- a/i18n/es.toml +++ b/i18n/es.toml @@ -13,7 +13,7 @@ legendTitle = "¿Cómo debería usarse la energía solar?" legendTopAutostart = "inicio automático" legendTopName = "carga soportada por bateria" legendTopSubline = "sin interrupciones" -modalTitle = "Ajustes de la batería" +modalTitle = "Batería doméstica" [batterySettings.bufferStart] above = "cuando esté por encima de {soc}" @@ -78,10 +78,22 @@ validateSave = "Validar y guardar" titleAdd = "Añadir contador solar" titleEdit = "Editar contador solar" +[config.section] +general = "General" +system = "Sistema" + [config.site] cancel = "Cancelar" save = "Guardar" +[config.system] +logs = "Registros" +restart = "Reiniciar" +restartRequiredDescription = "Por favor, reinicie para aplicar los cambios" +restartRequiredMessage = "La configuración cambió" +restartingDescription = "Espere por favor…" +restartingMessage = "Reiniciando evcc" + [config.title] description = "Se muestra en la pantalla principal y en la pestaña del navegador" label = "Título" @@ -200,6 +212,8 @@ discussionsButton = "Debates en GitHub" documentationButton = "Documentación" issueButton = "Reportar un error" issueDescription = "¿Encontraste un comportamiento extraño o incorrecto?" +logsButton = "Ver los registros" +logsDescription = "Compruebe los registros en busca de errores" modalTitle = "¿Necesitas ayuda?" primaryActions = "Si algo no funciona como debería, puedes obtener ayuda aqui." restartButton = "Reiniciar" @@ -213,12 +227,24 @@ description = "En circunstancias normales no debería ser necesario reiniciar. P disclaimer = "Nota: evcc terminará y dependerá del sistema operativo para reiniciar el servicio" modalTitle = "¿Seguro que quieres volver a empezar?" +[log] +areaLabel = "Filtrar por zona" +areas = "Todas las areas" +download = "Descargar registro completo" +levelLabel = "Filtrar por nivel de registro" +noResults = "No hay entradas en el registro coincidentes" +search = "Buscar" +showAll = "Mostrar todas las entradas" +title = "Registros" +update = "Actualización automática" + [loginModal] cancel = "Cancelar" error = "Error en el inicio de sesion: " invalid = "La contraseña no es válida." login = "Registro" password = "Contraseña" +reset = "¿Restablecer la contraseña?" title = "Autenticación" [main] @@ -402,6 +428,7 @@ waitForVehicle = "Listo para cargar. Esperando vehículo." [notifications] dismissAll = "Eliminar notificaciones" +logs = "Ver registros completos" modalTitle = "Notificaciónes" [offline] @@ -471,7 +498,12 @@ allVehicles = "todos los vehiculos" filter = "Filtrar" [settings] -title = "Ajustes generales" +title = "Interfaz de usuario" + +[settings.fullscreen] +enter = "Iniciar a pantalla completa" +exit = "Salir de la pantalla completa" +label = "Pantalla completa" [settings.gridDetails] co2 = "CO₂" diff --git a/i18n/fr.toml b/i18n/fr.toml index d8146fea2b..f8a199bd3d 100644 --- a/i18n/fr.toml +++ b/i18n/fr.toml @@ -78,10 +78,22 @@ validateSave = "Valider et enregistrer" titleAdd = "Ajouter un compteur pour le solaire" titleEdit = "Modifier le compteur pour le solaire" +[config.section] +general = "Général" +system = "Système" + [config.site] cancel = "Annuler" save = "Enregistrer" +[config.system] +logs = "Journaux" +restart = "Redémarrer" +restartRequiredDescription = "Veuillez redémarrer pour appliquer les changements." +restartRequiredMessage = "La configuration a changé." +restartingDescription = "Veuillez patienter…" +restartingMessage = "Redémarrage d’evcc en cours." + [config.title] description = "«Affiché sur l’écran principal et l’onglet du navigateur.»" label = "Titre" @@ -200,6 +212,8 @@ discussionsButton = "Discussions GitHub" documentationButton = "Documentation" issueButton = "Signaler un bug" issueDescription = "Trouvé un comportement étrange ou erroné?" +logsButton = "Voir les journaux" +logsDescription = "Vérifiez s’il y a des erreurs dans les journaux." modalTitle = "Besoin d'aide?" primaryActions = "Quelque chose ne fonctionne pas comme ça devrait ? Voici où trouver de l'aide." restartButton = "Redémarrer" @@ -213,12 +227,24 @@ description = "En temps normal, un redémarrage ne devrait pas être nécessaire disclaimer = "NB : evcc va s'arrêter et s'en remettre au système d'exploitation pour redémarrer le service." modalTitle = "Voulez-vous vraiment redémarrer ?" +[log] +areaLabel = "Filtrer par zone" +areas = "Toutes les zones" +download = "Télécharger les journaux complets" +levelLabel = "Filtrer par niveau" +noResults = "Aucune entrée de journal ne correspond." +search = "Rechercher" +showAll = "Voir toutes les entrées" +title = "Journaux" +update = "Mise à jour automatique" + [loginModal] cancel = "Annuler" error = "«Connection échouée:»" invalid = "«Mot de passe invalide.»" login = "Identifiant" password = "Mot de passe" +reset = "Réinitialiser le mot de passe ?" title = "Authentification" [main] @@ -399,6 +425,7 @@ waitForVehicle = "Prêt à charger. Attente du véhicule…" [notifications] dismissAll = "Supprimer tout" +logs = "Voir les journaux complets" modalTitle = "Notifications" [offline] @@ -470,6 +497,11 @@ filter = "Filtrer" [settings] title = "Réglages généraux" +[settings.fullscreen] +enter = "Passer en plein écran" +exit = "Quitter le plein écran" +label = "Plein écran" + [settings.hiddenFeatures] label = "Expérimental" value = "Afficher les fonctions expérimentales." diff --git a/i18n/lt.toml b/i18n/lt.toml index 932bd269b0..41647e7ff7 100644 --- a/i18n/lt.toml +++ b/i18n/lt.toml @@ -393,7 +393,7 @@ detectionActive = "Bandome atpažinti automobilį ..." fallbackName = "Automobilis" moreActions = "Daugiau veiksmų" none = "Nėra automobilio" -notReachable = "Automobilis buvo nepasiekiamas. Pabandykite restartuoti evcc." +notReachable = "Automobilis nepasiekiamas. Pabandykite restartuoti evcc." targetSoc = "Limitas" temp = "t." tempLimit = "Temp. limitas" @@ -500,6 +500,11 @@ filter = "Filtruoti" [settings] title = "Vartotojo sąsaja" +[settings.fullscreen] +enter = "Įjungti visą ekraną" +exit = "Išjungti visą ekraną" +label = "Visas ekranas" + [settings.gridDetails] co2 = "CO₂" label = "Tinklas detaliau" diff --git a/i18n/nl.toml b/i18n/nl.toml index 9a2d918fd2..bf6f066d2c 100644 --- a/i18n/nl.toml +++ b/i18n/nl.toml @@ -2,7 +2,7 @@ batteryLevel = "Batterij-niveau" capacity = "{energy} van {total}" control = "Batterijsturing" -discharge = "Voorkom ontladen in de snelle modus of bij gepland laden" +discharge = "Voorkom ontladen in de snelle modus en bij gepland laden" disclaimerHint = "Opgelet:" disclaimerText = "Deze instellingen beïnvloeden enkel de zonnemodus. Oplaadgedrag wordt overeenkomstig aangepast." legendBottomName = "huis heeft prioriteit" @@ -13,7 +13,7 @@ legendTitle = "Hoe moet zonne-energie gebruikt worden?" legendTopAutostart = "start automatisch" legendTopName = "batterij-ondersteund opladen" legendTopSubline = "zonder onderbrekingen" -modalTitle = "Batterij-instellingen" +modalTitle = "Thuisaccu" [batterySettings.bufferStart] above = "Indien boven {soc}" @@ -42,7 +42,7 @@ phaseVoltages = "Spanning L1..L3" power = "Vermogen" range = "Rijbereik" soc = "Batterijpercentage" -socLimit = "Hoeveelheid" +socLimit = "Limiet" [config.form] example = "Voorbeeld" @@ -78,10 +78,22 @@ validateSave = "Valideren en opslaan" titleAdd = "Voeg meter zonnepanelen toe" titleEdit = "Bewerk meter zonnepanelen" +[config.section] +general = "Algemeen" +system = "Systeem" + [config.site] cancel = "Annuleren" save = "Opslaan" +[config.system] +logs = "Logboeken" +restart = "Herstart" +restartRequiredDescription = "Herstart om het effect te zien" +restartRequiredMessage = "Configuratie gewijzigd." +restartingDescription = "Even geduld aub..." +restartingMessage = "Evcc wordt herstart." + [config.title] description = "Wordt weergegeven op het hoofdscherm en op het browsertabblad." label = "Titel" @@ -200,6 +212,8 @@ discussionsButton = "GitHub discussies" documentationButton = "Documentatie" issueButton = "Een bug melden" issueDescription = "Vreemd of foutief gedrag gevonden?" +logsButton = "Bekijk logboeken" +logsDescription = "Kijk in de logboeken voor foutmeldingen." modalTitle = "Hulp nodig?" primaryActions = "Werkt iets niet naar behoren? Er zijn goede plaatsen om hulp te krijgen." restartButton = "Herstart" @@ -213,6 +227,26 @@ description = "Herstarten niet nodig onder normale omstandigheden. Overweeg een disclaimer = "Nota: evcc zal afsluiten en rekenen op het besturingssysteem om de service te herstarten." modalTitle = "Ben je zeker dat je wil herstarten?" +[log] +areaLabel = "Filter op domain" +areas = "Alle domainen" +download = "Volledig logboek downloaden" +levelLabel = "Filter op log level" +noResults = "Geen overeenkomende log regels." +search = "Zoeken" +showAll = "Toon alles" +title = "Logboeken" +update = "Automatisch updaten" + +[loginModal] +cancel = "Annuleren" +error = "Login mislukt: " +invalid = "Wachtwoord onjuist." +login = "Inloggen" +password = "Wachtwoord" +reset = "Wachtwoord resetten?" +title = "Authenticatie" + [main] vehicles = "Parking" @@ -391,12 +425,26 @@ waitForVehicle = "Klaar. Wachten op voertuig..." [notifications] dismissAll = "Alles verwijderen" +logs = "Volledige logboeken bekijken" modalTitle = "Notificaties" [offline] message = "Niet verbonden met een server." reload = "Verversen?" +[passwordModal] +description = "Stel een wachtwoord in voor het afschermen van de instellingen. Het hoofdscherm blijft beschikbaar zonder wachtwoord." +empty = "Wachtwoord mag niet leeg zijn" +error = "Fout: " +labelCurrent = "Huidig wachtwoord" +labelNew = "Nieuw wachtwoord" +labelRepeat = "Wachtwoord herhalen" +newPassword = "Wachtwoord aanmaken" +noMatch = "Wachtwoorden komen niet overeen" +titleNew = "Stel beheerderswachtwoord in" +titleUpdate = "Beheerderswachtwoord bijwerken" +updatePassword = "Wachtwoord bijwerken" + [session] cancel = "Annuleer" co2 = "CO₂" @@ -447,7 +495,12 @@ allVehicles = "alle voertuigen" filter = "Filter" [settings] -title = "Algemene instellingen" +title = "Gebruikersinterface" + +[settings.fullscreen] +enter = "Open volledig scherm" +exit = "Volledig scherm afsluiten" +label = "Volledig scherm" [settings.gridDetails] co2 = "CO₂" diff --git a/i18n/sv.toml b/i18n/sv.toml index 75717a62d2..dc5ac453e5 100644 --- a/i18n/sv.toml +++ b/i18n/sv.toml @@ -13,7 +13,7 @@ legendTitle = "Hur ska solenergin användas?" legendTopAutostart = "startar automatiskt" legendTopName = "laddning med batteristöd" legendTopSubline = "utan avbrott" -modalTitle = "Batteri inställningar" +modalTitle = "Hemmabatteri" [batterySettings.bufferStart] above = "när den är över {soc}" @@ -78,10 +78,22 @@ validateSave = "Validera & spara" titleAdd = "Lägg till solcellsmätare" titleEdit = "Ändra solcellsmätare" +[config.section] +general = "Allmänna" +system = "System" + [config.site] cancel = "Avbryt" save = "Spara" +[config.system] +logs = "Loggar" +restart = "Omstart" +restartRequiredDescription = "Starta om för att de nya inställningarna ska börja gälla." +restartRequiredMessage = "Inställningar ändrade." +restartingDescription = "Vänligen vänta..." +restartingMessage = "Startar om evcc." + [config.title] description = "Visas på huvudskärm och tabbar." label = "Titel" @@ -195,6 +207,8 @@ discussionsButton = "GitHub diskussioner" documentationButton = "Dokumentation" issueButton = "Rapportera fel" issueDescription = "Hittat ett konstigt eller felaktigt beteende?" +logsButton = "Se loggar" +logsDescription = "Felsök loggar" modalTitle = "Behöver du hjälp?" primaryActions = "Fungerar det inte som tänkt? Här finns mycket hjälp att hitta." restartButton = "Omstart" @@ -208,12 +222,24 @@ description = "Normalt behövs inte en omstart. Rapportera ett fel om du behöve disclaimer = "Notera: evcc kommer stängas av och be operativsystemet starta om tjänsten." modalTitle = "Är du säker på att du vill starta om?" +[log] +areaLabel = "Filtrera per område" +areas = "Alla områden" +download = "Ladda hem alla loggar" +levelLabel = "Filtrera på loggnivå" +noResults = "Inga matchande loggposter." +search = "Sök" +showAll = "Visa allt" +title = "Loggar" +update = "Autouppdatera" + [loginModal] cancel = "Avbryt" error = "Inloggning misslyckades:" invalid = "Ogiltigt lösenord." login = "Logga in" password = "Lösenord" +reset = "Återställ lösenord?" title = "Autentisering" [main] @@ -397,6 +423,7 @@ waitForVehicle = "Redo. Väntar på fordon..." [notifications] dismissAll = "Avvisa alla" +logs = "Se kompletta loggar" modalTitle = "Meddelanden" [offline] @@ -466,7 +493,12 @@ allVehicles = "alla fordon" filter = "Filter" [settings] -title = "Allmänna inställningar" +title = "Användargränssnitt" + +[settings.fullscreen] +enter = "Till helskärmsläge" +exit = "Lämna helskärmsläge" +label = "Helskärmsläge" [settings.gridDetails] co2 = "CO₂" From 81d677b897df7c8c182dd1a470d193f8177d9e06 Mon Sep 17 00:00:00 2001 From: docolli <78646938+docolli@users.noreply.github.com> Date: Thu, 2 May 2024 11:52:58 +0200 Subject: [PATCH 020/168] E3/DC RSCP: add battery control docs (#13626) --- templates/definition/meter/e3dc-rscp.yaml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/templates/definition/meter/e3dc-rscp.yaml b/templates/definition/meter/e3dc-rscp.yaml index c855653f2f..b6d68f8a24 100644 --- a/templates/definition/meter/e3dc-rscp.yaml +++ b/templates/definition/meter/e3dc-rscp.yaml @@ -1,6 +1,17 @@ template: e3dc-rscp products: - brand: E3/DC +capabilities: ["battery-control"] +requirements: + description: + de: | + Benutzername und Passwort sind identisch zum Web-Portal bzw. My E3/DC App. Key (=RSCP-Passwort) muss im Hauskraftwerk unter Personalisieren/Benutzerprofil angelegt werden. + + **Achtung**: Die aktive Batteriesteuerung überschreibt Einstellungen im Smart-Power/Betriebsbereich. + en: | + Username and password are identical to Web Portal or My E3/DC App access. Key (=RSCP-Password) must be set in the E3/DC System at Personalize/User Profile. + + **Note**: Active battery control will override Smart-Power/Operating Range settings. params: - name: usage choice: ["grid", "pv", "battery"] From 70864237aef5a9dbd22181f848b38716d5d7d2d0 Mon Sep 17 00:00:00 2001 From: andig Date: Thu, 2 May 2024 16:16:27 +0200 Subject: [PATCH 021/168] Reapply "Fiat: require pin for updated soc (#13223)" This reverts commit f2572fb5dcfa42a4271262b28e1a6d7696243440. --- templates/definition/vehicle/fiat.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/templates/definition/vehicle/fiat.yaml b/templates/definition/vehicle/fiat.yaml index 78dfc81932..a6f2fd389e 100644 --- a/templates/definition/vehicle/fiat.yaml +++ b/templates/definition/vehicle/fiat.yaml @@ -14,5 +14,8 @@ render: | {{ include "vehicle-base" . }} {{- if .pin }} pin: {{ .pin }} # mandatory to deep refresh Soc + help: + de: Für eine regelmäßige Abfrage des SoC beim Laden hier die PIN von der FIAT-App eintragen + en: To trigger the regular refresh of the SoC while charging enter the PIN from your FIAT app {{- end }} {{ include "vehicle-identify" . }} From 7a3390a06a4032370555fa6f9b6f88ce3370ab27 Mon Sep 17 00:00:00 2001 From: andig Date: Thu, 2 May 2024 21:04:37 +0200 Subject: [PATCH 022/168] Fix crash retrieving log level --- util/logstash/element.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/util/logstash/element.go b/util/logstash/element.go index d2f55caebc..695e845909 100644 --- a/util/logstash/element.go +++ b/util/logstash/element.go @@ -13,7 +13,7 @@ var re = regexp.MustCompile(`^\[([a-zA-Z0-9-]+)\s*\] (\w+) `) func (e element) areaLevel() (string, jww.Threshold) { m := re.FindAllStringSubmatch(string(e), 1) - if len(m) != 1 && len(m[0]) != 3 { + if len(m) != 1 || len(m[0]) != 3 { return "", jww.LevelError } return m[0][1], LogLevelToThreshold(m[0][2]) From 8eea8086450b1b62e1d9a9db1ace47e561a744e3 Mon Sep 17 00:00:00 2001 From: andig Date: Fri, 3 May 2024 08:06:38 +0200 Subject: [PATCH 023/168] Revert "Reapply "Fiat: require pin for updated soc (#13223)"" This reverts commit 70864237aef5a9dbd22181f848b38716d5d7d2d0. --- templates/definition/vehicle/fiat.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/templates/definition/vehicle/fiat.yaml b/templates/definition/vehicle/fiat.yaml index a6f2fd389e..78dfc81932 100644 --- a/templates/definition/vehicle/fiat.yaml +++ b/templates/definition/vehicle/fiat.yaml @@ -14,8 +14,5 @@ render: | {{ include "vehicle-base" . }} {{- if .pin }} pin: {{ .pin }} # mandatory to deep refresh Soc - help: - de: Für eine regelmäßige Abfrage des SoC beim Laden hier die PIN von der FIAT-App eintragen - en: To trigger the regular refresh of the SoC while charging enter the PIN from your FIAT app {{- end }} {{ include "vehicle-identify" . }} From 25f9f6ce5004477ec4be763e3616f904ff6e40d3 Mon Sep 17 00:00:00 2001 From: andig Date: Fri, 3 May 2024 09:27:02 +0200 Subject: [PATCH 024/168] PSA: fix templates --- templates/definition/vehicle/citroen.yaml | 8 +++++++- templates/definition/vehicle/ds.yaml | 8 +++++++- templates/definition/vehicle/opel.yaml | 8 +++++++- templates/definition/vehicle/peugeot.yaml | 8 +++++++- 4 files changed, 28 insertions(+), 4 deletions(-) diff --git a/templates/definition/vehicle/citroen.yaml b/templates/definition/vehicle/citroen.yaml index 924bc1cbc7..85268b360f 100644 --- a/templates/definition/vehicle/citroen.yaml +++ b/templates/definition/vehicle/citroen.yaml @@ -16,7 +16,13 @@ params: required: true - name: password deprecated: true - - name: tokens + - name: accessToken + required: true + mask: true + help: + en: "See https://docs.evcc.io/en/docs/devices/vehicles#citroen" + de: "Siehe https://docs.evcc.io/docs/devices/vehicles#citroen" + - name: refreshToken required: true mask: true help: diff --git a/templates/definition/vehicle/ds.yaml b/templates/definition/vehicle/ds.yaml index c022ae5121..d53cb4e0ff 100644 --- a/templates/definition/vehicle/ds.yaml +++ b/templates/definition/vehicle/ds.yaml @@ -16,7 +16,13 @@ params: required: true - name: password deprecated: true - - name: tokens + - name: accessToken + required: true + mask: true + help: + en: "See https://docs.evcc.io/en/docs/devices/vehicles#ds" + de: "Siehe https://docs.evcc.io/docs/devices/vehicles#ds" + - name: refreshToken required: true mask: true help: diff --git a/templates/definition/vehicle/opel.yaml b/templates/definition/vehicle/opel.yaml index 031f2433ee..baf77e07d2 100644 --- a/templates/definition/vehicle/opel.yaml +++ b/templates/definition/vehicle/opel.yaml @@ -16,7 +16,13 @@ params: required: true - name: password deprecated: true - - name: tokens + - name: accessToken + required: true + mask: true + help: + en: "See https://docs.evcc.io/en/docs/devices/vehicles#opel" + de: "Siehe https://docs.evcc.io/docs/devices/vehicles#opel" + - name: refreshToken required: true mask: true help: diff --git a/templates/definition/vehicle/peugeot.yaml b/templates/definition/vehicle/peugeot.yaml index 734f1b161e..08be901f34 100644 --- a/templates/definition/vehicle/peugeot.yaml +++ b/templates/definition/vehicle/peugeot.yaml @@ -16,7 +16,13 @@ params: required: true - name: password deprecated: true - - name: tokens + - name: accessToken + required: true + mask: true + help: + en: "See https://docs.evcc.io/en/docs/devices/vehicles#peugeot" + de: "Siehe https://docs.evcc.io/docs/devices/vehicles#peugeot" + - name: refreshToken required: true mask: true help: From ce61b9c55d395eb3483093135749ca64f754b6c6 Mon Sep 17 00:00:00 2001 From: andig Date: Fri, 3 May 2024 11:30:00 +0200 Subject: [PATCH 025/168] Add All in Power (NL) tariff (#13691) --- tariff/helper.go | 4 ++++ tariff/tariff.go | 6 ++++-- tariff/template.go | 19 +++++++++++++++++++ templates/definition/tariff/allinpower.yaml | 12 ++++++++++++ 4 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 tariff/template.go create mode 100644 templates/definition/tariff/allinpower.yaml diff --git a/tariff/helper.go b/tariff/helper.go index 746badc268..0a8525f4fe 100644 --- a/tariff/helper.go +++ b/tariff/helper.go @@ -2,6 +2,7 @@ package tariff import ( "errors" + "strings" "time" "github.com/cenkalti/backoff/v4" @@ -23,5 +24,8 @@ func backoffPermanentError(err error) error { return backoff.Permanent(se) } } + if strings.HasPrefix(err.Error(), "jq: query failed") { + return backoff.Permanent(err) + } return err } diff --git a/tariff/tariff.go b/tariff/tariff.go index 7dd64fe56e..8db2502873 100644 --- a/tariff/tariff.go +++ b/tariff/tariff.go @@ -90,8 +90,10 @@ func (t *Tariff) run(forecastG func() (string, error), done chan error) { if err != nil { return backoffPermanentError(err) } - - return json.Unmarshal([]byte(s), &data) + if err := json.Unmarshal([]byte(s), &data); err != nil { + return backoff.Permanent(err) + } + return nil }, bo); err != nil { once.Do(func() { done <- err }) diff --git a/tariff/template.go b/tariff/template.go new file mode 100644 index 0000000000..ec1512e1a3 --- /dev/null +++ b/tariff/template.go @@ -0,0 +1,19 @@ +package tariff + +import ( + "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/util/templates" +) + +func init() { + registry.Add("template", NewTariffFromTemplateConfig) +} + +func NewTariffFromTemplateConfig(other map[string]interface{}) (api.Tariff, error) { + instance, err := templates.RenderInstance(templates.Tariff, other) + if err != nil { + return nil, err + } + + return NewFromConfig(instance.Type, instance.Other) +} diff --git a/templates/definition/tariff/allinpower.yaml b/templates/definition/tariff/allinpower.yaml new file mode 100644 index 0000000000..dcae9b6e8b --- /dev/null +++ b/templates/definition/tariff/allinpower.yaml @@ -0,0 +1,12 @@ +template: allinpower +products: + - brand: All in Power (NL) +params: + - preset: tariff-base +render: | + type: custom + {{ include "tariff-base" . }} + forecast: + source: http + uri: https://api.allinpower.nl/troodon/api/p/spot_market/prices/?product_type=ELK + jq: '[.timestamps, .prices] | transpose | map({ "start": (.[0] | strptime("%Y-%m-%dT%H:%M:%S.%f%z") | strftime("%Y-%m-%dT%H:%M:%SZ")), "end": (.[0] | strptime("%Y-%m-%dT%H:%M:%S.%f%z") | mktime + 3600 | strftime("%Y-%m-%dT%H:%M:%SZ")), "price": .[1] }) | tostring' From 887c96ec37c793f1d63c2b9309bd10dddb161cb9 Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Fri, 3 May 2024 11:30:34 +0200 Subject: [PATCH 026/168] UI: fix duplicate entries in smart cost options (#13704) --- assets/js/components/SmartCostLimit.vue | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/assets/js/components/SmartCostLimit.vue b/assets/js/components/SmartCostLimit.vue index e40c960291..7bee5a56a2 100644 --- a/assets/js/components/SmartCostLimit.vue +++ b/assets/js/components/SmartCostLimit.vue @@ -111,7 +111,7 @@ export default { for (let i = 1; i <= 100; i++) { const value = this.optionStartValue + stepSize * i; if (value != 0) { - values.push(value); + values.push(value.toFixed(2)); } } // add special entry if currently selected value is not in the scale @@ -225,12 +225,12 @@ export default { this.updateTariff(); }, smartCostLimit(limit) { - this.selectedSmartCostLimit = limit; + this.selectedSmartCostLimit = limit?.toFixed(2); }, }, mounted() { this.updateTariff(); - this.selectedSmartCostLimit = this.smartCostLimit; + this.selectedSmartCostLimit = this.smartCostLimit?.toFixed(2); }, methods: { updateTariff: async function () { From a9a080f6ffb82e4268674b00a9a50c9b374fb405 Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Fri, 3 May 2024 15:05:57 +0200 Subject: [PATCH 027/168] UI: fix duplicate product entries (#13709) --- util/templates/class.go | 2 +- util/templates/class_enumer.go | 4 ++-- util/templates/init.go | 22 +++++++--------------- 3 files changed, 10 insertions(+), 18 deletions(-) diff --git a/util/templates/class.go b/util/templates/class.go index 8e970ec687..42ea0c109c 100644 --- a/util/templates/class.go +++ b/util/templates/class.go @@ -2,7 +2,7 @@ package templates type Class int -//go:generate enumer -type Class +//go:generate enumer -type Class -transform=lower const ( _ Class = iota Charger diff --git a/util/templates/class_enumer.go b/util/templates/class_enumer.go index 0891e5c019..48acc8c8e1 100644 --- a/util/templates/class_enumer.go +++ b/util/templates/class_enumer.go @@ -1,4 +1,4 @@ -// Code generated by "enumer -type Class"; DO NOT EDIT. +// Code generated by "enumer -type Class -transform=lower"; DO NOT EDIT. package templates @@ -7,7 +7,7 @@ import ( "strings" ) -const _ClassName = "ChargerMeterVehicleTariffCircuit" +const _ClassName = "chargermetervehicletariffcircuit" var _ClassIndex = [...]uint8{0, 7, 12, 19, 25, 32} diff --git a/util/templates/init.go b/util/templates/init.go index 65a9461c6a..143c6c614e 100644 --- a/util/templates/init.go +++ b/util/templates/init.go @@ -5,7 +5,6 @@ import ( "embed" "fmt" "io/fs" - "path" "slices" "sync" "text/template" @@ -33,8 +32,8 @@ func init() { baseTmpl = template.Must(template.ParseFS(includeFS, "includes/*.tpl")) - for _, class := range ClassValues() { - loadTemplates(class) + for _, class := range []Class{Charger, Meter, Vehicle, Tariff} { + templates[class] = load(class) } } @@ -66,12 +65,8 @@ func FromBytes(b []byte) (Template, error) { return tmpl, err } -func loadTemplates(class Class) { - if templates[class] != nil { - return - } - - err := fs.WalkDir(definition.YamlTemplates, ".", func(filepath string, d fs.DirEntry, err error) error { +func load(class Class) (res []Template) { + err := fs.WalkDir(definition.YamlTemplates, class.String(), func(filepath string, d fs.DirEntry, err error) error { if err != nil { return err } @@ -89,18 +84,15 @@ func loadTemplates(class Class) { return fmt.Errorf("processing template '%s' failed: %w", filepath, err) } - class, err := ClassString(path.Dir(filepath)) - if err != nil { - return fmt.Errorf("invalid template class: '%s'", err) - } - - templates[class] = append(templates[class], tmpl) + res = append(res, tmpl) return nil }) if err != nil { panic(err) } + + return res } // EncoderLanguage sets the template language for encoding json From 8cebd762b4f5de7182c116bf45ee6756f9b090a2 Mon Sep 17 00:00:00 2001 From: andig Date: Fri, 3 May 2024 15:16:26 +0200 Subject: [PATCH 028/168] chore: cleanup --- templates/definition/vehicle/citroen.yaml | 4 ++-- templates/definition/vehicle/ds.yaml | 4 ++-- templates/definition/vehicle/mercedes.yaml | 4 ++-- templates/definition/vehicle/opel.yaml | 4 ++-- templates/definition/vehicle/peugeot.yaml | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/templates/definition/vehicle/citroen.yaml b/templates/definition/vehicle/citroen.yaml index 85268b360f..a62d64f0f8 100644 --- a/templates/definition/vehicle/citroen.yaml +++ b/templates/definition/vehicle/citroen.yaml @@ -4,9 +4,9 @@ products: requirements: description: de: | - Benötigt `access` und `refresh` Tokens. Diese können über den Befehl "evcc token [name]" generiert werden. + Benötigt `access` und `refresh` Tokens. Diese können über den Befehl `evcc token [name]` generiert werden. en: | - Citroën `access` and `refresh` tokens are required. These can be generated with command "evcc token [name]". + Requires `access` and `refresh` tokens. These can be generated with command `evcc token [name]`. params: - name: title - name: icon diff --git a/templates/definition/vehicle/ds.yaml b/templates/definition/vehicle/ds.yaml index d53cb4e0ff..d2abb63975 100644 --- a/templates/definition/vehicle/ds.yaml +++ b/templates/definition/vehicle/ds.yaml @@ -4,9 +4,9 @@ products: requirements: description: de: | - Benötigt `access` und `refresh` Tokens. Diese können über den Befehl "evcc token [name]" generiert werden. + Benötigt `access` und `refresh` Tokens. Diese können über den Befehl `evcc token [name]` generiert werden. en: | - DS `access` and `refresh` tokens are required. These can be generated with command "evcc token [name]". + Requires `access` and `refresh` tokens. These can be generated with command `evcc token [name]`. params: - name: title - name: icon diff --git a/templates/definition/vehicle/mercedes.yaml b/templates/definition/vehicle/mercedes.yaml index f6a9bc44ed..92b8847715 100644 --- a/templates/definition/vehicle/mercedes.yaml +++ b/templates/definition/vehicle/mercedes.yaml @@ -16,7 +16,7 @@ requirements: vin: W... # Erforderlich, wenn mehr als ein Fahrzeug im Account registriert capacity: 50 # Akkukapazität in kWh (optional) ``` - 2. Token Generierung: Ausführen von "./evcc token mercedes" oder "evcc token [name]", wenn name gesetzt ist. + 2. Token Generierung: Ausführen von `./evcc token mercedes` oder `evcc token [name]`, wenn name gesetzt ist. 3. Einfügen der Tokens in die evcc.yaml ``` vehicles: @@ -43,7 +43,7 @@ requirements: vin: W... # Required, if more then one car is registered in this account capacity: 50 # capacity in kWh (optional) ``` - 2. Token generation: execute "./evcc token mercedes" or "evcc token [name]", when name is defined + 2. Token generation: execute `./evcc token mercedes` or `evcc token [name]`, when name is defined 3. insert the tokens into evcc.yaml ``` vehicles: diff --git a/templates/definition/vehicle/opel.yaml b/templates/definition/vehicle/opel.yaml index baf77e07d2..73db782c30 100644 --- a/templates/definition/vehicle/opel.yaml +++ b/templates/definition/vehicle/opel.yaml @@ -4,9 +4,9 @@ products: requirements: description: de: | - Benötigt `access` und `refresh` Tokens. Diese können über den Befehl "evcc token [name]" generiert werden. + Benötigt `access` und `refresh` Tokens. Diese können über den Befehl `evcc token [name]` generiert werden. en: | - Opel `access` and `refresh` tokens are required. These can be generated with command "evcc token [name]". + Requires `access` and `refresh` tokens. These can be generated with command `evcc token [name]`. params: - name: title - name: icon diff --git a/templates/definition/vehicle/peugeot.yaml b/templates/definition/vehicle/peugeot.yaml index 08be901f34..ee15a0ef9e 100644 --- a/templates/definition/vehicle/peugeot.yaml +++ b/templates/definition/vehicle/peugeot.yaml @@ -4,9 +4,9 @@ products: requirements: description: de: | - Benötigt `access` und `refresh` Tokens. Diese können über den Befehl "evcc token [name]" generiert werden. + Benötigt `access` und `refresh` Tokens. Diese können über den Befehl `evcc token [name]` generiert werden. en: | - Peugeot `access` and `refresh` tokens are required. These can be generated with command "evcc token [name]". + Requires `access` and `refresh` tokens. These can be generated with command `evcc token [name]`. params: - name: title - name: icon From eeb42c4d5ce4abeb5c3a4f7b780bbd77d316e882 Mon Sep 17 00:00:00 2001 From: Oguzhan Tasimaz Date: Fri, 3 May 2024 23:37:59 +0300 Subject: [PATCH 029/168] Add Turkish translation (#13720) * add: turkish lang * fix: typo * fix toml --------- Co-authored-by: premultiply <4681172+premultiply@users.noreply.github.com> --- i18n/tr.toml | 537 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 537 insertions(+) diff --git a/i18n/tr.toml b/i18n/tr.toml index 7bff32e94d..4bafee6933 100644 --- a/i18n/tr.toml +++ b/i18n/tr.toml @@ -1,2 +1,539 @@ [batterySettings] batteryLevel = "Batarya seviyesi" +capacity = "{total} içinde {energy}" +control = "Pil kontrolü" +discharge = "Hızlı modda ve planlanan şarjda deşarjı önleyin." +disclaimerHint = "Not:" +disclaimerText = "Bu ayarlar yalnızca güneş enerjisi modunu etkiler. Şarj davranışı buna göre ayarlanır." +legendBottomName = "ev önceliği" +legendBottomSubline = "şarj için kullanılmaz" +legendMiddleName = "önce araç" +legendMiddleSubline = "ev ikincil" +legendTitle = "Güneş enerjisi nasıl kullanılmalıdır?" +legendTopAutostart = "otomatik başlar" +legendTopName = "batarya destekli şarj" +legendTopSubline = "kesintisiz" +modalTitle = "Batarya Ayarları" + +[batterySettings.bufferStart] +above = "{soc} üzerinde olduğunda" +full = "{soc} iken" +never = "yalnızca yeterli fazlalık varsa" + +[config] + +[config.battery] +titleAdd = "Batarya Ölçer Ekle" +titleEdit = "Batarya Ölçeri Düzenle" + +[config.deviceValue] +capacity = "Kapasite" +chargeStatus = "Durum" +chargedEnergy = "Şarj Edildi" +current = "Akım" +enabled = "Etkin" +energy = "Enerji" +odometer = "Kilometre Sayacı" +phaseCurrents = "Faz Akımı L1..L3" +phasePowers = "Faz Gücü L1..L3" +phaseVoltages = "Faz Voltajı L1..L3" +power = "Güç" +range = "Menzil" +soc = "SoC" +socLimit = "Limit" + +[config.form] +example = "Örnek" +optional = "opsiyonel" + +[config.general] +cancel = "İptal" +save = "Kaydet" + +[config.grid] +titleAdd = "Şebeke Ölçer Ekle" +titleEdit = "Şebeke Ölçeri Düzenle" + +[config.main] +addLoadpoint = "Şarj noktası ekle" +addPvBattery = "Güneş enerjisi veya pil ekle" +addVehicle = "Araç ekle" +edit = "düzenle" +title = "Konfigürasyon" +unconfigured = "yapılandırılmadı" +vehicles = "Araçlarım" +yaml = "evcc.yaml içerisinde yapılandırıldı. Kullanıcı arayüzünde düzenlenemez." + +[config.meter] +cancel = "İptal" +delete = "Sil" +save = "Kaydet" +template = "Üretici" +titleChoice = "Ne Eklemek İstersin:" +validateSave = "Doğrula ve kaydet" + +[config.pv] +titleAdd = "Helyograf Ekle" +titleEdit = "Helyografı Düzenle" + +[config.section] +general = "Genel" +system = "Sistem" + +[config.system] +logs = "Loglar" +restart = "Yeniden Başlat" +restartRequiredDescription = "Değişikliklerin yansıması için yeniden başlatma gereklidir." +restartRequiredMessage = "Yapılandırma ayarları değiştirildi." +restartingDescription = "Lütfen bekleyin…" +restartingMessage = "Evcc yeniden başlatılıyor…" + +[config.title] +description = "Ana ekranda ve tarayıcı sekmesinde görüntülenir." +label = "Başlık" +title = "Başlığı Düzenle" + +[config.validation] +failed = "başarısız" +label = "Durum" +running = "doğrulanıyor..." +success = "başarılı" +unknown = "bilinmiyor" +validate = "doğrula" + +[config.vehicle] +cancel = "İptal" +delete = "Sil" +generic = "Diğer entegrasyonlar" +offline = "Genel Araç" +online = "Çevrimiçi API'ye sahip araçlar" +save = "Kaydet" +scooter = "Scooter" +template = "Üretici" +titleAdd = "Araç Ekle" +titleEdit = "Aracı Düzenle" +validateSave = "Doğrula ve kaydet" + +[footer] + +[footer.community] +greenEnergy = "Güneş Enerjisi" +greenEnergySub1 = "evcc ile şarj edildi" +greenEnergySub2 = "Ekim 2022'den beri" +greenShare = "Güneş enerjisi paylaşımı" +greenShareSub1 = "tarafından sağlanan enerji" +greenShareSub2 = "güneş enerjisi ve batarya deposu" +power = "Şarj gücü" +powerSub1 = "{activeClients}/{totalClients} katılımcı" +powerSub2 = "şarj oluyor..." +tabTitle = "Canlı topluluk" + +[footer.savings] +co2Saved = "{value} kg CO₂ tasarruf edildi" +co2Title = "CO₂ Emisyonu" +configurePriceCo2 = "Fiyat ve CO₂ ayarlarını yapılandırmayı öğrenin." +footerLong = "{percent}% güneş enerjisi" +footerShort = "{percent}% güneş enerjisi" +modalTitle = "Tasarruf" +moneySaved = "{value} tasarruf edildi" +percentGrid = "{grid} kWh şebeke" +percentSelf = "{self} kWh kendi tüketimi" +percentTitle = "Güneş Enerjisi Kullanımı" +periodLabel = "Dönem" +priceTitle = "Fiyat" +referenceGrid = "Şebeke" +referenceLabel = "Referans" +tabTitle = "Tasarruf" + +[footer.savings.period] +30d = "son 30 gün" +365d = "son 365 gün" +total = "toplam" + +[footer.sponsor] +becomeSponsor = "Sponsor ol" +confetti = "Konfeti için hazır mısınız?" +confettiPromise = "Stickerlar ve dijital konfeti alırsınız." +sticker = "… ya da evcc stickerlarımız?" +supportUs = "Misyonumuz güneş enerjisini norm haline getirmektir. Evcc'ye değer verdiğiniz kadar ödeme yaparak yardımcı olun." +thanks = "Teşekkürler {sponsor}! Katkınız evcc'yi daha iyi yapmamıza yardımcı oluyor." +titleNoSponsor = "Bizi destekleyin" +titleSponsor = "Sponsorsunuz" + +[footer.telemetry] +optIn = "Telemetriye katıl" +optInMoreDetails = "Daha fazla bilgi {0}." +optInMoreDetailsLink = "buradan" +optInSponsorship = "Sponsorluk gerekli" + +[footer.version] +availableLong = "yeni versiyon mevcut" +modalCancel = "İptal" +modalDownload = "İndir" +modalInstalledVersion = "Yüklü versiyon" +modalNoReleaseNotes = "Yeni sürüm hakkında bilgi bulunamadı. Daha fazla bilgi:" +modalTitle = "Yeni sürüm mevcut" +modalUpdate = "Yükle" +modalUpdateNow = "Şimdi yükle" +modalUpdateStarted = "Yeni sürüm evcc başlatılıyor…" +modalUpdateStatusStart = "Yükleme başladı:" + +[header] +about = "Hakkında" +blog = "Blog" +docs = "Dökümanlar" +github = "GitHub" +login = "Araç Girişleri" +logout = "Çıkış" +nativeSettings = "Sunucu Değiştir" +needHelp = "Yardıma mı ihtiyacınız var?" +sessions = "Şarj Oturumları" + +[help] +discussionsButton = "GitHub tartışmaları" +documentationButton = "Dökümantasyon" +issueButton = "Hata bildir" +issueDescription = "Tuhaf veya yanlış bir durum mu buldunuz?" +logsButton = "Logları görüntüle" +logsDescription = "Hatalar için logları görüntüleyin." +modalTitle = "Yardıma mı ihtiyacınız var?" +primaryActions = "Bir şeyler beklediğiniz gibi çalışmıyor mu? Yardım alabileceğiniz iyi yerler bunlardır." +restartButton = "Yeniden Başlat" +restartDescription = "Kapatıp açmayı denediniz mi?" +secondaryActions = "Sorununuzu hala çözemediniz mi? İşte daha fazla müdahaleci seçenekler." + +[help.restart] +cancel = "İptal" +confirm = "Evet, yeniden başlat!" +description = "Normal koşullarda yeniden başlatma gerekli olmamalıdır. Eğer düzenli olarak evcc'yi yeniden başlatmanız gerekiyorsa, bir hata bildirimi yapmayı düşünün." +disclaimer = "Not: evcc sonlandırılacak ve işletim sistemi yeniden başlatma işlemini gerçekleştirecektir." +modalTitle = "Yeniden başlatmak istediğinizden emin misiniz?" + +[log] +areaLabel = "Bölgeye göre filtrele" +areas = "Tüm bölgeler" +download = "Bütün logları indir" +levelLabel = "Log seviyesine göre filtrele" +noResults = "Sonuç yok" +search = "Ara" +showAll = "Tümünü göster" +title = "Loglar" +update = "Otomatik güncelle" + +[loginModal] +cancel = "İptal" +error = "Giriş başarısız: " +invalid = "Hatalı giriş" +login = "Giriş yap" +password = "Şifre" +reset = "Şifreyi sıfırla?" +title = "Kimlik Doğrulama" + +[main] +vehicles = "Park" + +[main.chargingPlan] +active = "Aktif" +arrivalTab = "Varış" +day = "Gün" +departureTab = "Ayrılış" +goal = "Şarj hedefi" +modalTitle = "Şarj Planı" +none = "hiçbiri" +remove = "Kaldır" +time = "Zaman" +title = "Plan" +titleMinSoc = "Min şarj" +titleTargetCharge = "Ayrılış" +unsavedChanges = "Kaydedilmemiş değişiklikler var. Şimdi uygulansın mı?" +update = "Uygula" + +[main.energyflow] +battery = "Batarya" +batteryCharge = "Batarya şarj oluyor" +batteryDischarge = "Batarya deşarj oluyor" +batteryHold = "Batarya (kilitli)" +batteryTooltip = "{energy} / {total} ({soc})" +gridImport = "Şebeke kullanımı" +homePower = "Tüketim" +loadpoints = "Şarj Cihazı| Şarj Cihazı | {count} şarj cihazı" +noEnergy = "Ölçüm verisi yok" +pvExport = "Şebeke ihracatı" +pvProduction = "Üretim" +selfConsumption = "Kendi tüketimi" + +[main.heatingStatus] +charging = "Isıtılıyor…" +cheapEnergyCharging = "Ucuz enerjiyle ısıtılıyor: {price} (limit {limit})" +cleanEnergyCharging = "Temiz enerjiyle ısıtılıyor: {co2} (limit {limit})" +waitForVehicle = "Hazır. Isıtıcı bekleniyor…" + +[main.loadpoint] +avgPrice = "⌀ Fiyat" +charged = "Şarj edildi" +co2 = "⌀ CO₂" +duration = "Süre" +fallbackName = "Şarj noktası" +power = "Güç" +price = "Σ Fiyat" +remaining = "Kalan" +remoteDisabledHard = "{source}: kapatıldı" +remoteDisabledSoft = "{source}: uyumlu güneş enerjili şarj kapatıldı" +solar = "Güneş Enerjisi" + +[main.loadpointSettings] +currents = "Şarj Akımı" +default = "varsayılan" +disclaimerHint = "Not:" +onlyForSocBasedCharging = "Bu seçenekler, şarj seviyesi bilinen araçlar için kullanılabilir." +smartCostCheap = "Ucuz Şebeke Şarjı" +smartCostClean = "Temiz Şebeke Şarjı" +title = "Ayarlar {0}" +vehicle = "Araç" + +[main.loadpointSettings.limitSoc] +description = "Bu araç bağlandığında kullanılan şarj limiti." +label = "Varsayılan limit" + +[main.loadpointSettings.maxCurrent] +label = "Maks. Akım" + +[main.loadpointSettings.minCurrent] +label = "Min. Akım" + +[main.loadpointSettings.minSoc] +description = "Araç, güneş enerjisi modunda {0}% hızlı şarj edilir. Ardından güneş enerjisinin fazlasıyla devam eder. Kötü havalarda bile minimum bir menzil sağlamak için kullanışlıdır." +label = "Min. şarj %" + +[main.loadpointSettings.phasesConfigured] +label = "Fazlar" +no1p3pSupport = "Şarj cihazınız nasıl bağlı?" +phases_0 = "otomatik geçiş" +phases_1 = "1 faz" +phases_1_hint = "({min} - {max})" +phases_3 = "3 faz" +phases_3_hint = "({min} - {max})" + +[main.mode] +minpv = "Min+Güneş Enerjisi" +now = "Hızlı" +off = "Kapalı" +pv = "Güneş Enerjisi" + +[main.provider] +login = "giriş yap" +logout = "çıkış yap" + +[main.targetCharge] +activate = "Aktif Et" +co2Limit = "{co2} CO₂ sınırı" +costLimitIgnore = "Bu dönemde yapılandırılan {limit} yoksayılacak." +currentPlan = "Aktif plan" +descriptionEnergy = "{targetEnergy} ne zaman araca yüklenmelidir?" +descriptionSoc = "Araç {targetSoc}% şarj edilmeli mi?" +inactiveLabel = "Hedef zamanı" +notReachableInTime = "Hedef zamanında ulaşılamaz. Tahmini bitiş: {endTime}." +onlyInPvMode = "Şarj planı sadece güneş enerjisi modunda çalışır." +planDuration = "Şarj süresi" +planPeriodLabel = "Dönem" +planPeriodValue = "{start} - {end}" +planUnknown = "henüz bilinmiyor" +preview = "Planı Önizle" +priceLimit = "{price} fiyat sınırı" +remove = "Kaldır" +setTargetTime = "yok" +targetIsAboveLimit = "Bu dönemde yapılandırılan {limit} şarj sınırı yoksayılacak." +targetIsAboveVehicleLimit = "Şarj hedefine ulaşmak için araç limitini ({limit}) artırın." +targetIsInThePast = "Gelecekte bir zaman seçin, Marty." +targetIsTooFarInTheFuture = "Daha fazla bilgi edindiğimizde planı ayarlayacağız." +title = "Hedef Zamanı" +today = "bugün" +tomorrow = "yarın" +update = "Güncelle" +vehicleCapacityDocs = "Nasıl yapılandırılacağını öğrenin." +vehicleCapacityRequired = "Şarj süresini tahmin etmek için araç batarya kapasitesi gereklidir." + +[main.targetChargePlan] +chargeDuration = "Şarj süresi" +co2Label = "⌀ CO₂ emisyonu" +priceLabel = "Enerji fiyatı" +timeRange = "{day} {range} saat" +unknownPrice = "henüz bilinmiyor" + +[main.targetEnergy] +label = "Sınır" +noLimit = "yok" + +[main.vehicle] +addVehicle = "Araç Ekle" +changeVehicle = "Araç Değiştir" +detectionActive = "Araç algılanıyor…" +fallbackName = "Araç" +moreActions = "Daha Fazla İşlem" +none = "Araç Yok" +notReachable = "Araç ulaşılamadı. Evcc'yi yeniden başlatmayı deneyin." +targetSoc = "Sınır" +temp = "Sıcaklık" +tempLimit = "Sıcaklık sınırı" +unknown = "Misafir araç" +vehicleSoc = "Şarj" + +[main.vehicleSoc] +charging = "şarj oluyor" +connected = "bağlı" +disconnected = "bağlantı kesildi" +ready = "hazır" +vehicleTarget = "Araç sınırı: {soc}%" + +[main.vehicleStatus] +charging = "Şarj oluyor…" +cheapEnergyCharging = "Ucuz enerjiyle şarj oluyor: {price} (limit {limit})" +cleanEnergyCharging = "Temiz enerjiyle şarj oluyor: {co2} (limit {limit})" +climating = "Ön koşullandırma algılandı." +connected = "Bağlı." +disconnected = "Bağlantı kesildi." +minCharge = "Minimum şarj {soc}%." +pvDisable = "Yeterli fazla enerji yok. {remaining} içinde duraklatılıyor…" +pvEnable = "Fazla enerji mevcut. {remaining} içinde başlatılıyor…" +scale1p = "{remaining} içinde 1 fazlı şarja geçiliyor…" +scale3p = "{remaining} içinde 3 fazlı şarja geçiliyor…" +targetChargeActive = "Hedef şarj aktif…" +targetChargePlanned = "Hedef şarj {time} başlıyor." +targetChargeWaitForVehicle = "Hedef şarj hazır. Araç bekleniyor…" +unknown = "" +vehicleTargetReached = "Araç sınırı {soc}% ulaşıldı." +waitForVehicle = "Hazır. Araç bekleniyor…" + +[notifications] +dismissAll = "Tümünü Kapat" +logs = "Bütün logları görüntüle" +modalTitle = "Bildirimler" + +[offline] +message = "Bir sunucuya bağlı değil." +reload = "Tekrar yükle?" + +[passwordModal] +description = "Yapılandırma ayarlarını korumak için bir parola belirleyin. Ana ekranı kullanmak oturum açmadan da mümkündür." +empty = "Şifre boş olamaz." +error = "Hata: " +labelCurrent = "Mevcut şifre" +labelNew = "Yeni şifre" +labelRepeat = "Yeni şifre (tekrar)" +newPassword = "Şifre oluştur" +noMatch = "Şifreler eşleşmiyor." +titleNew = "Admin Şifresi Oluştur" +titleUpdate = "Admin Şifresini Güncelle" +updatePassword = "Şifreyi güncelle" + +[session] +cancel = "İptal" +co2 = "CO₂" +date = "Dönem" +delete = "Sil" +finished = "Tamamlandı" +meter = "Sayaç" +meterstart = "Sayaç başlangıcı" +meterstop = "Sayaç bitişi" +odometer = "Kilometre" +price = "Fiyat" +started = "Başladı" +title = "Şarj Oturumu" + +[sessions] +avgPower = "⌀ Güç" +avgPrice = "⌀ Fiyat" +chargeDuration = "Süre" +co2 = "⌀ CO₂" +csvMonth = "{month} CSV olarak indir" +csvTotal = "Toplam CSV olarak indir" +date = "Başlangıç" +downloadCsv = "CSV olarak indir" +energy = "Şarj edilen" +loadpoint = "Şarj Noktası" +noData = "Bu ay şarj oturumu yok." +price = "Σ Fiyat" +reallyDelete = "Bu oturumu gerçekten silmek istiyor musunuz?" +solar = "Güneş Enerjisi" +title = "Şarj Oturumları" +total = "Toplam" +vehicle = "Araç" + +[sessions.csv] +chargedenergy = "Enerji (kWh)" +created = "Oluşturuldu" +finished = "Tamamlandı" +identifier = "Kimlik" +loadpoint = "Şarj Noktası" +meterstart = "Sayaç başlangıcı (kWh)" +meterstop = "Sayaç bitişi (kWh)" +odometer = "Kilometre (km)" +vehicle = "Araç" + +[sessions.filter] +allLoadpoints = "tüm şarj noktaları" +allVehicles = "tüm araçlar" +filter = "Filtrele" + +[settings] +title = "Genel Ayarlar" + +[settings.fullscreen] +enter = "Tam ekrana geç" +exit = "Tam ekrandan çık" +label = "Tam ekran" + +[settings.hiddenFeatures] +label = "Deneysel" +value = "Deneysel kullanıcı arayüzü özelliklerini göster." + +[settings.language] +auto = "Otomatik" +label = "Dil" + +[settings.sponsorToken] +expires = "Sponsor jetonunuz {inXDays} süre sonra sona erer. {getNewToken} ve yapılandırma dosyanızı güncelleyin." +getNew = "Yeni bir tane alın" +hint = "Not: Bunun otomatik hale getireceğiz." + +[settings.telemetry] +label = "Telemetri" + +[settings.theme] +auto = "sistem" +dark = "koyu" +label = "Tasarım" +light = "açık" + +[settings.unit] +km = "km" +label = "Birimler" +mi = "mil" + +[smartCost] +activeHours = "{total} saat içinde {charging}" +activeHoursLabel = "Aktif saatler" +applyToAll = "Heryerde uygula?" +batteryDescription = "Ev bataryasını şebeke enerjisiyle şarj eder." +cheapTitle = "Ucuz Şebeke Şarjı" +cleanTitle = "Temiz Şebeke Şarjı" +co2Label = "CO₂ emisyonu" +co2Limit = "CO₂ sınırı" +loadpointDescription = "Güneş modunda geçici hızlı şarjı etkinleştirir." +modalTitle = "Akıllı Şebeke Şarjı" +none = "hiçbiri" +priceLabel = "Enerji fiyatı" +priceLimit = "Fiyat sınırı" +saved = "Kaydedildi." + +[startupError] +configFile = "Kullanılan yapılandırma dosyası:" +configuration = "Yapılandırma" +description = "Lütfen yapılandırma dosyanızı kontrol edin. Hata mesajı yardımcı olmazsa, {0}'e göz atın." +discussions = "GitHub Tartışmaları" +fixAndRestart = "Lütfen sorunu düzeltin ve sunucuyu yeniden başlatın." +hint = "Not: Ayrıca hatalı bir cihazınızın (inverter, sayaç, …) olabileceği de olabilir. Ağ bağlantılarınızı kontrol edin." +lineError = "{0} içinde hata." +lineErrorLink = "{0}. satır" +restartButton = "Yeniden Başlat" +title = "Başlangıç Hatası" From f4f7a90f3e95baa08d92d86987998668c7f8b7c4 Mon Sep 17 00:00:00 2001 From: andig Date: Sat, 4 May 2024 09:31:25 +0200 Subject: [PATCH 030/168] Guard against expiring tokens due to wrong database (#13693) --- cmd/charger.go | 2 +- cmd/charger_ramp.go | 2 +- cmd/check_config.go | 2 +- cmd/device.go | 2 +- cmd/discuss.go | 2 +- cmd/dump.go | 2 +- cmd/flags.go | 3 +++ cmd/meter.go | 2 +- cmd/password_reset.go | 2 +- cmd/password_set.go | 2 +- cmd/root.go | 10 ++++++---- cmd/settings-get.go | 2 +- cmd/settings-set.go | 2 +- cmd/setup.go | 32 +++++++++++++++++++++++++++++++- cmd/tariff.go | 2 +- cmd/token.go | 2 +- cmd/vehicle.go | 2 +- 17 files changed, 54 insertions(+), 19 deletions(-) diff --git a/cmd/charger.go b/cmd/charger.go index e9f5e6cf77..6ba7cf8917 100644 --- a/cmd/charger.go +++ b/cmd/charger.go @@ -31,7 +31,7 @@ func init() { func runCharger(cmd *cobra.Command, args []string) { // load config - if err := loadConfigFile(&conf); err != nil { + if err := loadConfigFile(&conf, !cmd.Flag(flagIgnoreDatabase).Changed); err != nil { log.FATAL.Fatal(err) } diff --git a/cmd/charger_ramp.go b/cmd/charger_ramp.go index abc759961f..70f52c9f26 100644 --- a/cmd/charger_ramp.go +++ b/cmd/charger_ramp.go @@ -66,7 +66,7 @@ func ramp(c api.Charger, digits int, delay time.Duration) { func runChargerRamp(cmd *cobra.Command, args []string) { // load config - if err := loadConfigFile(&conf); err != nil { + if err := loadConfigFile(&conf, !cmd.Flag(flagIgnoreDatabase).Changed); err != nil { log.FATAL.Fatal(err) } diff --git a/cmd/check_config.go b/cmd/check_config.go index da265bb22b..6ba015ad14 100644 --- a/cmd/check_config.go +++ b/cmd/check_config.go @@ -19,7 +19,7 @@ func init() { } func runConfigCheck(cmd *cobra.Command, args []string) { - err := loadConfigFile(&conf) + err := loadConfigFile(&conf, !cmd.Flag(flagIgnoreDatabase).Changed) if err != nil { log.FATAL.Println("config invalid:", err) diff --git a/cmd/device.go b/cmd/device.go index da5be89c24..1ece39b7d6 100644 --- a/cmd/device.go +++ b/cmd/device.go @@ -24,7 +24,7 @@ func init() { func runDevice(cmd *cobra.Command, args []string) { // load config - if err := loadConfigFile(&conf); err != nil { + if err := loadConfigFile(&conf, !cmd.Flag(flagIgnoreDatabase).Changed); err != nil { log.FATAL.Fatal(err) } diff --git a/cmd/discuss.go b/cmd/discuss.go index 58132f203e..4cacb7a509 100644 --- a/cmd/discuss.go +++ b/cmd/discuss.go @@ -35,7 +35,7 @@ func errorString(err error) string { } func runDiscuss(cmd *cobra.Command, args []string) { - cfgErr := loadConfigFile(&conf) + cfgErr := loadConfigFile(&conf, !cmd.Flag(flagIgnoreDatabase).Changed) file, pathErr := filepath.Abs(cfgFile) if pathErr != nil { diff --git a/cmd/dump.go b/cmd/dump.go index 01ed3a60b8..d346e54835 100644 --- a/cmd/dump.go +++ b/cmd/dump.go @@ -45,7 +45,7 @@ func handle[T any](name string, h config.Handler[T]) config.Device[T] { func runDump(cmd *cobra.Command, args []string) { // load config - err := loadConfigFile(&conf) + err := loadConfigFile(&conf, !cmd.Flag(flagIgnoreDatabase).Changed) // setup environment if err == nil { diff --git a/cmd/flags.go b/cmd/flags.go index 790d5d9854..bc3e470675 100644 --- a/cmd/flags.go +++ b/cmd/flags.go @@ -9,6 +9,9 @@ const ( flagHeaders = "log-headers" flagHeadersDescription = "Log headers" + flagIgnoreDatabase = "ignore-db" + flagIgnoreDatabaseDescription = "Run command ignoring service database" + flagBatteryMode = "battery-mode" flagBatteryModeDescription = "Set battery mode (normal, hold, charge)" flagBatteryModeWait = "battery-mode-wait" diff --git a/cmd/meter.go b/cmd/meter.go index d0ea6713c9..bc789815a3 100644 --- a/cmd/meter.go +++ b/cmd/meter.go @@ -25,7 +25,7 @@ func init() { func runMeter(cmd *cobra.Command, args []string) { // load config - if err := loadConfigFile(&conf); err != nil { + if err := loadConfigFile(&conf, !cmd.Flag(flagIgnoreDatabase).Changed); err != nil { log.FATAL.Fatal(err) } diff --git a/cmd/password_reset.go b/cmd/password_reset.go index 78a6d205e4..9f9b352f4d 100644 --- a/cmd/password_reset.go +++ b/cmd/password_reset.go @@ -19,7 +19,7 @@ func init() { func runPasswordReset(cmd *cobra.Command, args []string) { // load config - if err := loadConfigFile(&conf); err != nil { + if err := loadConfigFile(&conf, !cmd.Flag(flagIgnoreDatabase).Changed); err != nil { log.FATAL.Fatal(err) } diff --git a/cmd/password_set.go b/cmd/password_set.go index 1ee504a5ca..5775941e0c 100644 --- a/cmd/password_set.go +++ b/cmd/password_set.go @@ -19,7 +19,7 @@ func init() { func runPasswordSet(cmd *cobra.Command, args []string) { // load config - if err := loadConfigFile(&conf); err != nil { + if err := loadConfigFile(&conf, !cmd.Flag(flagIgnoreDatabase).Changed); err != nil { log.FATAL.Fatal(err) } diff --git a/cmd/root.go b/cmd/root.go index 9654f2fd43..229a3d15d5 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -29,7 +29,10 @@ import ( "github.com/spf13/viper" ) -const rebootDelay = 5 * time.Minute // delayed reboot on error +const ( + rebootDelay = 5 * time.Minute // delayed reboot on error + serviceDB = "/var/lib/evcc/evcc.db" +) var ( log = util.NewLogger("main") @@ -53,10 +56,9 @@ func init() { // global options rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "Config file (default \"~/evcc.yaml\" or \"/etc/evcc.yaml\")") - rootCmd.PersistentFlags().BoolP("help", "h", false, "Help") - rootCmd.PersistentFlags().Bool(flagHeaders, false, flagHeadersDescription) + rootCmd.PersistentFlags().Bool(flagIgnoreDatabase, false, flagIgnoreDatabaseDescription) // config file options rootCmd.PersistentFlags().StringP("log", "l", "info", "Log level (fatal, error, warn, info, debug, trace)") @@ -107,7 +109,7 @@ func Execute() { func runRoot(cmd *cobra.Command, args []string) { // load config and re-configure logging after reading config file var err error - if cfgErr := loadConfigFile(&conf); errors.As(cfgErr, &viper.ConfigFileNotFoundError{}) { + if cfgErr := loadConfigFile(&conf, !cmd.Flag(flagIgnoreDatabase).Changed); errors.As(cfgErr, &viper.ConfigFileNotFoundError{}) { log.INFO.Println("missing config file - switching into demo mode") if err := demoConfig(&conf); err != nil { log.FATAL.Fatal(err) diff --git a/cmd/settings-get.go b/cmd/settings-get.go index 12f4ecc881..59f0174733 100644 --- a/cmd/settings-get.go +++ b/cmd/settings-get.go @@ -24,7 +24,7 @@ func init() { func runSettingsGet(cmd *cobra.Command, args []string) { // load config - if err := loadConfigFile(&conf); err != nil { + if err := loadConfigFile(&conf, !cmd.Flag(flagIgnoreDatabase).Changed); err != nil { log.FATAL.Fatal(err) } diff --git a/cmd/settings-set.go b/cmd/settings-set.go index cfdbabe2c0..23d5147ae7 100644 --- a/cmd/settings-set.go +++ b/cmd/settings-set.go @@ -23,7 +23,7 @@ func init() { func runSettingsSet(cmd *cobra.Command, args []string) { // load config - if err := loadConfigFile(&conf); err != nil { + if err := loadConfigFile(&conf, !cmd.Flag(flagIgnoreDatabase).Changed); err != nil { log.FATAL.Fatal(err) } diff --git a/cmd/setup.go b/cmd/setup.go index 0fa77e7624..66c472ba61 100644 --- a/cmd/setup.go +++ b/cmd/setup.go @@ -7,6 +7,7 @@ import ( "fmt" "net" "net/http" + "os" "regexp" "slices" "strconv" @@ -162,7 +163,23 @@ func nameValid(name string) error { return nil } -func loadConfigFile(conf *globalConfig) error { +func tokenDanger(conf []config.Named) bool { + problematic := []string{"tesla", "psa", "opel", "citroen", "ds", "peugeot"} + + for _, cc := range conf { + if slices.Contains(problematic, cc.Type) { + return true + } + template, ok := cc.Other["template"].(string) + if ok && cc.Type == "template" && slices.Contains(problematic, template) { + return true + } + } + + return false +} + +func loadConfigFile(conf *globalConfig, checkDB bool) error { err := viper.ReadInConfig() if cfgFile = viper.ConfigFileUsed(); cfgFile == "" { @@ -177,6 +194,19 @@ func loadConfigFile(conf *globalConfig) error { } } + // check service database + if _, err := os.Stat(serviceDB); err == nil && checkDB && conf.Database.Dsn != serviceDB && tokenDanger(conf.Vehicles) { + log.FATAL.Fatal(` + +Found systemd service database at "` + serviceDB + `", evcc has been invoked with database "` + conf.Database.Dsn + `". +Running evcc with vehicles configured in evcc.yaml may lead to expiring the yaml configuration's vehicle tokens. +This is due to the fact, that the token refresh will be saved to the local instead of the service's database. +If you have vehicles with touchy tokens like PSA or Tesla, make sure to remove vehicle configuration from the yaml file. + +If you know what you're doing, you can run evcc ignoring the service database with the --ignore-db flag. +`) + } + // parse log levels after reading config if err == nil { parseLogLevels() diff --git a/cmd/tariff.go b/cmd/tariff.go index b5cc3a3eb3..25ed72c16e 100644 --- a/cmd/tariff.go +++ b/cmd/tariff.go @@ -24,7 +24,7 @@ func init() { func runTariff(cmd *cobra.Command, args []string) { // load config - if err := loadConfigFile(&conf); err != nil { + if err := loadConfigFile(&conf, !cmd.Flag(flagIgnoreDatabase).Changed); err != nil { fatal(err) } diff --git a/cmd/token.go b/cmd/token.go index 05e785a0ab..752dc28d8f 100644 --- a/cmd/token.go +++ b/cmd/token.go @@ -24,7 +24,7 @@ func init() { func runToken(cmd *cobra.Command, args []string) { // load config - if err := loadConfigFile(&conf); err != nil { + if err := loadConfigFile(&conf, !cmd.Flag(flagIgnoreDatabase).Changed); err != nil { log.FATAL.Fatal(err) } diff --git a/cmd/vehicle.go b/cmd/vehicle.go index a052f66ee2..8ecbfde072 100644 --- a/cmd/vehicle.go +++ b/cmd/vehicle.go @@ -28,7 +28,7 @@ func init() { func runVehicle(cmd *cobra.Command, args []string) { // load config - if err := loadConfigFile(&conf); err != nil { + if err := loadConfigFile(&conf, !cmd.Flag(flagIgnoreDatabase).Changed); err != nil { fatal(err) } From 577a50ed5dc3d31bf78284cb3a0ca8f7196b823a Mon Sep 17 00:00:00 2001 From: andig Date: Sat, 4 May 2024 10:05:00 +0200 Subject: [PATCH 031/168] chore: fix npe --- tariff/helper.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tariff/helper.go b/tariff/helper.go index 0a8525f4fe..75f93b78df 100644 --- a/tariff/helper.go +++ b/tariff/helper.go @@ -24,7 +24,7 @@ func backoffPermanentError(err error) error { return backoff.Permanent(se) } } - if strings.HasPrefix(err.Error(), "jq: query failed") { + if err != nil && strings.HasPrefix(err.Error(), "jq: query failed") { return backoff.Permanent(err) } return err From 64e45488e853fe188ccba933c32089a65497a83d Mon Sep 17 00:00:00 2001 From: andig Date: Sat, 4 May 2024 10:06:16 +0200 Subject: [PATCH 032/168] Fix cannot save guest vehicle --- server/http_session_handler.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/http_session_handler.go b/server/http_session_handler.go index 6d95e56392..2f6190a9b9 100644 --- a/server/http_session_handler.go +++ b/server/http_session_handler.go @@ -129,7 +129,8 @@ func updateSessionHandler(w http.ResponseWriter, r *http.Request) { return } - if txn := db.Instance.Table("sessions").Where("id = ?", id).Updates(&session); txn.Error != nil { + // https://github.com/evcc-io/evcc/issues/13738#issuecomment-2094070362 + if txn := db.Instance.Table("sessions").Where("id = ?", id).Select("vehicle").Updates(&session); txn.Error != nil { jsonError(w, http.StatusBadRequest, txn.Error) return } From 8327ddbb2188a780fa67c990c2ab5017d94dc5d5 Mon Sep 17 00:00:00 2001 From: andig Date: Sat, 4 May 2024 10:13:31 +0200 Subject: [PATCH 033/168] Renault: fix odometer not available --- vehicle/renault/kamereon/api.go | 2 +- vehicle/renault/kamereon/types.go | 2 +- vehicle/renault/provider.go | 9 ++++++--- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/vehicle/renault/kamereon/api.go b/vehicle/renault/kamereon/api.go index 226698f6af..a7fdef270e 100644 --- a/vehicle/renault/kamereon/api.go +++ b/vehicle/renault/kamereon/api.go @@ -118,7 +118,7 @@ func (v *API) Hvac(accountID string, vin string) (Response, error) { // Cockpit provides cockpit api response func (v *API) Cockpit(accountID string, vin string) (Response, error) { - uri := fmt.Sprintf("%s/commerce/v1/accounts/%s/kamereon/kca/car-adapter/v2/cars/%s/cockpit", v.keys.Target, accountID, vin) + uri := fmt.Sprintf("%s/commerce/v1/accounts/%s/kamereon/kca/car-adapter/v1/cars/%s/cockpit", v.keys.Target, accountID, vin) return v.request(uri, nil) } diff --git a/vehicle/renault/kamereon/types.go b/vehicle/renault/kamereon/types.go index 9d6b64b80c..59762efc8a 100644 --- a/vehicle/renault/kamereon/types.go +++ b/vehicle/renault/kamereon/types.go @@ -64,7 +64,7 @@ type attributes struct { ExternalTemperature float64 `json:"externalTemperature"` HvacStatus string `json:"hvacStatus"` // cockpit - TotalMileage float64 `json:"totalMileage"` + TotalMileage *float64 `json:"totalMileage"` // position Latitude float64 `json:"gpsLatitude"` Longitude float64 `json:"gpsLongitude"` diff --git a/vehicle/renault/provider.go b/vehicle/renault/provider.go index 45fd7e43cd..ad0a42682e 100644 --- a/vehicle/renault/provider.go +++ b/vehicle/renault/provider.go @@ -103,12 +103,15 @@ var _ api.VehicleOdometer = (*Provider)(nil) // Odometer implements the api.VehicleOdometer interface func (v *Provider) Odometer() (float64, error) { res, err := v.cockpitG() + if err != nil { + return 0, err + } - if err == nil { - return res.Data.Attributes.TotalMileage, nil + if res.Data.Attributes.TotalMileage != nil { + return *res.Data.Attributes.TotalMileage, nil } - return 0, err + return 0, api.ErrNotAvailable } var _ api.VehicleFinishTimer = (*Provider)(nil) From 3b85f16cf948433000c867d9426a96bc97a7e34f Mon Sep 17 00:00:00 2001 From: andig Date: Sat, 4 May 2024 10:45:09 +0200 Subject: [PATCH 034/168] E3dc: re-add capacity parameter (#13740) --- meter/e3dc.go | 72 ++--------------------- templates/definition/meter/e3dc-rscp.yaml | 9 ++- 2 files changed, 12 insertions(+), 69 deletions(-) diff --git a/meter/e3dc.go b/meter/e3dc.go index 016cf453f9..f76d6940c1 100644 --- a/meter/e3dc.go +++ b/meter/e3dc.go @@ -3,7 +3,6 @@ package meter import ( "errors" "net" - "slices" "strconv" "sync" "time" @@ -19,7 +18,6 @@ import ( ) type E3dc struct { - capacity float64 dischargeLimit uint32 usage templates.Usage // TODO check if we really want to depend on templates conn *rscp.Client @@ -33,12 +31,13 @@ func init() { func NewE3dcFromConfig(other map[string]interface{}) (api.Meter, error) { cc := struct { + capacity `mapstructure:",squash"` Usage templates.Usage Uri string User string Password string Key string - Battery uint16 // battery id + _ uint16 `mapstructure:"battery"` // deprecated DischargeLimit uint32 Timeout time.Duration }{ @@ -67,12 +66,12 @@ func NewE3dcFromConfig(other map[string]interface{}) (api.Meter, error) { ReceiveTimeout: cc.Timeout, } - return NewE3dc(cfg, cc.Usage, cc.Battery, cc.DischargeLimit) + return NewE3dc(cfg, cc.Usage, cc.DischargeLimit, cc.capacity.Decorator()) } var e3dcOnce sync.Once -func NewE3dc(cfg rscp.ClientConfig, usage templates.Usage, batteryId uint16, dischargeLimit uint32) (api.Meter, error) { +func NewE3dc(cfg rscp.ClientConfig, usage templates.Usage, dischargeLimit uint32, capacity func() float64) (api.Meter, error) { e3dcOnce.Do(func() { log := util.NewLogger("e3dc") rscp.Log.SetLevel(logrus.DebugLevel) @@ -92,51 +91,15 @@ func NewE3dc(cfg rscp.ClientConfig, usage templates.Usage, batteryId uint16, dis // decorate api.BatterySoc var ( - batterySoc func() (float64, error) batteryCapacity func() float64 + batterySoc func() (float64, error) batteryMode func(api.BatteryMode) error ) if usage == templates.UsageBattery { + batteryCapacity = capacity batterySoc = m.batterySoc - batteryCapacity = m.batteryCapacity batteryMode = m.setBatteryMode - - res, err := m.conn.Send(rscp.Message{ - Tag: rscp.BAT_REQ_DATA, - DataType: rscp.Container, - Value: []rscp.Message{ - { - Tag: rscp.BAT_INDEX, - DataType: rscp.UInt16, - Value: batteryId, - }, - { - Tag: rscp.BAT_REQ_SPECIFICATION, - DataType: rscp.None, - }, - }, - }) - if err != nil { - return nil, err - } - - batSpec, err := rscpContains(res, rscp.BAT_SPECIFICATION) - if err != nil { - return nil, err - } - - batCap, err := rscpContains(&batSpec, rscp.BAT_SPECIFIED_CAPACITY) - if err != nil { - return nil, err - } - - cap, err := rscpValue(batCap, cast.ToFloat64E) - if err != nil { - return nil, err - } - - m.capacity = cap / 1e3 } return decorateE3dc(m, batteryCapacity, batterySoc, batteryMode), nil @@ -184,10 +147,6 @@ func (m *E3dc) CurrentPower() (float64, error) { } } -func (m *E3dc) batteryCapacity() float64 { - return m.capacity -} - func (m *E3dc) batterySoc() (float64, error) { res, err := m.conn.Send(*rscp.NewMessage(rscp.EMS_REQ_BAT_SOC, nil)) if err != nil { @@ -257,25 +216,6 @@ func rscpError(msg ...rscp.Message) error { return errors.Join(errs...) } -func rscpContains(msg *rscp.Message, tag rscp.Tag) (rscp.Message, error) { - var zero rscp.Message - - slice, ok := msg.Value.([]rscp.Message) - if !ok { - return zero, errors.New("not a slice looking for " + tag.String()) - } - - idx := slices.IndexFunc(slice, func(m rscp.Message) bool { - return m.Tag == tag - }) - if idx < 0 { - return zero, errors.New("missing " + tag.String()) - } - - res := slice[idx] - return res, rscpError(res) -} - func rscpValue[T any](msg rscp.Message, fun func(any) (T, error)) (T, error) { var zero T if err := rscpError(msg); err != nil { diff --git a/templates/definition/meter/e3dc-rscp.yaml b/templates/definition/meter/e3dc-rscp.yaml index b6d68f8a24..858500d35c 100644 --- a/templates/definition/meter/e3dc-rscp.yaml +++ b/templates/definition/meter/e3dc-rscp.yaml @@ -23,8 +23,7 @@ params: - name: password - name: key - name: battery - type: number - advanced: true + deprecated: true - name: dischargelimit description: de: Entladelimit in W @@ -34,6 +33,8 @@ params: en: Limits discharge power in 'Hold' battery mode type: number advanced: true + - name: capacity + advanced: true render: | type: e3dc-rscp usage: {{ .usage }} @@ -41,5 +42,7 @@ render: | user: {{ .user }} password: {{ .password }} key: {{ .key }} - battery: {{ .battery }} + {{- if eq .usage "battery" }} dischargelimit: {{ .dischargelimit }} + capacity: {{ .capacity }} # kWh + {{- end }} From 4109b4bf74b8cfe7670df0492a71b01ecf1f6aea Mon Sep 17 00:00:00 2001 From: premultiply <4681172+premultiply@users.noreply.github.com> Date: Sat, 4 May 2024 11:00:17 +0200 Subject: [PATCH 035/168] Fix Sungrow charger (#13727) --- charger/sungrow.go | 90 ++++++++++++++++------- templates/definition/charger/sungrow.yaml | 5 +- 2 files changed, 65 insertions(+), 30 deletions(-) diff --git a/charger/sungrow.go b/charger/sungrow.go index 61e43dd13f..d7fe59cf68 100644 --- a/charger/sungrow.go +++ b/charger/sungrow.go @@ -32,27 +32,28 @@ import ( type Sungrow struct { log *util.Logger conn *modbus.Connection + curr uint16 } const ( // input (read only) - sgRegPhase = 21224 // uint16 [1: Single-phase, 3: Three-phase] - sgRegWorkMode = 21262 // uint16 [0: Network, 2: Plug&Play, 6: EMS] - sgRegRemCtrlStatus = 21267 // uint16 - sgRegPhasesState = 21269 // uint16 - sgRegTotalEnergy = 21299 // uint32s 1Wh - sgRegActivePower = 21307 // uint32s 1W - sgRegChargedEnergy = 21309 // uint32s 1Wh - sgRegStartMode = 21313 // uint16 [1: Started by EMS, 2: Started by swiping card] - sgRegPowerRequest = 21314 // uint16 [0: Enable, 1: Close] - sgRegPowerFlag = 21315 // uint16 [0: Charging or power regulation is not allowed; 1: Charging or power regulation is allowed] - sgRegState = 21316 // uint16 + sgRegPhase = 21224 // uint16 [1: Single-phase, 3: Three-phase] + sgRegWorkMode = 21262 // uint16 [0: Network, 2: Plug&Play, 6: EMS] + sgRegRemCtrlStatus = 21267 // uint16 + sgRegPhaseSwitchStatus = 21269 // uint16 + sgRegTotalEnergy = 21299 // uint32s 1Wh + sgRegActivePower = 21307 // uint32s 1W + sgRegChargedEnergy = 21309 // uint32s 1Wh + sgRegStartMode = 21313 // uint16 [1: Started by EMS, 2: Started by swiping card] + sgRegPowerRequest = 21314 // uint16 [0: Enable, 1: Close] + sgRegPowerFlag = 21315 // uint16 [0: Charging or power regulation is not allowed; 1: Charging or power regulation is allowed] + sgRegState = 21316 // uint16 // holding sgRegSetOutI = 21202 // uint16 0.01A sgRegPhaseSwitch = 21203 // uint16 sgRegUnavailable = 21210 // uint16 - sgRegRemoteControl = 21211 // uint16 + sgRegRemoteControl = 21211 // uint16 [0: Start, 1: Stop] ) var ( @@ -94,6 +95,7 @@ func NewSungrow(uri, device, comset string, baudrate int, proto modbus.Protocol, wb := &Sungrow{ log: log, conn: conn, + curr: 60, } return wb, err @@ -122,20 +124,20 @@ func (wb *Sungrow) Status() (api.ChargeStatus, error) { } switch s := binary.BigEndian.Uint16(b); s { - case 1: // "Idle" + case 1: // Idle return api.StatusA, nil case - 2, // "Standby" - 4, // "SuspendedEVSE" - 5, // "SuspendedEV" - 6: // "Completed" + 2, // Standby + 4, // SuspendedEVSE + 5, // SuspendedEV + 6: // Completed return api.StatusB, nil - case 3: // "Charging" + case 3: // Charging return api.StatusC, nil case - 7, // "Reserved" - 8, // "Disabled" - 9: // "Faulted" + 7, // Reserved + 8, // Disabled + 9: // Faulted return api.StatusF, nil default: return api.StatusNone, fmt.Errorf("invalid status: %d", s) @@ -144,12 +146,12 @@ func (wb *Sungrow) Status() (api.ChargeStatus, error) { // Enabled implements the api.Charger interface func (wb *Sungrow) Enabled() (bool, error) { - b, err := wb.conn.ReadInputRegisters(sgRegRemCtrlStatus, 1) + b, err := wb.conn.ReadHoldingRegisters(sgRegSetOutI, 1) if err != nil { return false, err } - return binary.BigEndian.Uint16(b) == 1, nil + return binary.BigEndian.Uint16(b) != 0, nil } // Enable implements the api.Charger interface @@ -161,6 +163,10 @@ func (wb *Sungrow) Enable(enable bool) error { _, err := wb.conn.WriteSingleRegister(sgRegRemoteControl, u) + if err == nil && enable { + _, err = wb.conn.WriteSingleRegister(sgRegSetOutI, wb.curr) + } + return err } @@ -177,7 +183,12 @@ func (wb *Sungrow) MaxCurrentMillis(current float64) error { return fmt.Errorf("invalid current %.1f", current) } - _, err := wb.conn.WriteSingleRegister(sgRegSetOutI, uint16(current*10)) + curr := uint16(10 * current) + + _, err := wb.conn.WriteSingleRegister(sgRegSetOutI, curr) + if err == nil { + wb.curr = curr + } return err } @@ -242,11 +253,38 @@ func (wb *Sungrow) Phases1p3p(phases int) error { u = 1 } - _, err := wb.conn.WriteSingleRegister(sgRegPhaseSwitch, u) + enabled, err := wb.Enabled() + if err == nil && enabled { + if err = wb.Enable(false); err != nil { + return err + } + } + + _, err = wb.conn.WriteSingleRegister(sgRegPhaseSwitch, u) + + if err == nil && enabled { + err = wb.Enable(true) + } return err } +var _ api.PhaseGetter = (*Sungrow)(nil) + +// GetPhases implements the api.PhaseGetter interface +func (wb *Sungrow) GetPhases() (int, error) { + b, err := wb.conn.ReadInputRegisters(sgRegPhaseSwitchStatus, 1) + if err != nil { + return 0, err + } + + if binary.BigEndian.Uint16(b) == 0 { + return 3, nil + } + + return 1, nil +} + var _ api.Diagnosis = (*Sungrow)(nil) // Diagnose implements the api.Diagnosis interface @@ -269,7 +307,7 @@ func (wb *Sungrow) Diagnose() { if b, err := wb.conn.ReadInputRegisters(sgRegPhase, 1); err == nil { fmt.Printf("\tPhase:\t%d\n", binary.BigEndian.Uint16(b)) } - if b, err := wb.conn.ReadInputRegisters(sgRegPhasesState, 1); err == nil { + if b, err := wb.conn.ReadInputRegisters(sgRegPhaseSwitchStatus, 1); err == nil { fmt.Printf("\tPhasesState:\t%d\n", binary.BigEndian.Uint16(b)) } if b, err := wb.conn.ReadInputRegisters(sgRegStartMode, 1); err == nil { diff --git a/templates/definition/charger/sungrow.yaml b/templates/definition/charger/sungrow.yaml index caced14287..dbe0fa4500 100644 --- a/templates/definition/charger/sungrow.yaml +++ b/templates/definition/charger/sungrow.yaml @@ -3,11 +3,8 @@ products: - brand: Sungrow description: generic: AC011E-01 -capabilities: ["mA"] +capabilities: ["mA", "1p3p"] requirements: - description: - de: Die Wallbox muss auf EMS-Arbeitsmodus und auf EMS-Start eingestellt werden. - en: Charger needs to be set to EMS working mode and start by EMS. evcc: ["sponsorship"] params: - name: modbus From bf584796c0b6434a348aaf906b5791f9ebebef64 Mon Sep 17 00:00:00 2001 From: Simon Schenk Date: Sat, 4 May 2024 11:00:36 +0200 Subject: [PATCH 036/168] Bluelink: add CCS api support (#13713) --- vehicle/bluelink.go | 2 +- vehicle/bluelink/api.go | 41 ++++-- vehicle/bluelink/provider.go | 108 ++++----------- vehicle/bluelink/types.go | 253 ++++++++++++++++++++++++++++++++++- 4 files changed, 310 insertions(+), 94 deletions(-) diff --git a/vehicle/bluelink.go b/vehicle/bluelink.go index 2883e4c92c..a85d9998fb 100644 --- a/vehicle/bluelink.go +++ b/vehicle/bluelink.go @@ -94,7 +94,7 @@ func newBluelinkFromConfig(brand string, other map[string]interface{}, settings v := &Bluelink{ embed: &cc.embed, - Provider: bluelink.NewProvider(api, vehicle.VehicleID, cc.Expiry, cc.Cache), + Provider: bluelink.NewProvider(api, vehicle, cc.Expiry, cc.Cache), } return v, nil diff --git a/vehicle/bluelink/api.go b/vehicle/bluelink/api.go index 5d9b102f31..870708ea85 100644 --- a/vehicle/bluelink/api.go +++ b/vehicle/bluelink/api.go @@ -13,9 +13,11 @@ import ( ) const ( - VehiclesURL = "vehicles" - StatusURL = "vehicles/%s/status" - StatusLatestURL = "vehicles/%s/status/latest" + VehiclesURL = "vehicles" + StatusURL = "vehicles/%s/status" + StatusLatestURL = "vehicles/%s/status/latest" + StatusURLCCS2 = "vehicles/%s/ccs2/carstatus" + StatusLatestURLCCS2 = "vehicles/%s/ccs2/carstatus/latest" ) const ( @@ -51,6 +53,7 @@ func NewAPI(log *util.Logger, baseURI string, decorator func(*http.Request) erro type Vehicle struct { VIN, VehicleName, VehicleID string + CcuCCS2ProtocolSupport int } func (v *API) Vehicles() ([]Vehicle, error) { @@ -63,27 +66,47 @@ func (v *API) Vehicles() ([]Vehicle, error) { } // StatusLatest retrieves the latest server-side status -func (v *API) StatusLatest(vid string) (StatusLatestResponse, error) { - var res StatusLatestResponse +func (v *API) StatusLatest(vehicle Vehicle) (BluelinkVehicleStatusLatest, error) { + vid := vehicle.VehicleID + + if vehicle.CcuCCS2ProtocolSupport != 0 { + var res StatusLatestResponseCCS + uri := fmt.Sprintf("%s/%s", v.baseURI, fmt.Sprintf(StatusLatestURLCCS2, vid)) + err := v.GetJSON(uri, &res) + if err == nil && res.RetCode != resOK { + err = fmt.Errorf("unexpected response: %s", res.RetCode) + } + return res, err + } + var res StatusLatestResponse uri := fmt.Sprintf("%s/%s", v.baseURI, fmt.Sprintf(StatusLatestURL, vid)) err := v.GetJSON(uri, &res) if err == nil && res.RetCode != resOK { err = fmt.Errorf("unexpected response: %s", res.RetCode) } - return res, err } // StatusPartial refreshes the status -func (v *API) StatusPartial(vid string) (StatusResponse, error) { - var res StatusResponse +func (v *API) StatusPartial(vehicle Vehicle) (BluelinkVehicleStatus, error) { + vid := vehicle.VehicleID + + if vehicle.CcuCCS2ProtocolSupport != 0 { + var res StatusLatestResponseCCS + uri := fmt.Sprintf("%s/%s", v.baseURI, fmt.Sprintf(StatusLatestURLCCS2, vid)) + err := v.GetJSON(uri, &res) + if err == nil && res.RetCode != resOK { + err = fmt.Errorf("unexpected response: %s", res.RetCode) + } + return res, err + } + var res StatusResponse uri := fmt.Sprintf("%s/%s", v.baseURI, fmt.Sprintf(StatusURL, vid)) err := v.GetJSON(uri, &res) if err == nil && res.RetCode != resOK { err = fmt.Errorf("unexpected response: %s", res.RetCode) } - return res, err } diff --git a/vehicle/bluelink/provider.go b/vehicle/bluelink/provider.go index ad7d023aa2..86a526f507 100644 --- a/vehicle/bluelink/provider.go +++ b/vehicle/bluelink/provider.go @@ -12,50 +12,50 @@ const refreshTimeout = 2 * time.Minute // Provider implements the vehicle api. // Based on https://github.com/Hacksore/bluelinky. type Provider struct { - statusG func() (VehicleStatus, error) - statusLG func() (StatusLatestResponse, error) - refreshG func() (StatusResponse, error) + statusG func() (BluelinkVehicleStatus, error) + statusLG func() (BluelinkVehicleStatusLatest, error) + refreshG func() (BluelinkVehicleStatus, error) expiry time.Duration refreshTime time.Time } // New creates a new BlueLink API -func NewProvider(api *API, vid string, expiry, cache time.Duration) *Provider { +func NewProvider(api *API, vehicle Vehicle, expiry, cache time.Duration) *Provider { v := &Provider{ - refreshG: func() (StatusResponse, error) { - return api.StatusPartial(vid) + refreshG: func() (BluelinkVehicleStatus, error) { + return api.StatusPartial(vehicle) }, expiry: expiry, } - v.statusG = provider.Cached(func() (VehicleStatus, error) { + v.statusG = provider.Cached(func() (BluelinkVehicleStatus, error) { return v.status( - func() (StatusLatestResponse, error) { return api.StatusLatest(vid) }, + func() (BluelinkVehicleStatus, error) { return api.StatusPartial(vehicle) }, ) }, cache) - v.statusLG = provider.Cached(func() (StatusLatestResponse, error) { - return api.StatusLatest(vid) + v.statusLG = provider.Cached(func() (BluelinkVehicleStatusLatest, error) { + return api.StatusLatest(vehicle) }, cache) return v } // status wraps the api status call and adds status refresh -func (v *Provider) status(statusG func() (StatusLatestResponse, error)) (VehicleStatus, error) { +func (v *Provider) status(statusG func() (BluelinkVehicleStatus, error)) (BluelinkVehicleStatus, error) { res, err := statusG() var ts time.Time if err == nil { - ts, err = res.ResMsg.VehicleStatusInfo.VehicleStatus.Updated() + ts, err = res.Updated() if err != nil { - return res.ResMsg.VehicleStatusInfo.VehicleStatus, err + return res, err } // return the current value if time.Since(ts) <= v.expiry { v.refreshTime = time.Time{} - return res.ResMsg.VehicleStatusInfo.VehicleStatus, nil + return res, nil } } @@ -66,15 +66,15 @@ func (v *Provider) status(statusG func() (StatusLatestResponse, error)) (Vehicle // TODO async refresh res, err := v.refreshG() if err == nil { - if ts, err = res.ResMsg.Updated(); err == nil && time.Since(ts) <= v.expiry { + if ts, err = res.Updated(); err == nil && time.Since(ts) <= v.expiry { v.refreshTime = time.Time{} - return res.ResMsg, nil + return res, nil } err = api.ErrMustRetry } - return VehicleStatus{}, err + return nil, err } // refresh finally expired @@ -88,7 +88,7 @@ func (v *Provider) status(statusG func() (StatusLatestResponse, error)) (Vehicle err = api.ErrMustRetry } - return VehicleStatus{}, err + return nil, err } var _ api.Battery = (*Provider)(nil) @@ -99,12 +99,7 @@ func (v *Provider) Soc() (float64, error) { if err != nil { return 0, err } - - if res.EvStatus != nil { - return res.EvStatus.BatteryStatus, nil - } - - return 0, api.ErrNotAvailable + return res.SoC() } var _ api.ChargeState = (*Provider)(nil) @@ -112,25 +107,11 @@ var _ api.ChargeState = (*Provider)(nil) // Status implements the api.Battery interface func (v *Provider) Status() (api.ChargeStatus, error) { status := api.StatusNone - res, err := v.statusG() if err != nil { return status, err } - - if res.EvStatus != nil { - status = api.StatusA - if res.EvStatus.BatteryPlugin > 0 || res.EvStatus.ChargePortDoorOpenStatus == 1 { - status = api.StatusB - } - if res.EvStatus.BatteryCharge { - status = api.StatusC - } - } else { - err = api.ErrNotAvailable - } - - return status, err + return res.Status() } var _ api.VehicleFinishTimer = (*Provider)(nil) @@ -141,19 +122,7 @@ func (v *Provider) FinishTime() (time.Time, error) { if err != nil { return time.Time{}, err } - - if res.EvStatus != nil { - remaining := res.EvStatus.RemainTime2.Atc.Value - - if remaining == 0 { - return time.Time{}, api.ErrNotAvailable - } - - ts, err := res.Updated() - return ts.Add(time.Duration(remaining) * time.Minute), err - } - - return time.Time{}, api.ErrNotAvailable + return res.FinishTime() } var _ api.VehicleRange = (*Provider)(nil) @@ -164,14 +133,7 @@ func (v *Provider) Range() (int64, error) { if err != nil { return 0, err } - - if res.EvStatus != nil { - if dist := res.EvStatus.DrvDistance; len(dist) == 1 { - return int64(dist[0].RangeByFuel.EvModeRange.Value), nil - } - } - - return 0, api.ErrNotAvailable + return res.Range() } var _ api.VehicleOdometer = (*Provider)(nil) @@ -182,12 +144,7 @@ func (v *Provider) Odometer() (float64, error) { if err != nil { return 0, err } - - if res.ResMsg.VehicleStatusInfo.Odometer != nil { - return res.ResMsg.VehicleStatusInfo.Odometer.Value, err - } - - return 0, api.ErrNotAvailable + return res.Odometer() } var _ api.SocLimiter = (*Provider)(nil) @@ -198,16 +155,7 @@ func (v *Provider) GetLimitSoc() (int64, error) { if err != nil { return 0, err } - - if res.EvStatus != nil { - for _, targetSOC := range res.EvStatus.ReservChargeInfos.TargetSocList { - if targetSOC.PlugType == plugTypeAC { - return int64(targetSOC.TargetSocLevel), nil - } - } - } - - return 0, api.ErrNotAvailable + return res.GetLimitSoc() } var _ api.VehiclePosition = (*Provider)(nil) @@ -218,13 +166,7 @@ func (v *Provider) Position() (float64, float64, error) { if err != nil { return 0, 0, err } - - if res.ResMsg.VehicleStatusInfo.VehicleLocation != nil { - coord := res.ResMsg.VehicleStatusInfo.VehicleLocation.Coord - return coord.Lat, coord.Lon, err - } - - return 0, 0, api.ErrNotAvailable + return res.Position() } var _ api.Resurrector = (*Provider)(nil) diff --git a/vehicle/bluelink/types.go b/vehicle/bluelink/types.go index fd658cba02..13e5576859 100644 --- a/vehicle/bluelink/types.go +++ b/vehicle/bluelink/types.go @@ -1,6 +1,25 @@ package bluelink -import "time" +import ( + "strconv" + "time" + + "github.com/evcc-io/evcc/api" +) + +type BluelinkVehicleStatus interface { + Updated() (time.Time, error) + SoC() (float64, error) + Status() (api.ChargeStatus, error) + FinishTime() (time.Time, error) + Range() (int64, error) + GetLimitSoc() (int64, error) +} + +type BluelinkVehicleStatusLatest interface { + Odometer() (float64, error) + Position() (float64, float64, error) +} type VehiclesResponse struct { RetCode string @@ -84,3 +103,235 @@ type TargetSoc struct { TargetSocLevel int PlugType int } + +func (d StatusResponse) Updated() (time.Time, error) { + return time.Parse(timeFormat, d.ResMsg.Time+timeOffset) +} + +func (d StatusResponse) SoC() (float64, error) { + if d.ResMsg.EvStatus != nil { + return d.ResMsg.EvStatus.BatteryStatus, nil + } + + return 0, api.ErrNotAvailable +} + +func (d StatusResponse) Status() (api.ChargeStatus, error) { + if d.ResMsg.EvStatus != nil { + status := api.StatusA + if d.ResMsg.EvStatus.BatteryPlugin > 0 || d.ResMsg.EvStatus.ChargePortDoorOpenStatus == 1 { + status = api.StatusB + } + if d.ResMsg.EvStatus.BatteryCharge { + status = api.StatusC + } + return status, nil + } + + return api.StatusNone, api.ErrNotAvailable +} + +func (d StatusResponse) FinishTime() (time.Time, error) { + if d.ResMsg.EvStatus != nil { + remaining := d.ResMsg.EvStatus.RemainTime2.Atc.Value + + if remaining != 0 { + ts, err := d.ResMsg.Updated() + return ts.Add(time.Duration(remaining) * time.Minute), err + } + } + + return time.Time{}, api.ErrNotAvailable +} + +func (d StatusResponse) Range() (int64, error) { + if d.ResMsg.EvStatus != nil { + if dist := d.ResMsg.EvStatus.DrvDistance; len(dist) == 1 { + return int64(dist[0].RangeByFuel.EvModeRange.Value), nil + } + } + return 0, api.ErrNotAvailable +} + +func (d StatusResponse) GetLimitSoc() (int64, error) { + if d.ResMsg.EvStatus != nil { + for _, targetSOC := range d.ResMsg.EvStatus.ReservChargeInfos.TargetSocList { + if targetSOC.PlugType == plugTypeAC { + return int64(targetSOC.TargetSocLevel), nil + } + } + } + return 0, api.ErrNotAvailable +} + +func (d StatusLatestResponse) Odometer() (float64, error) { + if d.ResMsg.VehicleStatusInfo.Odometer != nil { + return d.ResMsg.VehicleStatusInfo.Odometer.Value, nil + } + return 0, api.ErrNotAvailable +} + +func (d StatusLatestResponse) Position() (float64, float64, error) { + if d.ResMsg.VehicleStatusInfo.VehicleLocation != nil { + pos := d.ResMsg.VehicleStatusInfo.VehicleLocation.Coord + return pos.Lat, pos.Lon, nil + } + return 0, 0, api.ErrNotAvailable +} + +type StatusLatestResponseCCS struct { + RetCode string + ResCode string + ResMsg struct { + State struct { + Vehicle VehicleStatusCCS + } + LastUpdateTime string + } +} + +type VehicleStatusCCS struct { + Location *struct { + GeoCoord struct { + Latitude, Longitude, Altitude float64 + Type int + Date string + } + } + Green *struct { + BatteryManagement struct { + BatteryRemain struct { + Ratio float64 + Value float64 + } + BatteryCapacity struct { + Value float64 + } + SoH struct { + Ratio float64 + } + } + ChargingInformation struct { + ConnectorFastening struct { + // 1 connected + State int + } + Charging struct { + RemainTime float64 + RemainTimeUnit int + } + EstimatedTime struct { + Standard float64 + ICCB float64 + Quick float64 + Unit int + } + ExpectedTime struct { + StartDay int + StartHour int + StartMin int + EndDay int + EndHour int + EndMin int + } + TargetSoC struct { + Standard int64 + Quick int64 + } + DTE struct { + TargetSoC struct { + // in Drivetrain.FuelSystem.DTE.Unit + Standard float64 + Quick float64 + } + } + } + ChargingDoor struct { + // 0, 2 closed, 1 open + State int + } + Electric struct { + SmartGrid struct { + VehicleToLoad struct { + DischargeLimitation struct { + SoC float64 + RemainTime float64 + } + } + } + } + } + Drivetrain struct { + Odometer float64 + FuelSystem struct { + DTE struct { + Total int64 + } + } + } +} + +func (d StatusLatestResponseCCS) Updated() (time.Time, error) { + epoch, err := strconv.ParseInt(d.ResMsg.LastUpdateTime, 10, 64) + if err != nil { + return time.Now(), err + } + return time.UnixMilli(epoch), nil +} + +func (d StatusLatestResponseCCS) SoC() (float64, error) { + if d.ResMsg.State.Vehicle.Green != nil { + return d.ResMsg.State.Vehicle.Green.BatteryManagement.BatteryRemain.Ratio, nil + } + return 0, api.ErrNotAvailable +} + +func (d StatusLatestResponseCCS) Status() (api.ChargeStatus, error) { + if d.ResMsg.State.Vehicle.Green != nil { + if d.ResMsg.State.Vehicle.Green.ChargingInformation.ConnectorFastening.State == 1 { + return api.StatusB, nil + } + if d.ResMsg.State.Vehicle.Green.ChargingInformation.Charging.RemainTime > 0 { + return api.StatusC, nil + } + return api.StatusA, nil + } + return api.StatusNone, api.ErrNotAvailable +} + +func (d StatusLatestResponseCCS) FinishTime() (time.Time, error) { + if d.ResMsg.State.Vehicle.Green != nil { + remaining := d.ResMsg.State.Vehicle.Green.ChargingInformation.Charging.RemainTime + + if remaining == 0 { + return time.Time{}, api.ErrNotAvailable + } + + ts, err := d.Updated() + return ts.Add(time.Duration(remaining) * time.Minute), err + } + + return time.Now(), api.ErrNotAvailable +} + +func (d StatusLatestResponseCCS) Range() (int64, error) { + return d.ResMsg.State.Vehicle.Drivetrain.FuelSystem.DTE.Total, nil +} + +func (d StatusLatestResponseCCS) GetLimitSoc() (int64, error) { + if d.ResMsg.State.Vehicle.Green != nil { + return d.ResMsg.State.Vehicle.Green.ChargingInformation.TargetSoC.Standard, nil + } + return 0, api.ErrNotAvailable +} + +func (d StatusLatestResponseCCS) Odometer() (float64, error) { + return d.ResMsg.State.Vehicle.Drivetrain.Odometer, nil +} + +func (d StatusLatestResponseCCS) Position() (float64, float64, error) { + if d.ResMsg.State.Vehicle.Location != nil { + return d.ResMsg.State.Vehicle.Location.GeoCoord.Latitude, d.ResMsg.State.Vehicle.Location.GeoCoord.Longitude, nil + } + return 0, 0, api.ErrNotAvailable +} From 3794b3f2371a6b7adc55adfc96ff6d0802c31032 Mon Sep 17 00:00:00 2001 From: andig Date: Sat, 4 May 2024 11:00:55 +0200 Subject: [PATCH 037/168] Mqtt: publish pointer values (#13741) --- server/mqtt.go | 7 +++++-- server/mqtt_test.go | 20 +++++++++++++++----- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/server/mqtt.go b/server/mqtt.go index 97561dbaa0..f96c31f01c 100644 --- a/server/mqtt.go +++ b/server/mqtt.go @@ -107,9 +107,12 @@ func (m *MQTT) publishComplex(topic string, retained bool, payload interface{}) } case reflect.Pointer: - if reflect.ValueOf(payload).IsNil() { - payload = nil + if !reflect.ValueOf(payload).IsNil() { + m.publishComplex(topic, retained, reflect.Indirect(reflect.ValueOf(payload)).Interface()) + return } + + payload = nil fallthrough default: diff --git a/server/mqtt_test.go b/server/mqtt_test.go index 1ebe08b774..a7dd134791 100644 --- a/server/mqtt_test.go +++ b/server/mqtt_test.go @@ -42,15 +42,25 @@ func TestPublishTypes(t *testing.T) { }{ Foo: "bar", }) - require.Len(t, topics, 1) - assert.Equal(t, `test/foo`, topics[0], "struct mismatch") - assert.Equal(t, `bar`, payloads[0], "struct mismatch") + assert.Equal(t, []string{"test/foo"}, topics, "struct mismatch") + assert.Equal(t, []string{"bar"}, payloads, "struct mismatch") + reset() + + i := 1 + m.publish("test", false, struct { + Foo, Bar *int + }{ + Foo: &i, + Bar: nil, + }) + assert.Equal(t, []string{"test/foo", "test/bar"}, topics, "pointer mismatch") + assert.Equal(t, []string{"1", ""}, payloads, "pointer mismatch") reset() slice := []int{10, 20} m.publish("test", false, slice) require.Len(t, topics, 3) - assert.Equal(t, []string{`test`, `test/1`, `test/2`}, topics, "slice mismatch") - assert.Equal(t, []string{`2`, `10`, `20`}, payloads, "slice mismatch") + assert.Equal(t, []string{"test", "test/1", "test/2"}, topics, "slice mismatch") + assert.Equal(t, []string{"2", "10", "20"}, payloads, "slice mismatch") reset() } From 3bbec2aa5e734b1e88ae427cfa732ee3ebe4d1f1 Mon Sep 17 00:00:00 2001 From: andig Date: Sat, 4 May 2024 15:34:34 +0200 Subject: [PATCH 038/168] chore: handle e3dc test errors --- meter/template_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/meter/template_test.go b/meter/template_test.go index 0cc4a2aad4..9a4067bea9 100644 --- a/meter/template_test.go +++ b/meter/template_test.go @@ -27,6 +27,7 @@ var acceptable = []string{ "context deadline exceeded", // LG ESS "no ping response for 192.0.2.2", // SMA "no such network interface", // SMA + "missing config values: username, password, key", // E3DC } func TestTemplates(t *testing.T) { From 457284c28da8f81b5eee1d87a99d7933d54449ee Mon Sep 17 00:00:00 2001 From: andig Date: Sat, 4 May 2024 15:34:44 +0200 Subject: [PATCH 039/168] chore: test tariff templates --- tariff/template_test.go | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 tariff/template_test.go diff --git a/tariff/template_test.go b/tariff/template_test.go new file mode 100644 index 0000000000..4886f5a293 --- /dev/null +++ b/tariff/template_test.go @@ -0,0 +1,21 @@ +package tariff + +import ( + "testing" + + "github.com/evcc-io/evcc/util/templates" + "github.com/evcc-io/evcc/util/test" +) + +var acceptable = []string{} + +func TestTemplates(t *testing.T) { + templates.TestClass(t, templates.Tariff, func(t *testing.T, values map[string]any) { + t.Helper() + + if _, err := NewFromConfig("template", values); err != nil && !test.Acceptable(err, acceptable) { + t.Log(values) + t.Error(err) + } + }) +} From f19e04b39fe0c5ff137af70b167b24d10fcafacc Mon Sep 17 00:00:00 2001 From: andig Date: Sat, 4 May 2024 15:35:02 +0200 Subject: [PATCH 040/168] chore: handle socket connection errors in test --- provider/socket.go | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/provider/socket.go b/provider/socket.go index 77a909de20..2cc3bd39ac 100644 --- a/provider/socket.go +++ b/provider/socket.go @@ -2,9 +2,11 @@ package provider import ( "context" + "fmt" "math" "net/http" "strconv" + "sync" "time" "github.com/evcc-io/evcc/api" @@ -86,20 +88,25 @@ func NewSocketProviderFromConfig(other map[string]interface{}) (Provider, error) return nil, err } - go p.listen() + errC := make(chan error, 1) + go p.run(errC) if cc.Timeout > 0 { select { case <-p.val.Done(): case <-time.After(cc.Timeout): return nil, api.ErrTimeout + case err := <-errC: + return nil, err } } return p, nil } -func (p *Socket) listen() { +func (p *Socket) run(errC chan error) { + var once sync.Once + headers := make(http.Header) for k, v := range p.headers { headers.Set(k, v) @@ -115,6 +122,9 @@ func (p *Socket) listen() { cancel() if err != nil { + // handle initial connection error immediately + once.Do(func() { errC <- err }) + p.log.ERROR.Println(err) time.Sleep(retryDelay) continue @@ -154,6 +164,7 @@ func (p *Socket) FloatGetter() (func() (float64, error), error) { g, err := p.StringGetter() return func() (float64, error) { + fmt.Println("FloatGetter") s, err := g() if err != nil { return 0, err From 017080e9362d3c40d1f78229c60a0673a55fcaf1 Mon Sep 17 00:00:00 2001 From: andig Date: Sat, 4 May 2024 15:36:00 +0200 Subject: [PATCH 041/168] chore: cleanup --- provider/socket.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/provider/socket.go b/provider/socket.go index 2cc3bd39ac..39b45f164c 100644 --- a/provider/socket.go +++ b/provider/socket.go @@ -2,7 +2,6 @@ package provider import ( "context" - "fmt" "math" "net/http" "strconv" @@ -164,7 +163,6 @@ func (p *Socket) FloatGetter() (func() (float64, error), error) { g, err := p.StringGetter() return func() (float64, error) { - fmt.Println("FloatGetter") s, err := g() if err != nil { return 0, err From 87b4713ad6677070056de366505d1a1a6dbf2746 Mon Sep 17 00:00:00 2001 From: Sillium Date: Sun, 5 May 2024 09:50:51 +0200 Subject: [PATCH 042/168] chore: minor --- charger/easee.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charger/easee.go b/charger/easee.go index 6b0c3c101e..f246a09bd7 100644 --- a/charger/easee.go +++ b/charger/easee.go @@ -77,7 +77,7 @@ func init() { registry.Add("easee", NewEaseeFromConfig) } -// NewEaseeFromConfig creates a go-e charger from generic config +// NewEaseeFromConfig creates a Easee charger from generic config func NewEaseeFromConfig(other map[string]interface{}) (api.Charger, error) { cc := struct { User string From 1a4e213ba94587f784add7e3d830ec879bc0b40b Mon Sep 17 00:00:00 2001 From: premultiply <4681172+premultiply@users.noreply.github.com> Date: Sun, 5 May 2024 10:18:35 +0000 Subject: [PATCH 043/168] sungrow-charger: improve 1p3p switching --- charger/sungrow.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/charger/sungrow.go b/charger/sungrow.go index d7fe59cf68..5088250b42 100644 --- a/charger/sungrow.go +++ b/charger/sungrow.go @@ -262,10 +262,6 @@ func (wb *Sungrow) Phases1p3p(phases int) error { _, err = wb.conn.WriteSingleRegister(sgRegPhaseSwitch, u) - if err == nil && enabled { - err = wb.Enable(true) - } - return err } From 45e3df9fce5b68d12132b3d4ae4e47d6d0737e46 Mon Sep 17 00:00:00 2001 From: andig Date: Sun, 5 May 2024 19:57:30 +0200 Subject: [PATCH 044/168] Fix circuits disabling instead of reducing demand (#13768) --- core/circuit.go | 17 +++-- core/circuit_test.go | 178 ++++++++++++++++++++++--------------------- 2 files changed, 102 insertions(+), 93 deletions(-) diff --git a/core/circuit.go b/core/circuit.go index 9c4ee28bff..7feab2d66e 100644 --- a/core/circuit.go +++ b/core/circuit.go @@ -2,6 +2,7 @@ package core import ( "fmt" + "math" "sync" "github.com/evcc-io/evcc/api" @@ -176,7 +177,7 @@ func (c *Circuit) updateLoadpoints(loadpoints []api.CircuitLoad) { func (c *Circuit) updateMeters() error { if f, err := c.meter.CurrentPower(); err == nil { // TODO handle negative powers - c.power = f + c.power = math.Abs(f) } else { return fmt.Errorf("circuit power: %w", err) } @@ -184,7 +185,7 @@ func (c *Circuit) updateMeters() error { if phaseMeter, ok := c.meter.(api.PhaseCurrents); ok { if l1, l2, l3, err := phaseMeter.Currents(); err == nil { // TODO handle negative currents - c.current = max(l1, l2, l3) + c.current = max(math.Abs(l1), math.Abs(l2), math.Abs(l3)) } else { return fmt.Errorf("circuit currents: %w", err) } @@ -246,7 +247,7 @@ func (c *Circuit) ValidatePower(old, new float64) float64 { if c.maxPower != 0 { if c.power+delta > c.maxPower { - new = max(0, c.maxPower-c.power) + new = max(0, new-(c.power+delta-c.maxPower)) c.log.DEBUG.Printf("validate power: %gW -> %gW <= %gW at %gW: capped at %gW", old, new, c.maxPower, c.power, new) } else { c.log.TRACE.Printf("validate power: %gW -> %gW <= %gW at %gW: ok", old, new, c.maxPower, c.power) @@ -254,9 +255,9 @@ func (c *Circuit) ValidatePower(old, new float64) float64 { } if c.parent != nil { - res := c.parent.ValidatePower(c.power, new) + res := c.parent.ValidatePower(old, new) if res != new { - c.log.TRACE.Printf("validate power: %gW -> %gW at %gW: capped at %gW", old, new, c.power, new) + c.log.TRACE.Printf("validate power: %gW -> %gW at %gW: capped by parent at %gW", old, new, c.power, res) } return res } @@ -265,12 +266,12 @@ func (c *Circuit) ValidatePower(old, new float64) float64 { } // ValidateCurrent validates current request -func (c *Circuit) ValidateCurrent(old, new float64) (res float64) { +func (c *Circuit) ValidateCurrent(old, new float64) float64 { delta := max(0, new-old) if c.maxCurrent != 0 { if c.current+delta > c.maxCurrent { - new = max(0, c.maxCurrent-c.current) + new = max(0, new-(c.current+delta-c.maxCurrent)) c.log.DEBUG.Printf("validate current: %gA -> %gA <= %gA at %gA: capped at %gA", old, new, c.maxCurrent, c.current, new) } else { c.log.TRACE.Printf("validate current: %gA -> %gA <= %gA at %gA: ok", old, new, c.maxCurrent, c.current) @@ -278,7 +279,7 @@ func (c *Circuit) ValidateCurrent(old, new float64) (res float64) { } if c.parent != nil { - res := c.parent.ValidateCurrent(c.current, new) + res := c.parent.ValidateCurrent(old, new) if res != new { c.log.TRACE.Printf("validate current: %gA -> %gA at %gA: capped by parent at %gA", old, new, c.current, res) } diff --git a/core/circuit_test.go b/core/circuit_test.go index f29e67d74b..785ba15464 100644 --- a/core/circuit_test.go +++ b/core/circuit_test.go @@ -5,10 +5,57 @@ import ( "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/util" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" ) +type circuitTest struct { + // current values for parent, circuit 1, circuit 2 + p, c1, c2 float64 + // old/new demand values and allowed result + old, new, res float64 +} + +func circuitTests() []circuitTest { + return []circuitTest{ + // no load + {0, 0, 0, 0, 0, 0}, // = + {0, 0, 0, 0, 1, 1}, // + + {0, 0, 0, 0, 2, 1}, // + + + // circuit 1 loaded + {0, 1, 0, 0, 0, 0}, // = + {0, 1, 0, 0, 1, 0}, // + + {0, 1, 0, 0, 2, 0}, // + + {0, 1, 0, 1, 1, 1}, // = + {0, 1, 0, 2, 1, 1}, // - + + // circuit 1 overloaded + {0, 2, 0, 0, 0, 0}, // = + {0, 2, 0, 0, 1, 0}, // + + {0, 2, 0, 1, 1, 0}, // = + {0, 2, 0, 2, 2, 1}, // = + {0, 2, 0, 2, 3, 1}, // + + // {0, 2, 0, 2, 1, 1}, // - + + // parent loaded + {1, 0, 0, 0, 0, 0}, // = + {1, 0, 0, 0, 1, 0}, // + + {1, 0, 0, 0, 2, 0}, // + + {1, 0, 0, 1, 1, 1}, // = + {1, 0, 0, 2, 1, 1}, // - + + // parent overloaded + {2, 0, 0, 0, 0, 0}, // = + {2, 0, 0, 0, 1, 0}, // + + {2, 0, 0, 1, 1, 0}, // = + {2, 0, 0, 2, 2, 1}, // = + {2, 0, 0, 2, 3, 1}, // + + // {2, 0, 0, 2, 1, 1}, // - + } +} + func TestCircuitPower(t *testing.T) { log := util.NewLogger("foo") @@ -19,25 +66,7 @@ func TestCircuitPower(t *testing.T) { return c, m } - for _, tc := range []struct { - pm, cm1, cm2 float64 - req, res float64 - }{ - // no load - {0, 0, 0, 0, 0}, - {0, 0, 0, 1, 1}, - {0, 0, 0, 2, 1}, - - // c1 loaded - {0, 1, 0, 0, 0}, - {0, 1, 0, 1, 0}, - {0, 1, 0, 2, 0}, - - // pc loaded - {1, 0, 0, 0, 0}, - {1, 0, 0, 1, 0}, - {1, 0, 0, 2, 0}, - } { + for _, tc := range circuitTests() { ctrl := gomock.NewController(t) pc, pm := circ(t, ctrl, 1) @@ -48,76 +77,55 @@ func TestCircuitPower(t *testing.T) { c2.SetParent(pc) // update meters - pm.EXPECT().CurrentPower().Return(tc.pm, nil) - cm1.EXPECT().CurrentPower().Return(tc.cm1, nil) - cm2.EXPECT().CurrentPower().Return(tc.cm2, nil) + pm.EXPECT().CurrentPower().Return(tc.p, nil) + cm1.EXPECT().CurrentPower().Return(tc.c1, nil) + cm2.EXPECT().CurrentPower().Return(tc.c2, nil) require.NoError(t, pc.Update(nil)) - require.Equal(t, tc.res, c1.ValidatePower(0, tc.req)) + assert.Equal(t, tc.res, c1.ValidatePower(tc.old, tc.new), tc) ctrl.Finish() } } -// func TestCircuitCurrents(t *testing.T) { -// log := util.NewLogger("foo") - -// type mockMeter struct { -// *api.MockMeter -// *api.MockPhaseCurrents -// } - -// circ := func(t *testing.T, ctrl *gomock.Controller, maxP float64) (*Circuit, *mockMeter) { -// m := api.NewMockMeter(ctrl) -// mc := api.NewMockPhaseCurrents(ctrl) -// mm := &mockMeter{m, mc} -// c, err := NewCircuit(log, 0, maxP, mm) -// require.NoError(t, err) -// return c, mm -// } - -// for _, tc := range []struct { -// pm, cm1, cm2 float64 -// req, res float64 -// }{ -// // no load -// {0, 0, 0, 0, 0}, -// {0, 0, 0, 1, 1}, -// {0, 0, 0, 2, 1}, - -// // c1 loaded -// {0, 1, 0, 0, 0}, -// {0, 1, 0, 1, 0}, -// {0, 1, 0, 2, 0}, - -// // pc loaded -// {1, 0, 0, 0, 0}, -// {1, 0, 0, 1, 0}, -// {1, 0, 0, 2, 0}, -// } { -// ctrl := gomock.NewController(t) - -// pc, pm := circ(t, ctrl, 1) -// c1, cm1 := circ(t, ctrl, 1) -// c2, cm2 := circ(t, ctrl, 1) - -// c1.SetParent(pc) -// c2.SetParent(pc) - -// // update meters -// pm.MockMeter.EXPECT().CurrentPower().Return(tc.pm, nil) -// cm1.MockMeter.EXPECT().CurrentPower().Return(tc.cm1, nil) -// cm2.MockMeter.EXPECT().CurrentPower().Return(tc.cm2, nil) - -// // update meters -// pm.MockPhaseCurrents.EXPECT().Currents().Return(tc.pm, tc.pm, tc.pm, nil) -// cm1.MockPhaseCurrents.EXPECT().Currents().Return(tc.cm1, tc.cm1, tc.cm1, nil) -// cm2.MockPhaseCurrents.EXPECT().Currents().Return(tc.cm2, tc.cm2, tc.cm2, nil) -// require.NoError(t, pc.Update(nil)) - -// require.Equal(t, tc.res, c1.ValidatePower(0, tc.req)) -// require.Equal(t, tc.res, c1.ValidateCurrent(0, tc.req)) - -// ctrl.Finish() -// } -// } +func TestCircuitCurrents(t *testing.T) { + log := util.NewLogger("foo") + + type combined struct { + *api.MockMeter + *api.MockPhaseCurrents + } + circ := func(t *testing.T, ctrl *gomock.Controller, maxC float64) (*Circuit, combined) { + m := combined{ + api.NewMockMeter(ctrl), + api.NewMockPhaseCurrents(ctrl), + } + c, err := NewCircuit(log, "foo", maxC, 0, m) + require.NoError(t, err) + return c, m + } + + for _, tc := range circuitTests() { + ctrl := gomock.NewController(t) + + pc, pm := circ(t, ctrl, 1) + c1, cm1 := circ(t, ctrl, 1) + c2, cm2 := circ(t, ctrl, 1) + + c1.SetParent(pc) + c2.SetParent(pc) + + // update meters + pm.MockMeter.EXPECT().CurrentPower().AnyTimes().Return(0.0, nil) + cm1.MockMeter.EXPECT().CurrentPower().AnyTimes().Return(0.0, nil) + cm2.MockMeter.EXPECT().CurrentPower().AnyTimes().Return(0.0, nil) + pm.MockPhaseCurrents.EXPECT().Currents().Return(tc.p, tc.p, tc.p, nil) + cm1.MockPhaseCurrents.EXPECT().Currents().Return(tc.c1, tc.c1, tc.c1, nil) + cm2.MockPhaseCurrents.EXPECT().Currents().Return(tc.c2, tc.c2, tc.c2, nil) + require.NoError(t, pc.Update(nil)) + + assert.Equal(t, tc.res, c1.ValidateCurrent(tc.old, tc.new), tc) + + ctrl.Finish() + } +} From 7d67b502a7600a8231aabb98cc6c144990d62445 Mon Sep 17 00:00:00 2001 From: andig Date: Mon, 6 May 2024 18:19:10 +0200 Subject: [PATCH 045/168] Revert "Innogy: add api.MeterEnergy" This reverts commit da4af01d736503b77901648d8a03f45516a99591. --- charger/innogy.go | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/charger/innogy.go b/charger/innogy.go index dcbe429c03..7ef73a68b0 100644 --- a/charger/innogy.go +++ b/charger/innogy.go @@ -35,7 +35,6 @@ const ( igyRegManufacturer = 100 // Input igyRegFirmware = 200 // Input igyRegStatus = 275 // Input - igyRegEnergy = 307 // Input igyRegCurrents = 1006 // current readings per phase ) @@ -164,18 +163,6 @@ func (wb *Innogy) CurrentPower() (float64, error) { return 230 * (l1 + l2 + l3), err } -var _ api.MeterEnergy = (*Innogy)(nil) - -// TotalEnergy implements the api.MeterEnergy interface -func (wb *Innogy) TotalEnergy() (float64, error) { - b, err := wb.conn.ReadInputRegisters(igyRegEnergy, 2) - if err != nil { - return 0, err - } - - return float64(math.Float32frombits(binary.BigEndian.Uint32(b))), nil -} - var _ api.PhaseCurrents = (*Innogy)(nil) // Currents implements the api.PhaseCurrents interface From 0fcb2a48d815dca3ead1607e2a5370e9cd659fb0 Mon Sep 17 00:00:00 2001 From: Philip Porto Schiffer Date: Mon, 6 May 2024 20:25:08 +0200 Subject: [PATCH 046/168] sungrow-charger: use StartMode for enabled state (#13784) --- charger/sungrow.go | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/charger/sungrow.go b/charger/sungrow.go index 5088250b42..f696f0fe90 100644 --- a/charger/sungrow.go +++ b/charger/sungrow.go @@ -39,7 +39,7 @@ const ( // input (read only) sgRegPhase = 21224 // uint16 [1: Single-phase, 3: Three-phase] sgRegWorkMode = 21262 // uint16 [0: Network, 2: Plug&Play, 6: EMS] - sgRegRemCtrlStatus = 21267 // uint16 + sgRegRemCtrlStatus = 21267 // uint16 [0: Disable, 1: Enable] sgRegPhaseSwitchStatus = 21269 // uint16 sgRegTotalEnergy = 21299 // uint32s 1Wh sgRegActivePower = 21307 // uint32s 1W @@ -51,7 +51,7 @@ const ( // holding sgRegSetOutI = 21202 // uint16 0.01A - sgRegPhaseSwitch = 21203 // uint16 + sgRegPhaseSwitch = 21203 // uint16 [0: Three-phase, 1: Single-phase] sgRegUnavailable = 21210 // uint16 sgRegRemoteControl = 21211 // uint16 [0: Start, 1: Stop] ) @@ -146,7 +146,7 @@ func (wb *Sungrow) Status() (api.ChargeStatus, error) { // Enabled implements the api.Charger interface func (wb *Sungrow) Enabled() (bool, error) { - b, err := wb.conn.ReadHoldingRegisters(sgRegSetOutI, 1) + b, err := wb.conn.ReadInputRegisters(sgRegStartMode, 1) if err != nil { return false, err } @@ -260,8 +260,14 @@ func (wb *Sungrow) Phases1p3p(phases int) error { } } + // Switch phases _, err = wb.conn.WriteSingleRegister(sgRegPhaseSwitch, u) + // Re-enable charging if it was previously enabled + if err == nil && enabled { + err = wb.Enable(true) + } + return err } From a014175b09f2ac42933048ad52c40e963265eafd Mon Sep 17 00:00:00 2001 From: andig Date: Mon, 6 May 2024 21:09:08 +0200 Subject: [PATCH 047/168] Load mangement: fix handling overloaded circuits (#13787) --- core/circuit.go | 17 ++++++++--------- core/circuit_test.go | 4 ++-- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/core/circuit.go b/core/circuit.go index 7feab2d66e..cab7c12e48 100644 --- a/core/circuit.go +++ b/core/circuit.go @@ -2,7 +2,6 @@ package core import ( "fmt" - "math" "sync" "github.com/evcc-io/evcc/api" @@ -176,16 +175,14 @@ func (c *Circuit) updateLoadpoints(loadpoints []api.CircuitLoad) { func (c *Circuit) updateMeters() error { if f, err := c.meter.CurrentPower(); err == nil { - // TODO handle negative powers - c.power = math.Abs(f) + c.power = f } else { return fmt.Errorf("circuit power: %w", err) } if phaseMeter, ok := c.meter.(api.PhaseCurrents); ok { if l1, l2, l3, err := phaseMeter.Currents(); err == nil { - // TODO handle negative currents - c.current = max(math.Abs(l1), math.Abs(l2), math.Abs(l3)) + c.current = max(l1, l2, l3) } else { return fmt.Errorf("circuit currents: %w", err) } @@ -246,8 +243,9 @@ func (c *Circuit) ValidatePower(old, new float64) float64 { delta := max(0, new-old) if c.maxPower != 0 { - if c.power+delta > c.maxPower { - new = max(0, new-(c.power+delta-c.maxPower)) + potential := c.maxPower - c.power + if delta > potential { + new = max(0, old+potential) c.log.DEBUG.Printf("validate power: %gW -> %gW <= %gW at %gW: capped at %gW", old, new, c.maxPower, c.power, new) } else { c.log.TRACE.Printf("validate power: %gW -> %gW <= %gW at %gW: ok", old, new, c.maxPower, c.power) @@ -270,8 +268,9 @@ func (c *Circuit) ValidateCurrent(old, new float64) float64 { delta := max(0, new-old) if c.maxCurrent != 0 { - if c.current+delta > c.maxCurrent { - new = max(0, new-(c.current+delta-c.maxCurrent)) + potential := c.maxCurrent - c.current + if delta > potential { + new = max(0, old+potential) c.log.DEBUG.Printf("validate current: %gA -> %gA <= %gA at %gA: capped at %gA", old, new, c.maxCurrent, c.current, new) } else { c.log.TRACE.Printf("validate current: %gA -> %gA <= %gA at %gA: ok", old, new, c.maxCurrent, c.current) diff --git a/core/circuit_test.go b/core/circuit_test.go index 785ba15464..7340490864 100644 --- a/core/circuit_test.go +++ b/core/circuit_test.go @@ -37,7 +37,7 @@ func circuitTests() []circuitTest { {0, 2, 0, 1, 1, 0}, // = {0, 2, 0, 2, 2, 1}, // = {0, 2, 0, 2, 3, 1}, // + - // {0, 2, 0, 2, 1, 1}, // - + {0, 2, 0, 2, 1, 1}, // - // parent loaded {1, 0, 0, 0, 0, 0}, // = @@ -52,7 +52,7 @@ func circuitTests() []circuitTest { {2, 0, 0, 1, 1, 0}, // = {2, 0, 0, 2, 2, 1}, // = {2, 0, 0, 2, 3, 1}, // + - // {2, 0, 0, 2, 1, 1}, // - + {2, 0, 0, 2, 1, 1}, // - } } From 91e146dd0e15ee6293aaafe3029b0478c32c1bc1 Mon Sep 17 00:00:00 2001 From: andig Date: Mon, 6 May 2024 21:16:51 +0200 Subject: [PATCH 048/168] chore: simplify --- core/circuit.go | 39 ++++++--------------------------------- 1 file changed, 6 insertions(+), 33 deletions(-) diff --git a/core/circuit.go b/core/circuit.go index cab7c12e48..8abda96eb3 100644 --- a/core/circuit.go +++ b/core/circuit.go @@ -252,15 +252,11 @@ func (c *Circuit) ValidatePower(old, new float64) float64 { } } - if c.parent != nil { - res := c.parent.ValidatePower(old, new) - if res != new { - c.log.TRACE.Printf("validate power: %gW -> %gW at %gW: capped by parent at %gW", old, new, c.power, res) - } - return res + if c.parent == nil { + return new } - return new + return c.parent.ValidatePower(old, new) } // ValidateCurrent validates current request @@ -277,32 +273,9 @@ func (c *Circuit) ValidateCurrent(old, new float64) float64 { } } - if c.parent != nil { - res := c.parent.ValidateCurrent(old, new) - if res != new { - c.log.TRACE.Printf("validate current: %gA -> %gA at %gA: capped by parent at %gA", old, new, c.current, res) - } - return res + if c.parent == nil { + return new } - return new + return c.parent.ValidateCurrent(old, new) } - -// func (c *Circuit) validate(typ string, current, old, new float64, parentFunc func(o, n float64) float64) float64 { -// delta := max(0, new-old) - -// if c.maxPower != 0 { -// if c.power+delta > c.maxPower { -// new = max(0, c.maxPower-c.power) -// c.log.TRACE.Printf("validate power: %g -> %g <= %g at %g: capped at %g", old, new, c.maxPower, c.power, new) -// } else { -// c.log.TRACE.Printf("validate power: %g -> %g <= %g at %g: ok", old, new, c.maxPower, c.power) -// } -// } - -// if c.parent != nil { -// return c.parent.ValidatePower(c.power, new) -// } - -// return new -// } From 3590574aa0a5f5ce7b8b4c43fb8d740efe29a3e9 Mon Sep 17 00:00:00 2001 From: andig Date: Tue, 7 May 2024 08:31:02 +0200 Subject: [PATCH 049/168] E3dc: fix external consumption --- meter/e3dc.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/meter/e3dc.go b/meter/e3dc.go index f76d6940c1..0323e7fbda 100644 --- a/meter/e3dc.go +++ b/meter/e3dc.go @@ -11,7 +11,6 @@ import ( "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/request" "github.com/evcc-io/evcc/util/templates" - "github.com/samber/lo" "github.com/sirupsen/logrus" "github.com/spali/go-rscp/rscp" "github.com/spf13/cast" @@ -128,7 +127,7 @@ func (m *E3dc) CurrentPower() (float64, error) { return 0, err } - return lo.Sum(values), nil + return values[0] - values[1], nil case templates.UsageBattery: res, err := m.conn.Send(*rscp.NewMessage(rscp.EMS_REQ_POWER_BAT, nil)) From c868e195926c1e20e22e9d380632ac3710a3d164 Mon Sep 17 00:00:00 2001 From: StefanSchoof <4662023+StefanSchoof@users.noreply.github.com> Date: Tue, 7 May 2024 09:08:58 +0200 Subject: [PATCH 050/168] chore: make clear that the log need to show the issue (#13783) --- .github/ISSUE_TEMPLATE/bug_report.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 83c06b6c0e..3fc314b843 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -47,7 +47,7 @@ body: label: Log details render: text description: > - Show evcc log output by running with evcc --log debug. The evcc service **MUST** be stopped before running this command. + Show evcc log output of the issue, see https://docs.evcc.io/en/docs/faq#how-do-i-create-a-log-file-for-error-analysis for instructions. - type: dropdown validations: From 4db34908f48ea346337f3b4b9d01c6e7fb6e6348 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20He=C3=9F?= Date: Tue, 7 May 2024 10:53:02 +0200 Subject: [PATCH 051/168] chore: add CONTRIBUTING.md (#13114) --- CONTRIBUTING.md | 121 ++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 81 +------------------------------- 2 files changed, 122 insertions(+), 80 deletions(-) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..ebab34d473 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,121 @@ +## Contribute + +To build evcc from source, [Go][1] 1.22 and [Node][2] 18 are required. + +Build and run go backend. The UI becomes available at http://127.0.0.1:7070/ + +```sh +make install-ui +make ui +make install +make +./evcc +``` + +### Debugging in VS Code + +#### evcc Core +To debug a local evcc build in VS Code, add the following entry to your `launch.json`. +You can adjust the referred configuration as needed to e.g. use your live configuration. + +```json + { + "name": "Launch evcc local build with demo config", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${workspaceFolder}", + "args": ["-c", "${workspaceFolder}/cmd/demo.yaml"], + "cwd": "${workspaceFolder}", + }, +``` + +#### Decorator +Here's another `launch.json` configuration that can be used for specifically debugging the decorator. + +```json +{ + "name": "Debug decorator", + "type": "go", + "request": "launch", + "mode": "debug", + "program": "${workspaceFolder}/cmd/tools/decorate.go", + "args": ["-o", "decorator_test.go", "-p", "main", "-f", "decorateVehicle", "-b", "api.Vehicle", "-t", "api.VehicleChargeController,StartCharge,func() error", "-t", "api.VehicleChargeController,StopCharge,func() error"], +}, +``` + +### Cross Compile + +To compile a version for an ARM device like a Raspberry Pi set GO command variables as needed, eg: + +```sh +GOOS=linux GOARCH=arm GOARM=6 make +``` + +### UI development + +For frontend development start the Vue toolchain in dev-mode. Open http://127.0.0.1:7071/ to get to the livelreloading development server. It pulls its data from port 7070 (see above). + +```sh +npm install +npm run dev +``` + +### Integration tests + +We use Playwright for end-to-end integration tests. They start a local evcc instance with different configuration yamls and prefilled databases. To run them, you have to do a local build first. + +```sh +make ui build +npm run playwright +``` + +#### Simulating device state + +Since we don't want to run tests against real devices or cloud services, we've build a simple simulator that lets you emulated meters, vehicles and loadpoints. The simulators web interface runs on http://localhost:7072. + +``` +npm run simulator +``` + +Run an evcc instance that uses simulator data. This configuration runs with a very high refresh interval to speed up testing. + +``` +make ui build +./evcc --config tests/simulator.evcc.yaml +``` + +### Code formatting + +We use linters (golangci-lint, Prettier) to keep a coherent source code formatting. It's recommended to use the format-on-save feature of your editor. For VSCode use the [Go](https://marketplace.visualstudio.com/items?itemName=golang.Go), [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) and [Vetur](https://marketplace.visualstudio.com/items?itemName=octref.vetur) extension. You can manually reformat your code by running: + +```sh +make lint +make lint-ui +``` + +### Publishing docker images + +```sh +make docker DOCKER_IMAGE=my/docker DOCKER_TAG=0815 +``` + +### Changing templates + +evcc supports a massive amount of different devices. To keep our documentation and website in sync with the latest software the core project (this repo) generates meta-data that's pushed to the `docs` and `evcc.io` repository. Make sure to update this meta-data every time you make changes to a templates. + +```sh +make docs +``` + +If you miss one of the above steps Gitub Actions will likely trigger a **Porcelain** error. + +### Adding or modifying translations + +evcc already includes many translations for the UI. Weblate Hosted is used to maintain all languages. Feel free to add more languages or verify and edit existing translations. Weblate will automatically push all modifications on a regular base to the evcc repository. + +[![Weblate Hosted](https://hosted.weblate.org/widgets/evcc/-/evcc/287x66-grey.png)](https://hosted.weblate.org/engage/evcc/) +[![Languages](https://hosted.weblate.org/widgets/evcc/-/evcc/multi-auto.svg)](https://hosted.weblate.org/engage/evcc/) + +https://hosted.weblate.org/projects/evcc/evcc/ + diff --git a/README.md b/README.md index 69d7afb4db..e0649f4abe 100644 --- a/README.md +++ b/README.md @@ -42,86 +42,7 @@ You'll find everything you need in our [documentation](https://docs.evcc.io/). ## Contribute -To build evcc from source, [Go][1] 1.22 and [Node][2] 18 are required. - -Build and run go backend. The UI becomes available at http://127.0.0.1:7070/ - -```sh -make install-ui -make ui -make install -make -./evcc -``` - -### Cross Compile - -To compile a version for an ARM device like a Raspberry Pi set GO command variables as needed, eg: - -```sh -GOOS=linux GOARCH=arm GOARM=6 make -``` - -### UI development - -For frontend development start the Vue toolchain in dev-mode. Open http://127.0.0.1:7071/ to get to the livelreloading development server. It pulls its data from port 7070 (see above). - -```sh -npm install -npm run dev -``` - -### Integration tests - -We use Playwright for end-to-end integration tests. They start a local evcc instance with different configuration yamls and prefilled databases. To run them, you have to do a local build first. - -```sh -make ui build -npm run playwright -``` - -#### Simulating device state - -Since we don't want to run tests against real devices or cloud services, we've build a simple simulator that lets you emulated meters, vehicles and loadpoints. The simulators web interface runs on http://localhost:7072. - -``` -npm run simulator -``` - -Run an evcc instance that uses simulator data. This configuration runs with a very high refresh interval to speed up testing. - -``` -make ui build -./evcc --config tests/simulator.evcc.yaml -``` - -### Code formatting - -We use linters (golangci-lint, Prettier) to keep a coherent source code formatting. It's recommended to use the format-on-save feature of your editor. For VSCode use the [Go](https://marketplace.visualstudio.com/items?itemName=golang.Go), [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) and [Vetur](https://marketplace.visualstudio.com/items?itemName=octref.vetur) extension. You can manually reformat your code by running: - -```sh -make lint -make lint-ui -``` - -### Changing templates - -evcc supports a massive amount of different devices. To keep our documentation and website in sync with the latest software the core project (this repo) generates meta-data that's pushed to the `docs` and `evcc.io` repository. Make sure to update this meta-data every time you make changes to a templates. - -```sh -make docs -``` - -If you miss one of the above steps Gitub Actions will likely trigger a **Porcelain** error. - -### Adding or modifying translations - -evcc already includes many translations for the UI. Weblate Hosted is used to maintain all languages. Feel free to add more languages or verify and edit existing translations. Weblate will automatically push all modifications on a regular base to the evcc repository. - -[![Weblate Hosted](https://hosted.weblate.org/widgets/evcc/-/evcc/287x66-grey.png)](https://hosted.weblate.org/engage/evcc/) -[![Languages](https://hosted.weblate.org/widgets/evcc/-/evcc/multi-auto.svg)](https://hosted.weblate.org/engage/evcc/) - -https://hosted.weblate.org/projects/evcc/evcc/ +Technical details on how to build evcc and contribute can be found [here](CONTRIBUTING.md). ## Sponsorship From c92bb4b6cb9cc9509f0f7130d3d64837ad23fddc Mon Sep 17 00:00:00 2001 From: andig Date: Tue, 7 May 2024 11:07:24 +0200 Subject: [PATCH 052/168] chore: minor --- CONTRIBUTING.md | 10 +++++++--- README.md | 7 ++----- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ebab34d473..f845ac547b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,4 @@ -## Contribute +## Contributing To build evcc from source, [Go][1] 1.22 and [Node][2] 18 are required. @@ -15,8 +15,9 @@ make ### Debugging in VS Code #### evcc Core + To debug a local evcc build in VS Code, add the following entry to your `launch.json`. -You can adjust the referred configuration as needed to e.g. use your live configuration. +You can adjust the referred configuration as needed to e.g. use your live configuration. ```json { @@ -30,7 +31,8 @@ You can adjust the referred configuration as needed to e.g. use your live config }, ``` -#### Decorator +#### Decorators + Here's another `launch.json` configuration that can be used for specifically debugging the decorator. ```json @@ -119,3 +121,5 @@ evcc already includes many translations for the UI. Weblate Hosted is used to ma https://hosted.weblate.org/projects/evcc/evcc/ +[1]: https://go.dev +[2]: https://nodejs.org/ diff --git a/README.md b/README.md index e0649f4abe..ad03f84634 100644 --- a/README.md +++ b/README.md @@ -40,9 +40,9 @@ evcc is an extensible EV Charge Controller and home energy management system. Fe You'll find everything you need in our [documentation](https://docs.evcc.io/). -## Contribute +## Contributing -Technical details on how to build evcc and contribute can be found [here](CONTRIBUTING.md). +Technical details on how to contribute, how to add translations and how to build evcc from source can be found [here](CONTRIBUTING.md). ## Sponsorship @@ -54,6 +54,3 @@ Maintaining evcc consumes time and effort. With the vast amount of different dev While evcc is open source, we would also like to encourage vendors to provide open source hardware devices, public documentation and support open source projects like ours that provide additional value to otherwise closed hardware. Where this is not the case, evcc requires "sponsor token" to finance ongoing development and support of evcc. The personal sponsor token requires a [Github Sponsorship](https://github.com/sponsors/evcc-io) and can be requested at [sponsor.evcc.io](https://sponsor.evcc.io/). - -[1]: https://go.dev -[2]: https://nodejs.org/ From b3435035af50990677ad7f5105245acaaf141388 Mon Sep 17 00:00:00 2001 From: andig Date: Tue, 7 May 2024 11:19:55 +0200 Subject: [PATCH 053/168] chore: minor --- CONTRIBUTING.md | 99 +++++++++++++++++++++---------------------------- README.md | 2 + 2 files changed, 45 insertions(+), 56 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f845ac547b..42e770921b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,17 +1,55 @@ ## Contributing -To build evcc from source, [Go][1] 1.22 and [Node][2] 18 are required. +### Developing -Build and run go backend. The UI becomes available at http://127.0.0.1:7070/ +#### Development environment + +Developing evcc requires [Go][1] 1.22 and [Node][2] 18. We recommend VSCode with the [Go](https://marketplace.visualstudio.com/items?itemName=golang.Go), [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) and [Vetur](https://marketplace.visualstudio.com/items?itemName=octref.vetur) extensions. + +We use linters (golangci-lint, Prettier) to keep a coherent source code formatting. It's recommended to use the format-on-save feature of your editor. You can manually reformat your code by running: + +```sh +make lint +make lint-ui +``` + +#### Changing device templates + +evcc supports a massive amount of different devices. To keep our documentation and website in sync with the latest software the core project (this repo) generates meta-data that's pushed to the `docs` and `evcc.io` repository. Make sure to update this meta-data every time you make changes to a templates. + +```sh +make docs +``` + +If you miss one of the above steps Gitub Actions will likely trigger a **Porcelain** error. + +### Building from source + +To build and run the evcc go backend use: ```sh make install-ui -make ui make install make ./evcc ``` +The UI becomes available at http://127.0.0.1:7070/ + +#### Cross Compiling + +To compile a version for an ARM device like a Raspberry Pi set GO command variables as needed, eg: + +```sh +GOOS=linux GOARCH=arm GOARM=6 make +``` + +#### Publishing docker images + +```sh +make docker DOCKER_IMAGE=my/docker DOCKER_TAG=0815 +``` + ### Debugging in VS Code #### evcc Core @@ -31,29 +69,6 @@ You can adjust the referred configuration as needed to e.g. use your live config }, ``` -#### Decorators - -Here's another `launch.json` configuration that can be used for specifically debugging the decorator. - -```json -{ - "name": "Debug decorator", - "type": "go", - "request": "launch", - "mode": "debug", - "program": "${workspaceFolder}/cmd/tools/decorate.go", - "args": ["-o", "decorator_test.go", "-p", "main", "-f", "decorateVehicle", "-b", "api.Vehicle", "-t", "api.VehicleChargeController,StartCharge,func() error", "-t", "api.VehicleChargeController,StopCharge,func() error"], -}, -``` - -### Cross Compile - -To compile a version for an ARM device like a Raspberry Pi set GO command variables as needed, eg: - -```sh -GOOS=linux GOARCH=arm GOARM=6 make -``` - ### UI development For frontend development start the Vue toolchain in dev-mode. Open http://127.0.0.1:7071/ to get to the livelreloading development server. It pulls its data from port 7070 (see above). @@ -63,7 +78,7 @@ npm install npm run dev ``` -### Integration tests +#### Integration testing We use Playwright for end-to-end integration tests. They start a local evcc instance with different configuration yamls and prefilled databases. To run them, you have to do a local build first. @@ -87,39 +102,11 @@ make ui build ./evcc --config tests/simulator.evcc.yaml ``` -### Code formatting - -We use linters (golangci-lint, Prettier) to keep a coherent source code formatting. It's recommended to use the format-on-save feature of your editor. For VSCode use the [Go](https://marketplace.visualstudio.com/items?itemName=golang.Go), [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) and [Vetur](https://marketplace.visualstudio.com/items?itemName=octref.vetur) extension. You can manually reformat your code by running: - -```sh -make lint -make lint-ui -``` - -### Publishing docker images - -```sh -make docker DOCKER_IMAGE=my/docker DOCKER_TAG=0815 -``` - -### Changing templates - -evcc supports a massive amount of different devices. To keep our documentation and website in sync with the latest software the core project (this repo) generates meta-data that's pushed to the `docs` and `evcc.io` repository. Make sure to update this meta-data every time you make changes to a templates. - -```sh -make docs -``` - -If you miss one of the above steps Gitub Actions will likely trigger a **Porcelain** error. - ### Adding or modifying translations -evcc already includes many translations for the UI. Weblate Hosted is used to maintain all languages. Feel free to add more languages or verify and edit existing translations. Weblate will automatically push all modifications on a regular base to the evcc repository. +evcc already includes many translations for the UI. We're using [Weblate](https://hosted.weblate.org/projects/evcc/evcc/) to maintain translations. Feel free to add more languages or verify and edit existing translations. Weblate will automatically push all modifications to the evcc repository where they get reviewed and merged. -[![Weblate Hosted](https://hosted.weblate.org/widgets/evcc/-/evcc/287x66-grey.png)](https://hosted.weblate.org/engage/evcc/) [![Languages](https://hosted.weblate.org/widgets/evcc/-/evcc/multi-auto.svg)](https://hosted.weblate.org/engage/evcc/) -https://hosted.weblate.org/projects/evcc/evcc/ - [1]: https://go.dev [2]: https://nodejs.org/ diff --git a/README.md b/README.md index ad03f84634..3150a206b8 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,8 @@ You'll find everything you need in our [documentation](https://docs.evcc.io/). Technical details on how to contribute, how to add translations and how to build evcc from source can be found [here](CONTRIBUTING.md). +[![Weblate Hosted](https://hosted.weblate.org/widgets/evcc/-/evcc/287x66-grey.png)](https://hosted.weblate.org/engage/evcc/) + ## Sponsorship From 59519ada196b5dcda56df2bb02384893dbf61b4b Mon Sep 17 00:00:00 2001 From: andig Date: Tue, 7 May 2024 11:24:46 +0200 Subject: [PATCH 054/168] chore: minor --- CONTRIBUTING.md | 35 ++++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 42e770921b..e8ddf5e15a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -25,16 +25,25 @@ If you miss one of the above steps Gitub Actions will likely trigger a **Porcela ### Building from source -To build and run the evcc go backend use: +Install prerequisites (once): ```sh make install-ui make install +``` + +Build and run: + +```sh make ./evcc ``` -The UI becomes available at http://127.0.0.1:7070/ +Open UI at http://127.0.0.1:7070 + +To run without creating the `evcc` binary use: + + go run ./... #### Cross Compiling @@ -58,20 +67,20 @@ To debug a local evcc build in VS Code, add the following entry to your `launch. You can adjust the referred configuration as needed to e.g. use your live configuration. ```json - { - "name": "Launch evcc local build with demo config", - "type": "go", - "request": "launch", - "mode": "auto", - "program": "${workspaceFolder}", - "args": ["-c", "${workspaceFolder}/cmd/demo.yaml"], - "cwd": "${workspaceFolder}", - }, +{ + "name": "Launch evcc local build with demo config", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${workspaceFolder}", + "args": ["-c", "${workspaceFolder}/cmd/demo.yaml"], + "cwd": "${workspaceFolder}", +}, ``` -### UI development +#### UI -For frontend development start the Vue toolchain in dev-mode. Open http://127.0.0.1:7071/ to get to the livelreloading development server. It pulls its data from port 7070 (see above). +For frontend development start the Vue toolchain in dev-mode. Open http://127.0.0.1:7071/ to get to the live reloading development server. It pulls its data from port 7070 (see above). ```sh npm install From 1ff1513c38d3ca4dc249582c7249317c9d31ee13 Mon Sep 17 00:00:00 2001 From: Simon Schenk Date: Tue, 7 May 2024 13:50:42 +0200 Subject: [PATCH 055/168] Bluelink: fix refresh for old bluelink API (#13785) --- vehicle/bluelink/api.go | 2 +- vehicle/bluelink/provider.go | 10 +++---- vehicle/bluelink/types.go | 51 ++++++++++++++++++++---------------- 3 files changed, 34 insertions(+), 29 deletions(-) diff --git a/vehicle/bluelink/api.go b/vehicle/bluelink/api.go index 870708ea85..339e1808ef 100644 --- a/vehicle/bluelink/api.go +++ b/vehicle/bluelink/api.go @@ -108,5 +108,5 @@ func (v *API) StatusPartial(vehicle Vehicle) (BluelinkVehicleStatus, error) { if err == nil && res.RetCode != resOK { err = fmt.Errorf("unexpected response: %s", res.RetCode) } - return res, err + return res.ResMsg, err } diff --git a/vehicle/bluelink/provider.go b/vehicle/bluelink/provider.go index 86a526f507..bf9344ac02 100644 --- a/vehicle/bluelink/provider.go +++ b/vehicle/bluelink/provider.go @@ -30,7 +30,7 @@ func NewProvider(api *API, vehicle Vehicle, expiry, cache time.Duration) *Provid v.statusG = provider.Cached(func() (BluelinkVehicleStatus, error) { return v.status( - func() (BluelinkVehicleStatus, error) { return api.StatusPartial(vehicle) }, + func() (BluelinkVehicleStatusLatest, error) { return api.StatusLatest(vehicle) }, ) }, cache) @@ -42,20 +42,20 @@ func NewProvider(api *API, vehicle Vehicle, expiry, cache time.Duration) *Provid } // status wraps the api status call and adds status refresh -func (v *Provider) status(statusG func() (BluelinkVehicleStatus, error)) (BluelinkVehicleStatus, error) { +func (v *Provider) status(statusG func() (BluelinkVehicleStatusLatest, error)) (BluelinkVehicleStatus, error) { res, err := statusG() var ts time.Time if err == nil { - ts, err = res.Updated() + ts, err = res.BluelinkVehicleStatus().Updated() if err != nil { - return res, err + return res.BluelinkVehicleStatus(), err } // return the current value if time.Since(ts) <= v.expiry { v.refreshTime = time.Time{} - return res, nil + return res.BluelinkVehicleStatus(), nil } } diff --git a/vehicle/bluelink/types.go b/vehicle/bluelink/types.go index 13e5576859..06ee85d9a9 100644 --- a/vehicle/bluelink/types.go +++ b/vehicle/bluelink/types.go @@ -17,6 +17,7 @@ type BluelinkVehicleStatus interface { } type BluelinkVehicleStatusLatest interface { + BluelinkVehicleStatus() BluelinkVehicleStatus Odometer() (float64, error) Position() (float64, float64, error) } @@ -83,10 +84,6 @@ const ( plugTypeAC = 1 ) -func (d *VehicleStatus) Updated() (time.Time, error) { - return time.Parse(timeFormat, d.Time+timeOffset) -} - type DrivingDistance struct { RangeByFuel struct { EvModeRange struct { @@ -104,25 +101,25 @@ type TargetSoc struct { PlugType int } -func (d StatusResponse) Updated() (time.Time, error) { - return time.Parse(timeFormat, d.ResMsg.Time+timeOffset) +func (d VehicleStatus) Updated() (time.Time, error) { + return time.Parse(timeFormat, d.Time+timeOffset) } -func (d StatusResponse) SoC() (float64, error) { - if d.ResMsg.EvStatus != nil { - return d.ResMsg.EvStatus.BatteryStatus, nil +func (d VehicleStatus) SoC() (float64, error) { + if d.EvStatus != nil { + return d.EvStatus.BatteryStatus, nil } return 0, api.ErrNotAvailable } -func (d StatusResponse) Status() (api.ChargeStatus, error) { - if d.ResMsg.EvStatus != nil { +func (d VehicleStatus) Status() (api.ChargeStatus, error) { + if d.EvStatus != nil { status := api.StatusA - if d.ResMsg.EvStatus.BatteryPlugin > 0 || d.ResMsg.EvStatus.ChargePortDoorOpenStatus == 1 { + if d.EvStatus.BatteryPlugin > 0 || d.EvStatus.ChargePortDoorOpenStatus == 1 { status = api.StatusB } - if d.ResMsg.EvStatus.BatteryCharge { + if d.EvStatus.BatteryCharge { status = api.StatusC } return status, nil @@ -131,12 +128,12 @@ func (d StatusResponse) Status() (api.ChargeStatus, error) { return api.StatusNone, api.ErrNotAvailable } -func (d StatusResponse) FinishTime() (time.Time, error) { - if d.ResMsg.EvStatus != nil { - remaining := d.ResMsg.EvStatus.RemainTime2.Atc.Value +func (d VehicleStatus) FinishTime() (time.Time, error) { + if d.EvStatus != nil { + remaining := d.EvStatus.RemainTime2.Atc.Value if remaining != 0 { - ts, err := d.ResMsg.Updated() + ts, err := d.Updated() return ts.Add(time.Duration(remaining) * time.Minute), err } } @@ -144,18 +141,18 @@ func (d StatusResponse) FinishTime() (time.Time, error) { return time.Time{}, api.ErrNotAvailable } -func (d StatusResponse) Range() (int64, error) { - if d.ResMsg.EvStatus != nil { - if dist := d.ResMsg.EvStatus.DrvDistance; len(dist) == 1 { +func (d VehicleStatus) Range() (int64, error) { + if d.EvStatus != nil { + if dist := d.EvStatus.DrvDistance; len(dist) == 1 { return int64(dist[0].RangeByFuel.EvModeRange.Value), nil } } return 0, api.ErrNotAvailable } -func (d StatusResponse) GetLimitSoc() (int64, error) { - if d.ResMsg.EvStatus != nil { - for _, targetSOC := range d.ResMsg.EvStatus.ReservChargeInfos.TargetSocList { +func (d VehicleStatus) GetLimitSoc() (int64, error) { + if d.EvStatus != nil { + for _, targetSOC := range d.EvStatus.ReservChargeInfos.TargetSocList { if targetSOC.PlugType == plugTypeAC { return int64(targetSOC.TargetSocLevel), nil } @@ -164,6 +161,10 @@ func (d StatusResponse) GetLimitSoc() (int64, error) { return 0, api.ErrNotAvailable } +func (d StatusLatestResponse) BluelinkVehicleStatus() BluelinkVehicleStatus { + return d.ResMsg.VehicleStatusInfo.VehicleStatus +} + func (d StatusLatestResponse) Odometer() (float64, error) { if d.ResMsg.VehicleStatusInfo.Odometer != nil { return d.ResMsg.VehicleStatusInfo.Odometer.Value, nil @@ -271,6 +272,10 @@ type VehicleStatusCCS struct { } } +func (d StatusLatestResponseCCS) BluelinkVehicleStatus() BluelinkVehicleStatus { + return d +} + func (d StatusLatestResponseCCS) Updated() (time.Time, error) { epoch, err := strconv.ParseInt(d.ResMsg.LastUpdateTime, 10, 64) if err != nil { From 81ba7dcc29945edf97dc78a1cf6bd5affef6503d Mon Sep 17 00:00:00 2001 From: mdkeil Date: Tue, 7 May 2024 18:48:24 +0200 Subject: [PATCH 056/168] Add Frauenhofer ISE energy-charts-api | day ahead price (#13706) --- main.go | 4 +- provider/http.go | 14 ++++++- .../definition/tariff/energy-charts-api.yaml | 41 +++++++++++++++++++ 3 files changed, 56 insertions(+), 3 deletions(-) create mode 100644 templates/definition/tariff/energy-charts-api.yaml diff --git a/main.go b/main.go index 4900597272..60f47f1952 100644 --- a/main.go +++ b/main.go @@ -12,10 +12,10 @@ import ( ) var ( - //go:embed dist + // go:embed dist web embed.FS - //go:embed i18n/*.toml + // go:embed i18n/*.toml i18n embed.FS ) diff --git a/provider/http.go b/provider/http.go index 5a4dc0b1e2..a8c1a190fa 100644 --- a/provider/http.go +++ b/provider/http.go @@ -6,8 +6,10 @@ import ( "math" "strconv" "strings" + "text/template" "time" + "github.com/42atomys/sprout" "github.com/evcc-io/evcc/provider/pipeline" "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/request" @@ -162,8 +164,18 @@ func (p *HTTP) request(url string, body ...string) ([]byte, error) { b = strings.NewReader(body[0]) } + tmpl, err := template.New("url").Funcs(sprout.TxtFuncMap()).Parse(url) + if err != nil { + return nil, err + } + + builder := new(strings.Builder) + if err := tmpl.Execute(builder, nil); err != nil { + return nil, err + } + // empty method becomes GET - req, err := request.New(strings.ToUpper(p.method), url, b, p.headers) + req, err := request.New(strings.ToUpper(p.method), builder.String(), b, p.headers) if err != nil { return []byte{}, err } diff --git a/templates/definition/tariff/energy-charts-api.yaml b/templates/definition/tariff/energy-charts-api.yaml new file mode 100644 index 0000000000..0935da2d40 --- /dev/null +++ b/templates/definition/tariff/energy-charts-api.yaml @@ -0,0 +1,41 @@ +template: energy-charts-api +products: + - brand: Fraunhofer ISE + description: + de: "Day-ahead Energiepreise (je kWh) an der Börse. Kann ohne vorherige Anmeldung auf https://api.energy-charts.info/ abgerufen werden. Nutzbar u.a. für dynamische Stromtarife, wo der Anbieter bis dato auf der Kundenschnittstelle noch kein Preis-Vorhersagen anbietet." + en: "Day-ahead forecast of energy prices (per kWh) on the exchange. No prior registration for https://api.energy-charts.info/ necessary. Can be used for dynamic electricity tariffs, for example, where the supplier does not yet offer a price forecast on the customer interface." +params: + - name: bzn + type: string + required: true + validvalues: + [ + "AT", + "BE", + "CH", + "CZ", + "DE-LU", + "DE-AT-LU", + "DK1", + "DK2", + "FR", + "HU", + "IT-NORTH", + "NL", + "NO2", + "PL", + "SE4", + "SI", + ] + default: DE-LU + description: + de: "Gebotszonen - https://api.energy-charts.info/#/prices/day_ahead_price_price_get" + en: "Bidding zones - https://api.energy-charts.info/#/prices/day_ahead_price_price_get" + - preset: tariff-base +render: | + type: custom + {{ include "tariff-base" . }} + forecast: + source: http + uri: https://api.energy-charts.info/price?bzn={{ .bzn }}&end={{ now | dateModify "+24h" | date "2006-01-02" }} + jq: '[.unix_seconds, .price] | transpose | map({ "start": (.[0] | todate), "end": ((.[0]+3600) | todate), "price": (.[1]/1000)}) | tostring' From 8ba04a6a2266bf0bad01eccef611bfbdfa7e669d Mon Sep 17 00:00:00 2001 From: andig Date: Tue, 7 May 2024 19:10:06 +0200 Subject: [PATCH 057/168] chore: add more session logging --- core/loadpoint_session.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/core/loadpoint_session.go b/core/loadpoint_session.go index c58bec59d6..008ef92d32 100644 --- a/core/loadpoint_session.go +++ b/core/loadpoint_session.go @@ -63,6 +63,9 @@ func (lp *Loadpoint) stopSession() { } if chargedEnergy := lp.getChargedEnergy() / 1e3; chargedEnergy > s.ChargedEnergy { + { // TODO remove + lp.log.DEBUG.Printf("session: chargedEnergy=%.1f", s.ChargedEnergy) + } lp.sessionEnergy.Update(chargedEnergy) } @@ -74,6 +77,17 @@ func (lp *Loadpoint) stopSession() { s.ChargedEnergy = lp.sessionEnergy.TotalWh() / 1e3 s.ChargeDuration = &lp.chargeDuration + { // TODO remove + var meterStart, meterStop float64 + if s.MeterStart != nil { + meterStart = *s.MeterStart + } + if s.MeterStop != nil { + meterStop = *s.MeterStop + } + lp.log.DEBUG.Printf("session: start=%.3f stop=%.3f chargedEnergy=%.3f", meterStart, meterStop, s.ChargedEnergy) + } + lp.db.Persist(s) } From 72fbbd1bdb00fc7e424af58d2103c838843d94a5 Mon Sep 17 00:00:00 2001 From: andig Date: Tue, 7 May 2024 20:40:30 +0200 Subject: [PATCH 058/168] chore: add more session logging --- core/loadpoint.go | 3 +++ core/loadpoint_session.go | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/core/loadpoint.go b/core/loadpoint.go index 200caf8ac1..a19366c75a 100644 --- a/core/loadpoint.go +++ b/core/loadpoint.go @@ -1416,6 +1416,9 @@ func (lp *Loadpoint) publishChargeProgress() { // workaround for Go-E resetting during disconnect, see // https://github.com/evcc-io/evcc/issues/5092 if f > lp.chargedAtStartup { + { // TODO remove + lp.log.DEBUG.Printf("!! session: chargeRater.chargedEnergy=%.1f - chargedAtStartup=%.1f", f, lp.chargedAtStartup) + } added, addedGreen := lp.sessionEnergy.Update(f - lp.chargedAtStartup) if telemetry.Enabled() && added > 0 { telemetry.UpdateEnergy(added, addedGreen) diff --git a/core/loadpoint_session.go b/core/loadpoint_session.go index 008ef92d32..f72b2f0910 100644 --- a/core/loadpoint_session.go +++ b/core/loadpoint_session.go @@ -64,7 +64,7 @@ func (lp *Loadpoint) stopSession() { if chargedEnergy := lp.getChargedEnergy() / 1e3; chargedEnergy > s.ChargedEnergy { { // TODO remove - lp.log.DEBUG.Printf("session: chargedEnergy=%.1f", s.ChargedEnergy) + lp.log.DEBUG.Printf("!! session: chargedEnergy=%.1f > chargedEnergy=%.1f", chargedEnergy, s.ChargedEnergy) } lp.sessionEnergy.Update(chargedEnergy) } @@ -85,7 +85,7 @@ func (lp *Loadpoint) stopSession() { if s.MeterStop != nil { meterStop = *s.MeterStop } - lp.log.DEBUG.Printf("session: start=%.3f stop=%.3f chargedEnergy=%.3f", meterStart, meterStop, s.ChargedEnergy) + lp.log.DEBUG.Printf("!! session: start=%.3f stop=%.3f chargedEnergy=%.3f", meterStart, meterStop, s.ChargedEnergy) } lp.db.Persist(s) From d9b3d46633ccbf730ed43ac45ec4424c284fa658 Mon Sep 17 00:00:00 2001 From: duck Date: Tue, 7 May 2024 20:09:35 +0100 Subject: [PATCH 059/168] tariff/octopusenergy: fix parsing of tariff setting (#13799) --- tariff/octopus.go | 2 +- tariff/octopus_test.go | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 tariff/octopus_test.go diff --git a/tariff/octopus.go b/tariff/octopus.go index 1a496573ad..eb69484a70 100644 --- a/tariff/octopus.go +++ b/tariff/octopus.go @@ -48,7 +48,7 @@ func NewOctopusFromConfig(other map[string]interface{}) (api.Tariff, error) { if cc.Region == "" { return nil, errors.New("missing region") } - if cc.Tariff == "" { + if cc.Tariff != "" { // deprecated - copy to correct slot and WARN logger.WARN.Print("'tariff' is deprecated and will break in a future version - use 'productCode' instead") cc.ProductCode = cc.Tariff diff --git a/tariff/octopus_test.go b/tariff/octopus_test.go new file mode 100644 index 0000000000..30a4f52451 --- /dev/null +++ b/tariff/octopus_test.go @@ -0,0 +1,34 @@ +package tariff + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestOctopusConfigParse(t *testing.T) { + // This test will start failing if you remove the deprecated "tariff" config var. + validTariffConfig := map[string]interface{}{ + "region": "H", + "tariff": "GO-22-03-29", + } + + _, err := NewOctopusFromConfig(validTariffConfig) + require.NoError(t, err) + + validProductCodeConfig := map[string]interface{}{ + "region": "H", + "productcode": "GO-22-03-29", + } + + _, err = NewOctopusFromConfig(validProductCodeConfig) + require.NoError(t, err) + + invalidApiAndProductCodeConfig := map[string]interface{}{ + "region": "H", + "productcode": "GO-22-03-29", + "apikey": "nope", + } + _, err = NewOctopusFromConfig(invalidApiAndProductCodeConfig) + require.Error(t, err) +} From 52ca35f248bf491425df1e13f8691d52b2ca7c7a Mon Sep 17 00:00:00 2001 From: andig Date: Wed, 8 May 2024 08:35:47 +0200 Subject: [PATCH 060/168] chore: fix broken imports --- main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main.go b/main.go index 60f47f1952..4900597272 100644 --- a/main.go +++ b/main.go @@ -12,10 +12,10 @@ import ( ) var ( - // go:embed dist + //go:embed dist web embed.FS - // go:embed i18n/*.toml + //go:embed i18n/*.toml i18n embed.FS ) From b9738abf1531c37f5c23ec9e0859739a94e06b2f Mon Sep 17 00:00:00 2001 From: andig Date: Wed, 8 May 2024 11:45:37 +0200 Subject: [PATCH 061/168] Custom vehicle: lower case parameters identical to plugin name (#13804) --- templates/definition/vehicle/teslalogger.yaml | 2 +- util/format.go | 4 ++-- util/format_test.go | 14 +++++++------- vehicle/vehicle.go | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/templates/definition/vehicle/teslalogger.yaml b/templates/definition/vehicle/teslalogger.yaml index f1e65b8945..691f061559 100644 --- a/templates/definition/vehicle/teslalogger.yaml +++ b/templates/definition/vehicle/teslalogger.yaml @@ -78,7 +78,7 @@ render: | uri: {{ .url }}:{{ .port }}/command/{{ .id }}/wake_up chargeEnable: source: http - uri: {{ .url }}:{{ .port }}/command/{{ .id }}/charge_start_stop?${enable} + uri: {{ .url }}:{{ .port }}/command/{{ .id }}/charge_start_stop?${chargeenable} maxcurrent: source: http uri: {{ .url }}:{{ .port }}/command/{{ .id }}/set_charging_amps?${maxcurrent} diff --git a/util/format.go b/util/format.go index a92dfa9034..8f1cb5dc6a 100644 --- a/util/format.go +++ b/util/format.go @@ -11,7 +11,7 @@ import ( "github.com/42atomys/sprout" ) -var re = regexp.MustCompile(`\${(\w+)(:([a-zA-Z0-9%.]+))?}`) +var re = regexp.MustCompile(`(?i)\${(\w+)(:([a-zA-Z0-9%.]+))?}`) // Truish returns true if value is truish (true/1/on) func Truish(s string) bool { @@ -73,7 +73,7 @@ func ReplaceFormatted(s string, kv map[string]interface{}) (string, error) { match, key, format := m[0], m[1], m[3] // find key and replacement value - val, ok := kv[key] + val, ok := kv[strings.ToLower(key)] if !ok { wanted = append(wanted, key) format = "%s" diff --git a/util/format_test.go b/util/format_test.go index 7cc23bcd8e..da21fdcd39 100644 --- a/util/format_test.go +++ b/util/format_test.go @@ -4,6 +4,9 @@ import ( "math" "testing" "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestTruish(t *testing.T) { @@ -21,10 +24,7 @@ func TestTruish(t *testing.T) { } for _, c := range cases { - b := Truish(c.k) - if b != c.v { - t.Errorf("expected %v got %v", c.v, b) - } + assert.Equal(t, c.v, Truish(c.k)) } } @@ -36,6 +36,7 @@ func TestReplace(t *testing.T) { }{ // regex tests {"foo", true, "${foo}", "true"}, + {"foo", true, "${Foo}", "true"}, {"foo", "1", "abc${foo}${foo}", "abc11"}, {"foo", math.Pi, "${foo:%.2f}", "3.14"}, {"foo", math.Pi, "${foo:%.0f}%", "3%"}, @@ -47,9 +48,8 @@ func TestReplace(t *testing.T) { c.k: c.v, }) - if s != c.expected || err != nil { - t.Error(s, err) - } + require.NoError(t, err) + assert.Equal(t, c.expected, s) } } diff --git a/vehicle/vehicle.go b/vehicle/vehicle.go index 6c56fd402a..4f7a2d858c 100644 --- a/vehicle/vehicle.go +++ b/vehicle/vehicle.go @@ -107,7 +107,7 @@ func NewConfigurableFromConfig(other map[string]interface{}) (api.Vehicle, error // decorate maxCurrent var maxCurrent func(int64) error if cc.MaxCurrent != nil { - maxCurrent, err = provider.NewIntSetterFromConfig("maxCurrent", *cc.MaxCurrent) + maxCurrent, err = provider.NewIntSetterFromConfig("maxcurrent", *cc.MaxCurrent) if err != nil { return nil, fmt.Errorf("maxCurrent: %w", err) } @@ -153,7 +153,7 @@ func NewConfigurableFromConfig(other map[string]interface{}) (api.Vehicle, error // decorate chargeEnable var chargeEnable func(bool) error if cc.ChargeEnable != nil { - chargeEnable, err = provider.NewBoolSetterFromConfig("chargeEnable", *cc.ChargeEnable) + chargeEnable, err = provider.NewBoolSetterFromConfig("chargeenable", *cc.ChargeEnable) if err != nil { return nil, fmt.Errorf("chargeEnable: %w", err) } From f0e313c6d4c4f62f21a0191108c320d5dfd225fd Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Wed, 8 May 2024 11:49:04 +0200 Subject: [PATCH 062/168] chore: run playwright tests in parallel (#13809) --- .github/workflows/default.yml | 9 +- playwright.config.js | 35 ++++--- tests/auth.spec.js | 4 +- tests/basics.spec.js | 4 +- tests/config-meters.spec.js | 85 ++++++++++++++++ tests/config-vehicles.spec.js | 132 +++++++++++++++++++++++++ tests/config.spec.js | 172 +-------------------------------- tests/evcc.js | 42 +++++--- tests/heating.spec.js | 4 +- tests/limits.spec.js | 18 ++-- tests/logs.spec.js | 4 +- tests/modals.spec.js | 9 +- tests/plan.spec.js | 4 +- tests/sessions.spec.js | 4 +- tests/simulator.js | 35 +++++-- tests/smart-cost.spec.js | 10 +- tests/statistics.spec.js | 4 +- tests/vehicle-error.spec.js | 4 +- tests/vehicle-settings.spec.js | 14 +-- 19 files changed, 346 insertions(+), 247 deletions(-) create mode 100644 tests/config-meters.spec.js create mode 100644 tests/config-vehicles.spec.js diff --git a/.github/workflows/default.yml b/.github/workflows/default.yml index 95dcaa5a8b..de7f2441ea 100644 --- a/.github/workflows/default.yml +++ b/.github/workflows/default.yml @@ -177,11 +177,6 @@ jobs: integration: name: Integration runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - shardIndex: [1, 2] - shardTotal: [2] steps: - uses: actions/checkout@v4 @@ -224,7 +219,7 @@ jobs: run: npx playwright install --with-deps chromium - name: Run tests - run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} + run: npx playwright test # - name: Run tests # uses: docker://mcr.microsoft.com/playwright:v1.34.3-jammy @@ -234,6 +229,6 @@ jobs: - uses: actions/upload-artifact@v4 if: ${{ !cancelled() }} with: - name: playwright-report-${{ matrix.shardIndex }} + name: playwright-report path: playwright-report/ retention-days: 14 diff --git a/playwright.config.js b/playwright.config.js index 17932c99da..c10ac944db 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -4,22 +4,21 @@ import { defineConfig, devices } from "@playwright/test"; * @see https://playwright.dev/docs/test-configuration */ export default defineConfig({ - testDir: "./tests", - fullyParallel: false, - forbidOnly: !!process.env.CI, - retries: process.env.CI ? 2 : 0, - timeout: 15000, // 15s (default 30s) - expect: { timeout: 2500 }, // 2.5s (default 5s) - workers: 1, // run testfiles serially to avoid port and database conflicts - reporter: "html", - use: { - baseURL: "http://127.0.0.1:7070", - trace: "on-first-retry", - }, - projects: [ - { - name: "chromium", - use: { ...devices["Desktop Chrome"] }, - }, - ], + testDir: "./tests", + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + timeout: 15000, // 15s (default 30s) + expect: { timeout: 2500 }, // 2.5s (default 5s) + workers: process.env.CI ? 3 : 8, + reporter: "html", + use: { + baseURL: "http://127.0.0.1:7070", + trace: "on-first-retry", + }, + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + ], }); diff --git a/tests/auth.spec.js b/tests/auth.spec.js index 23620d8fb3..e7163cff58 100644 --- a/tests/auth.spec.js +++ b/tests/auth.spec.js @@ -1,5 +1,7 @@ import { test, expect } from "@playwright/test"; -import { start, stop } from "./evcc"; +import { start, stop, baseUrl } from "./evcc"; + +test.use({ baseURL: baseUrl() }); test.beforeEach(async ({ page }) => { await start("basics.evcc.yaml"); diff --git a/tests/basics.spec.js b/tests/basics.spec.js index 490d7c795e..4a85041430 100644 --- a/tests/basics.spec.js +++ b/tests/basics.spec.js @@ -1,5 +1,7 @@ import { test, expect } from "@playwright/test"; -import { start, stop } from "./evcc"; +import { start, stop, baseUrl } from "./evcc"; + +test.use({ baseURL: baseUrl() }); test.beforeAll(async () => { await start("basics.evcc.yaml", "password.sql"); diff --git a/tests/config-meters.spec.js b/tests/config-meters.spec.js new file mode 100644 index 0000000000..fa38234295 --- /dev/null +++ b/tests/config-meters.spec.js @@ -0,0 +1,85 @@ +import { test, expect } from "@playwright/test"; +import { start, stop, restart, baseUrl } from "./evcc"; +import { startSimulator, stopSimulator, simulatorUrl, simulatorHost } from "./simulator"; + +const CONFIG_EMPTY = "config-empty.evcc.yaml"; + +test.use({ baseURL: baseUrl() }); + +test.beforeAll(async () => { + await start(CONFIG_EMPTY, "password.sql"); + await startSimulator(); +}); +test.afterAll(async () => { + await stop(); + await stopSimulator(); +}); + +async function login(page) { + await page.locator("#loginPassword").fill("secret"); + await page.getByRole("button", { name: "Login" }).click(); +} + +async function enableExperimental(page) { + await page + .getByTestId("generalconfig-experimental") + .getByRole("link", { name: "change" }) + .click(); + await page.getByLabel("Experimental 🧪").click(); + await page.getByRole("button", { name: "Close" }).click(); +} + +test.describe("meters", async () => { + test("create, edit and remove battery meter", async ({ page }) => { + // setup test data for mock openems api + await page.goto(simulatorUrl()); + await page.getByLabel("Battery Power").fill("-2500"); + await page.getByLabel("Battery SoC").fill("75"); + await page.getByRole("button", { name: "Apply changes" }).click(); + + await page.goto("/#/config"); + await login(page); + await enableExperimental(page); + + await expect(page.getByTestId("battery")).toHaveCount(0); + + // create #1 + await page.getByRole("button", { name: "Add solar or battery" }).click(); + + const meterModal = page.getByTestId("meter-modal"); + await meterModal.getByRole("button", { name: "Add battery meter" }).click(); + await meterModal.getByLabel("Manufacturer").selectOption("OpenEMS"); + await meterModal.getByLabel("IP address or hostname").fill(simulatorHost()); + await expect(meterModal.getByRole("button", { name: "Validate & save" })).toBeVisible(); + await meterModal.getByRole("link", { name: "validate" }).click(); + await expect(meterModal.getByText("SoC: 75.0%")).toBeVisible(); + await expect(meterModal.getByText("Power: -2.5 kW")).toBeVisible(); + await meterModal.getByRole("button", { name: "Save" }).click(); + await expect(page.getByTestId("battery")).toBeVisible(1); + await expect(page.getByTestId("battery")).toContainText("openems"); + + // edit #1 + await page.getByTestId("battery").getByRole("button", { name: "edit" }).click(); + await meterModal.getByLabel("Battery capacity in kWh").fill("20"); + await meterModal.getByRole("button", { name: "Validate & save" }).click(); + + await expect(page.getByTestId("battery")).toBeVisible(1); + await expect(page.getByTestId("battery")).toContainText("openems"); + await expect(page.getByTestId("battery").getByText("SoC: 75.0%")).toBeVisible(); + await expect(page.getByTestId("battery").getByText("Power: -2.5 kW")).toBeVisible(); + await expect(page.getByTestId("battery").getByText("Capacity: 20.0 kWh")).toBeVisible(); + + // restart and check in main ui + await restart(CONFIG_EMPTY); + await page.goto("/"); + await page.getByTestId("visualization").click(); + await expect(page.getByTestId("energyflow")).toContainText("Battery charging75%2.5 kW"); + + // delete #1 + await page.goto("/#/config"); + await page.getByTestId("battery").getByRole("button", { name: "edit" }).click(); + await meterModal.getByRole("button", { name: "Delete" }).click(); + + await expect(page.getByTestId("battery")).toHaveCount(0); + }); +}); diff --git a/tests/config-vehicles.spec.js b/tests/config-vehicles.spec.js new file mode 100644 index 0000000000..5e7e4c5e42 --- /dev/null +++ b/tests/config-vehicles.spec.js @@ -0,0 +1,132 @@ +import { test, expect } from "@playwright/test"; +import { start, stop, restart, cleanRestart, baseUrl } from "./evcc"; + +const CONFIG_EMPTY = "config-empty.evcc.yaml"; +const CONFIG_WITH_VEHICLE = "config-with-vehicle.evcc.yaml"; + +test.use({ baseURL: baseUrl() }); + +test.beforeAll(async () => { + await start(CONFIG_EMPTY, "password.sql"); +}); +test.afterAll(async () => { + await stop(); +}); + +async function login(page) { + await page.locator("#loginPassword").fill("secret"); + await page.getByRole("button", { name: "Login" }).click(); +} + +async function enableExperimental(page) { + await page + .getByTestId("generalconfig-experimental") + .getByRole("link", { name: "change" }) + .click(); + await page.getByLabel("Experimental 🧪").click(); + await page.getByRole("button", { name: "Close" }).click(); +} + +test.describe("vehicles", async () => { + test("create, edit and delete vehicles", async ({ page }) => { + await page.goto("/#/config"); + await login(page); + await enableExperimental(page); + + await expect(page.getByTestId("vehicle")).toHaveCount(0); + const vehicleModal = page.getByTestId("vehicle-modal"); + + // create #1 + await page.getByTestId("add-vehicle").click(); + await vehicleModal.getByLabel("Manufacturer").selectOption("Generic vehicle"); + await vehicleModal.getByLabel("Title").fill("Green Car"); + await vehicleModal.getByRole("button", { name: "Validate & save" }).click(); + + await expect(page.getByTestId("vehicle")).toHaveCount(1); + + // create #2 + await page.getByTestId("add-vehicle").click(); + await vehicleModal.getByLabel("Manufacturer").selectOption("Generic vehicle"); + await vehicleModal.getByLabel("Title").fill("Yellow Van"); + await vehicleModal.getByRole("button", { name: "Validate & save" }).click(); + + await expect(page.getByTestId("vehicle")).toHaveCount(2); + await expect(page.getByTestId("vehicle").nth(0)).toHaveText(/Green Car/); + await expect(page.getByTestId("vehicle").nth(1)).toHaveText(/Yellow Van/); + + // edit #1 + await page.getByTestId("vehicle").nth(0).getByRole("button", { name: "edit" }).click(); + await expect(vehicleModal.getByLabel("Title")).toHaveValue("Green Car"); + await vehicleModal.getByLabel("Title").fill("Fancy Car"); + await vehicleModal.getByRole("button", { name: "Validate & save" }).click(); + + await expect(page.getByTestId("vehicle")).toHaveCount(2); + await expect(page.getByTestId("vehicle").nth(0)).toHaveText(/Fancy Car/); + + // delete #1 + await page.getByTestId("vehicle").nth(0).getByRole("button", { name: "edit" }).click(); + await vehicleModal.getByRole("button", { name: "Delete" }).click(); + + await expect(page.getByTestId("vehicle")).toHaveCount(1); + await expect(page.getByTestId("vehicle").nth(0)).toHaveText(/Yellow Van/); + + // delete #2 + await page.getByTestId("vehicle").nth(0).getByRole("button", { name: "edit" }).click(); + await vehicleModal.getByRole("button", { name: "Delete" }).click(); + + await expect(page.getByTestId("vehicle")).toHaveCount(0); + }); + + test("config should survive restart", async ({ page }) => { + await page.goto("/#/config"); + await login(page); + await enableExperimental(page); + + await expect(page.getByTestId("vehicle")).toHaveCount(0); + const vehicleModal = page.getByTestId("vehicle-modal"); + + // create #1 & #2 + await page.getByTestId("add-vehicle").click(); + await vehicleModal.getByLabel("Manufacturer").selectOption("Generic vehicle"); + await vehicleModal.getByLabel("Title").fill("Green Car"); + await vehicleModal.getByRole("button", { name: "Validate & save" }).click(); + + await page.getByTestId("add-vehicle").click(); + await vehicleModal.getByLabel("Manufacturer").selectOption("Generic vehicle"); + await vehicleModal.getByLabel("Title").fill("Yellow Van"); + await vehicleModal.getByLabel("car").click(); + await vehicleModal.getByLabel("van").check(); + await vehicleModal.getByRole("button", { name: "Validate & save" }).click(); + + await expect(page.getByTestId("vehicle")).toHaveCount(2); + + // restart evcc + await restart(CONFIG_EMPTY); + await page.reload(); + + await expect(page.getByTestId("vehicle")).toHaveCount(2); + await expect(page.getByTestId("vehicle").nth(0)).toHaveText(/Green Car/); + await expect(page.getByTestId("vehicle").nth(1)).toHaveText(/Yellow Van/); + }); + + test("mixed config (yaml + db)", async ({ page }) => { + await cleanRestart(CONFIG_WITH_VEHICLE, "password.sql"); + + await page.goto("/#/config"); + await login(page); + await enableExperimental(page); + + await expect(page.getByTestId("vehicle")).toHaveCount(1); + const vehicleModal = page.getByTestId("vehicle-modal"); + + // create #1 + await page.getByTestId("add-vehicle").click(); + await vehicleModal.getByLabel("Manufacturer").selectOption("Generic vehicle"); + await vehicleModal.getByLabel("Title").fill("Green Car"); + await vehicleModal.getByRole("button", { name: "Validate & save" }).click(); + + await expect(page.getByTestId("vehicle")).toHaveCount(2); + await expect(page.getByTestId("vehicle").nth(0)).toHaveText(/YAML Bike/); + await expect(page.getByTestId("vehicle").nth(1)).toHaveText(/Green Car/); + }); +}); diff --git a/tests/config.spec.js b/tests/config.spec.js index 6edeccb0f6..15a9da81ab 100644 --- a/tests/config.spec.js +++ b/tests/config.spec.js @@ -1,9 +1,9 @@ import { test, expect } from "@playwright/test"; -import { start, stop, restart, cleanRestart } from "./evcc"; -import { startSimulator, stopSimulator, SIMULATOR_URL, SIMULATOR_HOST } from "./simulator"; +import { start, stop, baseUrl } from "./evcc"; const CONFIG_EMPTY = "config-empty.evcc.yaml"; -const CONFIG_WITH_VEHICLE = "config-with-vehicle.evcc.yaml"; + +test.use({ baseURL: baseUrl() }); test.beforeAll(async () => { await start(CONFIG_EMPTY, "password.sql"); @@ -42,172 +42,6 @@ test.describe("basics", async () => { }); }); -test.describe("vehicles", async () => { - test("create, edit and delete vehicles", async ({ page }) => { - await page.goto("/#/config"); - await login(page); - await enableExperimental(page); - - await expect(page.getByTestId("vehicle")).toHaveCount(0); - const vehicleModal = page.getByTestId("vehicle-modal"); - - // create #1 - await page.getByTestId("add-vehicle").click(); - await vehicleModal.getByLabel("Manufacturer").selectOption("Generic vehicle"); - await vehicleModal.getByLabel("Title").fill("Green Car"); - await vehicleModal.getByRole("button", { name: "Validate & save" }).click(); - - await expect(page.getByTestId("vehicle")).toHaveCount(1); - - // create #2 - await page.getByTestId("add-vehicle").click(); - await vehicleModal.getByLabel("Manufacturer").selectOption("Generic vehicle"); - await vehicleModal.getByLabel("Title").fill("Yellow Van"); - await vehicleModal.getByRole("button", { name: "Validate & save" }).click(); - - await expect(page.getByTestId("vehicle")).toHaveCount(2); - await expect(page.getByTestId("vehicle").nth(0)).toHaveText(/Green Car/); - await expect(page.getByTestId("vehicle").nth(1)).toHaveText(/Yellow Van/); - - // edit #1 - await page.getByTestId("vehicle").nth(0).getByRole("button", { name: "edit" }).click(); - await expect(vehicleModal.getByLabel("Title")).toHaveValue("Green Car"); - await vehicleModal.getByLabel("Title").fill("Fancy Car"); - await vehicleModal.getByRole("button", { name: "Validate & save" }).click(); - - await expect(page.getByTestId("vehicle")).toHaveCount(2); - await expect(page.getByTestId("vehicle").nth(0)).toHaveText(/Fancy Car/); - - // delete #1 - await page.getByTestId("vehicle").nth(0).getByRole("button", { name: "edit" }).click(); - await vehicleModal.getByRole("button", { name: "Delete" }).click(); - - await expect(page.getByTestId("vehicle")).toHaveCount(1); - await expect(page.getByTestId("vehicle").nth(0)).toHaveText(/Yellow Van/); - - // delete #2 - await page.getByTestId("vehicle").nth(0).getByRole("button", { name: "edit" }).click(); - await vehicleModal.getByRole("button", { name: "Delete" }).click(); - - await expect(page.getByTestId("vehicle")).toHaveCount(0); - }); - - test("config should survive restart", async ({ page }) => { - await page.goto("/#/config"); - await login(page); - await enableExperimental(page); - - await expect(page.getByTestId("vehicle")).toHaveCount(0); - const vehicleModal = page.getByTestId("vehicle-modal"); - - // create #1 & #2 - await page.getByTestId("add-vehicle").click(); - await vehicleModal.getByLabel("Manufacturer").selectOption("Generic vehicle"); - await vehicleModal.getByLabel("Title").fill("Green Car"); - await vehicleModal.getByRole("button", { name: "Validate & save" }).click(); - - await page.getByTestId("add-vehicle").click(); - await vehicleModal.getByLabel("Manufacturer").selectOption("Generic vehicle"); - await vehicleModal.getByLabel("Title").fill("Yellow Van"); - await vehicleModal.getByLabel("car").click(); - await vehicleModal.getByLabel("van").check(); - await vehicleModal.getByRole("button", { name: "Validate & save" }).click(); - - await expect(page.getByTestId("vehicle")).toHaveCount(2); - - // restart evcc - await restart(CONFIG_EMPTY); - await page.reload(); - - await expect(page.getByTestId("vehicle")).toHaveCount(2); - await expect(page.getByTestId("vehicle").nth(0)).toHaveText(/Green Car/); - await expect(page.getByTestId("vehicle").nth(1)).toHaveText(/Yellow Van/); - }); - - test("mixed config (yaml + db)", async ({ page }) => { - await cleanRestart(CONFIG_WITH_VEHICLE, "password.sql"); - - await page.goto("/#/config"); - await login(page); - await enableExperimental(page); - - await expect(page.getByTestId("vehicle")).toHaveCount(1); - const vehicleModal = page.getByTestId("vehicle-modal"); - - // create #1 - await page.getByTestId("add-vehicle").click(); - await vehicleModal.getByLabel("Manufacturer").selectOption("Generic vehicle"); - await vehicleModal.getByLabel("Title").fill("Green Car"); - await vehicleModal.getByRole("button", { name: "Validate & save" }).click(); - - await expect(page.getByTestId("vehicle")).toHaveCount(2); - await expect(page.getByTestId("vehicle").nth(0)).toHaveText(/YAML Bike/); - await expect(page.getByTestId("vehicle").nth(1)).toHaveText(/Green Car/); - }); -}); - -test.describe("meters", async () => { - test.beforeAll(async () => { - await startSimulator(); - }); - test.afterAll(async () => { - await stopSimulator(); - }); - - test("create, edit and remove battery meter", async ({ page }) => { - // setup test data for mock openems api - await page.goto(SIMULATOR_URL); - await page.getByLabel("Battery Power").fill("-2500"); - await page.getByLabel("Battery SoC").fill("75"); - await page.getByRole("button", { name: "Apply changes" }).click(); - - await page.goto("/#/config"); - await login(page); - await enableExperimental(page); - - await expect(page.getByTestId("battery")).toHaveCount(0); - - // create #1 - await page.getByRole("button", { name: "Add solar or battery" }).click(); - - const meterModal = page.getByTestId("meter-modal"); - await meterModal.getByRole("button", { name: "Add battery meter" }).click(); - await meterModal.getByLabel("Manufacturer").selectOption("OpenEMS"); - await meterModal.getByLabel("IP address or hostname").fill(SIMULATOR_HOST); - await expect(meterModal.getByRole("button", { name: "Validate & save" })).toBeVisible(); - await meterModal.getByRole("link", { name: "validate" }).click(); - await expect(meterModal.getByText("SoC: 75.0%")).toBeVisible(); - await expect(meterModal.getByText("Power: -2.5 kW")).toBeVisible(); - await meterModal.getByRole("button", { name: "Save" }).click(); - await expect(page.getByTestId("battery")).toBeVisible(1); - await expect(page.getByTestId("battery")).toContainText("openems"); - - // edit #1 - await page.getByTestId("battery").getByRole("button", { name: "edit" }).click(); - await meterModal.getByLabel("Battery capacity in kWh").fill("20"); - await meterModal.getByRole("button", { name: "Validate & save" }).click(); - - await expect(page.getByTestId("battery")).toBeVisible(1); - await expect(page.getByTestId("battery")).toContainText("openems"); - await expect(page.getByTestId("battery").getByText("SoC: 75.0%")).toBeVisible(); - await expect(page.getByTestId("battery").getByText("Power: -2.5 kW")).toBeVisible(); - await expect(page.getByTestId("battery").getByText("Capacity: 20.0 kWh")).toBeVisible(); - - // restart and check in main ui - await restart(CONFIG_EMPTY); - await page.goto("/"); - await page.getByTestId("visualization").click(); - await expect(page.getByTestId("energyflow")).toContainText("Battery charging75%2.5 kW"); - - // delete #1 - await page.goto("/#/config"); - await page.getByTestId("battery").getByRole("button", { name: "edit" }).click(); - await meterModal.getByRole("button", { name: "Delete" }).click(); - - await expect(page.getByTestId("battery")).toHaveCount(0); - }); -}); - test.describe("general", async () => { test("change site title", async ({ page }) => { // initial value on main ui diff --git a/tests/evcc.js b/tests/evcc.js index 906c300684..b8ef6dd400 100644 --- a/tests/evcc.js +++ b/tests/evcc.js @@ -2,13 +2,25 @@ import fs from "fs"; import waitOn from "wait-on"; import axios from "axios"; import { exec, execSync } from "child_process"; -import playwrightConfig from "../playwright.config.js"; +import os from "os"; +import path from "path"; -const BASE_URL = playwrightConfig.use.baseURL; - -const DB_PATH = "./evcc.db"; const BINARY = "./evcc"; +function port() { + const index = process.env.TEST_WORKER_INDEX * 1; + return 11000 + index; +} + +export function baseUrl() { + return `http://localhost:${port()}`; +} + +function dbPath() { + const file = `evcc-${port()}.db`; + return path.join(os.tmpdir(), file); +} + export async function start(config, sqlDumps) { await _clean(); if (sqlDumps) { @@ -39,14 +51,17 @@ export async function cleanRestart(config, sqlDumps) { async function _restoreDatabase(sqlDumps) { const dumps = Array.isArray(sqlDumps) ? sqlDumps : [sqlDumps]; for (const dump of dumps) { - console.log("loading database", dump); - execSync(`sqlite3 ${DB_PATH} < tests/${dump}`); + console.log("loading database", dbPath(), dump); + execSync(`sqlite3 ${dbPath()} < tests/${dump}`); } } async function _start(config) { + const configFile = config.includes("/") ? config : `tests/${config}`; console.log("starting evcc", { config }); - const instance = exec(`EVCC_DATABASE_DSN=${DB_PATH} ${BINARY} --config tests/${config}`); + const instance = exec( + `EVCC_NETWORK_PORT=${port()} EVCC_DATABASE_DSN=${dbPath()} ${BINARY} --config ${configFile}` + ); instance.stdout.pipe(process.stdout); instance.stderr.pipe(process.stderr); instance.on("exit", (code) => { @@ -54,20 +69,21 @@ async function _start(config) { throw new Error("evcc terminated", code); } }); - await waitOn({ resources: [BASE_URL] }); + await waitOn({ resources: [baseUrl()] }); } async function _stop() { console.log("shutting down evcc"); - const res = await axios.post(BASE_URL + "/api/auth/login", { password: "secret" }); + const res = await axios.post(`${baseUrl()}/api/auth/login`, { password: "secret" }); console.log(res.status, res.statusText); const cookie = res.headers["set-cookie"]; - await axios.post(BASE_URL + "/api/system/shutdown", {}, { headers: { cookie } }); + await axios.post(`${baseUrl()}/api/system/shutdown`, {}, { headers: { cookie } }); } async function _clean() { - if (fs.existsSync(DB_PATH)) { - console.log("delete database", DB_PATH); - fs.unlinkSync(DB_PATH); + const db = dbPath(); + if (fs.existsSync(db)) { + console.log("delete database", db); + fs.unlinkSync(db); } } diff --git a/tests/heating.spec.js b/tests/heating.spec.js index 4cafc8322c..d11df1968e 100644 --- a/tests/heating.spec.js +++ b/tests/heating.spec.js @@ -1,5 +1,7 @@ import { test, expect } from "@playwright/test"; -import { start, stop } from "./evcc"; +import { start, stop, baseUrl } from "./evcc"; + +test.use({ baseURL: baseUrl() }); test.beforeAll(async () => { await start("heating.evcc.yaml", "password.sql"); diff --git a/tests/limits.spec.js b/tests/limits.spec.js index e7244f7129..520cfcb3da 100644 --- a/tests/limits.spec.js +++ b/tests/limits.spec.js @@ -1,8 +1,8 @@ import { test, expect } from "@playwright/test"; -import { start, stop } from "./evcc"; -import { startSimulator, stopSimulator, SIMULATOR_URL } from "./simulator"; +import { start, stop, baseUrl } from "./evcc"; +import { startSimulator, stopSimulator, simulatorUrl, simulatorConfig } from "./simulator"; -const CONFIG = "simulator.evcc.yaml"; +test.use({ baseURL: baseUrl() }); test.beforeAll(async () => { await startSimulator(); @@ -12,9 +12,9 @@ test.afterAll(async () => { }); test.beforeEach(async ({ page }) => { - await start(CONFIG, "password.sql"); + await start(simulatorConfig(), "password.sql"); - await page.goto(SIMULATOR_URL); + await page.goto(simulatorUrl()); await page.getByLabel("Grid Power").fill("500"); await page.getByTestId("vehicle0").getByLabel("SoC").fill("20"); await page.getByTestId("loadpoint0").getByText("B (connected)").click(); @@ -39,7 +39,7 @@ test.describe("limitSoc", async () => { }); test("can be set even if vehicle isn't connected yet", async ({ page }) => { - await page.goto(SIMULATOR_URL); + await page.goto(simulatorUrl()); await page.getByTestId("loadpoint0").getByText("A (disconnected)").click(); await page.getByRole("button", { name: "Apply changes" }).click(); @@ -50,7 +50,7 @@ test.describe("limitSoc", async () => { await page.getByTestId("limit-soc").getByRole("combobox").selectOption("50%"); await expect(page.getByTestId("limit-soc-value")).toHaveText("50%"); - await page.goto(SIMULATOR_URL); + await page.goto(simulatorUrl()); await page.getByTestId("loadpoint0").getByText("B (connected)").click(); await page.getByRole("button", { name: "Apply changes" }).click(); @@ -65,7 +65,7 @@ test.describe("limitSoc", async () => { await expect(page.getByTestId("limit-soc-value")).toHaveText("50%"); // disconnect - await page.goto(SIMULATOR_URL); + await page.goto(simulatorUrl()); await page.getByTestId("loadpoint0").getByText("A (disconnected)").click(); await page.getByRole("button", { name: "Apply changes" }).click(); @@ -74,7 +74,7 @@ test.describe("limitSoc", async () => { await expect(page.getByTestId("limit-soc-value")).toHaveText("100%"); // connect - await page.goto(SIMULATOR_URL); + await page.goto(simulatorUrl()); await page.getByTestId("loadpoint0").getByText("B (connected)").click(); await page.getByRole("button", { name: "Apply changes" }).click(); diff --git a/tests/logs.spec.js b/tests/logs.spec.js index 91bd0d902f..867ec493db 100644 --- a/tests/logs.spec.js +++ b/tests/logs.spec.js @@ -1,5 +1,7 @@ import { test, expect } from "@playwright/test"; -import { start, stop } from "./evcc"; +import { start, stop, baseUrl } from "./evcc"; + +test.use({ baseURL: baseUrl() }); test.beforeAll(async () => { await start("basics.evcc.yaml", "password.sql"); diff --git a/tests/modals.spec.js b/tests/modals.spec.js index 62f229327e..7d6fe553a1 100644 --- a/tests/modals.spec.js +++ b/tests/modals.spec.js @@ -1,12 +1,13 @@ import { test, expect } from "@playwright/test"; -import { start, stop } from "./evcc"; -import { startSimulator, stopSimulator } from "./simulator"; +import { start, stop, baseUrl } from "./evcc"; +import { startSimulator, stopSimulator, simulatorConfig } from "./simulator"; const BASICS_CONFIG = "basics.evcc.yaml"; -const SIMULATOR_CONFIG = "simulator.evcc.yaml"; const UI_ROUTES = ["/", "/#/sessions", "/#/config"]; +test.use({ baseURL: baseUrl() }); + async function login(page) { await page.locator("#loginPassword").fill("secret"); await page.getByRole("button", { name: "Login" }).click(); @@ -55,7 +56,7 @@ test.describe("Basics", async () => { test.describe("Advanced", async () => { test.beforeAll(async () => { - await start(SIMULATOR_CONFIG, "password.sql"); + await start(simulatorConfig(), "password.sql"); await startSimulator(); }); diff --git a/tests/plan.spec.js b/tests/plan.spec.js index db0e0ce71f..2836339bff 100644 --- a/tests/plan.spec.js +++ b/tests/plan.spec.js @@ -1,5 +1,7 @@ import { test, expect } from "@playwright/test"; -import { start, stop } from "./evcc"; +import { start, stop, baseUrl } from "./evcc"; + +test.use({ baseURL: baseUrl() }); const CONFIG = "plan.evcc.yaml"; diff --git a/tests/sessions.spec.js b/tests/sessions.spec.js index 20f98bb2e4..e46c6d6bbe 100644 --- a/tests/sessions.spec.js +++ b/tests/sessions.spec.js @@ -1,5 +1,7 @@ import { test, expect, devices } from "@playwright/test"; -import { start, stop } from "./evcc"; +import { start, stop, baseUrl } from "./evcc"; + +test.use({ baseURL: baseUrl() }); const mobile = devices["iPhone 12 Mini"].viewport; const desktop = devices["Desktop Chrome"].viewport; diff --git a/tests/simulator.js b/tests/simulator.js index 27d91e2c29..8e8ad7b8e1 100644 --- a/tests/simulator.js +++ b/tests/simulator.js @@ -1,15 +1,36 @@ +import os from "os"; +import path from "path"; +import fs from "fs"; import waitOn from "wait-on"; import axios from "axios"; import { exec } from "child_process"; -export const SIMULATOR_HOST = "localhost:7072"; -export const SIMULATOR_URL = `http://${SIMULATOR_HOST}/`; -const HEALTH_URL = SIMULATOR_URL + "api/state"; -const SHUTDOWN_URL = SIMULATOR_URL + "api/shutdown"; +function port() { + const index = process.env.TEST_PARALLEL_INDEX * 1; + return 12000 + index; +} + +export function simulatorHost() { + return `localhost:${port()}`; +} + +export function simulatorUrl() { + return `http://${simulatorHost()}`; +} + +export function simulatorConfig() { + const input = "./tests/simulator.evcc.yaml"; + const content = fs.readFileSync(input, "utf8"); + const result = content.replace(/localhost:7072/g, simulatorHost()); + const resultName = "simulator.evcc.generated.yaml"; + const resultPath = path.join(os.tmpdir(), resultName); + fs.writeFileSync(resultPath, result); + return resultPath; +} export async function startSimulator() { console.log("starting simulator"); - const instance = exec("npm run simulator"); + const instance = exec(`npm run simulator -- --port ${port()}`); console.log("exec end"); instance.stdout.pipe(process.stdout); instance.stderr.pipe(process.stderr); @@ -20,10 +41,10 @@ export async function startSimulator() { } }); console.log("waiton"); - await waitOn({ resources: [HEALTH_URL], log: true }); + await waitOn({ resources: [`${simulatorUrl()}/api/state`], log: true }); } export async function stopSimulator() { console.log("shutting down simulator"); - await axios.post(SHUTDOWN_URL); + await axios.post(`${simulatorUrl()}/api/shutdown`); } diff --git a/tests/smart-cost.spec.js b/tests/smart-cost.spec.js index dc2dd845b8..8e62838dad 100644 --- a/tests/smart-cost.spec.js +++ b/tests/smart-cost.spec.js @@ -1,11 +1,11 @@ import { test, expect } from "@playwright/test"; -import { start, stop } from "./evcc"; -import { startSimulator, stopSimulator, SIMULATOR_URL } from "./simulator"; +import { start, stop, baseUrl } from "./evcc"; +import { startSimulator, stopSimulator, simulatorUrl, simulatorConfig } from "./simulator"; -const CONFIG = "simulator.evcc.yaml"; +test.use({ baseURL: baseUrl() }); test.beforeAll(async () => { - await start(CONFIG, "password.sql"); + await start(simulatorConfig(), "password.sql"); await startSimulator(); }); test.afterAll(async () => { @@ -14,7 +14,7 @@ test.afterAll(async () => { }); test.beforeEach(async ({ page }) => { - await page.goto(SIMULATOR_URL); + await page.goto(simulatorUrl()); await page.getByLabel("PV Power").fill("6000"); await page.getByTestId("loadpoint0").getByLabel("Power").fill("6000"); await page.getByTestId("loadpoint0").getByText("C (charging)").click(); diff --git a/tests/statistics.spec.js b/tests/statistics.spec.js index 249517505f..33d63a6d97 100644 --- a/tests/statistics.spec.js +++ b/tests/statistics.spec.js @@ -1,5 +1,7 @@ import { test, expect } from "@playwright/test"; -import { start, stop } from "./evcc"; +import { start, stop, baseUrl } from "./evcc"; + +test.use({ baseURL: baseUrl() }); test.beforeAll(async () => { await start("statistics.evcc.yaml", ["password.sql", "statistics.sql"]); diff --git a/tests/vehicle-error.spec.js b/tests/vehicle-error.spec.js index 381655f71e..9a58a80a99 100644 --- a/tests/vehicle-error.spec.js +++ b/tests/vehicle-error.spec.js @@ -1,5 +1,7 @@ import { test, expect } from "@playwright/test"; -import { start, stop } from "./evcc"; +import { start, stop, baseUrl } from "./evcc"; + +test.use({ baseURL: baseUrl() }); test.beforeAll(async () => { await start("vehicle-error.evcc.yaml", "password.sql"); diff --git a/tests/vehicle-settings.spec.js b/tests/vehicle-settings.spec.js index a02f8375cb..52adfb7f95 100644 --- a/tests/vehicle-settings.spec.js +++ b/tests/vehicle-settings.spec.js @@ -1,8 +1,8 @@ import { test, expect } from "@playwright/test"; -import { start, stop, restart } from "./evcc"; -import { startSimulator, stopSimulator, SIMULATOR_URL } from "./simulator"; +import { start, stop, restart, baseUrl } from "./evcc"; +import { startSimulator, stopSimulator, simulatorUrl, simulatorConfig } from "./simulator"; -const CONFIG = "simulator.evcc.yaml"; +test.use({ baseURL: baseUrl() }); test.beforeAll(async () => { await startSimulator(); @@ -12,9 +12,9 @@ test.afterAll(async () => { }); test.beforeEach(async ({ page }) => { - await start(CONFIG, "password.sql"); + await start(simulatorConfig(), "password.sql"); - await page.goto(SIMULATOR_URL); + await page.goto(simulatorUrl()); await page.getByLabel("Grid Power").fill("500"); await page.getByTestId("vehicle0").getByLabel("SoC").fill("20"); await page.getByTestId("loadpoint0").getByText("B (connected)").click(); @@ -36,7 +36,7 @@ test.describe("minSoc", async () => { await page.getByRole("combobox", { name: "Min. charge %" }).selectOption("20%"); await expect(page.getByText("charged to 20% in solar mode")).toBeVisible(); - await restart(CONFIG); + await restart(simulatorConfig()); await page.reload(); await page.getByTestId("charging-plan").getByRole("button", { name: "none" }).click(); @@ -75,7 +75,7 @@ test.describe("limitSoc", async () => { await page.getByRole("button", { name: "Close" }).click(); await expect(page.getByTestId("limit-soc-value")).toContainText("80%"); - await restart(CONFIG); + await restart(simulatorConfig()); await page.reload(); await expect(page.getByTestId("limit-soc-value")).toContainText("80%"); From 7ed3f04821d31bf7fd81be59bed67983a4c342c9 Mon Sep 17 00:00:00 2001 From: mdkeil Date: Wed, 8 May 2024 11:52:50 +0200 Subject: [PATCH 063/168] chore: mask template brackets (#13801) --- templates/definition/tariff/energy-charts-api.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/definition/tariff/energy-charts-api.yaml b/templates/definition/tariff/energy-charts-api.yaml index 0935da2d40..0dc46413e6 100644 --- a/templates/definition/tariff/energy-charts-api.yaml +++ b/templates/definition/tariff/energy-charts-api.yaml @@ -37,5 +37,5 @@ render: | {{ include "tariff-base" . }} forecast: source: http - uri: https://api.energy-charts.info/price?bzn={{ .bzn }}&end={{ now | dateModify "+24h" | date "2006-01-02" }} + uri: https://api.energy-charts.info/price?bzn={{ .bzn }}&end={{"{{"}} now | dateModify "+24h" | date "2006-01-02" {{"}}"}} jq: '[.unix_seconds, .price] | transpose | map({ "start": (.[0] | todate), "end": ((.[0]+3600) | todate), "price": (.[1]/1000)}) | tostring' From e675e408cd684ad59f8ab86a6d9d8e1e9e38c2c4 Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Wed, 8 May 2024 11:54:47 +0200 Subject: [PATCH 064/168] Docs: add tariff templates (#13756) --- templates/definition/tariff/allinpower.yaml | 7 ++++- templates/definition/tariff/amber.yaml | 11 +++++--- templates/definition/tariff/awattar.yaml | 11 ++++++-- .../definition/tariff/electricitymaps.yaml | 27 +++++++++++++++++++ templates/definition/tariff/elering.yaml | 19 +++++++++++++ templates/definition/tariff/energinet.yaml | 17 ++++++++++++ templates/definition/tariff/entsoe.yaml | 27 +++++++++++++++++++ templates/definition/tariff/fixed.yaml | 12 --------- templates/definition/tariff/groupe-e.yaml | 7 ++++- .../definition/tariff/gruenstromindex.yaml | 6 +++++ templates/definition/tariff/ngeso.yaml | 27 +++++++++++++++++++ templates/definition/tariff/octopus-api.yaml | 11 +++++--- .../tariff/octopus-productcode.yaml | 13 ++++++--- templates/definition/tariff/pun.yaml | 7 ++++- templates/definition/tariff/smartenergy.yaml | 7 ++++- templates/definition/tariff/tibber.yaml | 15 +++++++++-- util/templates/defaults.yaml | 14 ++++++++-- 17 files changed, 206 insertions(+), 32 deletions(-) create mode 100644 templates/definition/tariff/electricitymaps.yaml create mode 100644 templates/definition/tariff/elering.yaml create mode 100644 templates/definition/tariff/energinet.yaml create mode 100644 templates/definition/tariff/entsoe.yaml delete mode 100644 templates/definition/tariff/fixed.yaml create mode 100644 templates/definition/tariff/ngeso.yaml diff --git a/templates/definition/tariff/allinpower.yaml b/templates/definition/tariff/allinpower.yaml index dcae9b6e8b..87bc455aea 100644 --- a/templates/definition/tariff/allinpower.yaml +++ b/templates/definition/tariff/allinpower.yaml @@ -1,6 +1,11 @@ template: allinpower products: - - brand: All in Power (NL) + - brand: All in Power +requirements: + description: + de: "Nur für die Niederlande verfügbar." + en: "Only available for the Netherlands." +group: price params: - preset: tariff-base render: | diff --git a/templates/definition/tariff/amber.yaml b/templates/definition/tariff/amber.yaml index c5787d1864..a8ea6620dd 100644 --- a/templates/definition/tariff/amber.yaml +++ b/templates/definition/tariff/amber.yaml @@ -1,14 +1,19 @@ template: amber products: - - brand: Amber Electric (AU) + - brand: Amber Electric +requirements: + description: + de: "Nur für Australien verfügbar." + en: "Only available for Australia." +group: price params: - - preset: tariff-base - name: token - name: siteid - name: channel + - preset: tariff-base render: | type: amber - {{ include "tariff-base" . }} token: {{ .token }} siteid: {{ .siteid }} channel: {{ .channel }} + {{ include "tariff-base" . }} diff --git a/templates/definition/tariff/awattar.yaml b/templates/definition/tariff/awattar.yaml index 70bdb6664b..9e9794a866 100644 --- a/templates/definition/tariff/awattar.yaml +++ b/templates/definition/tariff/awattar.yaml @@ -1,10 +1,17 @@ template: awattar products: - brand: Awattar +requirements: + description: + de: "Nur für Deutschland und Österreich verfügbar." + en: "Only available for Germany and Austria." +group: price params: - - preset: tariff-base - name: region + example: AT + validvalues: ["DE", "AT"] + - preset: tariff-base render: | type: awattar - {{ include "tariff-base" . }} region: {{ .region }} + {{ include "tariff-base" . }} diff --git a/templates/definition/tariff/electricitymaps.yaml b/templates/definition/tariff/electricitymaps.yaml new file mode 100644 index 0000000000..a5eec70b8c --- /dev/null +++ b/templates/definition/tariff/electricitymaps.yaml @@ -0,0 +1,27 @@ +template: electricitymaps +products: + - brand: Electricity Maps + description: + generic: Commercial API +requirements: + description: + de: "CO₂-Daten für viele Länder von https://electricitymaps.com/. Der 'Free Personal Tier' beinhaltet leider keine Prognosedaten. Dafür benötigen Sie einen kommerziellen Account von https://api-portal.electricitymaps.com/. Kostenloser Testmonat verfügbar." + en: "CO₂ data for many countries from https://electricitymaps.com/. The 'Free Personal Tier' unfortunately does not include forecast data. You'll need a commercial account from https://api-portal.electricitymaps.com/. Free trial available." +group: co2 +params: + - name: uri + required: true + example: "https://api-access.electricitymaps.com/2w...1g/" + - name: token + required: true + - name: zone + required: true + example: "DE" + help: + de: "siehe https://api.electricitymap.org/v3/zones" + en: "see https://api.electricitymap.org/v3/zones" +render: | + type: electricitymaps + uri: {{ .uri }} + token: {{ .token }} + zone: {{ .zone }} diff --git a/templates/definition/tariff/elering.yaml b/templates/definition/tariff/elering.yaml new file mode 100644 index 0000000000..bdafd882ec --- /dev/null +++ b/templates/definition/tariff/elering.yaml @@ -0,0 +1,19 @@ +template: elering +products: + - brand: Nordpool + description: + generic: "Elering" +requirements: + description: + de: "Nur für Estland verfügbar." + en: "Only available for Estonia." +group: price +params: + - name: region + example: ee + validvalues: ["ee", "lt", "lv", "fi"] + - preset: tariff-base +render: | + type: elering + region: {{ .region }} + {{ include "tariff-base" . }} diff --git a/templates/definition/tariff/energinet.yaml b/templates/definition/tariff/energinet.yaml new file mode 100644 index 0000000000..c3e448ee28 --- /dev/null +++ b/templates/definition/tariff/energinet.yaml @@ -0,0 +1,17 @@ +template: energinet +products: + - brand: Energinet +requirements: + description: + de: "Nur für Dänemark verfügbar." + en: "Only available for Denmark." +group: price +params: + - name: region + example: dk1 + validvalues: ["dk1", "dk2"] + - preset: tariff-base +render: | + type: energinet + region: {{ .region }} + {{ include "tariff-base" . }} diff --git a/templates/definition/tariff/entsoe.yaml b/templates/definition/tariff/entsoe.yaml new file mode 100644 index 0000000000..e2493283ab --- /dev/null +++ b/templates/definition/tariff/entsoe.yaml @@ -0,0 +1,27 @@ +template: entsoe +products: + - brand: ENTSO-E +requirements: + description: + de: | + Day-ahead-Preise für den europäischen Strommarkt. Siehe https://transparency.entsoe.eu für weitere Informationen. + Basis für viele dynamische Tarife. + en: | + Day-ahead prices for the European electricity market. See https://transparency.entsoe.eu for more information. + Basis for many dynamic tariffs. +group: price +params: + - name: securitytoken + help: + de: "Registrierung und anschließende Helpdesk-Anfrage erforderlich. Details zum Ablauf gibts hier https://transparency.entsoe.eu/content/static_content/Static%20content/web%20api/Guide.html#_authentication_and_authorisation" + en: "Registration and subsequent helpdesk request required. Details on the process can be found here https://transparency.entsoe.eu/content/static_content/Static%20content/web%20api/Guide.html#_authentication_and_authorisation" + - name: domain + example: BZN|DE-LU + help: + de: "siehe https://transparency.entsoe.eu/content/static_content/Static%20content/web%20api/Guide.html#_areas" + en: "see https://transparency.entsoe.eu/content/static_content/Static%20content/web%20api/Guide.html#_areas" + - preset: tariff-base +render: | + type: entsoe + region: {{ .region }} + {{ include "tariff-base" . }} diff --git a/templates/definition/tariff/fixed.yaml b/templates/definition/tariff/fixed.yaml deleted file mode 100644 index d34253e241..0000000000 --- a/templates/definition/tariff/fixed.yaml +++ /dev/null @@ -1,12 +0,0 @@ -template: fixed -products: - - brand: Standard -params: - - name: price - type: float - description: - en: "Price per kWh" - de: "Preis je kWh" -render: | - type: fixed - price: {{ .price }} diff --git a/templates/definition/tariff/groupe-e.yaml b/templates/definition/tariff/groupe-e.yaml index a2c288ba6f..542a3f5ed9 100644 --- a/templates/definition/tariff/groupe-e.yaml +++ b/templates/definition/tariff/groupe-e.yaml @@ -1,8 +1,13 @@ template: groupe-e products: - - brand: Groupe E (CH) + - brand: Groupe E description: generic: Vario Plus +requirements: + description: + de: "Nur für die Schweiz verfügbar." + en: "Only available for Switzerland." +group: price params: - preset: tariff-base render: | diff --git a/templates/definition/tariff/gruenstromindex.yaml b/templates/definition/tariff/gruenstromindex.yaml index bed61b5ed5..95c78cf1cd 100644 --- a/templates/definition/tariff/gruenstromindex.yaml +++ b/templates/definition/tariff/gruenstromindex.yaml @@ -1,8 +1,14 @@ template: grünstromindex products: - brand: Grünstromindex +requirements: + description: + de: "Regionale Emissionsdaten von https://gruenstromindex.de. Nur für Deutschland verfügbar." + en: "Regional emission data from https://gruenstromindex.de. Only available for Germany." +group: co2 params: - name: zip + required: true render: | type: grünstromindex zip: {{ .zip }} diff --git a/templates/definition/tariff/ngeso.yaml b/templates/definition/tariff/ngeso.yaml new file mode 100644 index 0000000000..ac6c9de6f6 --- /dev/null +++ b/templates/definition/tariff/ngeso.yaml @@ -0,0 +1,27 @@ +template: ngeso +products: + - brand: National Grid ESO +requirements: + description: + de: "Nur für Großbritannien verfügbar." + en: "Only available for the United Kingdom." +group: co2 +params: + - name: region + type: string + required: false + example: 1 + help: + de: "Ungenauer als die Verwendung eines Postleitzahl. Siehe https://carbon-intensity.github.io/api-definitions/#region-list" + en: "Coarser than using a postcode. See https://carbon-intensity.github.io/api-definitions/#region-list" + - name: postalcode + type: string + example: "SW1" + required: false + help: + de: "Postleitzahl z.B. RG41 oder SW1 oder TF8. Nicht die vollständige Postleitzahl, nur die ersten Stellen." + en: "Outward postcode i.e. RG41 or SW1 or TF8. Do not include full postcode, outward postcode only." +render: | + type: ngeso + region: {{ .region }} + postalcode: {{ .postalcode }} diff --git a/templates/definition/tariff/octopus-api.yaml b/templates/definition/tariff/octopus-api.yaml index 6121f7b8af..c492f7b203 100644 --- a/templates/definition/tariff/octopus-api.yaml +++ b/templates/definition/tariff/octopus-api.yaml @@ -2,13 +2,18 @@ template: octopus-api products: - brand: Octopus Energy description: - en: Octopus Energy - API + generic: API +requirements: + description: + de: "Den API-Key bekommst du im Octopus Portal https://octopus.energy/dashboard/new/accounts/personal-details/api-access" + en: "You can get the API key in the Octopus portal https://octopus.energy/dashboard/new/accounts/personal-details/api-access" +group: price params: - name: apiKey type: string required: true - description: - en: "Your Octopus Energy API Key. You can find it here: https://octopus.energy/dashboard/new/accounts/personal-details/api-access" + help: + generic: "Octopus Energy API Key." render: | type: octopusenergy apikey: {{ .apikey }} diff --git a/templates/definition/tariff/octopus-productcode.yaml b/templates/definition/tariff/octopus-productcode.yaml index 0dfcd3edcd..5b033d724d 100644 --- a/templates/definition/tariff/octopus-productcode.yaml +++ b/templates/definition/tariff/octopus-productcode.yaml @@ -2,17 +2,22 @@ template: octopus-productcode products: - brand: Octopus Energy description: - en: Octopus Energy - Product Code + generic: Product Code +group: price params: - name: productCode type: string required: true - description: - en: "The tariff code for your energy contract. Make sure this is set to your import tariff code. It'll look something like this: AGILE-FLEX-22-11-25" + example: AGILE-FLEX-22-11-25 + help: + de: "Der Tarifcode für Ihren Energievertrag. Stellen Sie sicher, dass dieser auf Ihren Importtarifcode eingestellt ist." + en: "The tariff code for your energy contract. Make sure this is set to your import tariff code." - name: region type: string + validvalues: ["A", "B", "C", "D", "E", "F", "G", "H", "J", "K", "L", "M", "N", "P"] required: true - description: + help: + de: "Die DNO-Region, in der Sie sich befinden. Weitere Informationen: https://www.energy-stats.uk/dno-region-codes-explained/" en: "The DNO region you are located in. More information: https://www.energy-stats.uk/dno-region-codes-explained/" render: | type: octopusenergy diff --git a/templates/definition/tariff/pun.yaml b/templates/definition/tariff/pun.yaml index 2c4acda2e7..91b4c826c7 100644 --- a/templates/definition/tariff/pun.yaml +++ b/templates/definition/tariff/pun.yaml @@ -1,6 +1,11 @@ template: pun products: - - brand: PUN Orario (IT) + - brand: PUN Orario +requirements: + description: + de: "Preisdaten von https://www.mercatoelettrico.org/it/. Wird oft zur Einspeisung ins Netz verwendet. Nur für Italien verfügbar." + en: "Price data from https://www.mercatoelettrico.org/it/. Often used for feeding into the grid. Only available for Italy." +group: price params: - preset: tariff-base render: | diff --git a/templates/definition/tariff/smartenergy.yaml b/templates/definition/tariff/smartenergy.yaml index 1d10830655..6d7762eaff 100644 --- a/templates/definition/tariff/smartenergy.yaml +++ b/templates/definition/tariff/smartenergy.yaml @@ -1,8 +1,13 @@ template: smartenergy products: - - brand: SmartEnergy (AT) + - brand: SmartEnergy description: generic: smartCONTROL +requirements: + description: + de: Nur für Österreich verfügbar. + en: Only available for Austria. +group: price params: - preset: tariff-base render: | diff --git a/templates/definition/tariff/tibber.yaml b/templates/definition/tariff/tibber.yaml index b057032ced..e4f1bc89ca 100644 --- a/templates/definition/tariff/tibber.yaml +++ b/templates/definition/tariff/tibber.yaml @@ -1,12 +1,23 @@ template: tibber products: - brand: Tibber +requirements: + description: + en: "Get your API token from the Tibber developer portal: https://developer.tibber.com/" + de: "Hol dir deinen API-Token aus dem Tibber-Entwicklerportal: https://developer.tibber.com/" +group: price params: - - preset: tariff-base - name: token + example: 476c477d8a039529478ebd690d35ddd80e3308ffc49b59c65b142321aee963a4 + required: true - name: homeid + example: cc83e83e-8cbf-4595-9bf7-c3cf192f7d9c + help: + de: Nur erforderlich, wenn du mehrere Häuser in deinem Tibber-Konto hast. + en: Only required if you have multiple homes in your Tibber account. + - preset: tariff-base render: | type: tibber - {{ include "tariff-base" . }} token: {{ .token }} homeid: {{ .homeid }} + {{ include "tariff-base" . }} diff --git a/util/templates/defaults.yaml b/util/templates/defaults.yaml index 58103ae7de..e33c58d273 100644 --- a/util/templates/defaults.yaml +++ b/util/templates/defaults.yaml @@ -319,10 +319,14 @@ presets: params: - name: costs type: number - advanced: true + help: + de: Zusätzlicher fester Aufschlag pro kWh (z.B. 0.05 für 5 Cent) + en: Additional fixed charge per kWh (e.g. 0.05 for 5 cents) - name: tax type: number - advanced: true + help: + de: Zusätzlicher prozentualer Aufschlag (z.B. 0.2 für 20%) + en: Additional percentage charge (e.g. 0.2 for 20%) eebus: params: - name: ski @@ -461,3 +465,9 @@ devicegroups: scooter: de: Scooter en: Scooter + price: + de: Dynamischer Strompreis + en: Dynamic electricity price + co2: + de: CO₂ Vorhersage + en: CO₂ forecast From 495cb511c5c48151333664cd8512675546d20f55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20He=C3=9F?= Date: Wed, 8 May 2024 12:01:40 +0200 Subject: [PATCH 065/168] chore: fix rendering templates for tests aborts on usage == 0 (#13805) --- tariff/template_test.go | 7 ++++++- util/templates/render_testing.go | 2 +- vehicle/template_test.go | 3 +++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/tariff/template_test.go b/tariff/template_test.go index 4886f5a293..429efad99a 100644 --- a/tariff/template_test.go +++ b/tariff/template_test.go @@ -7,7 +7,12 @@ import ( "github.com/evcc-io/evcc/util/test" ) -var acceptable = []string{} +var acceptable = []string{ + "missing token", // amber, tibber + "invalid zipcode", // grünstromindex + "invalid apikey format", // octopusenergy + "missing region", // octopusenergy +} func TestTemplates(t *testing.T) { templates.TestClass(t, templates.Tariff, func(t *testing.T, values map[string]any) { diff --git a/util/templates/render_testing.go b/util/templates/render_testing.go index 17b272c881..5ad7b912c4 100644 --- a/util/templates/render_testing.go +++ b/util/templates/render_testing.go @@ -69,7 +69,7 @@ func TestClass(t *testing.T, class Class, instantiate func(t *testing.T, values }) }) - return + continue } for _, u := range usages { diff --git a/vehicle/template_test.go b/vehicle/template_test.go index c7f3a544dd..a9780a1c73 100644 --- a/vehicle/template_test.go +++ b/vehicle/template_test.go @@ -22,6 +22,9 @@ var acceptable = []string{ "missing credentials", // Tesla "missing credentials id", // Tronity "missing access and/or refresh token, use `evcc token` to create", // Tesla + "login failed: code not found", //Polestar + "empty instance type- check for missing usage", // Merces + "invalid vehicle type: tesla", //Tesla } func TestTemplates(t *testing.T) { From 4a7ea3fe42f8d77308a1852f938627d9c9905244 Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Wed, 8 May 2024 12:49:50 +0200 Subject: [PATCH 066/168] docs: fixed energy charts description; added group --- templates/definition/tariff/energy-charts-api.yaml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/templates/definition/tariff/energy-charts-api.yaml b/templates/definition/tariff/energy-charts-api.yaml index 0dc46413e6..4dd8bf7723 100644 --- a/templates/definition/tariff/energy-charts-api.yaml +++ b/templates/definition/tariff/energy-charts-api.yaml @@ -1,9 +1,11 @@ template: energy-charts-api products: - brand: Fraunhofer ISE - description: - de: "Day-ahead Energiepreise (je kWh) an der Börse. Kann ohne vorherige Anmeldung auf https://api.energy-charts.info/ abgerufen werden. Nutzbar u.a. für dynamische Stromtarife, wo der Anbieter bis dato auf der Kundenschnittstelle noch kein Preis-Vorhersagen anbietet." - en: "Day-ahead forecast of energy prices (per kWh) on the exchange. No prior registration for https://api.energy-charts.info/ necessary. Can be used for dynamic electricity tariffs, for example, where the supplier does not yet offer a price forecast on the customer interface." +requirements: + description: + de: "Day-ahead Energiepreise (je kWh) an der Börse. Kann ohne vorherige Anmeldung auf https://api.energy-charts.info/ abgerufen werden. Nutzbar u.a. für dynamische Stromtarife, wo der Anbieter bis dato auf der Kundenschnittstelle noch kein Preis-Vorhersagen anbietet." + en: "Day-ahead forecast of energy prices (per kWh) on the exchange. No prior registration for https://api.energy-charts.info/ necessary. Can be used for dynamic electricity tariffs, for example, where the supplier does not yet offer a price forecast on the customer interface." +group: price params: - name: bzn type: string From 3808ed12b472e96d9bcf64643dfd7036f83660d2 Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Wed, 8 May 2024 14:08:15 +0200 Subject: [PATCH 067/168] docs: acciental Siezen --- templates/definition/tariff/electricitymaps.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/definition/tariff/electricitymaps.yaml b/templates/definition/tariff/electricitymaps.yaml index a5eec70b8c..a360dfa789 100644 --- a/templates/definition/tariff/electricitymaps.yaml +++ b/templates/definition/tariff/electricitymaps.yaml @@ -5,7 +5,7 @@ products: generic: Commercial API requirements: description: - de: "CO₂-Daten für viele Länder von https://electricitymaps.com/. Der 'Free Personal Tier' beinhaltet leider keine Prognosedaten. Dafür benötigen Sie einen kommerziellen Account von https://api-portal.electricitymaps.com/. Kostenloser Testmonat verfügbar." + de: "CO₂-Daten für viele Länder von https://electricitymaps.com/. Der 'Free Personal Tier' beinhaltet leider keine Prognosedaten. Dafür benötigst du einen kommerziellen Account von https://api-portal.electricitymaps.com/. Kostenloser Testmonat verfügbar." en: "CO₂ data for many countries from https://electricitymaps.com/. The 'Free Personal Tier' unfortunately does not include forecast data. You'll need a commercial account from https://api-portal.electricitymaps.com/. Free trial available." group: co2 params: From 9b2ff916433c54cac4ef52299a8104a4cb072aaf Mon Sep 17 00:00:00 2001 From: andig Date: Wed, 8 May 2024 14:43:51 +0200 Subject: [PATCH 068/168] Energy Charts tariff: add 1h cache --- templates/definition/tariff/energy-charts-api.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/templates/definition/tariff/energy-charts-api.yaml b/templates/definition/tariff/energy-charts-api.yaml index 4dd8bf7723..76d98048e0 100644 --- a/templates/definition/tariff/energy-charts-api.yaml +++ b/templates/definition/tariff/energy-charts-api.yaml @@ -41,3 +41,4 @@ render: | source: http uri: https://api.energy-charts.info/price?bzn={{ .bzn }}&end={{"{{"}} now | dateModify "+24h" | date "2006-01-02" {{"}}"}} jq: '[.unix_seconds, .price] | transpose | map({ "start": (.[0] | todate), "end": ((.[0]+3600) | todate), "price": (.[1]/1000)}) | tostring' + cache: 1h From 2220684eb5699ac9c514f2174fbfecbd27e99f0f Mon Sep 17 00:00:00 2001 From: andig Date: Wed, 8 May 2024 14:44:46 +0200 Subject: [PATCH 069/168] Revert "Energy Charts tariff: add 1h cache" This reverts commit 9b2ff916433c54cac4ef52299a8104a4cb072aaf. --- templates/definition/tariff/energy-charts-api.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/templates/definition/tariff/energy-charts-api.yaml b/templates/definition/tariff/energy-charts-api.yaml index 76d98048e0..4dd8bf7723 100644 --- a/templates/definition/tariff/energy-charts-api.yaml +++ b/templates/definition/tariff/energy-charts-api.yaml @@ -41,4 +41,3 @@ render: | source: http uri: https://api.energy-charts.info/price?bzn={{ .bzn }}&end={{"{{"}} now | dateModify "+24h" | date "2006-01-02" {{"}}"}} jq: '[.unix_seconds, .price] | transpose | map({ "start": (.[0] | todate), "end": ((.[0]+3600) | todate), "price": (.[1]/1000)}) | tostring' - cache: 1h From 10016a79327aef1c6ace27c8e2dfd8dd32db3317 Mon Sep 17 00:00:00 2001 From: Torge Rosendahl Date: Wed, 8 May 2024 16:28:51 +0200 Subject: [PATCH 070/168] chore: add wildcard '*' to __debug_bin (#13814) --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 4b8c120c44..81e4c508cf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -__debug_bin +__debug_bin* .vscode .cache .DS_store From 3dc6caf88a51a5c34c73fc5ca0e82037bc8d7ef7 Mon Sep 17 00:00:00 2001 From: andig Date: Wed, 8 May 2024 16:40:44 +0200 Subject: [PATCH 071/168] chore: minor --- templates/definition/meter/lg-ess-home-8-10.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/definition/meter/lg-ess-home-8-10.yaml b/templates/definition/meter/lg-ess-home-8-10.yaml index 2ecd69c60e..4e229dcb75 100644 --- a/templates/definition/meter/lg-ess-home-8-10.yaml +++ b/templates/definition/meter/lg-ess-home-8-10.yaml @@ -15,7 +15,7 @@ params: Alteratively, use registration id for admin login. de: > Benutzerpasswort, siehe https://github.com/Morluktom/ioBroker.lg-ess-home/tree/master#getting-the-password. - Alterativ kann die Registriernummer für Administratorlogin verwendet werden. + Alternativ kann die Registriernummer für Administratorlogin verwendet werden. - name: registration advanced: true example: "DE200..." From d80149bc06abe5b064d92cb7b202dc37033930e0 Mon Sep 17 00:00:00 2001 From: andig Date: Wed, 8 May 2024 16:54:47 +0200 Subject: [PATCH 072/168] chore: use strconv.Itoa where appropriate --- charger/hardybarth-ecb1.go | 3 ++- util/templates/template_modbus.go | 7 ++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/charger/hardybarth-ecb1.go b/charger/hardybarth-ecb1.go index c964f982ed..2102db2c35 100644 --- a/charger/hardybarth-ecb1.go +++ b/charger/hardybarth-ecb1.go @@ -22,6 +22,7 @@ import ( "fmt" "net/http" "net/url" + "strconv" "strings" "time" @@ -188,7 +189,7 @@ func (wb *HardyBarth) post(uri string, data url.Values) error { // MaxCurrent implements the api.Charger interface func (wb *HardyBarth) MaxCurrent(current int64) error { uri := fmt.Sprintf("%s/chargecontrols/%d/mode/manual/ampere", wb.uri, wb.chargecontrol) - data := url.Values{"manualmodeamp": {fmt.Sprintf("%d", current)}} + data := url.Values{"manualmodeamp": {strconv.FormatInt(current, 10)}} return wb.post(uri, data) } diff --git a/util/templates/template_modbus.go b/util/templates/template_modbus.go index a1974ffdd4..4fe43d7454 100644 --- a/util/templates/template_modbus.go +++ b/util/templates/template_modbus.go @@ -3,6 +3,7 @@ package templates import ( _ "embed" "fmt" + "strconv" "strings" ) @@ -75,15 +76,15 @@ func (t *Template) ModbusValues(renderMode int, values map[string]interface{}) { switch p.Name { case ModbusParamNameId: if modbusParam.ID != 0 { - defaultValue = fmt.Sprintf("%d", modbusParam.ID) + defaultValue = strconv.Itoa(modbusParam.ID) } case ModbusParamNamePort: if modbusParam.Port != 0 { - defaultValue = fmt.Sprintf("%d", modbusParam.Port) + defaultValue = strconv.Itoa(modbusParam.Port) } case ModbusParamNameBaudrate: if modbusParam.Baudrate != 0 { - defaultValue = fmt.Sprintf("%d", modbusParam.Baudrate) + defaultValue = strconv.Itoa(modbusParam.Baudrate) } case ModbusParamNameComset: if modbusParam.Comset != "" { From 20d032013793b76daf737ffce88ee09c90ac330e Mon Sep 17 00:00:00 2001 From: andig Date: Wed, 8 May 2024 19:50:59 +0200 Subject: [PATCH 073/168] chore: upgrade modules --- go.mod | 56 ++++++++++++------------- go.sum | 128 ++++++++++++++++++++++++++++----------------------------- 2 files changed, 92 insertions(+), 92 deletions(-) diff --git a/go.mod b/go.mod index ed19251166..ef5dc3f569 100644 --- a/go.mod +++ b/go.mod @@ -7,12 +7,12 @@ require ( github.com/42atomys/sprout v0.2.0 github.com/AlecAivazis/survey/v2 v2.3.7 github.com/BurntSushi/toml v1.3.2 - github.com/PuerkitoBio/goquery v1.9.1 + github.com/PuerkitoBio/goquery v1.9.2 github.com/andig/go-powerwall v0.2.1-0.20230808194509-dd70cdb6e140 github.com/andig/gosunspec v0.0.0-20231205122018-1daccfa17912 github.com/andig/mbserver v0.0.0-20230310211055-1d29cbb5820e github.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef - github.com/aws/aws-sdk-go v1.51.21 + github.com/aws/aws-sdk-go v1.52.4 github.com/basgys/goxml2json v1.1.0 github.com/basvdlei/gotsmart v0.0.3 github.com/benbjohnson/clock v1.3.5 @@ -31,7 +31,7 @@ require ( github.com/fatih/structs v1.1.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.19.0 + github.com/go-playground/validator/v10 v10.20.0 github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 github.com/godbus/dbus/v5 v5.1.0 github.com/gokrazy/updater v0.0.0-20240113102150-4ac511a17e33 @@ -40,13 +40,13 @@ require ( github.com/google/uuid v1.6.0 github.com/gorilla/handlers v1.5.2 github.com/gorilla/mux v1.8.1 - github.com/gregdel/pushover v1.3.0 + github.com/gregdel/pushover v1.3.1 github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 - github.com/grid-x/modbus v0.0.0-20240214112450-0d4922fba364 + github.com/grid-x/modbus v0.0.0-20240503115206-582f2ab60a18 github.com/hashicorp/go-version v1.6.0 github.com/hasura/go-graphql-client v0.12.1 github.com/influxdata/influxdb-client-go/v2 v2.13.0 - github.com/insomniacslk/tapo v1.0.0 + github.com/insomniacslk/tapo v1.0.1 github.com/itchyny/gojq v0.12.15 github.com/jeremywohl/flatten v1.0.1 github.com/jinzhu/copier v0.4.0 @@ -60,7 +60,7 @@ require ( github.com/libp2p/zeroconf/v2 v2.2.0 github.com/lorenzodonini/ocpp-go v0.18.0 github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 - github.com/mabunixda/wattpilot v1.6.6 + github.com/mabunixda/wattpilot v1.7.0 github.com/mitchellh/go-homedir v1.1.0 github.com/mitchellh/mapstructure v1.5.0 github.com/mlnoga/rct v0.1.2-0.20240421173556-1c5b75037e2f @@ -72,12 +72,12 @@ require ( github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/prometheus-community/pro-bing v0.4.0 github.com/prometheus/client_golang v1.19.0 - github.com/prometheus/common v0.52.3 - github.com/robertkrimen/otto v0.3.0 + github.com/prometheus/common v0.53.0 + github.com/robertkrimen/otto v0.4.0 github.com/samber/lo v1.39.0 github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 github.com/smallnest/chanx v1.2.0 - github.com/spali/go-rscp v0.2.0-beta4 + github.com/spali/go-rscp v0.2.0 github.com/spf13/cast v1.6.0 github.com/spf13/cobra v1.8.0 github.com/spf13/jwalterweatherman v1.1.0 @@ -91,16 +91,16 @@ require ( github.com/writeas/go-strip-markdown/v2 v2.1.1 gitlab.com/bboehmke/sunny v0.16.0 go.uber.org/mock v0.4.0 - golang.org/x/crypto/x509roots/fallback v0.0.0-20240404165943-d042a396a6de - golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8 - golang.org/x/net v0.24.0 - golang.org/x/oauth2 v0.19.0 + golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595 + golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 + golang.org/x/net v0.25.0 + golang.org/x/oauth2 v0.20.0 golang.org/x/sync v0.7.0 - golang.org/x/text v0.14.0 + golang.org/x/text v0.15.0 google.golang.org/grpc v1.63.2 - google.golang.org/protobuf v1.33.0 + google.golang.org/protobuf v1.34.1 gopkg.in/yaml.v3 v3.0.1 - gorm.io/gorm v1.25.9 + gorm.io/gorm v1.25.10 nhooyr.io/websocket v1.8.11 ) @@ -130,7 +130,7 @@ require ( github.com/go-playground/universal-translator v0.18.1 // indirect github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect - github.com/gobwas/ws v1.3.2 // indirect + github.com/gobwas/ws v1.4.0 // indirect github.com/golang/mock v1.6.0 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/gorilla/websocket v1.5.1 // indirect @@ -152,7 +152,7 @@ require ( github.com/mattn/go-runewidth v0.0.15 // indirect github.com/mergermarket/go-pkcs7 v0.0.0-20170926155232-153b18ea13c9 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect - github.com/miekg/dns v1.1.58 // indirect + github.com/miekg/dns v1.1.59 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect @@ -161,11 +161,11 @@ require ( github.com/oapi-codegen/runtime v1.1.1 // indirect github.com/onsi/ginkgo v1.16.5 // indirect github.com/pascaldekloe/name v1.0.1 // indirect - github.com/pelletier/go-toml/v2 v2.2.1 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/procfs v0.13.0 // indirect + github.com/prometheus/procfs v0.14.0 // indirect github.com/relvacode/iso8601 v1.4.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rickb777/date v1.20.6 // indirect @@ -185,19 +185,19 @@ require ( github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect gitlab.com/c0b/go-ordered-json v0.0.0-20201030195603-febf46534d5a // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.22.0 + golang.org/x/crypto v0.23.0 golang.org/x/mod v0.17.0 // indirect - golang.org/x/sys v0.19.0 // indirect - golang.org/x/term v0.19.0 // indirect - golang.org/x/tools v0.20.0 - google.golang.org/genproto/googleapis/rpc v0.0.0-20240412170617-26222e5d3d56 // indirect + golang.org/x/sys v0.20.0 // indirect + golang.org/x/term v0.20.0 // indirect + golang.org/x/tools v0.21.0 + google.golang.org/genproto/googleapis/rpc v0.0.0-20240506185236-b8a5c65736ae // indirect gopkg.in/go-playground/validator.v9 v9.31.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/sourcemap.v1 v1.0.5 // indirect - modernc.org/libc v1.49.3 // indirect + modernc.org/libc v1.50.5 // indirect modernc.org/mathutil v1.6.0 // indirect modernc.org/memory v1.8.0 // indirect - modernc.org/sqlite v1.29.6 // indirect + modernc.org/sqlite v1.29.9 // indirect ) replace github.com/spf13/viper => github.com/spf13/viper v1.18.1 diff --git a/go.sum b/go.sum index 1eaddfabe0..40f5108547 100644 --- a/go.sum +++ b/go.sum @@ -14,8 +14,8 @@ github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0 github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= -github.com/PuerkitoBio/goquery v1.9.1 h1:mTL6XjbJTZdpfL+Gwl5U2h1l9yEkJjhmlTeV9VPW7UI= -github.com/PuerkitoBio/goquery v1.9.1/go.mod h1:cW1n6TmIMDoORQU5IU/P1T3tGFunOeXEpGP2WHRwkbY= +github.com/PuerkitoBio/goquery v1.9.2 h1:4/wZksC3KgkQw7SQgkKotmKljk0M6V8TUvA8Wb4yPeE= +github.com/PuerkitoBio/goquery v1.9.2/go.mod h1:GHPCaP0ODyyxqcNoFGYlAprUFH81NuRPd0GX3Zu2Mvk= github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/toxiproxy v2.1.4+incompatible h1:TKdv8HiTLgE5wdJuEML90aBgNWsokNbMijUGhmcoBJc= @@ -49,8 +49,8 @@ github.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef h1:2JGTg6JapxP github.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef/go.mod h1:JS7hed4L1fj0hXcyEejnW57/7LCetXggd+vwrRnYeII= github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= -github.com/aws/aws-sdk-go v1.51.21 h1:UrT6JC9R9PkYYXDZBV0qDKTualMr+bfK2eboTknMgbs= -github.com/aws/aws-sdk-go v1.51.21/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= +github.com/aws/aws-sdk-go v1.52.4 h1:9VsBVJ2TKf8xPP3+yIPGSYcEBIEymXsJzQoFgQuyvA0= +github.com/aws/aws-sdk-go v1.52.4/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= github.com/azihsoyn/rijndael256 v0.0.0-20200316065338-d14eefa2b66b h1:/2dABok/UswXOj5rjbR5bZ411ApGBq1pAEZdy5rvFrY= github.com/azihsoyn/rijndael256 v0.0.0-20200316065338-d14eefa2b66b/go.mod h1:ef+2vMUkiKcy2Tz7HykB01KbgUnkK4gQKq4ZeR4RYVs= @@ -181,8 +181,8 @@ github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/Nu github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.19.0 h1:ol+5Fu+cSq9JD7SoSqe04GMI92cbn0+wvQ3bZ8b/AU4= -github.com/go-playground/validator/v10 v10.19.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= +github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= @@ -198,8 +198,8 @@ github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= -github.com/gobwas/ws v1.3.2 h1:zlnbNHxumkRvfPWgfXu8RBwyNR1x8wh9cf5PTOCqs9Q= -github.com/gobwas/ws v1.3.2/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= +github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs= +github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc= github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= @@ -247,8 +247,8 @@ github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= -github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= +github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo= +github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -268,13 +268,13 @@ github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= -github.com/gregdel/pushover v1.3.0 h1:CewbxqsThoN/1imgwkDKFkRkltaQMoyBV0K9IquQLtw= -github.com/gregdel/pushover v1.3.0/go.mod h1:EcaO66Nn1StkpEm1iKtBTV3d2A16SoMsVER1PthX7to= +github.com/gregdel/pushover v1.3.1 h1:4bMLITOZ15+Zpi6qqoGqOPuVHCwSUvMCgVnN5Xhilfo= +github.com/gregdel/pushover v1.3.1/go.mod h1:EcaO66Nn1StkpEm1iKtBTV3d2A16SoMsVER1PthX7to= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grid-x/modbus v0.0.0-20210714071042-7af2b65ec03b/go.mod h1:YaK0rKJenZ74vZFcSSLlAQqtG74PMI68eDjpDCDDmTw= -github.com/grid-x/modbus v0.0.0-20240214112450-0d4922fba364 h1:Add3h+2rV6K85KUuvaaEWiuPZe7xtqtUabkSpc8pxnQ= -github.com/grid-x/modbus v0.0.0-20240214112450-0d4922fba364/go.mod h1:ei49YhPP0v5cVJZAX9k/VdqQakUYNKOEvKB/6vnkZaU= +github.com/grid-x/modbus v0.0.0-20240503115206-582f2ab60a18 h1:8V5xRtdD70kGC4/IHqFq+kcBSWr4k6nscAUgWwJ6A5k= +github.com/grid-x/modbus v0.0.0-20240503115206-582f2ab60a18/go.mod h1:WpbUAyptAAi0VAriSRopZa6uhiJOJCTz7KFvgGtNRXc= github.com/grid-x/serial v0.0.0-20191104121038-e24bc9bf6f08/go.mod h1:kdOd86/VGFWRrtkNwf1MPk0u1gIjc4Y7R2j7nhwc7Rk= github.com/grid-x/serial v0.0.0-20211107191517-583c7356b3aa h1:Rsn6ARgNkXrsXJIzhkE4vQr5Gbx2LvtEMv4BJOK4LyU= github.com/grid-x/serial v0.0.0-20211107191517-583c7356b3aa/go.mod h1:kdOd86/VGFWRrtkNwf1MPk0u1gIjc4Y7R2j7nhwc7Rk= @@ -323,8 +323,8 @@ github.com/influxdata/influxdb-client-go/v2 v2.13.0/go.mod h1:k+spCbt9hcvqvUiz0s github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf h1:7JTmneyiNEwVBOHSjoMxiWAqB992atOeepeFYegn5RU= github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= -github.com/insomniacslk/tapo v1.0.0 h1:9IXJAm1JAby+5tO4h1RuR1LxELk5XR4Ybi6zJmOMpAo= -github.com/insomniacslk/tapo v1.0.0/go.mod h1:KV+GyrClNVzrM1DXdbgvrARwjtuGkjV+r0XSbuK2a3w= +github.com/insomniacslk/tapo v1.0.1 h1:W7K/1SXR8fvCNqY1Qg+GB3ELUjsHAqXtBjBjes7aRQ0= +github.com/insomniacslk/tapo v1.0.1/go.mod h1:1wuMYu0+alZ4oE4BIzxAwQNveZgbb2tRqiIUwIe7SZE= github.com/insomniacslk/xjson v0.0.0-20240314172816-ab1449dc107f h1:fU9XEYZOydvaOH7AjYcTyyhR2kRvDjiN2s7pRyWY2pM= github.com/insomniacslk/xjson v0.0.0-20240314172816-ab1449dc107f/go.mod h1:Z4EVr4bVv9LZbbje9xyZEyOLpdCOmCvr5S9BJtrdTfw= github.com/itchyny/gojq v0.12.15 h1:WC1Nxbx4Ifw5U2oQWACYz32JK8G9qxNtHzrvW4KEcqI= @@ -397,8 +397,8 @@ github.com/lorenzodonini/ocpp-go v0.18.0/go.mod h1:ZynYDWGw6CslG3vyPuucLsy6AyE+h github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 h1:EnfXoSqDfSNJv0VBNqY/88RNnhSGYkrHaO0mmFGbVsc= github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40/go.mod h1:vy1vK6wD6j7xX6O6hXe621WabdtNkou2h7uRtTfRMyg= github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= -github.com/mabunixda/wattpilot v1.6.6 h1:kUU+8Sp3edYzvtAorc2uZpirE71KtysKHMMlqL9vnjQ= -github.com/mabunixda/wattpilot v1.6.6/go.mod h1:cfndLU/u8ANvy/HKNrT4ShsaNhPhIHlCRrDY8SyETFA= +github.com/mabunixda/wattpilot v1.7.0 h1:dLbsfsZi8+H4m6DuQUQzCAmLqVbZOMHn+m31zi8OmWc= +github.com/mabunixda/wattpilot v1.7.0/go.mod h1:cfndLU/u8ANvy/HKNrT4ShsaNhPhIHlCRrDY8SyETFA= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= @@ -423,8 +423,8 @@ github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQ github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4= -github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4= -github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY= +github.com/miekg/dns v1.1.59 h1:C9EXc/UToRwKLhK5wKU/I4QVsBUc8kE6MkHBkeypWZs= +github.com/miekg/dns v1.1.59/go.mod h1:nZpewl5p6IvctfgrckopVx2OlSEHPRO/U4SYkRklrEk= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= @@ -506,8 +506,8 @@ github.com/pascaldekloe/name v1.0.1 h1:9lnXOHeqeHHnWLbKfH6X98+4+ETVqFqxN09UXSjcM github.com/pascaldekloe/name v1.0.1/go.mod h1:Z//MfYJnH4jVpQ9wkclwu2I2MkHmXTlT9wR5UZScttM= github.com/paypal/gatt v0.0.0-20151011220935-4ae819d591cf/go.mod h1:+AwQL2mK3Pd3S+TUwg0tYQjid0q1txyNUJuuSmz8Kdk= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= -github.com/pelletier/go-toml/v2 v2.2.1 h1:9TA9+T8+8CUCO2+WYnDLCgrYi9+omqKXyjDtosvtEhg= -github.com/pelletier/go-toml/v2 v2.2.1/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= github.com/peterbourgon/ff v1.2.0/go.mod h1:ljiF7yxtUvZaxUDyUqQa0+uiEOgwVboj+Q2S2+0nq40= github.com/philippseith/signalr v0.6.3 h1:zCpVCdVq3LXRW7wXMOBGhHDqaijUdTPVhsIHBOnlbVg= @@ -548,16 +548,16 @@ github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8 github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.18.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s= -github.com/prometheus/common v0.52.3 h1:5f8uj6ZwHSscOGNdIQg6OiZv/ybiK2CO2q2drVZAQSA= -github.com/prometheus/common v0.52.3/go.mod h1:BrxBKv3FWBIGXw89Mg1AeBq7FSyRzXWI3l3e7W3RN5U= +github.com/prometheus/common v0.53.0 h1:U2pL9w9nmJwJDa4qqLQ3ZaePJ6ZTwt7cMD3AG3+aLCE= +github.com/prometheus/common v0.53.0/go.mod h1:BrxBKv3FWBIGXw89Mg1AeBq7FSyRzXWI3l3e7W3RN5U= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.13.0 h1:GqzLlQyfsPbaEHaQkO7tbDlriv/4o5Hudv6OXHGKX7o= -github.com/prometheus/procfs v0.13.0/go.mod h1:cd4PFCR54QLnGKPaKGA6l+cfuNXtht43ZKY6tow0Y1g= +github.com/prometheus/procfs v0.14.0 h1:Lw4VdGGoKEZilJsayHf0B+9YgLGREba2C6xr+Fdfq6s= +github.com/prometheus/procfs v0.14.0/go.mod h1:XL+Iwz8k8ZabyZfMFHPiilCniixqQarAy5Mu67pHlNQ= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/relvacode/iso8601 v1.3.0/go.mod h1:FlNp+jz+TXpyRqgmM7tnzHHzBnz776kmAH2h3sZCn0I= github.com/relvacode/iso8601 v1.4.0 h1:GsInVSEJfkYuirYFxa80nMLbH2aydgZpIf52gYZXUJs= @@ -571,8 +571,8 @@ github.com/rickb777/plural v1.4.2/go.mod h1:kdmXUpmKBJTS0FtG/TFumd//VBWsNTD7zOw7 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/robertkrimen/otto v0.3.0 h1:5RI+8860NSxvXywDY9ddF5HcPw0puRsd8EgbXV0oqRE= -github.com/robertkrimen/otto v0.3.0/go.mod h1:uW9yN1CYflmUQYvAMS0m+ZiNo3dMzRUDQJX0jWbzgxw= +github.com/robertkrimen/otto v0.4.0 h1:/c0GRrK1XDPcgIasAsnlpBT5DelIeB9U/Z/JCQsgr7E= +github.com/robertkrimen/otto v0.4.0/go.mod h1:uW9yN1CYflmUQYvAMS0m+ZiNo3dMzRUDQJX0jWbzgxw= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= @@ -612,8 +612,8 @@ github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4k github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= -github.com/spali/go-rscp v0.2.0-beta4 h1:ct9YZTCmTW2IMg74O16nJu0QntGF26dxY5ZejRvl280= -github.com/spali/go-rscp v0.2.0-beta4/go.mod h1:yPHx7clunJmpCLFDc60XL04/lp8p/DrrhfeBqM3J8cc= +github.com/spali/go-rscp v0.2.0 h1:bGzcsMx4PvZLZ/u20LowvngQl+Pz2jQH2Xl5JVKsrPE= +github.com/spali/go-rscp v0.2.0/go.mod h1:yPHx7clunJmpCLFDc60XL04/lp8p/DrrhfeBqM3J8cc= github.com/spali/go-slicereader v0.0.0-20201122145524-8e262e1a5127 h1:YDqvwAH/l3S4ZULmKlUYszPyLBjHq73CLuUPU+2jJeE= github.com/spali/go-slicereader v0.0.0-20201122145524-8e262e1a5127/go.mod h1:nf5bOq6n8UugtmQiD3l0BzkE5VP4NvyngFZVkH3ZzgM= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= @@ -709,13 +709,13 @@ golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= -golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= -golang.org/x/crypto/x509roots/fallback v0.0.0-20240404165943-d042a396a6de h1:C/4YHQ2Yo1mfk96afFDQ8hmky1PMCWwM9I96RB7GikA= -golang.org/x/crypto/x509roots/fallback v0.0.0-20240404165943-d042a396a6de/go.mod h1:kNa9WdvYnzFwC79zRpLRMJbdEFlhyM5RPFBBZp/wWH8= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595 h1:TgSqweA595vD0Zt86JzLv3Pb/syKg8gd5KMGGbJPYFw= +golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595/go.mod h1:kNa9WdvYnzFwC79zRpLRMJbdEFlhyM5RPFBBZp/wWH8= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8 h1:ESSUROHIBHg7USnszlcdmjBEwdMj9VUvU+OPk4yl2mc= -golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= +golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= +golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= 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= @@ -756,12 +756,12 @@ golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= -golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= -golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= 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.19.0 h1:9+E/EZBCbTLNrbN35fHv/a/d/mOBatymz1zbtQrXpIg= -golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8= +golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo= +golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -820,14 +820,14 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= -golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= -golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= +golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= 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= @@ -836,8 +836,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 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= @@ -861,8 +861,8 @@ golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 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.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY= -golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg= +golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw= +golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -876,8 +876,8 @@ google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRn google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240412170617-26222e5d3d56 h1:zviK8GX4VlMstrK3JkexM5UHjH1VOkRebH9y3jhSBGk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240412170617-26222e5d3d56/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240506185236-b8a5c65736ae h1:c55+MER4zkBS14uJhSZMGGmya0yJx5iHV4x/fpOSNRk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240506185236-b8a5c65736ae/go.mod h1:I7Y+G38R2bu5j1aLzfFmQfTcU/WnFuqDwLZAbvKTKpM= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= @@ -895,8 +895,8 @@ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQ google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -934,22 +934,22 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gorm.io/gorm v1.25.9 h1:wct0gxZIELDk8+ZqF/MVnHLkA1rvYlBWUMv2EdsK1g8= -gorm.io/gorm v1.25.9/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +gorm.io/gorm v1.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s= +gorm.io/gorm v1.25.10/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -modernc.org/cc/v4 v4.20.0 h1:45Or8mQfbUqJOG9WaxvlFYOAQO0lQ5RvqBcFCXngjxk= -modernc.org/cc/v4 v4.20.0/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= -modernc.org/ccgo/v4 v4.16.0 h1:ofwORa6vx2FMm0916/CkZjpFPSR70VwTjUCe2Eg5BnA= -modernc.org/ccgo/v4 v4.16.0/go.mod h1:dkNyWIjFrVIZ68DTo36vHK+6/ShBn4ysU61So6PIqCI= +modernc.org/cc/v4 v4.21.0 h1:D/gLKtcztomvWbsbvBKo3leKQv+86f+DdqEZBBXhnag= +modernc.org/cc/v4 v4.21.0/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= +modernc.org/ccgo/v4 v4.17.3 h1:t2CQci84jnxKw3GGnHvjGKjiNZeZqyQx/023spkk4hU= +modernc.org/ccgo/v4 v4.17.3/go.mod h1:1FCbAtWYJoKuc+AviS+dH+vGNtYmFJqBeRWjmnDWsIg= modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw= modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU= -modernc.org/libc v1.49.3 h1:j2MRCRdwJI2ls/sGbeSk0t2bypOG/uvPZUsGQFDulqg= -modernc.org/libc v1.49.3/go.mod h1:yMZuGkn7pXbKfoT/M35gFJOAEdSKdxL0q64sF7KqCDo= +modernc.org/libc v1.50.5 h1:ZzeUd0dIc/sUtoPTCYIrgypkuzoGzNu6kbEWj2VuEmk= +modernc.org/libc v1.50.5/go.mod h1:rhzrUx5oePTSTIzBgM0mTftwWHK8tiT9aNFUt1mldl0= modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= @@ -958,15 +958,15 @@ modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc= modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss= -modernc.org/sqlite v1.29.6 h1:0lOXGrycJPptfHDuohfYgNqoe4hu+gYuN/pKgY5XjS4= -modernc.org/sqlite v1.29.6/go.mod h1:S02dvcmm7TnTRvGhv8IGYyLnIt7AS2KPaB1F/71p75U= +modernc.org/sqlite v1.29.9 h1:9RhNMklxJs+1596GNuAX+O/6040bvOwacTxuFcRuQow= +modernc.org/sqlite v1.29.9/go.mod h1:ItX2a1OVGgNsFh6Dv60JQvGfJfTPHPVpV6DF59akYOA= modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= nhooyr.io/websocket v1.8.11 h1:f/qXNc2/3DpoSZkHt1DQu6rj4zGC8JmkkLkWss0MgN0= nhooyr.io/websocket v1.8.11/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c= -pgregory.net/rapid v0.4.7 h1:MTNRktPuv5FNqOO151TM9mDTa+XHcX6ypYeISDVD14g= -pgregory.net/rapid v0.4.7/go.mod h1:UYpPVyjFHzYBGHIxLFoupi8vwk6rXNzRY9OMvVxFIOU= +pgregory.net/rapid v1.1.0 h1:CMa0sjHSru3puNx+J0MIAuiiEV4N0qj8/cMWGBBCsjw= +pgregory.net/rapid v1.1.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= From 86b320704533cfd3f43400ac281f2e494f81b34d Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Wed, 8 May 2024 20:08:46 +0200 Subject: [PATCH 074/168] Config UI: use modbus defaults when testing/creating device (#13815) --- assets/js/components/Config/MeterModal.vue | 10 ++++++++++ i18n/de.toml | 10 ++++++++++ i18n/en.toml | 10 ++++++++++ 3 files changed, 30 insertions(+) diff --git a/assets/js/components/Config/MeterModal.vue b/assets/js/components/Config/MeterModal.vue index f2990d9686..6af8d7b894 100644 --- a/assets/js/components/Config/MeterModal.vue +++ b/assets/js/components/Config/MeterModal.vue @@ -235,9 +235,19 @@ export default { modbusCapabilities() { return this.modbus?.Choice || []; }, + modbusDefaults() { + const { ID, Comset, Baudrate, Port } = this.modbus || {}; + return { + id: ID, + comset: Comset, + baudrate: Baudrate, + port: Port, + }; + }, apiData() { return { template: this.templateName, + ...this.modbusDefaults, ...this.values, usage: this.meterType, }; diff --git a/i18n/de.toml b/i18n/de.toml index 27b3c04259..0c3666ec63 100644 --- a/i18n/de.toml +++ b/i18n/de.toml @@ -72,6 +72,16 @@ template = "Hersteller" titleChoice = "Was möchtest du hinzufügen?" validateSave = "Überprüfen & speichern" +[config.options] + +[config.options.endianness] +big = "big-endian" +little = "little-endian" + +[config.options.schema] +http = "HTTP (unverschlüsselt)" +https = "HTTPS (verschlüsselt)" + [config.pv] titleAdd = "Zähler (PV) hinzufügen" titleEdit = "Zähler (PV) bearbeiten" diff --git a/i18n/en.toml b/i18n/en.toml index 5ac1f7920c..02245c2e29 100644 --- a/i18n/en.toml +++ b/i18n/en.toml @@ -72,6 +72,16 @@ template = "Manufacturer" titleChoice = "What Do You Want To Add?" validateSave = "Validate & save" +[config.options] + +[config.options.endianness] +big = "big-endian" +little = "little-endian" + +[config.options.schema] +http = "HTTP (unencrypted)" +https = "HTTPS (encrypted)" + [config.pv] titleAdd = "Add Solar Meter" titleEdit = "Edit Solar Meter" From b2523b32a67fec77fa27433a41bcb9c8818ef752 Mon Sep 17 00:00:00 2001 From: andig Date: Thu, 9 May 2024 18:11:23 +0200 Subject: [PATCH 075/168] Mercedes: fix configured vehicle cannot be modified (#13812) --- cmd/token_mercedes.go | 19 ++--- templates/definition/vehicle/mercedes.yaml | 95 ++++++++++------------ 2 files changed, 51 insertions(+), 63 deletions(-) diff --git a/cmd/token_mercedes.go b/cmd/token_mercedes.go index 6c27130747..9bc0b83579 100644 --- a/cmd/token_mercedes.go +++ b/cmd/token_mercedes.go @@ -43,7 +43,7 @@ func mercedesPinPrompt() (string, error) { } func mercedesToken() (*oauth2.Token, error) { - // Get username and region from user to initate the email process + // Get username and region from user to initiate the email process username, region, err := mercedesUsernameAndRegionPrompt() if err != nil { return nil, err @@ -55,17 +55,14 @@ func mercedesToken() (*oauth2.Token, error) { return nil, err } - if result { - pin, err := mercedesPinPrompt() - if err != nil { - return nil, err - } + if !result { + return nil, errors.New("unknown PinResponse - 200, result empty") + } - token, err := api.RequestAccessToken(*nonce, pin) - if err == nil { - return token, nil - } + pin, err := mercedesPinPrompt() + if err != nil { + return nil, err } - return nil, errors.New("unknown PinResponse - 200, Email empty") + return api.RequestAccessToken(*nonce, pin) } diff --git a/templates/definition/vehicle/mercedes.yaml b/templates/definition/vehicle/mercedes.yaml index 92b8847715..1cde282843 100644 --- a/templates/definition/vehicle/mercedes.yaml +++ b/templates/definition/vehicle/mercedes.yaml @@ -4,56 +4,47 @@ products: requirements: description: de: | - Die Konfiguration der Mercedes-Benz Integration nur im yaml Modus möglich. - Ablauf: - 1. Hinzufügen der Konfiguration in die evcc.yaml (ohne Token) - ``` - vehicles: - - name: my_car - type: mercedes - account: # Mercedes Me Nutzer-Id (email) - region: # MB me Region (EMEA, APAC, NORAM) - vin: W... # Erforderlich, wenn mehr als ein Fahrzeug im Account registriert - capacity: 50 # Akkukapazität in kWh (optional) - ``` - 2. Token Generierung: Ausführen von `./evcc token mercedes` oder `evcc token [name]`, wenn name gesetzt ist. - 3. Einfügen der Tokens in die evcc.yaml - ``` - vehicles: - - name: my_car - type: mercedes - account: # Mercedes Me Nutzer-Id (email) - region: # MB me Region (EMEA, APAC, NORAM) - vin: W... # , wenn mehr als ein Fahrzeug im Account registriert - capacity: 50 # Akkukapazität in kWh (optional) - tokens: - access: token... - refresh: token... - ``` + Benötigt `access` und `refresh` Tokens. Diese können über den Befehl `evcc token [name]` generiert werden. en: | - The configuration of the Mercedes-Benz integration is only possible in yaml mode. - Procedure: - 1. add the configuration to evcc.yaml (without token) - ``` - vehicles: - - name: my_car - type: mercedes - account: # Mercedes Me user-Id (email) - region: # MB me region (EMEA, APAC, NORAM) - vin: W... # Required, if more then one car is registered in this account - capacity: 50 # capacity in kWh (optional) - ``` - 2. Token generation: execute `./evcc token mercedes` or `evcc token [name]`, when name is defined - 3. insert the tokens into evcc.yaml - ``` - vehicles: - - name: my_car - type: mercedes - account: # Mercedes Me user-id (email) - region: # MB me region (EMEA, APAC, NORAM) - vin: W... # Required, if more then one car is registered in this account - capacity: 50 # capacity in kWh (optional) - tokens: - access: token... - refresh: token... - ``` + Requires `access` and `refresh` tokens. These can be generated with command `evcc token [name]`. +params: + - name: title + - name: icon + default: car + advanced: true + - name: user + required: true + - name: region + required: true + validvalues: [EMEA, APAC, NORAM] + default: EMEA + - name: accessToken + required: true + mask: true + help: + en: "See https://docs.evcc.io/en/docs/devices/vehicles#mercedes" + de: "Siehe https://docs.evcc.io/docs/devices/vehicles#mercedes" + - name: refreshToken + required: true + mask: true + help: + en: "See https://docs.evcc.io/en/docs/devices/vehicles#mercedes" + de: "Siehe https://docs.evcc.io/docs/devices/vehicles#mercedes" + - name: vin + example: V... + - name: capacity + - name: phases + advanced: true + - preset: vehicle-identify +render: | + type: mercedes + title: {{ .title }} + icon: {{ .icon }} + user: {{ .user }} + tokens: + access: {{ .accessToken }} + refresh: {{ .refreshToken }} + capacity: {{ .capacity }} + phases: {{ .phases }} + vin: {{ .vin }} + {{ include "vehicle-identify" . }} From 2254fedde18b13ac5405eb2d32270c4b2944c76c Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Fri, 10 May 2024 01:11:34 +0200 Subject: [PATCH 076/168] Translations update from Hosted Weblate (#13749) * Translated using Weblate (French) Currently translated at 100.0% (412 of 412 strings) Co-authored-by: FraBoCH Translate-URL: https://hosted.weblate.org/projects/evcc/evcc/fr/ Translation: evcc/evcc * Translated using Weblate (Hungarian) Currently translated at 100.0% (416 of 416 strings) Translated using Weblate (Hungarian) Currently translated at 100.0% (412 of 412 strings) Added translation using Weblate (Hungarian) Co-authored-by: interstart Translate-URL: https://hosted.weblate.org/projects/evcc/evcc/hu/ Translation: evcc/evcc * Translated using Weblate (Turkish) Currently translated at 99.7% (415 of 416 strings) Translated using Weblate (Turkish) Currently translated at 99.7% (415 of 416 strings) Translated using Weblate (Turkish) Currently translated at 99.7% (415 of 416 strings) Translated using Weblate (Turkish) Currently translated at 99.7% (415 of 416 strings) Translated using Weblate (Turkish) Currently translated at 99.7% (415 of 416 strings) Translated using Weblate (Turkish) Currently translated at 98.7% (411 of 416 strings) Translated using Weblate (Turkish) Currently translated at 98.7% (411 of 416 strings) Translated using Weblate (Turkish) Currently translated at 98.7% (411 of 416 strings) Translated using Weblate (Turkish) Currently translated at 99.7% (411 of 412 strings) Translated using Weblate (Turkish) Currently translated at 99.7% (411 of 412 strings) Translated using Weblate (Turkish) Currently translated at 99.7% (411 of 412 strings) Translated using Weblate (Turkish) Currently translated at 99.7% (411 of 412 strings) Translated using Weblate (Turkish) Currently translated at 99.7% (411 of 412 strings) Translated using Weblate (Turkish) Currently translated at 99.7% (411 of 412 strings) Translated using Weblate (Turkish) Currently translated at 100.0% (412 of 412 strings) Translated using Weblate (Turkish) Currently translated at 100.0% (412 of 412 strings) Co-authored-by: aerucu Translate-URL: https://hosted.weblate.org/projects/evcc/evcc/tr/ Translation: evcc/evcc * Translated using Weblate (Italian) Currently translated at 46.3% (191 of 412 strings) Co-authored-by: albertofralbe Translate-URL: https://hosted.weblate.org/projects/evcc/evcc/it/ Translation: evcc/evcc * Translated using Weblate (Lithuanian) Currently translated at 100.0% (416 of 416 strings) Co-authored-by: RTTTC <94727758+RTTTC@users.noreply.github.com> Translate-URL: https://hosted.weblate.org/projects/evcc/evcc/lt/ Translation: evcc/evcc * Translated using Weblate (Spanish) Currently translated at 100.0% (416 of 416 strings) Co-authored-by: gallegonovato Translate-URL: https://hosted.weblate.org/projects/evcc/evcc/es/ Translation: evcc/evcc * fix toml --------- Co-authored-by: FraBoCH Co-authored-by: interstart Co-authored-by: aerucu Co-authored-by: albertofralbe Co-authored-by: RTTTC <94727758+RTTTC@users.noreply.github.com> Co-authored-by: gallegonovato Co-authored-by: premultiply <4681172+premultiply@users.noreply.github.com> --- i18n/es.toml | 10 + i18n/fr.toml | 18 +- i18n/hu.toml | 549 +++++++++++++++++++++++++++++++++++++++++++++++++++ i18n/it.toml | 17 ++ i18n/lt.toml | 10 + i18n/tr.toml | 352 +++++++++++++++++---------------- 6 files changed, 777 insertions(+), 179 deletions(-) create mode 100644 i18n/hu.toml diff --git a/i18n/es.toml b/i18n/es.toml index 22a10c58e3..2282c872a5 100644 --- a/i18n/es.toml +++ b/i18n/es.toml @@ -74,6 +74,16 @@ template = "Fabricante" titleChoice = "¿Qué quieres añadir?" validateSave = "Validar y guardar" +[config.options] + +[config.options.endianness] +big = "big endian" +little = "little endian" + +[config.options.schema] +http = "HTTP (sin cifrar)" +https = "HTTPS (cifrado)" + [config.pv] titleAdd = "Añadir contador solar" titleEdit = "Editar contador solar" diff --git a/i18n/fr.toml b/i18n/fr.toml index f8a199bd3d..a4c465ace2 100644 --- a/i18n/fr.toml +++ b/i18n/fr.toml @@ -13,7 +13,7 @@ legendTitle = "Comment l'énergie solaire doit-elle être utilisée?" legendTopAutostart = "démarre automatiquement" legendTopName = "recharge soutenue par la batterie" legendTopSubline = "sans interruption" -modalTitle = "Réglages de la batterie" +modalTitle = "Batterie domestique" [batterySettings.bufferStart] above = "lorsque chargé à plus que {soc}" @@ -59,7 +59,7 @@ titleEdit = "Modifier compteur pour réseau" [config.main] addLoadpoint = "Ajouter un point de charge" addPvBattery = "Ajouter du solaire ou une batterie" -addVehicle = "Ajouter le véhicule" +addVehicle = "Ajouter un véhicule" edit = "éditer" title = "Configuration" unconfigured = "non configuré" @@ -95,7 +95,7 @@ restartingDescription = "Veuillez patienter…" restartingMessage = "Redémarrage d’evcc en cours." [config.title] -description = "«Affiché sur l’écran principal et l’onglet du navigateur.»" +description = "Affiché sur l’écran principal et l’onglet du navigateur." label = "Titre" title = "Modifier le titre" @@ -240,8 +240,8 @@ update = "Mise à jour automatique" [loginModal] cancel = "Annuler" -error = "«Connection échouée:»" -invalid = "«Mot de passe invalide.»" +error = "Connection échouée:" +invalid = "Mot de passe invalide." login = "Identifiant" password = "Mot de passe" reset = "Réinitialiser le mot de passe ?" @@ -390,7 +390,7 @@ detectionActive = "Détection du véhicule…" fallbackName = "Véhicule" moreActions = "Plus d'actions" none = "Pas de véhicule" -notReachable = "«Impossible d’atteindre le véhicule. Essayez de redémarrer evcc.»" +notReachable = "Impossible d’atteindre le véhicule. Essayez de redémarrer evcc." targetSoc = "Limite" temp = "Temp." tempLimit = "Temp. limite" @@ -433,9 +433,9 @@ message = "Pas de connexion au serveur." reload = "Recharger?" [passwordModal] -description = "«Configurez un mot de passe pour protéger la configuration. L’utilisation de l’écran principal et toujours possible sans authentification.»" +description = "Configurez un mot de passe pour protéger la configuration. L’utilisation de l’écran principal et toujours possible sans authentification." empty = "Le mot de passe ne doit pas être vide" -error = "«Erreur:»" +error = "Erreur:" labelCurrent = "Mot de passe actuel" labelNew = "Nouveau mot de passe" labelRepeat = "Répétez le mot de passe" @@ -495,7 +495,7 @@ allVehicles = "tous les véhicules" filter = "Filtrer" [settings] -title = "Réglages généraux" +title = "Interface utilisateur" [settings.fullscreen] enter = "Passer en plein écran" diff --git a/i18n/hu.toml b/i18n/hu.toml new file mode 100644 index 0000000000..f9b9c5aaed --- /dev/null +++ b/i18n/hu.toml @@ -0,0 +1,549 @@ +[batterySettings] +batteryLevel = "Akkumulátor szint" +capacity = "{energy} / {total}" +control = "Akkumulátor vezérlés" +discharge = "Kisütés megakadályoza gyorstöltési és tervezett töltési üzemmódban." +disclaimerHint = "Megjegyzés:" +disclaimerText = "Ezek a beállítások csak a szolár üzemmódot érintik. A töltés módja ennek megfelelően kerül beállításra." +legendBottomName = "otthoni prioritás" +legendBottomSubline = "nincs töltésre használva" +legendMiddleName = "jármű először" +legendMiddleSubline = "otthon másodszor" +legendTitle = "Hogyan használjuk fel a napenergiát?" +legendTopAutostart = "automatikusan indul" +legendTopName = "energiatárolóval támogatott töltés" +legendTopSubline = "megszakítások nélkül" +modalTitle = "Otthoni Akkumulátor" + +[batterySettings.bufferStart] +above = "amikor {soc} felett van" +full = "amikor {soc}-on van" +never = "csak ha van elég többlet" + +[config] + +[config.battery] +titleAdd = "Energiatároló Mérő Hozzáadása" +titleEdit = "Energiatároló Mérő Szerkesztése" + +[config.deviceValue] +capacity = "Kapacitás" +chargeStatus = "Státusz" +chargedEnergy = "Töltve" +current = "Jelenlegi" +enabled = "Engedélyezve" +energy = "Energia" +odometer = "Óraállás" +phaseCurrents = "Áram L1..L3" +phasePowers = "Teljesítmény L1..L3" +phaseVoltages = "Feszültség L1..L3" +power = "Teljesítmény" +range = "Hatótáv" +soc = "SoC" +socLimit = "Limit" + +[config.form] +example = "Példa" +optional = "opcionális" + +[config.general] +cancel = "Mégse" +save = "Mentés" + +[config.grid] +titleAdd = "Hálózati Mérő Hozzáadása" +titleEdit = "Hálózati Mérő Szerkesztése" + +[config.main] +addLoadpoint = "Töltőpont hozzáadása" +addPvBattery = "Napelem vagy energiatároló hozzáadása" +addVehicle = "Jármű hozzáadása" +edit = "szerkesztés" +title = "Konfiguráció" +unconfigured = "nincs konfigurálva" +vehicles = "Járműveim" +yaml = "Az evcc.yaml fájlban konfigurálva. Nem szerkeszthető az UI-ról." + +[config.meter] +cancel = "Mégse" +delete = "Törlés" +save = "Mentés" +template = "Gyártó" +titleChoice = "Mit szeretnél hozzáadni?" +validateSave = "Ellenőrzés & mentés" + +[config.options] + +[config.options.endianness] +big = "big-endian" +little = "little-endian" + +[config.options.schema] +http = "HTTP (titkosítatlan)" +https = "HTTPS (titkosított)" + +[config.pv] +titleAdd = "Napelemes Mérő Hozzáadása" +titleEdit = "Napelemes Mérő Szerkesztése" + +[config.section] +general = "Általános" +system = "Rendszer" + +[config.system] +logs = "Naplók" +restart = "Újraindítás" +restartRequiredDescription = "Kérlek indítsd újra a hatás eléréséhez." +restartRequiredMessage = "A Konfiguráció megváltozott." +restartingDescription = "Kérlek várj…" +restartingMessage = "evcc újraindítása." + +[config.title] +description = "Ez jelenik meg a főképernyőn és a böngésző címsorában.Displayed on main screen and browser tab." +label = "Megnevezés" +title = "Megnevezés Szerkesztése" + +[config.validation] +failed = "sikertelen" +label = "Státusz" +running = "ellenőrzés…" +success = "sikeres" +unknown = "ismeretlen" +validate = "ellenőrzés" + +[config.vehicle] +cancel = "Mégse" +delete = "Törlés" +generic = "Egyéb integrációk" +offline = "Általános jármű" +online = "Járművek online API-val" +save = "Mentés" +scooter = "Roller" +template = "Gyártó" +titleAdd = "Jármű hozzáadása" +titleEdit = "Jármű szerkesztése" +validateSave = "Ellenőrzés & mentés" + +[footer] + +[footer.community] +greenEnergy = "Napenergia" +greenEnergySub1 = "lett töltve evcc-vel" +greenEnergySub2 = "2022 Októbere óta" +greenShare = "Napenergia aránya" +greenShareSub1 = "energiát biztosított a" +greenShareSub2 = "napenergia, és az akkumulátor tárolók" +power = "Töltési energia" +powerSub1 = "{activeClients} / {totalClients} résztvevőből" +powerSub2 = "tölt éppen…" +tabTitle = "Élő közösség" + +[footer.savings] +co2Saved = "{value} megtakarítva" +co2Title = "CO₂ Emisszió" +configurePriceCo2 = "Ismerje meg az ár- és CO₂-adatok konfigurálását." +footerLong = "{percent}% napenergia" +footerShort = "{percent}% nap" +modalTitle = "Töltési Energia Áttekintés" +moneySaved = "{value} megtakarítva" +percentGrid = "{grid} kWh hálózat" +percentSelf = "{self} kWh napenergia" +percentTitle = "Napenergia" +periodLabel = "Periódus:" +priceTitle = "Energia Ára" +referenceGrid = "hálózat" +referenceLabel = "Referencia adat:" +tabTitle = "Az adataim" + +[footer.savings.period] +30d = "elmúlt 30 nap" +365d = "elmúlt 365 nap" +total = "összesen" + +[footer.sponsor] +becomeSponsor = "Legyél Támogató" +confetti = "Készen állsz a konfettire?" +confettiPromise = "Kapsz matricákat és digitális konfettit" +sticker = "… vagy evcc matricákat?" +supportUs = "A küldetésünk az, hogy a napenergiát normává tegyük. Segítsd az evcc-t annyival, amennyit megér neked." +thanks = "Köszönjük, {sponsor}! A hozzájárulásod segít tovább fejleszteni az evcc-t." +titleNoSponsor = "Támogass minket" +titleSponsor = "Te már támogató vagy" + +[footer.telemetry] +optIn = "Szeretnék hozzájárulni az adataimmal." +optInMoreDetails = "Részletek {0}." +optInMoreDetailsLink = "itt" +optInSponsorship = "Szponzorálás szükséges." + +[footer.version] +availableLong = "új verzió elérhető" +modalCancel = "Mégse" +modalDownload = "Letöltés" +modalInstalledVersion = "Telepített verzió" +modalNoReleaseNotes = "Nem érhető el kiadási jegyzet. További információ az új verzióról:" +modalTitle = "Új verzió elérhető" +modalUpdate = "Telepítés" +modalUpdateNow = "Telepítés most" +modalUpdateStarted = "Az új evcc verzió indítása…" +modalUpdateStatusStart = "A telepítés elkezdődött:" + +[header] +about = "Névjegy" +blog = "Blog" +docs = "Dokumentáció" +github = "GitHub" +login = "Jármű Bejelentkezések" +logout = "Kijelentkezés" +nativeSettings = "Szerver Váltás" +needHelp = "Szükséged van segítségre?" +sessions = "Töltési Munkamenetek" + +[help] +discussionsButton = "GitHub Közösségi oldalát" +documentationButton = "Dokumentáció" +issueButton = "Hibabejelentés" +issueDescription = "Furcsa vagy hibás működést észleltél?" +logsButton = "Napló megtekintése" +logsDescription = "Ellenőrizd a naplót hiba esetén." +modalTitle = "Segítségre van szükséged?" +primaryActions = "Valami nem úgy működik, ahogy működnie kellene? Ezek jó helyek, hogy választ kapj a problémádra." +restartButton = "Újraindítás" +restartDescription = "Próbáltad már be- és kikapcsolni?" +secondaryActions = "Még mindíg nem oldódott meg a problémád? Itt van néhány keményebb lehetőség." + +[help.restart] +cancel = "Mégse" +confirm = "Igen, újraindítás!" +description = "Normál körülmények között nem szükséges az újraindítás. Kérlek fontold meg a hibabejelentést, ha az evcc-t gyakran újra kell indítanod." +disclaimer = "Megjegyzés: az evcc bezáródik és az operációs rendszertől függően újraindítja a szolgáltatást." +modalTitle = "Biztosan újra szeretnéd indítani?" + +[log] +areaLabel = "Szűrés terület alapján" +areas = "Minden terület" +download = "Teljes napló letöltése" +levelLabel = "Szűrés a napló szintje alapján" +noResults = "Nincs egyező napló bejegyzés." +search = "Keresés" +showAll = "Az összes bejegyzés megjelenítése" +title = "Naplók" +update = "Automatikus frissítés" + +[loginModal] +cancel = "Mégse" +error = "Sikertelen bejelentkezés: " +invalid = "A jelszó érvénytelen." +login = "Bejelentkezés" +password = "Jelszó" +reset = "Jelszó Visszaállítás?" +title = "Hitelesítés" + +[main] +vehicles = "Parkolás" + +[main.chargingPlan] +active = "Aktív" +arrivalTab = "Érkezés" +day = "Nap" +departureTab = "Indulás" +goal = "Töltési cél" +modalTitle = "Töltési Tervezet" +none = "nincs" +remove = "Törlés" +time = "Idő" +title = "Terv" +titleMinSoc = "Min töltés" +titleTargetCharge = "Indulás" +unsavedChanges = "Vannak nem mentett módosítások. Alkalmazza most?" +update = "Alkalmazás" + +[main.energyflow] +battery = "Battery" +batteryCharge = "Energiatároló töltés" +batteryDischarge = "Energiatároló kisütés" +batteryHold = "Energiatároló (lezárva)" +batteryTooltip = "{energy} / {total} ({soc})" +gridImport = "Hálózatból import" +homePower = "Fogyasztás" +loadpoints = "Töltő| Töltő | {count} töltők" +noEnergy = "Nincs mérési adat" +pvExport = "Hálózatba export" +pvProduction = "Napelem termelés" +selfConsumption = "Saját fogyasztás" + +[main.heatingStatus] +charging = "Fűtés…" +cheapEnergyCharging = "Fűtés olcsó energiával: {price} (limit {limit})" +cleanEnergyCharging = "Fűtés tiszta energiával: {co2} (limit {limit})" +waitForVehicle = "Üzemkész. Várakozás a fűtésre…" + +[main.loadpoint] +avgPrice = "⌀ Ár" +charged = "Töltve" +co2 = "⌀ CO₂" +duration = "Időtarta," +fallbackName = "Töltőpont" +power = "Teljesítmény" +price = "Σ Ár" +remaining = "Hátralévő" +remoteDisabledHard = "{source}: kikapcsolva" +remoteDisabledSoft = "{source}: kikapcsolva, adaptív napelemes töltés" +solar = "Nap" + +[main.loadpointSettings] +currents = "Töltőáram" +default = "alapértelmezett" +disclaimerHint = "Megjegyzés:" +onlyForSocBasedCharging = "Ezek a beállítások csak olyan járművekre elérhetőek, amiknek ismert a töltési szintje." +smartCostCheap = "Olcsó Hálózati Töltés" +smartCostClean = "Tiszta Hálózati Töltés" +title = "Beállítások {0}" +vehicle = "Jármű" + +[main.loadpointSettings.limitSoc] +description = "Töltési korlát, amikor a jármű csatlakoztatva van." +label = "Alapértelmezett limit" + +[main.loadpointSettings.maxCurrent] +label = "Max. Áram" + +[main.loadpointSettings.minCurrent] +label = "Min. Áram" + +[main.loadpointSettings.minSoc] +description = "A jármű gyorstöltve lesz {0}%-ra szolár üzemmódban. Ezután folytatódik a töltés a napelemes többletenergiával. Hasznos egy minimum tartományt megadni a felhősebb napokra." +label = "Min. töltés %" + +[main.loadpointSettings.phasesConfigured] +label = "Fázis" +no1p3pSupport = "Hogyan van csatlakoztatva a töltőd?" +phases_0 = "auto kapcsolás" +phases_1 = "1 fázis" +phases_1_hint = "({min}-tól {max}-ig)" +phases_3 = "3 fázis" +phases_3_hint = "({min}-tól {max}-ig)" + +[main.mode] +minpv = "Min+Szolár" +now = "Gyors" +off = "Ki" +pv = "Szolár" + +[main.provider] +login = "bejelentkezés" +logout = "kijelentkezés" + +[main.targetCharge] +activate = "Aktiválás" +co2Limit = "CO₂ limit, ami {co2}" +costLimitIgnore = "A konfigurált {limit} figyelmen kívűl lesz hagyva ezen időszakban." +currentPlan = "Aktív terv" +descriptionEnergy = "Meddig kell a {targetEnergy}-t tölteni a járműbe?" +descriptionSoc = "Mikor legyen a jármű feltöltve {targetSoc}%-ra?" +inactiveLabel = "Tervezett idő" +notReachableInTime = "A tervezet nem teljesíthető a megadott időben. Várható befejezés: {endTime}." +onlyInPvMode = "A töltési idő csak napelemes üzemmódban működik." +planDuration = "Töltési idő" +planPeriodLabel = "Periódus" +planPeriodValue = "{start} to {end}" +planUnknown = "még nem ismert" +preview = "Terv előnézet" +priceLimit = "ár limit: {price}" +remove = "Eltávolítás" +setTargetTime = "nincs" +targetIsAboveLimit = "A konfigurált töltési limit, ami {limit} figyelmen kívül lesz hagyva ebben a periódusban." +targetIsAboveVehicleLimit = "Növelje a jármű limitet ({limit}) hogy elérje a töltési célt." +targetIsInThePast = "Válassz egy időpontot a jövőben, Marty." +targetIsTooFarInTheFuture = "Hozzáigazítjuk a tervet, amint többet tudunk a jövőről." +title = "Cél Idő" +today = "ma" +tomorrow = "holnap" +update = "Frissítés" +vehicleCapacityDocs = "Ismerje meg, hogyan kell konfigurálni." +vehicleCapacityRequired = "A jármű akkumulátor kapacitása szükséges a hozzávetőleges idő meghatározásához." + +[main.targetChargePlan] +chargeDuration = "Töltési idő" +co2Label = "CO₂ emisszió ⌀" +priceLabel = "Energia ára" +timeRange = "{day} {range} h" +unknownPrice = "még ismeretlen" + +[main.targetEnergy] +label = "Limit" +noLimit = "nincs" + +[main.vehicle] +addVehicle = "Jármű hozzáadása" +changeVehicle = "Jármű cseréje" +detectionActive = "Jármű detektálása…" +fallbackName = "Jármű" +moreActions = "További Műveletek" +none = "Nincs jármű" +notReachable = "A jármű nem elérhető. Próbálja meg újraindítani az evcc-t." +targetSoc = "Limit" +temp = "Hőm." +tempLimit = "Hőm. limit" +unknown = "Vendég jármű" +vehicleSoc = "Töltés" + +[main.vehicleSoc] +charging = "tölt" +connected = "csatlakoztatva" +disconnected = "lecsatlakoztatva" +ready = "üzemkész" +vehicleLimit = "Jármű limit: {soc}%" + +[main.vehicleStatus] +charging = "Töltés…" +cheapEnergyCharging = "Olcsó energiával töltés: {price} (limit {limit})" +cleanEnergyCharging = "Tiszta energiával töltés: {co2} (limit {limit})" +climating = "Elő-kondícionálás érzékelve." +connected = "Csatlakoztatva." +disconnected = "Lecsatlakoztatva." +minCharge = "Minimális töltés {soc}%-ig." +pvDisable = "Nincs elég többlet. Szüneteltetés ekkor: {remaining}…" +pvEnable = "Többlet elérhető. Indítás ekkor: {remaining}…" +scale1p = "1 Fázisú töltésre váltás ekkor: {remaining}…" +scale3p = "3 Fázisú töltésre váltás ekkor: {remaining}…" +targetChargeActive = "Töltési terv aktív…" +targetChargePlanned = "A töltési tervezet ekkor keződik: {time}." +targetChargeWaitForVehicle = "Töltési terv üzemkész. Várakozás járműre…" +unknown = "" +vehicleLimitReached = "Jármű limit {soc}% elérve." +waitForVehicle = "Üzemkész. Járműre várakozás…" + +[notifications] +dismissAll = "Összeset figyelmen kívül hagyja" +logs = "Teljes napló megtekintése" +modalTitle = "Értesítések" + +[offline] +message = "Nem csatlakozik a szerverhez." +reload = "Újratöltés?" + +[passwordModal] +description = "Állítson be egy jelszót a konfigurációs beállítások védelmére. A főképernyő használata továbbra is lehetséges bejelentkezés nélkül." +empty = "A jelszó nem lehet üres" +error = "Hiba: " +labelCurrent = "Jelenlegi jelszó" +labelNew = "Új jelszó" +labelRepeat = "Jelszó megismétlése" +newPassword = "Jelszó készítése" +noMatch = "A jelszavak nem egyeznek" +titleNew = "Adminisztrátor jelszó beállítása" +titleUpdate = "Adminisztrátor jelszó módosítása" +updatePassword = "Jelszó módosítás" + +[session] +cancel = "Mégse" +co2 = "CO₂" +date = "Periódus" +delete = "Törlés" +finished = "Befejeződött" +meter = "Mérő" +meterstart = "Mérő indítás" +meterstop = "Mérő leállítás" +odometer = "Futásteljesítmény" +price = "Ár" +started = "Elkezdődött" +title = "Töltési munkamenet" + +[sessions] +avgPower = "⌀ Teljesítmény" +avgPrice = "⌀ Ár" +chargeDuration = "Időtartam" +co2 = "⌀ CO₂" +csvMonth = "Letöltés - {month} .CSV" +csvTotal = "Letöltés - Összes időszak .CSV" +date = "Indulás" +downloadCsv = "Letöltés mint: CSV" +energy = "Töltve" +loadpoint = "Töltőpont" +noData = "Nincs töltési munkamenet ebben a hónapban." +price = "Σ Ár" +reallyDelete = "Biztosan szeretné törölni ezt a munkamenetet?" +solar = "Napenergia" +title = "Töltési munkamenetek" +total = "Összesen" +vehicle = "Jármű" + +[sessions.csv] +chargedenergy = "Energia (kWh)" +created = "Elindítva" +finished = "Befejezve" +identifier = "Azonosító" +loadpoint = "Töltőpont" +meterstart = "Fogyasztásmérő kezdeti állás (kWh)" +meterstop = "Fogyasztásmérő befejező állás (kWh)" +odometer = "Óraállás (km)" +vehicle = "Jármű" + +[sessions.filter] +allLoadpoints = "minden töltőpont" +allVehicles = "minden jármű" +filter = "Szűrés" + +[settings] +title = "Felhasználói felület" + +[settings.fullscreen] +enter = "Teljes képernyő" +exit = "Kilépés a teljes képernyőből" +label = "Teljes képernyő" + +[settings.hiddenFeatures] +label = "Kísérlet" +value = "Kísérleti UI funkciók megjelenítése." + +[settings.language] +auto = "Automatikus" +label = "Nyelv" + +[settings.sponsorToken] +expires = "A szponzor token-ed le fog járni {inXDays} nap múlva. {getNewToken} és frissítsd a konfigurációs fájlodat." +getNew = "Kérj egy újat" +hint = "Megjegyzés: Ezt a jövőben automatizálni fogjuk." + +[settings.telemetry] +label = "Telemetria" + +[settings.theme] +auto = "rendszer" +dark = "sötét" +label = "Megjelenés" +light = "világos" + +[settings.unit] +km = "km" +label = "Mértékegység" +mi = "mérföld" + +[smartCost] +activeHours = "{charging} / {total}" +activeHoursLabel = "Aktív órák" +applyToAll = "Minenhol alkalmazás?" +batteryDescription = "Az otthoni töltéstároló töltése a hálózatról." +cheapTitle = "Olcsó Hálózati Töltés" +cleanTitle = "Tiszta Hálózati Töltés" +co2Label = "CO₂ emisszió" +co2Limit = "CO₂ limit" +loadpointDescription = "Engedélyezi az átmeneti gyorstöltést szolár üzemmódban." +modalTitle = "Okos Hálózati Töltés" +none = "nincs" +priceLabel = "Energia ár" +priceLimit = "Ár limit" +saved = "Elmentve." + +[startupError] +configFile = "Konfigurációs fájl használatban:" +configuration = "Konfiguráció" +description = "Kérlek ellenőrizd a konfigurációs fájlodat. Ha a hibaüzenet nem segít, akkor nézd meg a {0}." +discussions = "GitHub Közösségi oldalát" +fixAndRestart = "Kérlek javítsd ki a problémát és indítsd újra a szervert." +hint = "Megjegyzés: Lehet, hogy van egy hibás eszközöd (inverter, mérő, …) Ellenőrizd a hálózati kapcsolatokat." +lineError = "Hiba a {0}." +lineErrorLink = "sor {0}" +restartButton = "Újraindítás" +title = "Indítási hiba" diff --git a/i18n/it.toml b/i18n/it.toml index 9481668405..924fbf84bc 100644 --- a/i18n/it.toml +++ b/i18n/it.toml @@ -1,6 +1,23 @@ [batterySettings] batteryLevel = "Livello batteria" capacity = "{energy} di {total}" +control = "Controllo batteria" +disclaimerHint = "“Nota:”" +disclaimerText = "“Queste impostazioni riguardano solamente la modalità solare. Il comportamento di ricarica è adattato di conseguenza.”" +legendBottomName = "“priorità casa”" +legendBottomSubline = "“Non usato per ricarica”" +legendMiddleName = "“Priorità al veicolo”" +legendMiddleSubline = "“Casa seconda”" +legendTitle = "“Come dovrebbe essere utilizzata l’energia solare?”" +legendTopAutostart = "“Parte automaticamente”" +legendTopName = "“Ricarica supportata dalla batteria”" +legendTopSubline = "“Senza interruzioni”" +modalTitle = "“Batteria di casa”" + +[batterySettings.bufferStart] +above = "“Quando supera {soc}”" +full = "“Quando è a {soc}”" +never = "“Solo con abbastanza surplus”" [footer] diff --git a/i18n/lt.toml b/i18n/lt.toml index 41647e7ff7..118699e9eb 100644 --- a/i18n/lt.toml +++ b/i18n/lt.toml @@ -74,6 +74,16 @@ template = "Gamintojas" titleChoice = "Ką norite pridėti?" validateSave = "Patikrinti ir išsaugoti" +[config.options] + +[config.options.endianness] +big = "mažėjantys baitai" +little = "didėjantys baitai" + +[config.options.schema] +http = "HTTP (nešifruotas)" +https = "HTTPS (užšifruotas)" + [config.pv] titleAdd = "Pridėti saulės skaitiklį" titleEdit = "Redaguoti saulės skaitiklį" diff --git a/i18n/tr.toml b/i18n/tr.toml index 4bafee6933..e0132161ea 100644 --- a/i18n/tr.toml +++ b/i18n/tr.toml @@ -1,35 +1,35 @@ [batterySettings] batteryLevel = "Batarya seviyesi" capacity = "{total} içinde {energy}" -control = "Pil kontrolü" -discharge = "Hızlı modda ve planlanan şarjda deşarjı önleyin." +control = "Batarya kontrolü" +discharge = "Hızlı modda ve planlanan doldurmada boşalmayı önleyin." disclaimerHint = "Not:" -disclaimerText = "Bu ayarlar yalnızca güneş enerjisi modunu etkiler. Şarj davranışı buna göre ayarlanır." +disclaimerText = "Bu ayarlar yalnızca güneş enerjisi modunu etkiler. Doldurma davranışı buna göre ayarlanır." legendBottomName = "ev önceliği" -legendBottomSubline = "şarj için kullanılmaz" +legendBottomSubline = "doldurma için kullanılmaz" legendMiddleName = "önce araç" legendMiddleSubline = "ev ikincil" legendTitle = "Güneş enerjisi nasıl kullanılmalıdır?" -legendTopAutostart = "otomatik başlar" -legendTopName = "batarya destekli şarj" +legendTopAutostart = "otomatik olarak başlar" +legendTopName = "batarya destekli doldurma" legendTopSubline = "kesintisiz" -modalTitle = "Batarya Ayarları" +modalTitle = "Ev bataryası" [batterySettings.bufferStart] above = "{soc} üzerinde olduğunda" full = "{soc} iken" -never = "yalnızca yeterli fazlalık varsa" +never = "yalnızca yeterli güneş enerjisi fazlalığı ile" [config] [config.battery] -titleAdd = "Batarya Ölçer Ekle" -titleEdit = "Batarya Ölçeri Düzenle" +titleAdd = "Batarya Sayacı Ekle" +titleEdit = "Batarya Sayacını Düzenle" [config.deviceValue] capacity = "Kapasite" chargeStatus = "Durum" -chargedEnergy = "Şarj Edildi" +chargedEnergy = "Doldu" current = "Akım" enabled = "Etkin" energy = "Enerji" @@ -39,27 +39,27 @@ phasePowers = "Faz Gücü L1..L3" phaseVoltages = "Faz Voltajı L1..L3" power = "Güç" range = "Menzil" -soc = "SoC" -socLimit = "Limit" +soc = "Dolum durumu" +socLimit = "Dolum Sınırlaması" [config.form] example = "Örnek" -optional = "opsiyonel" +optional = "isteğe bağlı" [config.general] cancel = "İptal" save = "Kaydet" [config.grid] -titleAdd = "Şebeke Ölçer Ekle" -titleEdit = "Şebeke Ölçeri Düzenle" +titleAdd = "Elektrik Sayacı Ekle" +titleEdit = "Elektrik Sayacını Düzenle" [config.main] -addLoadpoint = "Şarj noktası ekle" -addPvBattery = "Güneş enerjisi veya pil ekle" +addLoadpoint = "Doldurma noktası ekle" +addPvBattery = "Güneş enerjisi veya batarya ekle" addVehicle = "Araç ekle" edit = "düzenle" -title = "Konfigürasyon" +title = "Yapılandırma" unconfigured = "yapılandırılmadı" vehicles = "Araçlarım" yaml = "evcc.yaml içerisinde yapılandırıldı. Kullanıcı arayüzünde düzenlenemez." @@ -72,9 +72,19 @@ template = "Üretici" titleChoice = "Ne Eklemek İstersin:" validateSave = "Doğrula ve kaydet" +[config.options] + +[config.options.endianness] +big = "büyük uçlu" +little = "küçük uçlu" + +[config.options.schema] +http = "HTTP (şifrelenmemiş)" +https = "HTTPS (şifrelenmiş)" + [config.pv] -titleAdd = "Helyograf Ekle" -titleEdit = "Helyografı Düzenle" +titleAdd = "Güneş Enerjisi (GE) Sayacı Ekle" +titleEdit = "Güneş Enerjisi (GE) Sayacını Düzenle" [config.section] general = "Genel" @@ -85,8 +95,8 @@ logs = "Loglar" restart = "Yeniden Başlat" restartRequiredDescription = "Değişikliklerin yansıması için yeniden başlatma gereklidir." restartRequiredMessage = "Yapılandırma ayarları değiştirildi." -restartingDescription = "Lütfen bekleyin…" -restartingMessage = "Evcc yeniden başlatılıyor…" +restartingDescription = "Lütfen bekle…" +restartingMessage = "evcc yeniden başlatılıyor…" [config.title] description = "Ana ekranda ve tarayıcı sekmesinde görüntülenir." @@ -103,8 +113,8 @@ validate = "doğrula" [config.vehicle] cancel = "İptal" -delete = "Sil" -generic = "Diğer entegrasyonlar" +delete = "Aracı sil" +generic = "Diğer bütünleştirmeler" offline = "Genel Araç" online = "Çevrimiçi API'ye sahip araçlar" save = "Kaydet" @@ -118,112 +128,112 @@ validateSave = "Doğrula ve kaydet" [footer.community] greenEnergy = "Güneş Enerjisi" -greenEnergySub1 = "evcc ile şarj edildi" +greenEnergySub1 = "evcc ile dolduruldu" greenEnergySub2 = "Ekim 2022'den beri" -greenShare = "Güneş enerjisi paylaşımı" -greenShareSub1 = "tarafından sağlanan enerji" -greenShareSub2 = "güneş enerjisi ve batarya deposu" -power = "Şarj gücü" -powerSub1 = "{activeClients}/{totalClients} katılımcı" -powerSub2 = "şarj oluyor..." +greenShare = "Güneş enerjisi payı" +greenShareSub1 = "güneş enerjisi ve batarya deposu" +greenShareSub2 = "tarafından sağlanan enerji" +power = "Doldurma gücü" +powerSub1 = "{totalClients} katılımcıdan {activeClients} katılımcı" +powerSub2 = "dolduruyor..." tabTitle = "Canlı topluluk" [footer.savings] -co2Saved = "{value} kg CO₂ tasarruf edildi" +co2Saved = "{value} CO₂ tasarruf edildi" co2Title = "CO₂ Emisyonu" -configurePriceCo2 = "Fiyat ve CO₂ ayarlarını yapılandırmayı öğrenin." -footerLong = "{percent}% güneş enerjisi" -footerShort = "{percent}% güneş enerjisi" -modalTitle = "Tasarruf" +configurePriceCo2 = "Fiyat ve CO₂ emisyonlarını yapılandır." +footerLong = "%{percent} güneş enerjisi" +footerShort = "%{percent} güneş" +modalTitle = "Doldurma Enerjisi Genel Bakışı" moneySaved = "{value} tasarruf edildi" -percentGrid = "{grid} kWh şebeke" -percentSelf = "{self} kWh kendi tüketimi" -percentTitle = "Güneş Enerjisi Kullanımı" -periodLabel = "Dönem" -priceTitle = "Fiyat" +percentGrid = "{grid} kWh şebekeden" +percentSelf = "{self} kWh güneşden" +percentTitle = "Güneş Enerjisi" +periodLabel = "Zaman aralığı" +priceTitle = "Enerji fiyatı" referenceGrid = "Şebeke" -referenceLabel = "Referans" -tabTitle = "Tasarruf" +referenceLabel = "Referans verileri:" +tabTitle = "Verilerim" [footer.savings.period] 30d = "son 30 gün" 365d = "son 365 gün" -total = "toplam" +total = "tüm zaman" [footer.sponsor] -becomeSponsor = "Sponsor ol" -confetti = "Konfeti için hazır mısınız?" -confettiPromise = "Stickerlar ve dijital konfeti alırsınız." -sticker = "… ya da evcc stickerlarımız?" -supportUs = "Misyonumuz güneş enerjisini norm haline getirmektir. Evcc'ye değer verdiğiniz kadar ödeme yaparak yardımcı olun." -thanks = "Teşekkürler {sponsor}! Katkınız evcc'yi daha iyi yapmamıza yardımcı oluyor." -titleNoSponsor = "Bizi destekleyin" -titleSponsor = "Sponsorsunuz" +becomeSponsor = "Destekçi ol" +confetti = "Konfeti istermisin?" +confettiPromise = "Stickerler ve dijital konfeti de var" +sticker = "… ya da evcc stickerları?" +supportUs = "Hedefimiz güneş enerjisi ile yakıt ikmalini gelenek haline getirmek. Bize yardım et ve evcc'yi maddi olarak destekle." +thanks = "Teşekkürler {sponsor}! Katkın evcc'yi daha da geliştirmemize yardımcı oluyor." +titleNoSponsor = "Bize destek ol" +titleSponsor = "Destekçisin" [footer.telemetry] -optIn = "Telemetriye katıl" -optInMoreDetails = "Daha fazla bilgi {0}." -optInMoreDetailsLink = "buradan" -optInSponsorship = "Sponsorluk gerekli" +optIn = "Doldurma verilerimi paylaşmak istiyorum." +optInMoreDetails = "Daha fazla ayrıntı {0}." +optInMoreDetailsLink = "burada" +optInSponsorship = "Destekçi olman gerekiyor." [footer.version] -availableLong = "yeni versiyon mevcut" +availableLong = "yeni sürüm mevcut" modalCancel = "İptal" modalDownload = "İndir" -modalInstalledVersion = "Yüklü versiyon" -modalNoReleaseNotes = "Yeni sürüm hakkında bilgi bulunamadı. Daha fazla bilgi:" +modalInstalledVersion = "Kurulu sürüm" +modalNoReleaseNotes = "Sürüm bilgileri mevcut değil. Yeni sürüm hakkında bilgiler:" modalTitle = "Yeni sürüm mevcut" -modalUpdate = "Yükle" -modalUpdateNow = "Şimdi yükle" -modalUpdateStarted = "Yeni sürüm evcc başlatılıyor…" -modalUpdateStatusStart = "Yükleme başladı:" +modalUpdate = "Kur" +modalUpdateNow = "Şimdi kur" +modalUpdateStarted = "evcc'nin yeni sürümü başlatılıyor…" +modalUpdateStatusStart = "Kurulum başladı:" [header] -about = "Hakkında" +about = "evcc hakkında" blog = "Blog" -docs = "Dökümanlar" +docs = "Belgelendirme" github = "GitHub" login = "Araç Girişleri" logout = "Çıkış" -nativeSettings = "Sunucu Değiştir" -needHelp = "Yardıma mı ihtiyacınız var?" -sessions = "Şarj Oturumları" +nativeSettings = "Ana makine Değiştir" +needHelp = "Yardıma mı ihtiyacın var?" +sessions = "Doldurma Oturumları" [help] discussionsButton = "GitHub tartışmaları" -documentationButton = "Dökümantasyon" +documentationButton = "Belgeler" issueButton = "Hata bildir" -issueDescription = "Tuhaf veya yanlış bir durum mu buldunuz?" +issueDescription = "Tuhaf yada yanlış bir durum mu buldun?" logsButton = "Logları görüntüle" -logsDescription = "Hatalar için logları görüntüleyin." -modalTitle = "Yardıma mı ihtiyacınız var?" -primaryActions = "Bir şeyler beklediğiniz gibi çalışmıyor mu? Yardım alabileceğiniz iyi yerler bunlardır." +logsDescription = "Hatalar için logları gözden geçir." +modalTitle = "Yardıma mı ihtiyacın var?" +primaryActions = "Bir şeyler çalışması gerektiği gibi çalışmıyor mu? Bunlar yardım almak için iyi yerler." restartButton = "Yeniden Başlat" -restartDescription = "Kapatıp açmayı denediniz mi?" -secondaryActions = "Sorununuzu hala çözemediniz mi? İşte daha fazla müdahaleci seçenekler." +restartDescription = "Cihazı kapatıp tekrar açmayı denedin mi?" +secondaryActions = "Hâlâ bir çözüm bulamadın mı? Burada birkaç seçenek daha var.." [help.restart] cancel = "İptal" confirm = "Evet, yeniden başlat!" -description = "Normal koşullarda yeniden başlatma gerekli olmamalıdır. Eğer düzenli olarak evcc'yi yeniden başlatmanız gerekiyorsa, bir hata bildirimi yapmayı düşünün." -disclaimer = "Not: evcc sonlandırılacak ve işletim sistemi yeniden başlatma işlemini gerçekleştirecektir." -modalTitle = "Yeniden başlatmak istediğinizden emin misiniz?" +description = "Normal koşullarda yeniden başlatma gerekli olmamalı. Eğer evcc'yi sürekli olarak yeniden başlatman gerekiyorsa, bir hata bildirimi yap." +disclaimer = "Not: evcc kendini sonlandıracak ve işletim sistemi tarafindan yeniden başlatılacağına güveniyor." +modalTitle = "Yeniden başlatmak istediğine emin misin?" [log] -areaLabel = "Bölgeye göre filtrele" -areas = "Tüm bölgeler" +areaLabel = "Alana göre filtrele" +areas = "Tüm alanlar" download = "Bütün logları indir" levelLabel = "Log seviyesine göre filtrele" -noResults = "Sonuç yok" +noResults = "Uygun log kaydı bulunamadı." search = "Ara" -showAll = "Tümünü göster" +showAll = "Tüm kayıtları göster" title = "Loglar" update = "Otomatik güncelle" [loginModal] cancel = "İptal" error = "Giriş başarısız: " -invalid = "Hatalı giriş" +invalid = "Şifre geçersiz." login = "Giriş yap" password = "Şifre" reset = "Şifreyi sıfırla?" @@ -237,120 +247,120 @@ active = "Aktif" arrivalTab = "Varış" day = "Gün" departureTab = "Ayrılış" -goal = "Şarj hedefi" -modalTitle = "Şarj Planı" +goal = "Doldurma hedefi" +modalTitle = "Doldurma Planı" none = "hiçbiri" remove = "Kaldır" time = "Zaman" title = "Plan" -titleMinSoc = "Min şarj" +titleMinSoc = "Asgari doldurma" titleTargetCharge = "Ayrılış" unsavedChanges = "Kaydedilmemiş değişiklikler var. Şimdi uygulansın mı?" update = "Uygula" [main.energyflow] battery = "Batarya" -batteryCharge = "Batarya şarj oluyor" -batteryDischarge = "Batarya deşarj oluyor" +batteryCharge = "Batarya doldurma" +batteryDischarge = "Batarya boşaltma" batteryHold = "Batarya (kilitli)" batteryTooltip = "{energy} / {total} ({soc})" gridImport = "Şebeke kullanımı" homePower = "Tüketim" -loadpoints = "Şarj Cihazı| Şarj Cihazı | {count} şarj cihazı" +loadpoints = "Doldurma Cihazı| Doldurma Cihazı | {count} doldurma cihazları" noEnergy = "Ölçüm verisi yok" -pvExport = "Şebeke ihracatı" +pvExport = "Şebekeye ihracat" pvProduction = "Üretim" -selfConsumption = "Kendi tüketimi" +selfConsumption = "Öz tüketim" [main.heatingStatus] charging = "Isıtılıyor…" -cheapEnergyCharging = "Ucuz enerjiyle ısıtılıyor: {price} (limit {limit})" -cleanEnergyCharging = "Temiz enerjiyle ısıtılıyor: {co2} (limit {limit})" +cheapEnergyCharging = "Ucuz enerji ile ısıtılıyor: {price} (sınır {limit})" +cleanEnergyCharging = "Temiz enerji ile ısıtılıyor: {co2} (sınır {limit})" waitForVehicle = "Hazır. Isıtıcı bekleniyor…" [main.loadpoint] avgPrice = "⌀ Fiyat" -charged = "Şarj edildi" +charged = "Doldu" co2 = "⌀ CO₂" duration = "Süre" -fallbackName = "Şarj noktası" +fallbackName = "Doldurma noktası" power = "Güç" price = "Σ Fiyat" -remaining = "Kalan" +remaining = "Kalan zaman" remoteDisabledHard = "{source}: kapatıldı" -remoteDisabledSoft = "{source}: uyumlu güneş enerjili şarj kapatıldı" +remoteDisabledSoft = "{source}: uyumlu güneş enerjili doldurma kapatıldı" solar = "Güneş Enerjisi" [main.loadpointSettings] -currents = "Şarj Akımı" +currents = "Doldurma Akımı" default = "varsayılan" disclaimerHint = "Not:" -onlyForSocBasedCharging = "Bu seçenekler, şarj seviyesi bilinen araçlar için kullanılabilir." -smartCostCheap = "Ucuz Şebeke Şarjı" -smartCostClean = "Temiz Şebeke Şarjı" +onlyForSocBasedCharging = "Bu seçenekler, sadece doluluk seviyesi bilinen araçlar için mevcut." +smartCostCheap = "Ucuz Şebeke Dolumu" +smartCostClean = "Temiz Şebeke Dolumu" title = "Ayarlar {0}" vehicle = "Araç" [main.loadpointSettings.limitSoc] -description = "Bu araç bağlandığında kullanılan şarj limiti." -label = "Varsayılan limit" +description = "Araç bağlandığında kullanılan dolum sınırı." +label = "Varsayılan dolum sınır" [main.loadpointSettings.maxCurrent] -label = "Maks. Akım" +label = "Azami Akım" [main.loadpointSettings.minCurrent] -label = "Min. Akım" +label = "Asgari Akım" [main.loadpointSettings.minSoc] -description = "Araç, güneş enerjisi modunda {0}% hızlı şarj edilir. Ardından güneş enerjisinin fazlasıyla devam eder. Kötü havalarda bile minimum bir menzil sağlamak için kullanışlıdır." -label = "Min. şarj %" +description = "Araç, güneş enerjisi modunda %{0} seviyesine hızlı doldurulur. Ardından güneş enerjisi fazlalığıyla devam eder. Karanlık havalarda dahi asgari bir menzil sağlamak için kullanışlıdır." +label = "Asgari dolum %" [main.loadpointSettings.phasesConfigured] label = "Fazlar" -no1p3pSupport = "Şarj cihazınız nasıl bağlı?" +no1p3pSupport = "Doldurma cihazınız nasıl bağlı?" phases_0 = "otomatik geçiş" -phases_1 = "1 faz" -phases_1_hint = "({min} - {max})" -phases_3 = "3 faz" -phases_3_hint = "({min} - {max})" +phases_1 = "1 fazlı" +phases_1_hint = "({min}'dan {max}'a kadar)" +phases_3 = "3 fazlı" +phases_3_hint = "({min}'dan {max}'a kadar)" [main.mode] -minpv = "Min+Güneş Enerjisi" +minpv = "Asg.+Güneş" now = "Hızlı" off = "Kapalı" -pv = "Güneş Enerjisi" +pv = "Güneş" [main.provider] login = "giriş yap" logout = "çıkış yap" [main.targetCharge] -activate = "Aktif Et" +activate = "Etkinleştir" co2Limit = "{co2} CO₂ sınırı" -costLimitIgnore = "Bu dönemde yapılandırılan {limit} yoksayılacak." -currentPlan = "Aktif plan" -descriptionEnergy = "{targetEnergy} ne zaman araca yüklenmelidir?" -descriptionSoc = "Araç {targetSoc}% şarj edilmeli mi?" +costLimitIgnore = "Bu zaman aralığında yapılandırılan {limit} yoksayılacak." +currentPlan = "Etkin plan" +descriptionEnergy = "{targetEnergy} ne zamana kadar araca doldurulmalı?" +descriptionSoc = "Araç ne zaman %{targetSoc} seviyesine şarj edilmeli?" inactiveLabel = "Hedef zamanı" -notReachableInTime = "Hedef zamanında ulaşılamaz. Tahmini bitiş: {endTime}." +notReachableInTime = "Hedefe zamanında ulaşılamaz. Tahmini bitiş: {endTime}." onlyInPvMode = "Şarj planı sadece güneş enerjisi modunda çalışır." -planDuration = "Şarj süresi" -planPeriodLabel = "Dönem" -planPeriodValue = "{start} - {end}" +planDuration = "Doldurma süresi" +planPeriodLabel = "Zaman aralığı" +planPeriodValue = "{start}'dan {end}'a kadar" planUnknown = "henüz bilinmiyor" -preview = "Planı Önizle" +preview = "Plan Önizleme" priceLimit = "{price} fiyat sınırı" remove = "Kaldır" setTargetTime = "yok" -targetIsAboveLimit = "Bu dönemde yapılandırılan {limit} şarj sınırı yoksayılacak." -targetIsAboveVehicleLimit = "Şarj hedefine ulaşmak için araç limitini ({limit}) artırın." -targetIsInThePast = "Gelecekte bir zaman seçin, Marty." -targetIsTooFarInTheFuture = "Daha fazla bilgi edindiğimizde planı ayarlayacağız." +targetIsAboveLimit = "Yapılandırılan {limit} seviyesindeki şarj sınırı bu zaman aralığında yoksayılacaktır." +targetIsAboveVehicleLimit = "Şarj hedefine ulaşmak için araç sınırını ({limit}) artır." +targetIsInThePast = "Gelecekte bir zaman seç, Marty." +targetIsTooFarInTheFuture = "Gelecek hakkında daha fazla bilgi edindiğimizde planı uyarlayacağız." title = "Hedef Zamanı" today = "bugün" tomorrow = "yarın" update = "Güncelle" -vehicleCapacityDocs = "Nasıl yapılandırılacağını öğrenin." +vehicleCapacityDocs = "Nasıl yapılandırılacağını öğren." vehicleCapacityRequired = "Şarj süresini tahmin etmek için araç batarya kapasitesi gereklidir." [main.targetChargePlan] @@ -366,12 +376,12 @@ noLimit = "yok" [main.vehicle] addVehicle = "Araç Ekle" -changeVehicle = "Araç Değiştir" +changeVehicle = "Araç değiştir" detectionActive = "Araç algılanıyor…" fallbackName = "Araç" moreActions = "Daha Fazla İşlem" none = "Araç Yok" -notReachable = "Araç ulaşılamadı. Evcc'yi yeniden başlatmayı deneyin." +notReachable = "Araca ulaşılamadı. Evcc'yi yeniden başlatmayı dene." targetSoc = "Sınır" temp = "Sıcaklık" tempLimit = "Sıcaklık sınırı" @@ -383,53 +393,55 @@ charging = "şarj oluyor" connected = "bağlı" disconnected = "bağlantı kesildi" ready = "hazır" +vehicleLimit = "Araç sınırı: %{soc}" vehicleTarget = "Araç sınırı: {soc}%" [main.vehicleStatus] charging = "Şarj oluyor…" cheapEnergyCharging = "Ucuz enerjiyle şarj oluyor: {price} (limit {limit})" -cleanEnergyCharging = "Temiz enerjiyle şarj oluyor: {co2} (limit {limit})" -climating = "Ön koşullandırma algılandı." +cleanEnergyCharging = "Temiz enerjiyle şarj oluyor: {co2} (sınır {limit})" +climating = "Ön iklimlendirme algılandı." connected = "Bağlı." disconnected = "Bağlantı kesildi." -minCharge = "Minimum şarj {soc}%." -pvDisable = "Yeterli fazla enerji yok. {remaining} içinde duraklatılıyor…" -pvEnable = "Fazla enerji mevcut. {remaining} içinde başlatılıyor…" +minCharge = "Asgari şarj %{soc}." +pvDisable = "Yeterli güneş enerjisi fazlalığı yok. {remaining} içinde duraklatılıyor…" +pvEnable = "Güneş enerjisi fazlalığı mevcut. {remaining} içinde başlatılıyor…" scale1p = "{remaining} içinde 1 fazlı şarja geçiliyor…" scale3p = "{remaining} içinde 3 fazlı şarja geçiliyor…" targetChargeActive = "Hedef şarj aktif…" targetChargePlanned = "Hedef şarj {time} başlıyor." targetChargeWaitForVehicle = "Hedef şarj hazır. Araç bekleniyor…" unknown = "" +vehicleLimitReached = "%{soc} araç sınırına ulaşıldı." vehicleTargetReached = "Araç sınırı {soc}% ulaşıldı." waitForVehicle = "Hazır. Araç bekleniyor…" [notifications] -dismissAll = "Tümünü Kapat" +dismissAll = "Bildirimleri kaldır" logs = "Bütün logları görüntüle" modalTitle = "Bildirimler" [offline] -message = "Bir sunucuya bağlı değil." -reload = "Tekrar yükle?" +message = "Ana makineye bağlantı yok." +reload = "Tekrar yüklensin mi?" [passwordModal] -description = "Yapılandırma ayarlarını korumak için bir parola belirleyin. Ana ekranı kullanmak oturum açmadan da mümkündür." +description = "Yapılandırma ayarlarını korumak için bir şifre belirle. Ana görünüme erişim oturum açmadan da mümkün." empty = "Şifre boş olamaz." error = "Hata: " labelCurrent = "Mevcut şifre" labelNew = "Yeni şifre" -labelRepeat = "Yeni şifre (tekrar)" +labelRepeat = "Yeni şifreyi tekrarla" newPassword = "Şifre oluştur" noMatch = "Şifreler eşleşmiyor." -titleNew = "Admin Şifresi Oluştur" -titleUpdate = "Admin Şifresini Güncelle" +titleNew = "Yönetici Şifresi Oluştur" +titleUpdate = "Yönetici Şifresini Güncelle" updatePassword = "Şifreyi güncelle" [session] cancel = "İptal" co2 = "CO₂" -date = "Dönem" +date = "Zaman aralığı" delete = "Sil" finished = "Tamamlandı" meter = "Sayaç" @@ -446,14 +458,14 @@ avgPrice = "⌀ Fiyat" chargeDuration = "Süre" co2 = "⌀ CO₂" csvMonth = "{month} CSV olarak indir" -csvTotal = "Toplam CSV olarak indir" +csvTotal = "CSV'nin tamamını indir" date = "Başlangıç" downloadCsv = "CSV olarak indir" energy = "Şarj edilen" loadpoint = "Şarj Noktası" -noData = "Bu ay şarj oturumu yok." +noData = "Bu ay henüz şarj oturumu yok." price = "Σ Fiyat" -reallyDelete = "Bu oturumu gerçekten silmek istiyor musunuz?" +reallyDelete = "Bu oturumu gerçekten silmek istiyor musun?" solar = "Güneş Enerjisi" title = "Şarj Oturumları" total = "Toplam" @@ -461,18 +473,18 @@ vehicle = "Araç" [sessions.csv] chargedenergy = "Enerji (kWh)" -created = "Oluşturuldu" -finished = "Tamamlandı" -identifier = "Kimlik" +created = "Başlama zamanı" +finished = "Bitiş zamanı" +identifier = "Tanımlayıcı" loadpoint = "Şarj Noktası" meterstart = "Sayaç başlangıcı (kWh)" meterstop = "Sayaç bitişi (kWh)" -odometer = "Kilometre (km)" +odometer = "Kilometre sayacı (km)" vehicle = "Araç" [sessions.filter] allLoadpoints = "tüm şarj noktaları" -allVehicles = "tüm araçlar" +allVehicles = "Tüm araçlar" filter = "Filtrele" [settings] @@ -492,34 +504,34 @@ auto = "Otomatik" label = "Dil" [settings.sponsorToken] -expires = "Sponsor jetonunuz {inXDays} süre sonra sona erer. {getNewToken} ve yapılandırma dosyanızı güncelleyin." -getNew = "Yeni bir tane alın" -hint = "Not: Bunun otomatik hale getireceğiz." +expires = "Sponsor jetonun {inXDays} sonra sona erecek. {getNewToken} ve yapılandırma dosyanızı güncelle." +getNew = "Yeni bir tane al" +hint = "Not: İleride bunu otomatik hale getireceğiz." [settings.telemetry] label = "Telemetri" [settings.theme] -auto = "sistem" -dark = "koyu" -label = "Tasarım" -light = "açık" +auto = "Sistem" +dark = "Karanlık" +label = "Görünüm" +light = "Aydınlık" [settings.unit] km = "km" -label = "Birimler" -mi = "mil" +label = "Birim" +mi = "Mil" [smartCost] activeHours = "{total} saat içinde {charging}" activeHoursLabel = "Aktif saatler" -applyToAll = "Heryerde uygula?" -batteryDescription = "Ev bataryasını şebeke enerjisiyle şarj eder." +applyToAll = "Heryerde uygulansın mı?" +batteryDescription = "Ev bataryasını şebekeden şarj eder." cheapTitle = "Ucuz Şebeke Şarjı" cleanTitle = "Temiz Şebeke Şarjı" co2Label = "CO₂ emisyonu" co2Limit = "CO₂ sınırı" -loadpointDescription = "Güneş modunda geçici hızlı şarjı etkinleştirir." +loadpointDescription = "Güneş enerjisi modunda hızlı şarjı geçici olarak etkinleştirir." modalTitle = "Akıllı Şebeke Şarjı" none = "hiçbiri" priceLabel = "Enerji fiyatı" @@ -529,11 +541,11 @@ saved = "Kaydedildi." [startupError] configFile = "Kullanılan yapılandırma dosyası:" configuration = "Yapılandırma" -description = "Lütfen yapılandırma dosyanızı kontrol edin. Hata mesajı yardımcı olmazsa, {0}'e göz atın." +description = "Lütfen yapılandırma dosyanızı kontrol et. Hata mesajı yardımcı olmuyorsa, çözüm için {0}'e göz at." discussions = "GitHub Tartışmaları" -fixAndRestart = "Lütfen sorunu düzeltin ve sunucuyu yeniden başlatın." -hint = "Not: Ayrıca hatalı bir cihazınızın (inverter, sayaç, …) olabileceği de olabilir. Ağ bağlantılarınızı kontrol edin." -lineError = "{0} içinde hata." +fixAndRestart = "Lütfen sorunu düzelt ve ana makineyi yeniden başlat." +hint = "Not: Ayrıca hatalı bir cihaz da (inverter, sayaç, …) sebeb olabilir. Ağ bağlantılarını gözden geçir." +lineError = "{0} içinde hata bulundu." lineErrorLink = "{0}. satır" restartButton = "Yeniden Başlat" -title = "Başlangıç Hatası" +title = "Başlama Hatası" From c8b3e24fe2cb93dbdff4b66ccfbf0b6f3f20f936 Mon Sep 17 00:00:00 2001 From: andig Date: Fri, 10 May 2024 14:02:32 +0200 Subject: [PATCH 077/168] chore: fix help --- templates/definition/tariff/energy-charts-api.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/definition/tariff/energy-charts-api.yaml b/templates/definition/tariff/energy-charts-api.yaml index 4dd8bf7723..65042f88a5 100644 --- a/templates/definition/tariff/energy-charts-api.yaml +++ b/templates/definition/tariff/energy-charts-api.yaml @@ -30,7 +30,7 @@ params: "SI", ] default: DE-LU - description: + help: de: "Gebotszonen - https://api.energy-charts.info/#/prices/day_ahead_price_price_get" en: "Bidding zones - https://api.energy-charts.info/#/prices/day_ahead_price_price_get" - preset: tariff-base From 5750d0d1e98393e04c4ae3ff08fdb660165da4b7 Mon Sep 17 00:00:00 2001 From: andig Date: Sat, 11 May 2024 09:56:00 +0200 Subject: [PATCH 078/168] Mqtt: add batteryDischargeControl and smartCostLimit (#13864) --- server/mqtt.go | 9 ++++++++- server/mqtt_setter.go | 8 ++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/server/mqtt.go b/server/mqtt.go index f96c31f01c..199d8d6357 100644 --- a/server/mqtt.go +++ b/server/mqtt.go @@ -173,10 +173,17 @@ func (m *MQTT) Listen(site site.API) error { func (m *MQTT) listenSiteSetters(topic string, site site.API) error { for _, s := range []setter{ - {"/prioritySoc", floatSetter(site.SetPrioritySoc)}, {"/bufferSoc", floatSetter(site.SetBufferSoc)}, {"/bufferStartSoc", floatSetter(site.SetBufferStartSoc)}, + {"/batteryDischargeControl", boolSetter(site.SetBatteryDischargeControl)}, + {"/prioritySoc", floatSetter(site.SetPrioritySoc)}, {"/residualPower", floatSetter(site.SetResidualPower)}, + {"/smartCostLimit", floatSetter(func(limit float64) error { + for _, lp := range site.Loadpoints() { + lp.SetSmartCostLimit(limit) + } + return nil + })}, } { if err := m.Handler.ListenSetter(topic+s.topic, s.fun); err != nil { return err diff --git a/server/mqtt_setter.go b/server/mqtt_setter.go index 44c215da38..a1a648d994 100644 --- a/server/mqtt_setter.go +++ b/server/mqtt_setter.go @@ -2,6 +2,8 @@ package server import ( "strconv" + + "github.com/spf13/cast" ) type setter struct { @@ -26,3 +28,9 @@ func floatSetter(set func(float64) error) func(string) error { func intSetter(set func(int) error) func(string) error { return setterFunc(strconv.Atoi, set) } + +func boolSetter(set func(bool) error) func(string) error { + return setterFunc(func(v string) (bool, error) { + return cast.ToBoolE(v) + }, set) +} From df4b8baf5b1a647f67980ebc1f36376b4658de27 Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Sat, 11 May 2024 15:10:14 +0200 Subject: [PATCH 079/168] UI: handle missing smart cost limit (#13857) --- assets/js/components/Loadpoint.vue | 2 +- assets/js/components/Loadpoints.vue | 2 -- assets/js/components/Site.vue | 3 --- assets/js/components/SmartCostLimit.vue | 2 +- 4 files changed, 2 insertions(+), 7 deletions(-) diff --git a/assets/js/components/Loadpoint.vue b/assets/js/components/Loadpoint.vue index e8374af872..ab9ef1d024 100644 --- a/assets/js/components/Loadpoint.vue +++ b/assets/js/components/Loadpoint.vue @@ -182,7 +182,7 @@ export default { phaseRemaining: Number, pvRemaining: Number, pvAction: String, - smartCostLimit: Number, + smartCostLimit: { type: Number, default: 0 }, smartCostType: String, smartCostActive: Boolean, tariffGrid: Number, diff --git a/assets/js/components/Loadpoints.vue b/assets/js/components/Loadpoints.vue index 341e0a7858..648f0e87ba 100644 --- a/assets/js/components/Loadpoints.vue +++ b/assets/js/components/Loadpoints.vue @@ -63,9 +63,7 @@ export default { props: { loadpoints: Array, vehicles: Array, - smartCostLimit: Number, smartCostType: String, - smartCostActive: Boolean, tariffGrid: Number, tariffCo2: Number, currency: String, diff --git a/assets/js/components/Site.vue b/assets/js/components/Site.vue index 98a67d5807..87ba61b28b 100644 --- a/assets/js/components/Site.vue +++ b/assets/js/components/Site.vue @@ -21,9 +21,7 @@ class="mt-1 mt-sm-2 flex-grow-1" :loadpoints="loadpoints" :vehicles="vehicleList" - :smartCostLimit="smartCostLimit" :smartCostType="smartCostType" - :smartCostActive="smartCostActive" :tariffGrid="tariffGrid" :tariffCo2="tariffCo2" :currency="currency" @@ -97,7 +95,6 @@ export default { uploadProgress: Number, sponsor: String, sponsorTokenExpires: Number, - smartCostLimit: Number, smartCostType: String, smartCostActive: Boolean, }, diff --git a/assets/js/components/SmartCostLimit.vue b/assets/js/components/SmartCostLimit.vue index 7bee5a56a2..bd549a0764 100644 --- a/assets/js/components/SmartCostLimit.vue +++ b/assets/js/components/SmartCostLimit.vue @@ -85,7 +85,7 @@ export default { components: { TariffChart }, mixins: [formatter], props: { - smartCostLimit: { type: Number, default: 0 }, + smartCostLimit: Number, smartCostType: String, tariffGrid: Number, currency: String, From f64d5e0c3049285c78bc51f14c8e3ca7937cd2a2 Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Mon, 13 May 2024 13:49:04 +0200 Subject: [PATCH 080/168] UI: fix mobile mobile visualization (#13882) --- assets/js/components/Energyflow/Visualization.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/js/components/Energyflow/Visualization.vue b/assets/js/components/Energyflow/Visualization.vue index a5f90fab64..e373c03781 100644 --- a/assets/js/components/Energyflow/Visualization.vue +++ b/assets/js/components/Energyflow/Visualization.vue @@ -202,7 +202,7 @@ export default { return { value, hideIcon: this.hideLabelIcon(value, minWidth), - style: { width: this.widthTotal(value) }, + style: { "flex-basis": this.widthTotal(value) }, [position]: true, }; }, From c5330fbbc97265a8f6f92ef55489129254d98dd3 Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Mon, 13 May 2024 18:19:22 +0200 Subject: [PATCH 081/168] Sessions CSV: added missing translations (#13892) --- i18n/de.toml | 5 +++++ i18n/en.toml | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/i18n/de.toml b/i18n/de.toml index 0c3666ec63..21747eeec5 100644 --- a/i18n/de.toml +++ b/i18n/de.toml @@ -472,6 +472,8 @@ vehicle = "Fahrzeug" [sessions.csv] chargedenergy = "Energie (kWh)" +chargeduration = "Ladedauer" +co2perkwh = "CO₂/kWh" created = "Startzeit" finished = "Endzeit" identifier = "Kennung" @@ -479,6 +481,9 @@ loadpoint = "Ladepunkt" meterstart = "Anfangszählerstand (kWh)" meterstop = "Endzählerstand (kWh)" odometer = "Kilometerstand (km)" +price = "Preis" +priceperkwh = "Preis/kWh" +solarpercentage = "Sonne (%)" vehicle = "Fahrzeug" [sessions.filter] diff --git a/i18n/en.toml b/i18n/en.toml index 02245c2e29..51599d99b5 100644 --- a/i18n/en.toml +++ b/i18n/en.toml @@ -471,6 +471,8 @@ vehicle = "Vehicle" [sessions.csv] chargedenergy = "Energy (kWh)" +chargeduration = "Duration" +co2perkwh = "CO₂/kWh" created = "Created" finished = "Finished" identifier = "Identifier" @@ -478,6 +480,9 @@ loadpoint = "Charging point" meterstart = "Meter start (kWh)" meterstop = "Meter stop (kWh)" odometer = "Mileage (km)" +price = "Price" +priceperkwh = "Price/kWh" +solarpercentage = "Solar (%)" vehicle = "Vehicle" [sessions.filter] From d4e80f84035efe319f23e3442e4219f51558d123 Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Tue, 14 May 2024 07:16:37 +0200 Subject: [PATCH 082/168] Chore: npm deps upgrade (#13891) --- package-lock.json | 1095 ++++++++++++++++++++------------------------- 1 file changed, 491 insertions(+), 604 deletions(-) diff --git a/package-lock.json b/package-lock.json index 060240dabf..f556a063a5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -58,14 +58,6 @@ "npm": ">=8.0.0" } }, - "node_modules/@aashutoshrathi/word-wrap": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", - "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/@akryum/tinypool": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/@akryum/tinypool/-/tinypool-0.3.1.tgz", @@ -107,20 +99,20 @@ } }, "node_modules/@babel/core": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.4.tgz", - "integrity": "sha512-MBVlMXP+kkl5394RBLSxxk/iLTeVGuXTV3cIDXavPpMMqnSnt6apKgan/U8O3USWZCWZT/TbgfEpKa4uMgN4Dg==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.5.tgz", + "integrity": "sha512-tVQRucExLQ02Boi4vdPp49svNGcfL2GhdTCT9aldhXgCJVAI21EtRfBettiuLUwce/7r6bFdgs6JFkcdTiFttA==", "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.24.2", - "@babel/generator": "^7.24.4", + "@babel/generator": "^7.24.5", "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helpers": "^7.24.4", - "@babel/parser": "^7.24.4", + "@babel/helper-module-transforms": "^7.24.5", + "@babel/helpers": "^7.24.5", + "@babel/parser": "^7.24.5", "@babel/template": "^7.24.0", - "@babel/traverse": "^7.24.1", - "@babel/types": "^7.24.0", + "@babel/traverse": "^7.24.5", + "@babel/types": "^7.24.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -136,11 +128,11 @@ } }, "node_modules/@babel/generator": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.4.tgz", - "integrity": "sha512-Xd6+v6SnjWVx/nus+y0l1sxMOTOMBkyL4+BIdbALyatQnAe/SRVjANeDPSCYaX+i1iJmuGSKf3Z+E+V/va1Hvw==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.5.tgz", + "integrity": "sha512-x32i4hEXvr+iI0NEoEfDKzlemF8AmtOP8CcrRaEcpzysWuoEb1KknpcvMsHKPONoKZiDuItklgWhB18xEhr9PA==", "dependencies": { - "@babel/types": "^7.24.0", + "@babel/types": "^7.24.5", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^2.5.1" @@ -187,18 +179,18 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.4.tgz", - "integrity": "sha512-lG75yeuUSVu0pIcbhiYMXBXANHrpUPaOfu7ryAzskCgKUHuAxRQI5ssrtmF0X9UXldPlvT0XM/A4F44OXRt6iQ==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.5.tgz", + "integrity": "sha512-uRc4Cv8UQWnE4NXlYTIIdM7wfFkOqlFztcC/gVXDKohKoVB3OyonfelUBaJzSwpBntZ2KYGF/9S7asCHsXwW6g==", "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-function-name": "^7.23.0", - "@babel/helper-member-expression-to-functions": "^7.23.0", + "@babel/helper-member-expression-to-functions": "^7.24.5", "@babel/helper-optimise-call-expression": "^7.22.5", "@babel/helper-replace-supers": "^7.24.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-split-export-declaration": "^7.24.5", "semver": "^6.3.1" }, "engines": { @@ -271,11 +263,11 @@ } }, "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.23.0.tgz", - "integrity": "sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.5.tgz", + "integrity": "sha512-4owRteeihKWKamtqg4JmWSsEZU445xpFRXPEwp44HbgbxdWlUV1b4Agg4lkA806Lil5XM/e+FJyS0vj5T6vmcA==", "dependencies": { - "@babel/types": "^7.23.0" + "@babel/types": "^7.24.5" }, "engines": { "node": ">=6.9.0" @@ -293,15 +285,15 @@ } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", - "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.5.tgz", + "integrity": "sha512-9GxeY8c2d2mdQUP1Dye0ks3VDyIMS98kt/llQ2nUId8IsWqTF0l1LkSX0/uP7l7MCDrzXS009Hyhe2gzTiGW8A==", "dependencies": { "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-simple-access": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/helper-validator-identifier": "^7.22.20" + "@babel/helper-module-imports": "^7.24.3", + "@babel/helper-simple-access": "^7.24.5", + "@babel/helper-split-export-declaration": "^7.24.5", + "@babel/helper-validator-identifier": "^7.24.5" }, "engines": { "node": ">=6.9.0" @@ -322,9 +314,9 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.0.tgz", - "integrity": "sha512-9cUznXMG0+FxRuJfvL82QlTqIzhVW9sL0KjMPHhAOOvpQGL8QtdxnBKILjBqxlHyliz0yCa1G903ZXI/FuHy2w==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.5.tgz", + "integrity": "sha512-xjNLDopRzW2o6ba0gKbkZq5YWEBaK3PCyTOY1K2P/O07LGMhMqlMXPxwN4S5/RhWuCobT8z0jrlKGlYmeR1OhQ==", "engines": { "node": ">=6.9.0" } @@ -362,11 +354,11 @@ } }, "node_modules/@babel/helper-simple-access": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", - "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.5.tgz", + "integrity": "sha512-uH3Hmf5q5n7n8mz7arjUlDOCbttY/DW4DYhE6FUsjKJ/oYC1kQQUvwEQWxRwUpX9qQKRXeqLwWxrqilMrf32sQ==", "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.24.5" }, "engines": { "node": ">=6.9.0" @@ -384,11 +376,11 @@ } }, "node_modules/@babel/helper-split-export-declaration": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", - "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.5.tgz", + "integrity": "sha512-5CHncttXohrHk8GWOFCcCl4oRD9fKosWlIRgWm4ql9VYioKm52Mk2xsmoohvm7f3JoiLSM5ZgJuRaf5QZZYd3Q==", "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.24.5" }, "engines": { "node": ">=6.9.0" @@ -403,9 +395,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz", + "integrity": "sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==", "engines": { "node": ">=6.9.0" } @@ -419,37 +411,37 @@ } }, "node_modules/@babel/helper-wrap-function": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.22.20.tgz", - "integrity": "sha512-pms/UwkOpnQe/PDAEdV/d7dVCoBbB+R4FvYoHGZz+4VPcg7RtYy2KP7S2lbuWM6FCSgob5wshfGESbC/hzNXZw==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.24.5.tgz", + "integrity": "sha512-/xxzuNvgRl4/HLNKvnFwdhdgN3cpLxgLROeLDl83Yx0AJ1SGvq1ak0OszTOjDfiB8Vx03eJbeDWh9r+jCCWttw==", "dependencies": { - "@babel/helper-function-name": "^7.22.5", - "@babel/template": "^7.22.15", - "@babel/types": "^7.22.19" + "@babel/helper-function-name": "^7.23.0", + "@babel/template": "^7.24.0", + "@babel/types": "^7.24.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.4.tgz", - "integrity": "sha512-FewdlZbSiwaVGlgT1DPANDuCHaDMiOo+D/IDYRFYjHOuv66xMSJ7fQwwODwRNAPkADIO/z1EoF/l2BCWlWABDw==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.5.tgz", + "integrity": "sha512-CiQmBMMpMQHwM5m01YnrM6imUG1ebgYJ+fAIW4FZe6m4qHTPaRHti+R8cggAwkdz4oXhtO4/K9JWlh+8hIfR2Q==", "dependencies": { "@babel/template": "^7.24.0", - "@babel/traverse": "^7.24.1", - "@babel/types": "^7.24.0" + "@babel/traverse": "^7.24.5", + "@babel/types": "^7.24.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.24.2", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.2.tgz", - "integrity": "sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.5.tgz", + "integrity": "sha512-8lLmua6AVh/8SLJRRVD6V8p73Hir9w5mJrhE+IPpILG31KKlI9iz5zmBYKcWPS59qSfgP9RaSBQSHHE81WKuEw==", "dependencies": { - "@babel/helper-validator-identifier": "^7.22.20", + "@babel/helper-validator-identifier": "^7.24.5", "chalk": "^2.4.2", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" @@ -459,9 +451,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.4.tgz", - "integrity": "sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.5.tgz", + "integrity": "sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==", "bin": { "parser": "bin/babel-parser.js" }, @@ -470,12 +462,12 @@ } }, "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.24.4.tgz", - "integrity": "sha512-qpl6vOOEEzTLLcsuqYYo8yDtrTocmu2xkGvgNebvPjT9DTtfFYGmgDqY+rBYXNlqL4s9qLDn6xkrJv4RxAPiTA==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.24.5.tgz", + "integrity": "sha512-LdXRi1wEMTrHVR4Zc9F8OewC3vdm5h4QB6L71zy6StmYeqGi1b3ttIO8UC+BfZKcH9jdr4aI249rBkm+3+YvHw==", "dependencies": { "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.5" }, "engines": { "node": ">=6.9.0" @@ -819,11 +811,11 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.24.4.tgz", - "integrity": "sha512-nIFUZIpGKDf9O9ttyRXpHFpKC+X3Y5mtshZONuEUYBomAKoM4y029Jr+uB1bHGPhNmK8YXHevDtKDOLmtRrp6g==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.24.5.tgz", + "integrity": "sha512-sMfBc3OxghjC95BkYrYocHL3NaOplrcaunblzwXhGmlPwpmfsxr4vK+mBBt49r+S240vahmv+kUxkeKgs+haCw==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.5" }, "engines": { "node": ">=6.9.0" @@ -864,17 +856,17 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.24.1.tgz", - "integrity": "sha512-ZTIe3W7UejJd3/3R4p7ScyyOoafetUShSf4kCqV0O7F/RiHxVj/wRaRnQlrGwflvcehNA8M42HkAiEDYZu2F1Q==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.24.5.tgz", + "integrity": "sha512-gWkLP25DFj2dwe9Ck8uwMOpko4YsqyfZJrOmqqcegeDYEbp7rmn4U6UQZNj08UF6MaX39XenSpKRCvpDRBtZ7Q==", "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", "@babel/helper-compilation-targets": "^7.23.6", "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-function-name": "^7.23.0", - "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-plugin-utils": "^7.24.5", "@babel/helper-replace-supers": "^7.24.1", - "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-split-export-declaration": "^7.24.5", "globals": "^11.1.0" }, "engines": { @@ -900,11 +892,11 @@ } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.1.tgz", - "integrity": "sha512-ow8jciWqNxR3RYbSNVuF4U2Jx130nwnBnhRw6N6h1bOejNkABmcI5X5oz29K4alWX7vf1C+o6gtKXikzRKkVdw==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.5.tgz", + "integrity": "sha512-SZuuLyfxvsm+Ah57I/i1HVjveBENYK9ue8MJ7qkc7ndoNjqquJiElzA7f5yaAXjyW2hKojosOTAQQRX50bPSVg==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.5" }, "engines": { "node": ">=6.9.0" @@ -1199,14 +1191,14 @@ } }, "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.1.tgz", - "integrity": "sha512-XjD5f0YqOtebto4HGISLNfiNMTTs6tbkFf2TOqJlYKYmbo+mN9Dnpl4SRoofiziuOWMIyq3sZEUqLo3hLITFEA==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.5.tgz", + "integrity": "sha512-7EauQHszLGM3ay7a161tTQH7fj+3vVM/gThlz5HpFtnygTxjrlvoeq7MPVA1Vy9Q555OB8SnAOsMkLShNkkrHA==", "dependencies": { "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-plugin-utils": "^7.24.5", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.24.1" + "@babel/plugin-transform-parameters": "^7.24.5" }, "engines": { "node": ">=6.9.0" @@ -1246,11 +1238,11 @@ } }, "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.1.tgz", - "integrity": "sha512-n03wmDt+987qXwAgcBlnUUivrZBPZ8z1plL0YvgQalLm+ZE5BMhGm94jhxXtA1wzv1Cu2aaOv1BM9vbVttrzSg==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.5.tgz", + "integrity": "sha512-xWCkmwKT+ihmA6l7SSTpk8e4qQl/274iNbSKRRS8mpqFR32ksy36+a+LWY8OXCCEefF8WFlnOHVsaDI2231wBg==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-plugin-utils": "^7.24.5", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", "@babel/plugin-syntax-optional-chaining": "^7.8.3" }, @@ -1262,11 +1254,11 @@ } }, "node_modules/@babel/plugin-transform-parameters": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.24.1.tgz", - "integrity": "sha512-8Jl6V24g+Uw5OGPeWNKrKqXPDw2YDjLc53ojwfMcKwlEoETKU9rU0mHUtcg9JntWI/QYzGAXNWEcVHZ+fR+XXg==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.24.5.tgz", + "integrity": "sha512-9Co00MqZ2aoky+4j2jhofErthm6QVLKbpQrvz20c3CH9KQCLHyNB+t2ya4/UrRpQGR+Wrwjg9foopoeSdnHOkA==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.5" }, "engines": { "node": ">=6.9.0" @@ -1291,13 +1283,13 @@ } }, "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.24.1.tgz", - "integrity": "sha512-pTHxDVa0BpUbvAgX3Gat+7cSciXqUcY9j2VZKTbSB6+VQGpNgNO9ailxTGHSXlqOnX1Hcx1Enme2+yv7VqP9bg==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.24.5.tgz", + "integrity": "sha512-JM4MHZqnWR04jPMujQDTBVRnqxpLLpx2tkn7iPn+Hmsc0Gnb79yvRWOkvqFOx3Z7P7VxiRIR22c4eGSNj87OBQ==", "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-create-class-features-plugin": "^7.24.1", - "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-create-class-features-plugin": "^7.24.5", + "@babel/helper-plugin-utils": "^7.24.5", "@babel/plugin-syntax-private-property-in-object": "^7.14.5" }, "engines": { @@ -1408,11 +1400,11 @@ } }, "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.1.tgz", - "integrity": "sha512-CBfU4l/A+KruSUoW+vTQthwcAdwuqbpRNB8HQKlZABwHRhsdHZ9fezp4Sn18PeAlYxTNiLMlx4xUBV3AWfg1BA==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.5.tgz", + "integrity": "sha512-UTGnhYVZtTAjdwOTzT+sCyXmTn8AhaxOS/MjG9REclZ6ULHWF9KoCZur0HSGU7hk8PdBFKKbYe6+gqdXWz84Jg==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.5" }, "engines": { "node": ">=6.9.0" @@ -1481,15 +1473,15 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.24.4.tgz", - "integrity": "sha512-7Kl6cSmYkak0FK/FXjSEnLJ1N9T/WA2RkMhu17gZ/dsxKJUuTYNIylahPTzqpLyJN4WhDif8X0XK1R8Wsguo/A==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.24.5.tgz", + "integrity": "sha512-UGK2ifKtcC8i5AI4cH+sbLLuLc2ktYSFJgBAXorKAsHUZmrQ1q6aQ6i3BvU24wWs2AAKqQB6kq3N9V9Gw1HiMQ==", "dependencies": { "@babel/compat-data": "^7.24.4", "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-plugin-utils": "^7.24.5", "@babel/helper-validator-option": "^7.23.5", - "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.24.4", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.24.5", "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.24.1", "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.24.1", "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.24.1", @@ -1516,12 +1508,12 @@ "@babel/plugin-transform-async-generator-functions": "^7.24.3", "@babel/plugin-transform-async-to-generator": "^7.24.1", "@babel/plugin-transform-block-scoped-functions": "^7.24.1", - "@babel/plugin-transform-block-scoping": "^7.24.4", + "@babel/plugin-transform-block-scoping": "^7.24.5", "@babel/plugin-transform-class-properties": "^7.24.1", "@babel/plugin-transform-class-static-block": "^7.24.4", - "@babel/plugin-transform-classes": "^7.24.1", + "@babel/plugin-transform-classes": "^7.24.5", "@babel/plugin-transform-computed-properties": "^7.24.1", - "@babel/plugin-transform-destructuring": "^7.24.1", + "@babel/plugin-transform-destructuring": "^7.24.5", "@babel/plugin-transform-dotall-regex": "^7.24.1", "@babel/plugin-transform-duplicate-keys": "^7.24.1", "@babel/plugin-transform-dynamic-import": "^7.24.1", @@ -1541,13 +1533,13 @@ "@babel/plugin-transform-new-target": "^7.24.1", "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.1", "@babel/plugin-transform-numeric-separator": "^7.24.1", - "@babel/plugin-transform-object-rest-spread": "^7.24.1", + "@babel/plugin-transform-object-rest-spread": "^7.24.5", "@babel/plugin-transform-object-super": "^7.24.1", "@babel/plugin-transform-optional-catch-binding": "^7.24.1", - "@babel/plugin-transform-optional-chaining": "^7.24.1", - "@babel/plugin-transform-parameters": "^7.24.1", + "@babel/plugin-transform-optional-chaining": "^7.24.5", + "@babel/plugin-transform-parameters": "^7.24.5", "@babel/plugin-transform-private-methods": "^7.24.1", - "@babel/plugin-transform-private-property-in-object": "^7.24.1", + "@babel/plugin-transform-private-property-in-object": "^7.24.5", "@babel/plugin-transform-property-literals": "^7.24.1", "@babel/plugin-transform-regenerator": "^7.24.1", "@babel/plugin-transform-reserved-words": "^7.24.1", @@ -1555,7 +1547,7 @@ "@babel/plugin-transform-spread": "^7.24.1", "@babel/plugin-transform-sticky-regex": "^7.24.1", "@babel/plugin-transform-template-literals": "^7.24.1", - "@babel/plugin-transform-typeof-symbol": "^7.24.1", + "@babel/plugin-transform-typeof-symbol": "^7.24.5", "@babel/plugin-transform-unicode-escapes": "^7.24.1", "@babel/plugin-transform-unicode-property-regex": "^7.24.1", "@babel/plugin-transform-unicode-regex": "^7.24.1", @@ -1593,9 +1585,9 @@ "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==" }, "node_modules/@babel/runtime": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.4.tgz", - "integrity": "sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.5.tgz", + "integrity": "sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -1617,18 +1609,18 @@ } }, "node_modules/@babel/traverse": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.1.tgz", - "integrity": "sha512-xuU6o9m68KeqZbQuDt2TcKSxUw/mrsvavlEqQ1leZ/B+C9tk6E4sRWy97WaXgvq5E+nU3cXMxv3WKOCanVMCmQ==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.5.tgz", + "integrity": "sha512-7aaBLeDQ4zYcUFDUD41lJc1fG8+5IU9DaNSJAgal866FGvmD5EbWQgnEC6kO1gGLsX0esNkfnJSndbTXA3r7UA==", "dependencies": { - "@babel/code-frame": "^7.24.1", - "@babel/generator": "^7.24.1", + "@babel/code-frame": "^7.24.2", + "@babel/generator": "^7.24.5", "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-function-name": "^7.23.0", "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.24.1", - "@babel/types": "^7.24.0", + "@babel/helper-split-export-declaration": "^7.24.5", + "@babel/parser": "^7.24.5", + "@babel/types": "^7.24.5", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -1637,12 +1629,12 @@ } }, "node_modules/@babel/types": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz", - "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.5.tgz", + "integrity": "sha512-6mQNsaLeXTw0nxYUYu+NSa4Hx4BlF1x1x8/PMFbiR+GBSr+2DkECc69b8hgy2frEodNcvPffeH8YfWd3LI6jhQ==", "dependencies": { - "@babel/helper-string-parser": "^7.23.4", - "@babel/helper-validator-identifier": "^7.22.20", + "@babel/helper-string-parser": "^7.24.1", + "@babel/helper-validator-identifier": "^7.24.5", "to-fast-properties": "^2.0.0" }, "engines": { @@ -1683,9 +1675,9 @@ } }, "node_modules/@codemirror/lint": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.5.0.tgz", - "integrity": "sha512-+5YyicIaaAZKU8K43IQi8TBy6mF6giGeWAH7N96Z5LC30Wm5JMjqxOYIE9mxwMG1NbhT2mA3l9hA4uuKUM3E5g==", + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.7.0.tgz", + "integrity": "sha512-LTLOL2nT41ADNSCCCCw8Q/UmdAFzB23OUYSjsHTdsVaH0XEo+orhuqbDNWzrzodm14w6FOxqxpmy4LF8Lixqjw==", "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", @@ -2520,11 +2512,11 @@ } }, "node_modules/@playwright/test": { - "version": "1.43.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.43.1.tgz", - "integrity": "sha512-HgtQzFgNEEo4TE22K/X7sYTYNqEMMTZmFS8kTq6m8hXj+m1D8TgwgIbumHddJa9h4yl4GkKb8/bgAl2+g7eDgA==", + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.44.0.tgz", + "integrity": "sha512-rNX5lbNidamSUorBhB4XZ9SQTjAqfe5M+p37Z8ic0jPFBMo5iCtQz1kRWkEMg+rYOKSlVycpQmpqjSFq7LXOfg==", "dependencies": { - "playwright": "1.43.1" + "playwright": "1.44.0" }, "bin": { "playwright": "cli.js" @@ -2569,9 +2561,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.16.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.16.4.tgz", - "integrity": "sha512-GkhjAaQ8oUTOKE4g4gsZ0u8K/IHU1+2WQSgS1TwTcYvL+sjbaQjNHFXbOJ6kgqGHIO1DfUhI/Sphi9GkRT9K+Q==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.17.2.tgz", + "integrity": "sha512-NM0jFxY8bB8QLkoKxIQeObCaDlJKewVlIEkuyYKm5An1tdVZ966w2+MPQ2l8LBZLjR+SgyV+nRkTIunzOYBMLQ==", "cpu": [ "arm" ], @@ -2581,9 +2573,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.16.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.16.4.tgz", - "integrity": "sha512-Bvm6D+NPbGMQOcxvS1zUl8H7DWlywSXsphAeOnVeiZLQ+0J6Is8T7SrjGTH29KtYkiY9vld8ZnpV3G2EPbom+w==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.17.2.tgz", + "integrity": "sha512-yeX/Usk7daNIVwkq2uGoq2BYJKZY1JfyLTaHO/jaiSwi/lsf8fTFoQW/n6IdAsx5tx+iotu2zCJwz8MxI6D/Bw==", "cpu": [ "arm64" ], @@ -2593,9 +2585,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.16.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.16.4.tgz", - "integrity": "sha512-i5d64MlnYBO9EkCOGe5vPR/EeDwjnKOGGdd7zKFhU5y8haKhQZTN2DgVtpODDMxUr4t2K90wTUJg7ilgND6bXw==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.17.2.tgz", + "integrity": "sha512-kcMLpE6uCwls023+kknm71ug7MZOrtXo+y5p/tsg6jltpDtgQY1Eq5sGfHcQfb+lfuKwhBmEURDga9N0ol4YPw==", "cpu": [ "arm64" ], @@ -2605,9 +2597,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.16.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.16.4.tgz", - "integrity": "sha512-WZupV1+CdUYehaZqjaFTClJI72fjJEgTXdf4NbW69I9XyvdmztUExBtcI2yIIU6hJtYvtwS6pkTkHJz+k08mAQ==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.17.2.tgz", + "integrity": "sha512-AtKwD0VEx0zWkL0ZjixEkp5tbNLzX+FCqGG1SvOu993HnSz4qDI6S4kGzubrEJAljpVkhRSlg5bzpV//E6ysTQ==", "cpu": [ "x64" ], @@ -2617,9 +2609,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.16.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.16.4.tgz", - "integrity": "sha512-ADm/xt86JUnmAfA9mBqFcRp//RVRt1ohGOYF6yL+IFCYqOBNwy5lbEK05xTsEoJq+/tJzg8ICUtS82WinJRuIw==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.17.2.tgz", + "integrity": "sha512-3reX2fUHqN7sffBNqmEyMQVj/CKhIHZd4y631duy0hZqI8Qoqf6lTtmAKvJFYa6bhU95B1D0WgzHkmTg33In0A==", "cpu": [ "arm" ], @@ -2629,9 +2621,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.16.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.16.4.tgz", - "integrity": "sha512-tJfJaXPiFAG+Jn3cutp7mCs1ePltuAgRqdDZrzb1aeE3TktWWJ+g7xK9SNlaSUFw6IU4QgOxAY4rA+wZUT5Wfg==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.17.2.tgz", + "integrity": "sha512-uSqpsp91mheRgw96xtyAGP9FW5ChctTFEoXP0r5FAzj/3ZRv3Uxjtc7taRQSaQM/q85KEKjKsZuiZM3GyUivRg==", "cpu": [ "arm" ], @@ -2641,9 +2633,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.16.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.16.4.tgz", - "integrity": "sha512-7dy1BzQkgYlUTapDTvK997cgi0Orh5Iu7JlZVBy1MBURk7/HSbHkzRnXZa19ozy+wwD8/SlpJnOOckuNZtJR9w==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.17.2.tgz", + "integrity": "sha512-EMMPHkiCRtE8Wdk3Qhtciq6BndLtstqZIroHiiGzB3C5LDJmIZcSzVtLRbwuXuUft1Cnv+9fxuDtDxz3k3EW2A==", "cpu": [ "arm64" ], @@ -2653,9 +2645,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.16.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.16.4.tgz", - "integrity": "sha512-zsFwdUw5XLD1gQe0aoU2HVceI6NEW7q7m05wA46eUAyrkeNYExObfRFQcvA6zw8lfRc5BHtan3tBpo+kqEOxmg==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.17.2.tgz", + "integrity": "sha512-NMPylUUZ1i0z/xJUIx6VUhISZDRT+uTWpBcjdv0/zkp7b/bQDF+NfnfdzuTiB1G6HTodgoFa93hp0O1xl+/UbA==", "cpu": [ "arm64" ], @@ -2665,9 +2657,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.16.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.16.4.tgz", - "integrity": "sha512-p8C3NnxXooRdNrdv6dBmRTddEapfESEUflpICDNKXpHvTjRRq1J82CbU5G3XfebIZyI3B0s074JHMWD36qOW6w==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.17.2.tgz", + "integrity": "sha512-T19My13y8uYXPw/L/k0JYaX1fJKFT/PWdXiHr8mTbXWxjVF1t+8Xl31DgBBvEKclw+1b00Chg0hxE2O7bTG7GQ==", "cpu": [ "ppc64" ], @@ -2677,9 +2669,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.16.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.16.4.tgz", - "integrity": "sha512-Lh/8ckoar4s4Id2foY7jNgitTOUQczwMWNYi+Mjt0eQ9LKhr6sK477REqQkmy8YHY3Ca3A2JJVdXnfb3Rrwkng==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.17.2.tgz", + "integrity": "sha512-BOaNfthf3X3fOWAB+IJ9kxTgPmMqPPH5f5k2DcCsRrBIbWnaJCgX2ll77dV1TdSy9SaXTR5iDXRL8n7AnoP5cg==", "cpu": [ "riscv64" ], @@ -2689,9 +2681,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.16.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.16.4.tgz", - "integrity": "sha512-1xwwn9ZCQYuqGmulGsTZoKrrn0z2fAur2ujE60QgyDpHmBbXbxLaQiEvzJWDrscRq43c8DnuHx3QorhMTZgisQ==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.17.2.tgz", + "integrity": "sha512-W0UP/x7bnn3xN2eYMql2T/+wpASLE5SjObXILTMPUBDB/Fg/FxC+gX4nvCfPBCbNhz51C+HcqQp2qQ4u25ok6g==", "cpu": [ "s390x" ], @@ -2701,9 +2693,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.16.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.16.4.tgz", - "integrity": "sha512-LuOGGKAJ7dfRtxVnO1i3qWc6N9sh0Em/8aZ3CezixSTM+E9Oq3OvTsvC4sm6wWjzpsIlOCnZjdluINKESflJLA==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.17.2.tgz", + "integrity": "sha512-Hy7pLwByUOuyaFC6mAr7m+oMC+V7qyifzs/nW2OJfC8H4hbCzOX07Ov0VFk/zP3kBsELWNFi7rJtgbKYsav9QQ==", "cpu": [ "x64" ], @@ -2713,9 +2705,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.16.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.16.4.tgz", - "integrity": "sha512-ch86i7KkJKkLybDP2AtySFTRi5fM3KXp0PnHocHuJMdZwu7BuyIKi35BE9guMlmTpwwBTB3ljHj9IQXnTCD0vA==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.17.2.tgz", + "integrity": "sha512-h1+yTWeYbRdAyJ/jMiVw0l6fOOm/0D1vNLui9iPuqgRGnXA0u21gAqOyB5iHjlM9MMfNOm9RHCQ7zLIzT0x11Q==", "cpu": [ "x64" ], @@ -2725,9 +2717,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.16.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.16.4.tgz", - "integrity": "sha512-Ma4PwyLfOWZWayfEsNQzTDBVW8PZ6TUUN1uFTBQbF2Chv/+sjenE86lpiEwj2FiviSmSZ4Ap4MaAfl1ciF4aSA==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.17.2.tgz", + "integrity": "sha512-tmdtXMfKAjy5+IQsVtDiCfqbynAQE/TQRpWdVataHmhMb9DCoJxp9vLcCBjEQWMiUYxO1QprH/HbY9ragCEFLA==", "cpu": [ "arm64" ], @@ -2737,9 +2729,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.16.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.16.4.tgz", - "integrity": "sha512-9m/ZDrQsdo/c06uOlP3W9G2ENRVzgzbSXmXHT4hwVaDQhYcRpi9bgBT0FTG9OhESxwK0WjQxYOSfv40cU+T69w==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.17.2.tgz", + "integrity": "sha512-7II/QCSTAHuE5vdZaQEwJq2ZACkBpQDOmQsE6D6XUbnBHW8IAhm4eTufL6msLJorzrHDFv3CF8oCA/hSIRuZeQ==", "cpu": [ "ia32" ], @@ -2749,9 +2741,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.16.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.16.4.tgz", - "integrity": "sha512-YunpoOAyGLDseanENHmbFvQSfVL5BxW3k7hhy0eN4rb3gS/ct75dVD0EXOWIqFT/nE8XYW6LP6vz6ctKRi0k9A==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.17.2.tgz", + "integrity": "sha512-TGGO7v7qOq4CYmSBVEYpI1Y5xDuCEnbVC5Vth8mOsW0gDSzxNrVERPc790IGHsrT2dQSimgMr9Ub3Y1Jci5/8w==", "cpu": [ "x64" ], @@ -2833,9 +2825,9 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==" }, "node_modules/@types/linkify-it": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.5.tgz", - "integrity": "sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==" + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==" }, "node_modules/@types/markdown-it": { "version": "12.2.3", @@ -2847,14 +2839,14 @@ } }, "node_modules/@types/mdurl": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.5.tgz", - "integrity": "sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==" }, "node_modules/@types/node": { - "version": "20.12.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz", - "integrity": "sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==", + "version": "20.12.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.11.tgz", + "integrity": "sha512-vDg9PZ/zi+Nqp6boSOT7plNuthRugEKixDv5sFTIpkE89MmNtEArAShI4mxuX2+UrLEe9pxC1vm2cjm9YlWbJw==", "dependencies": { "undici-types": "~5.26.4" } @@ -2865,15 +2857,15 @@ "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.7.1.tgz", - "integrity": "sha512-KwfdWXJBOviaBVhxO3p5TJiLpNuh2iyXyjmWN0f1nU87pwyvfS0EmjC6ukQVYVFJd/K1+0NWGPDXiyEyQorn0Q==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.8.0.tgz", + "integrity": "sha512-gFTT+ezJmkwutUPmB0skOj3GZJtlEGnlssems4AjkVweUPGj7jRwwqg0Hhg7++kPGJqKtTYx+R05Ftww372aIg==", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "7.7.1", - "@typescript-eslint/type-utils": "7.7.1", - "@typescript-eslint/utils": "7.7.1", - "@typescript-eslint/visitor-keys": "7.7.1", + "@typescript-eslint/scope-manager": "7.8.0", + "@typescript-eslint/type-utils": "7.8.0", + "@typescript-eslint/utils": "7.8.0", + "@typescript-eslint/visitor-keys": "7.8.0", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.3.1", @@ -2898,24 +2890,10 @@ } } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "dependencies": { - "lru-cache": "^6.0.0" - }, + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", "bin": { "semver": "bin/semver.js" }, @@ -2923,20 +2901,15 @@ "node": ">=10" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, "node_modules/@typescript-eslint/parser": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.7.1.tgz", - "integrity": "sha512-vmPzBOOtz48F6JAGVS/kZYk4EkXao6iGrD838sp1w3NQQC0W8ry/q641KU4PrG7AKNAf56NOcR8GOpH8l9FPCw==", - "dependencies": { - "@typescript-eslint/scope-manager": "7.7.1", - "@typescript-eslint/types": "7.7.1", - "@typescript-eslint/typescript-estree": "7.7.1", - "@typescript-eslint/visitor-keys": "7.7.1", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.8.0.tgz", + "integrity": "sha512-KgKQly1pv0l4ltcftP59uQZCi4HUYswCLbTqVZEJu7uLX8CTLyswqMLqLN+2QFz4jCptqWVV4SB7vdxcH2+0kQ==", + "dependencies": { + "@typescript-eslint/scope-manager": "7.8.0", + "@typescript-eslint/types": "7.8.0", + "@typescript-eslint/typescript-estree": "7.8.0", + "@typescript-eslint/visitor-keys": "7.8.0", "debug": "^4.3.4" }, "engines": { @@ -2956,12 +2929,12 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.7.1.tgz", - "integrity": "sha512-PytBif2SF+9SpEUKynYn5g1RHFddJUcyynGpztX3l/ik7KmZEv19WCMhUBkHXPU9es/VWGD3/zg3wg90+Dh2rA==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.8.0.tgz", + "integrity": "sha512-viEmZ1LmwsGcnr85gIq+FCYI7nO90DVbE37/ll51hjv9aG+YZMb4WDE2fyWpUR4O/UrhGRpYXK/XajcGTk2B8g==", "dependencies": { - "@typescript-eslint/types": "7.7.1", - "@typescript-eslint/visitor-keys": "7.7.1" + "@typescript-eslint/types": "7.8.0", + "@typescript-eslint/visitor-keys": "7.8.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -2972,12 +2945,12 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.7.1.tgz", - "integrity": "sha512-ZksJLW3WF7o75zaBPScdW1Gbkwhd/lyeXGf1kQCxJaOeITscoSl0MjynVvCzuV5boUz/3fOI06Lz8La55mu29Q==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.8.0.tgz", + "integrity": "sha512-H70R3AefQDQpz9mGv13Uhi121FNMh+WEaRqcXTX09YEDky21km4dV1ZXJIp8QjXc4ZaVkXVdohvWDzbnbHDS+A==", "dependencies": { - "@typescript-eslint/typescript-estree": "7.7.1", - "@typescript-eslint/utils": "7.7.1", + "@typescript-eslint/typescript-estree": "7.8.0", + "@typescript-eslint/utils": "7.8.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -2998,9 +2971,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.7.1.tgz", - "integrity": "sha512-AmPmnGW1ZLTpWa+/2omPrPfR7BcbUU4oha5VIbSbS1a1Tv966bklvLNXxp3mrbc+P2j4MNOTfDffNsk4o0c6/w==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.8.0.tgz", + "integrity": "sha512-wf0peJ+ZGlcH+2ZS23aJbOv+ztjeeP8uQ9GgwMJGVLx/Nj9CJt17GWgWWoSmoRVKAX2X+7fzEnAjxdvK2gqCLw==", "engines": { "node": "^18.18.0 || >=20.0.0" }, @@ -3010,12 +2983,12 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.7.1.tgz", - "integrity": "sha512-CXe0JHCXru8Fa36dteXqmH2YxngKJjkQLjxzoj6LYwzZ7qZvgsLSc+eqItCrqIop8Vl2UKoAi0StVWu97FQZIQ==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.8.0.tgz", + "integrity": "sha512-5pfUCOwK5yjPaJQNy44prjCwtr981dO8Qo9J9PwYXZ0MosgAbfEMB008dJ5sNo3+/BN6ytBPuSvXUg9SAqB0dg==", "dependencies": { - "@typescript-eslint/types": "7.7.1", - "@typescript-eslint/visitor-keys": "7.7.1", + "@typescript-eslint/types": "7.8.0", + "@typescript-eslint/visitor-keys": "7.8.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -3055,24 +3028,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "dependencies": { - "lru-cache": "^6.0.0" - }, + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", "bin": { "semver": "bin/semver.js" }, @@ -3088,22 +3047,17 @@ "node": ">=8" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, "node_modules/@typescript-eslint/utils": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.7.1.tgz", - "integrity": "sha512-QUvBxPEaBXf41ZBbaidKICgVL8Hin0p6prQDu6bbetWo39BKbWJxRsErOzMNT1rXvTll+J7ChrbmMCXM9rsvOQ==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.8.0.tgz", + "integrity": "sha512-L0yFqOCflVqXxiZyXrDr80lnahQfSOfc9ELAAZ75sqicqp2i36kEZZGuUymHNFoYOqxRT05up760b4iGsl02nQ==", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.15", "@types/semver": "^7.5.8", - "@typescript-eslint/scope-manager": "7.7.1", - "@typescript-eslint/types": "7.7.1", - "@typescript-eslint/typescript-estree": "7.7.1", + "@typescript-eslint/scope-manager": "7.8.0", + "@typescript-eslint/types": "7.8.0", + "@typescript-eslint/typescript-estree": "7.8.0", "semver": "^7.6.0" }, "engines": { @@ -3117,24 +3071,10 @@ "eslint": "^8.56.0" } }, - "node_modules/@typescript-eslint/utils/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@typescript-eslint/utils/node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "dependencies": { - "lru-cache": "^6.0.0" - }, + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", "bin": { "semver": "bin/semver.js" }, @@ -3142,17 +3082,12 @@ "node": ">=10" } }, - "node_modules/@typescript-eslint/utils/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.7.1.tgz", - "integrity": "sha512-gBL3Eq25uADw1LQ9kVpf3hRM+DWzs0uZknHYK3hq4jcTPqVCClHGDnB6UUUV2SFeBeA4KWHWbbLqmbGcZ4FYbw==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.8.0.tgz", + "integrity": "sha512-q4/gibTNBQNA0lGyYQCmWRS5D15n8rXh4QjK3KV+MBPlTYHpfBUT3D3PaPR/HeNiI9W6R7FvlkcGhNyAoP+caA==", "dependencies": { - "@typescript-eslint/types": "7.7.1", + "@typescript-eslint/types": "7.8.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -3169,21 +3104,21 @@ "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" }, "node_modules/@unhead/dom": { - "version": "1.9.7", - "resolved": "https://registry.npmjs.org/@unhead/dom/-/dom-1.9.7.tgz", - "integrity": "sha512-suZVi8apZCNEMKuasGboBB3njJJm+gd8G0NA89geVozJ0bz40FvLyLEJZ9LirbzpujmhgHhsUSvlq4QyslRqdQ==", + "version": "1.9.10", + "resolved": "https://registry.npmjs.org/@unhead/dom/-/dom-1.9.10.tgz", + "integrity": "sha512-F4sBrmd8kG8MEqcVTGL0Y6tXbJMdWK724pznUzefpZTs1GaVypFikLluaLt4EnICcVhOBSe4TkGrc8N21IJJzQ==", "dependencies": { - "@unhead/schema": "1.9.7", - "@unhead/shared": "1.9.7" + "@unhead/schema": "1.9.10", + "@unhead/shared": "1.9.10" }, "funding": { "url": "https://github.com/sponsors/harlan-zw" } }, "node_modules/@unhead/schema": { - "version": "1.9.7", - "resolved": "https://registry.npmjs.org/@unhead/schema/-/schema-1.9.7.tgz", - "integrity": "sha512-naQGY1gQqq8DmQCxVTOeeXIqaRwbqnLEgvQl12zPEDviYxmg7TCbmKyN9uT4ZarQbJ2WYT2UtYvdSrmTXcwlBw==", + "version": "1.9.10", + "resolved": "https://registry.npmjs.org/@unhead/schema/-/schema-1.9.10.tgz", + "integrity": "sha512-3ROh0doKfA7cIcU0zmjYVvNOiJuxSOcjInL+7iOFIxQovEWr1PcDnrnbEWGJsXrLA8eqjrjmhuDqAr3JbMGsLg==", "dependencies": { "hookable": "^5.5.3", "zhead": "^2.2.4" @@ -3193,25 +3128,25 @@ } }, "node_modules/@unhead/shared": { - "version": "1.9.7", - "resolved": "https://registry.npmjs.org/@unhead/shared/-/shared-1.9.7.tgz", - "integrity": "sha512-srji+qaBkkGOTdtTmFxt3AebFYcpt1qQHeQva7X3dSm5nZJDoKj35BJJTZfBSRCjgvkTtsdVUT14f9p9/4BCMA==", + "version": "1.9.10", + "resolved": "https://registry.npmjs.org/@unhead/shared/-/shared-1.9.10.tgz", + "integrity": "sha512-LBXxm/8ahY4FZ0FbWVaM1ANFO5QpPzvaYwjAQhgHANsrqFP2EqoGcOv1CfhdQbxg8vpGXkjI7m0r/8E9d3JoDA==", "dependencies": { - "@unhead/schema": "1.9.7" + "@unhead/schema": "1.9.10" }, "funding": { "url": "https://github.com/sponsors/harlan-zw" } }, "node_modules/@unhead/vue": { - "version": "1.9.7", - "resolved": "https://registry.npmjs.org/@unhead/vue/-/vue-1.9.7.tgz", - "integrity": "sha512-c5pcNvi3FwMfqd+lfD3XUyRKPDv/AVPrep84CFXaqB7ebb+2OQTgtxBiCoRsa8+DtdhYI50lYJ/yeVdfLI9XUw==", + "version": "1.9.10", + "resolved": "https://registry.npmjs.org/@unhead/vue/-/vue-1.9.10.tgz", + "integrity": "sha512-Zi65eTU5IIaqqXAVOVJ4fnwJRR751FZIFlzYOjIekf1eNkISy+A4xyz3NIEQWSlXCrOiDNgDhT0YgKUcx5FfHQ==", "dependencies": { - "@unhead/schema": "1.9.7", - "@unhead/shared": "1.9.7", + "@unhead/schema": "1.9.10", + "@unhead/shared": "1.9.10", "hookable": "^5.5.3", - "unhead": "1.9.7" + "unhead": "1.9.10" }, "funding": { "url": "https://github.com/sponsors/harlan-zw" @@ -3221,18 +3156,18 @@ } }, "node_modules/@vitejs/plugin-legacy": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-legacy/-/plugin-legacy-5.3.2.tgz", - "integrity": "sha512-8moCOrIMaZ/Rjln0Q6GsH6s8fAt1JOI3k8nmfX4tXUxE5KAExVctSyOBk+A25GClsdSWqIk2yaUthH3KJ2X4tg==", + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-legacy/-/plugin-legacy-5.4.0.tgz", + "integrity": "sha512-Z7o44IbOIir/appjqtVzxnmLeGD8DjWGNm48lfPWZn4hxjzUjTkMX7BDwncpauWAQ/0VIz6uPeMHl3Za0Rw7wA==", "dependencies": { - "@babel/core": "^7.23.9", - "@babel/preset-env": "^7.23.9", + "@babel/core": "^7.24.5", + "@babel/preset-env": "^7.24.5", "browserslist": "^4.23.0", "browserslist-to-esbuild": "^2.1.1", - "core-js": "^3.36.0", - "magic-string": "^0.30.7", + "core-js": "^3.37.0", + "magic-string": "^0.30.10", "regenerator-runtime": "^0.14.1", - "systemjs": "^6.14.3" + "systemjs": "^6.15.1" }, "engines": { "node": "^18.0.0 || >=20.0.0" @@ -3258,12 +3193,12 @@ } }, "node_modules/@vitest/expect": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.5.2.tgz", - "integrity": "sha512-rf7MTD1WCoDlN3FfYJ9Llfp0PbdtOMZ3FIF0AVkDnKbp3oiMW1c8AmvRZBcqbAhDUAvF52e9zx4WQM1r3oraVA==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.0.tgz", + "integrity": "sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==", "dependencies": { - "@vitest/spy": "1.5.2", - "@vitest/utils": "1.5.2", + "@vitest/spy": "1.6.0", + "@vitest/utils": "1.6.0", "chai": "^4.3.10" }, "funding": { @@ -3271,11 +3206,11 @@ } }, "node_modules/@vitest/runner": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.5.2.tgz", - "integrity": "sha512-7IJ7sJhMZrqx7HIEpv3WrMYcq8ZNz9L6alo81Y6f8hV5mIE6yVZsFoivLZmr0D777klm1ReqonE9LyChdcmw6g==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.0.tgz", + "integrity": "sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg==", "dependencies": { - "@vitest/utils": "1.5.2", + "@vitest/utils": "1.6.0", "p-limit": "^5.0.0", "pathe": "^1.1.1" }, @@ -3309,9 +3244,9 @@ } }, "node_modules/@vitest/snapshot": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.5.2.tgz", - "integrity": "sha512-CTEp/lTYos8fuCc9+Z55Ga5NVPKUgExritjF5VY7heRFUfheoAqBneUlvXSUJHUZPjnPmyZA96yLRJDP1QATFQ==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.0.tgz", + "integrity": "sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ==", "dependencies": { "magic-string": "^0.30.5", "pathe": "^1.1.1", @@ -3322,9 +3257,9 @@ } }, "node_modules/@vitest/spy": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.5.2.tgz", - "integrity": "sha512-xCcPvI8JpCtgikT9nLpHPL1/81AYqZy1GCy4+MCHBE7xi8jgsYkULpW5hrx5PGLgOQjUpb6fd15lqcriJ40tfQ==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.0.tgz", + "integrity": "sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw==", "dependencies": { "tinyspy": "^2.2.0" }, @@ -3333,9 +3268,9 @@ } }, "node_modules/@vitest/utils": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.5.2.tgz", - "integrity": "sha512-sWOmyofuXLJ85VvXNsroZur7mOJGiQeM0JN3/0D1uU8U9bGFM69X1iqHaRXl6R8BwaLY6yPCogP257zxTzkUdA==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.0.tgz", + "integrity": "sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==", "dependencies": { "diff-sequences": "^29.6.3", "estree-walker": "^3.0.3", @@ -3355,36 +3290,36 @@ } }, "node_modules/@vue/compiler-core": { - "version": "3.4.25", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.25.tgz", - "integrity": "sha512-Y2pLLopaElgWnMNolgG8w3C5nNUVev80L7hdQ5iIKPtMJvhVpG0zhnBG/g3UajJmZdvW0fktyZTotEHD1Srhbg==", + "version": "3.4.27", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.27.tgz", + "integrity": "sha512-E+RyqY24KnyDXsCuQrI+mlcdW3ALND6U7Gqa/+bVwbcpcR3BRRIckFoz7Qyd4TTlnugtwuI7YgjbvsLmxb+yvg==", "dependencies": { "@babel/parser": "^7.24.4", - "@vue/shared": "3.4.25", + "@vue/shared": "3.4.27", "entities": "^4.5.0", "estree-walker": "^2.0.2", "source-map-js": "^1.2.0" } }, "node_modules/@vue/compiler-dom": { - "version": "3.4.25", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.25.tgz", - "integrity": "sha512-Ugz5DusW57+HjllAugLci19NsDK+VyjGvmbB2TXaTcSlQxwL++2PETHx/+Qv6qFwNLzSt7HKepPe4DcTE3pBWg==", + "version": "3.4.27", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.27.tgz", + "integrity": "sha512-kUTvochG/oVgE1w5ViSr3KUBh9X7CWirebA3bezTbB5ZKBQZwR2Mwj9uoSKRMFcz4gSMzzLXBPD6KpCLb9nvWw==", "dependencies": { - "@vue/compiler-core": "3.4.25", - "@vue/shared": "3.4.25" + "@vue/compiler-core": "3.4.27", + "@vue/shared": "3.4.27" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.4.25", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.25.tgz", - "integrity": "sha512-m7rryuqzIoQpOBZ18wKyq05IwL6qEpZxFZfRxlNYuIPDqywrXQxgUwLXIvoU72gs6cRdY6wHD0WVZIFE4OEaAQ==", + "version": "3.4.27", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.27.tgz", + "integrity": "sha512-nDwntUEADssW8e0rrmE0+OrONwmRlegDA1pD6QhVeXxjIytV03yDqTey9SBDiALsvAd5U4ZrEKbMyVXhX6mCGA==", "dependencies": { "@babel/parser": "^7.24.4", - "@vue/compiler-core": "3.4.25", - "@vue/compiler-dom": "3.4.25", - "@vue/compiler-ssr": "3.4.25", - "@vue/shared": "3.4.25", + "@vue/compiler-core": "3.4.27", + "@vue/compiler-dom": "3.4.27", + "@vue/compiler-ssr": "3.4.27", + "@vue/shared": "3.4.27", "estree-walker": "^2.0.2", "magic-string": "^0.30.10", "postcss": "^8.4.38", @@ -3392,12 +3327,12 @@ } }, "node_modules/@vue/compiler-ssr": { - "version": "3.4.25", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.25.tgz", - "integrity": "sha512-H2ohvM/Pf6LelGxDBnfbbXFPyM4NE3hrw0e/EpwuSiYu8c819wx+SVGdJ65p/sFrYDd6OnSDxN1MB2mN07hRSQ==", + "version": "3.4.27", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.27.tgz", + "integrity": "sha512-CVRzSJIltzMG5FcidsW0jKNQnNRYC8bT21VegyMMtHmhW3UOI7knmUehzswXLrExDLE6lQCZdrhD4ogI7c+vuw==", "dependencies": { - "@vue/compiler-dom": "3.4.25", - "@vue/shared": "3.4.25" + "@vue/compiler-dom": "3.4.27", + "@vue/shared": "3.4.27" } }, "node_modules/@vue/devtools-api": { @@ -3442,53 +3377,53 @@ } }, "node_modules/@vue/reactivity": { - "version": "3.4.25", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.25.tgz", - "integrity": "sha512-mKbEtKr1iTxZkAG3vm3BtKHAOhuI4zzsVcN0epDldU/THsrvfXRKzq+lZnjczZGnTdh3ojd86/WrP+u9M51pWQ==", + "version": "3.4.27", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.27.tgz", + "integrity": "sha512-kK0g4NknW6JX2yySLpsm2jlunZJl2/RJGZ0H9ddHdfBVHcNzxmQ0sS0b09ipmBoQpY8JM2KmUw+a6sO8Zo+zIA==", "dependencies": { - "@vue/shared": "3.4.25" + "@vue/shared": "3.4.27" } }, "node_modules/@vue/runtime-core": { - "version": "3.4.25", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.25.tgz", - "integrity": "sha512-3qhsTqbEh8BMH3pXf009epCI5E7bKu28fJLi9O6W+ZGt/6xgSfMuGPqa5HRbUxLoehTNp5uWvzCr60KuiRIL0Q==", + "version": "3.4.27", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.27.tgz", + "integrity": "sha512-7aYA9GEbOOdviqVvcuweTLe5Za4qBZkUY7SvET6vE8kyypxVgaT1ixHLg4urtOlrApdgcdgHoTZCUuTGap/5WA==", "dependencies": { - "@vue/reactivity": "3.4.25", - "@vue/shared": "3.4.25" + "@vue/reactivity": "3.4.27", + "@vue/shared": "3.4.27" } }, "node_modules/@vue/runtime-dom": { - "version": "3.4.25", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.25.tgz", - "integrity": "sha512-ode0sj77kuwXwSc+2Yhk8JMHZh1sZp9F/51wdBiz3KGaWltbKtdihlJFhQG4H6AY+A06zzeMLkq6qu8uDSsaoA==", + "version": "3.4.27", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.27.tgz", + "integrity": "sha512-ScOmP70/3NPM+TW9hvVAz6VWWtZJqkbdf7w6ySsws+EsqtHvkhxaWLecrTorFxsawelM5Ys9FnDEMt6BPBDS0Q==", "dependencies": { - "@vue/runtime-core": "3.4.25", - "@vue/shared": "3.4.25", + "@vue/runtime-core": "3.4.27", + "@vue/shared": "3.4.27", "csstype": "^3.1.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.4.25", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.25.tgz", - "integrity": "sha512-8VTwq0Zcu3K4dWV0jOwIVINESE/gha3ifYCOKEhxOj6MEl5K5y8J8clQncTcDhKF+9U765nRw4UdUEXvrGhyVQ==", + "version": "3.4.27", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.27.tgz", + "integrity": "sha512-dlAMEuvmeA3rJsOMJ2J1kXU7o7pOxgsNHVr9K8hB3ImIkSuBrIdy0vF66h8gf8Tuinf1TK3mPAz2+2sqyf3KzA==", "dependencies": { - "@vue/compiler-ssr": "3.4.25", - "@vue/shared": "3.4.25" + "@vue/compiler-ssr": "3.4.27", + "@vue/shared": "3.4.27" }, "peerDependencies": { - "vue": "3.4.25" + "vue": "3.4.27" } }, "node_modules/@vue/shared": { - "version": "3.4.25", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.25.tgz", - "integrity": "sha512-k0yappJ77g2+KNrIaF0FFnzwLvUBLUYr8VOwz+/6vLsmItFp51AcxLL7Ey3iPd7BIRyWPOcqUjMnm7OkahXllA==" + "version": "3.4.27", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.27.tgz", + "integrity": "sha512-DL3NmY2OFlqmYYrzp39yi3LDkKxa5vZVwxWdQ3rG0ekuWscHraeIbnI8t+aZK7qhYqEqWKTUdijadunb9pnrgA==" }, "node_modules/@vue/test-utils": { - "version": "2.4.5", - "resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-2.4.5.tgz", - "integrity": "sha512-oo2u7vktOyKUked36R93NB7mg2B+N7Plr8lxp2JBGwr18ch6EggFjixSCdIVVLkT6Qr0z359Xvnafc9dcKyDUg==", + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-2.4.6.tgz", + "integrity": "sha512-FMxEjOpYNYiFe0GkaHsnJPXFHxQ6m4t8vI/ElPGpMWxZKpmRvQ33OIrvRXemy6yha03RxhOlQuy+gZMC3CQSow==", "dependencies": { "js-beautify": "^1.14.9", "vue-component-type-helpers": "^2.0.0" @@ -4008,9 +3943,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001612", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001612.tgz", - "integrity": "sha512-lFgnZ07UhaCcsSZgWW0K5j4e69dK1u/ltrL9lTUiFOwNHs12S3UMIEYgBV0Z6C6hRDev7iRnMzzYmKabYdXF9g==", + "version": "1.0.30001617", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001617.tgz", + "integrity": "sha512-mLyjzNI9I+Pix8zwcrpxEbGlfqOkF9kM3ptzmKNw5tizSyYwMe+nGLTqMK9cO+0E+Bh6TsBxNAaHWEM8xwSsmA==", "funding": [ { "type": "opencollective", @@ -4027,9 +3962,9 @@ ] }, "node_modules/canvas-confetti": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/canvas-confetti/-/canvas-confetti-1.9.2.tgz", - "integrity": "sha512-6Xi7aHHzKwxZsem4mCKoqP6YwUG3HamaHHAlz1hTNQPCqXhARFpSXnkC9TWlahHY5CG6hSL5XexNjxK8irVErg==", + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/canvas-confetti/-/canvas-confetti-1.9.3.tgz", + "integrity": "sha512-rFfTURMvmVEX1gyXFgn5QMn81bYk70qa0HLzcIOSVEyl57n6o9ItHeBtUSWdvKAPY0xlvBHno4/v3QPrT83q9g==", "funding": { "type": "donate", "url": "https://www.paypal.me/kirilvatev" @@ -4634,17 +4569,6 @@ "node": ">=14" } }, - "node_modules/editorconfig/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/editorconfig/node_modules/minimatch": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz", @@ -4660,12 +4584,9 @@ } }, "node_modules/editorconfig/node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "dependencies": { - "lru-cache": "^6.0.0" - }, + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", "bin": { "semver": "bin/semver.js" }, @@ -4673,20 +4594,15 @@ "node": ">=10" } }, - "node_modules/editorconfig/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "node_modules/electron-to-chromium": { - "version": "1.4.749", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.749.tgz", - "integrity": "sha512-LRMMrM9ITOvue0PoBrvNIraVmuDbJV5QC9ierz/z5VilMdPOVMjOtpICNld3PuXuTZ3CHH/UPxX9gHhAPwi+0Q==" + "version": "1.4.763", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.763.tgz", + "integrity": "sha512-k4J8NrtJ9QrvHLRo8Q18OncqBCB7tIUyqxRcJnlonQ0ioHKYB988GcDFF3ZePmnb8eHEopDs/wPHR/iGAFgoUQ==" }, "node_modules/emoji-regex": { "version": "9.2.2", @@ -5190,9 +5106,9 @@ } }, "node_modules/eslint-plugin-vue": { - "version": "9.25.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.25.0.tgz", - "integrity": "sha512-tDWlx14bVe6Bs+Nnh3IGrD+hb11kf2nukfm6jLsmJIhmiRQ1SUaksvwY9U5MvPB0pcrg0QK0xapQkfITs3RKOA==", + "version": "9.26.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.26.0.tgz", + "integrity": "sha512-eTvlxXgd4ijE1cdur850G6KalZqk65k1JKoOI2d1kT3hr8sPD07j1q98FRFdNnpxBELGPWxZmInxeHGF/GxtqQ==", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "globals": "^13.24.0", @@ -5224,24 +5140,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint-plugin-vue/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/eslint-plugin-vue/node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "dependencies": { - "lru-cache": "^6.0.0" - }, + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", "bin": { "semver": "bin/semver.js" }, @@ -5249,11 +5151,6 @@ "node": ">=10" } }, - "node_modules/eslint-plugin-vue/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, "node_modules/eslint-scope": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", @@ -5859,21 +5756,21 @@ } }, "node_modules/glob": { - "version": "10.3.12", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz", - "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==", + "version": "10.3.15", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.15.tgz", + "integrity": "sha512-0c6RlJt1TICLyvJYIApxb8GsXoai0KUP7AxKKAtsYXdgJR1mGEUa7DgwShbdk1nly0PYoZj01xd4hzbq3fsjpw==", "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^2.3.6", "minimatch": "^9.0.1", "minipass": "^7.0.4", - "path-scurry": "^1.10.2" + "path-scurry": "^1.11.0" }, "bin": { "glob": "dist/esm/bin.mjs" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=16 || 14 >=14.18" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -5899,11 +5796,12 @@ } }, "node_modules/globalthis": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", - "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", "dependencies": { - "define-properties": "^1.1.3" + "define-properties": "^1.2.1", + "gopd": "^1.0.1" }, "engines": { "node": ">= 0.4" @@ -5986,9 +5884,9 @@ } }, "node_modules/happy-dom": { - "version": "14.7.1", - "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-14.7.1.tgz", - "integrity": "sha512-v60Q0evZ4clvMcrAh5/F8EdxDdfHdFrtffz/CNe10jKD+nFweZVxM91tW+UyY2L4AtpgIaXdZ7TQmiO1pfcwbg==", + "version": "14.10.1", + "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-14.10.1.tgz", + "integrity": "sha512-GRbrZYIezi8+tTtffF4v2QcF8bk1h2loUTO5VYQz3GZdrL08Vk0fI+bwf/vFEBf4C/qVf/easLJ/MY1wwdhytA==", "dependencies": { "entities": "^4.5.0", "webidl-conversions": "^7.0.0", @@ -6608,9 +6506,9 @@ } }, "node_modules/joi": { - "version": "17.13.0", - "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.0.tgz", - "integrity": "sha512-9qcrTyoBmFZRNHeVP4edKqIUEgFzq7MHvTNSDuHSqkpOPtiBkgNgcmTSqmiw1kw9tdKaiddvIDv/eCJDxmqWCA==", + "version": "17.13.1", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.1.tgz", + "integrity": "sha512-vaBlIKCyo4FCUtCm7Eu4QZd/q02bWcxfUO6YSXAZOWF6gzcLBeba8kwotUdYJjDLW8Cz8RywsSOqiNJZW0mNvg==", "dependencies": { "@hapi/hoek": "^9.3.0", "@hapi/topo": "^5.1.0", @@ -7039,22 +6937,22 @@ } }, "node_modules/minipass": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", - "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.1.tgz", + "integrity": "sha512-UZ7eQ+h8ywIRAW1hIEl2AqdwzJucU/Kp59+8kkZeSvafXhZjul247BvIJjEVFVeON6d7lM46XX1HXCduKAS8VA==", "engines": { "node": ">=16 || 14 >=14.17" } }, "node_modules/mlly": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.6.1.tgz", - "integrity": "sha512-vLgaHvaeunuOXHSmEbZ9izxPx3USsk8KCQ8iC+aTlp5sKRSoZvwhHh5L9VbKSaVC6sJDqbyohIS76E2VmHIPAA==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.0.tgz", + "integrity": "sha512-U9SDaXGEREBYQgfejV97coK0UL1r+qnF2SyO9A3qcI8MzKnsIFKHNVEkrDyNncQTKQQumsasmeq84eNMdBfsNQ==", "dependencies": { "acorn": "^8.11.3", "pathe": "^1.1.2", - "pkg-types": "^1.0.3", - "ufo": "^1.3.2" + "pkg-types": "^1.1.0", + "ufo": "^1.5.3" } }, "node_modules/mri": { @@ -7115,9 +7013,9 @@ "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==" }, "node_modules/nopt": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.0.tgz", - "integrity": "sha512-CVDtwCdhYIvnAzFoJ6NJ6dX3oga9/HyciQDnG1vQDjSLMeKLJ4A93ZqYKDrgYSr1FBY5/hMYC+2VCi24pgpkGA==", + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", + "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", "dependencies": { "abbrev": "^2.0.0" }, @@ -7173,9 +7071,9 @@ } }, "node_modules/nwsapi": { - "version": "2.2.9", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.9.tgz", - "integrity": "sha512-2f3F0SEEer8bBu0dsNCFF50N0cTThV1nWFYcEYFZttdW0lDAoybv9cQoK7X7/68Z89S7FoRrVjP1LPX4XRf9vg==" + "version": "2.2.10", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.10.tgz", + "integrity": "sha512-QK0sRs7MKv0tKe1+5uZIQk/C8XGza4DAnztJG8iD+TpJIORARrCxczA738awHrZoHeTjSSoHqao2teO0dC/gFQ==" }, "node_modules/object-inspect": { "version": "1.13.1", @@ -7306,16 +7204,16 @@ } }, "node_modules/optionator": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", - "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dependencies": { - "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", - "type-check": "^0.4.0" + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" }, "engines": { "node": ">= 0.8.0" @@ -7436,24 +7334,24 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "node_modules/path-scurry": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.2.tgz", - "integrity": "sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=16 || 14 >=14.18" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.2.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.1.tgz", - "integrity": "sha512-tS24spDe/zXhWbNPErCHs/AGOzbKGHT+ybSBqmdLm8WZ1xXLWvH8Qn71QPAlqVhd0qUTWjy+Kl9JmISgDdEjsA==", + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", + "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", "engines": { "node": "14 || >=16.14" } @@ -7496,21 +7394,21 @@ } }, "node_modules/pkg-types": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.1.0.tgz", - "integrity": "sha512-/RpmvKdxKf8uILTtoOhAgf30wYbP2Qw+L9p3Rvshx1JZVX+XQNZQFjlbmGHEGIm4CkVPlSn+NXmIM8+9oWQaSA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.1.1.tgz", + "integrity": "sha512-ko14TjmDuQJ14zsotODv7dBlwxKhUKQEhuhmbqo1uCi9BB0Z2alo/wAXg6q1dTR5TyuqYyWhjtfe/Tsh+X28jQ==", "dependencies": { "confbox": "^0.1.7", - "mlly": "^1.6.1", + "mlly": "^1.7.0", "pathe": "^1.1.2" } }, "node_modules/playwright": { - "version": "1.43.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.43.1.tgz", - "integrity": "sha512-V7SoH0ai2kNt1Md9E3Gwas5B9m8KR2GVvwZnAI6Pg0m3sh7UvgiYhRrhsziCmqMJNouPckiOhk8T+9bSAK0VIA==", + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.44.0.tgz", + "integrity": "sha512-F9b3GUCLQ3Nffrfb6dunPOkE5Mh68tR7zN32L4jCk4FjQamgesGay7/dAAe1WaMEGV04DkdJfcJzjoCKygUaRQ==", "dependencies": { - "playwright-core": "1.43.1" + "playwright-core": "1.44.0" }, "bin": { "playwright": "cli.js" @@ -7523,9 +7421,9 @@ } }, "node_modules/playwright-core": { - "version": "1.43.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.43.1.tgz", - "integrity": "sha512-EI36Mto2Vrx6VF7rm708qSnesVQKbxEWvPrfA1IPY6HgczBplDx7ENtx+K2n4kJ41sLLkuGfmb0ZLSSXlDhqPg==", + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.44.0.tgz", + "integrity": "sha512-ZTbkNpFfYcGWohvTTl+xewITm7EOuqIqex0c7dNZ+aXsbrLj0qI8XlGKfPpipjm0Wny/4Lt4CJsWJk1stVS5qQ==", "bin": { "playwright-core": "cli.js" }, @@ -7743,9 +7641,9 @@ } }, "node_modules/react-is": { - "version": "18.3.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.0.tgz", - "integrity": "sha512-wRiUsea88TjKDc4FBEn+sLvIDesp6brMbGWnJGjew2waAc9evdhja/2LvePc898HJbHw0L+MTWy7NhpnELAvLQ==" + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" }, "node_modules/readdirp": { "version": "3.6.0", @@ -7950,9 +7848,9 @@ } }, "node_modules/rollup": { - "version": "4.16.4", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.16.4.tgz", - "integrity": "sha512-kuaTJSUbz+Wsb2ATGvEknkI12XV40vIiHmLuFlejoo7HtDok/O5eDDD0UpCVY5bBX5U5RYo8wWP83H7ZsqVEnA==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.17.2.tgz", + "integrity": "sha512-/9ClTJPByC0U4zNLowV1tMBe8yMEAxewtR3cUNX5BoEpGH3dQEWpJLr6CLp0fPdYRF/fzVOgvDb1zXuakwF5kQ==", "dependencies": { "@types/estree": "1.0.5" }, @@ -7964,22 +7862,22 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.16.4", - "@rollup/rollup-android-arm64": "4.16.4", - "@rollup/rollup-darwin-arm64": "4.16.4", - "@rollup/rollup-darwin-x64": "4.16.4", - "@rollup/rollup-linux-arm-gnueabihf": "4.16.4", - "@rollup/rollup-linux-arm-musleabihf": "4.16.4", - "@rollup/rollup-linux-arm64-gnu": "4.16.4", - "@rollup/rollup-linux-arm64-musl": "4.16.4", - "@rollup/rollup-linux-powerpc64le-gnu": "4.16.4", - "@rollup/rollup-linux-riscv64-gnu": "4.16.4", - "@rollup/rollup-linux-s390x-gnu": "4.16.4", - "@rollup/rollup-linux-x64-gnu": "4.16.4", - "@rollup/rollup-linux-x64-musl": "4.16.4", - "@rollup/rollup-win32-arm64-msvc": "4.16.4", - "@rollup/rollup-win32-ia32-msvc": "4.16.4", - "@rollup/rollup-win32-x64-msvc": "4.16.4", + "@rollup/rollup-android-arm-eabi": "4.17.2", + "@rollup/rollup-android-arm64": "4.17.2", + "@rollup/rollup-darwin-arm64": "4.17.2", + "@rollup/rollup-darwin-x64": "4.17.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.17.2", + "@rollup/rollup-linux-arm-musleabihf": "4.17.2", + "@rollup/rollup-linux-arm64-gnu": "4.17.2", + "@rollup/rollup-linux-arm64-musl": "4.17.2", + "@rollup/rollup-linux-powerpc64le-gnu": "4.17.2", + "@rollup/rollup-linux-riscv64-gnu": "4.17.2", + "@rollup/rollup-linux-s390x-gnu": "4.17.2", + "@rollup/rollup-linux-x64-gnu": "4.17.2", + "@rollup/rollup-linux-x64-musl": "4.17.2", + "@rollup/rollup-win32-arm64-msvc": "4.17.2", + "@rollup/rollup-win32-ia32-msvc": "4.17.2", + "@rollup/rollup-win32-x64-msvc": "4.17.2", "fsevents": "~2.3.2" } }, @@ -8563,14 +8461,14 @@ } }, "node_modules/systemjs": { - "version": "6.14.3", - "resolved": "https://registry.npmjs.org/systemjs/-/systemjs-6.14.3.tgz", - "integrity": "sha512-hQv45irdhXudAOr8r6SVSpJSGtogdGZUbJBRKCE5nsIS7tsxxvnIHqT4IOPWj+P+HcSzeWzHlGCGpmhPDIKe+w==" + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/systemjs/-/systemjs-6.15.1.tgz", + "integrity": "sha512-Nk8c4lXvMB98MtbmjX7JwJRgJOL8fluecYCfCeYBznwmpOs8Bf15hLM6z4z71EDAhQVrQrI+wt1aLWSXZq+hXA==" }, "node_modules/terser": { - "version": "5.30.4", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.30.4.tgz", - "integrity": "sha512-xRdd0v64a8mFK9bnsKVdoNP9GQIKUAaJPTaqEQDL4w/J8WaW4sWXXoMZ+6SimPkfT5bElreXf8m9HnmPc3E1BQ==", + "version": "5.31.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.31.0.tgz", + "integrity": "sha512-Q1JFAoUKE5IMfI4Z/lkE/E6+SwgzO+x4tq4v1AyBLRj8VSYvRO6A/rQrPg1yud4g0En9EKI1TvFRF2tQFcoUkg==", "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", @@ -8667,9 +8565,9 @@ } }, "node_modules/tough-cookie": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", - "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", "dependencies": { "psl": "^1.1.33", "punycode": "^2.1.1", @@ -8890,13 +8788,13 @@ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" }, "node_modules/unhead": { - "version": "1.9.7", - "resolved": "https://registry.npmjs.org/unhead/-/unhead-1.9.7.tgz", - "integrity": "sha512-Kv7aU5l41qiq36t9qMks8Pgsj7adaTBm9aDS6USlmodTXioeqlJ5vEu9DI+8ZZPwRlmof3aDlo1kubyaXdSNmQ==", + "version": "1.9.10", + "resolved": "https://registry.npmjs.org/unhead/-/unhead-1.9.10.tgz", + "integrity": "sha512-Y3w+j1x1YFig2YuE+W2sER+SciRR7MQktYRHNqvZJ0iUNCCJTS8Z/SdSMUEeuFV28daXeASlR3fy7Ry3O2indg==", "dependencies": { - "@unhead/dom": "1.9.7", - "@unhead/schema": "1.9.7", - "@unhead/shared": "1.9.7", + "@unhead/dom": "1.9.10", + "@unhead/schema": "1.9.10", + "@unhead/shared": "1.9.10", "hookable": "^5.5.3" }, "funding": { @@ -8956,9 +8854,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", - "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.15.tgz", + "integrity": "sha512-K9HWH62x3/EalU1U6sjSZiylm9C8tgq2mSvshZpqc7QE69RaA2qjhkW2HlNA0tFpEbtyFz7HTqbSdN4MSwUodA==", "funding": [ { "type": "opencollective", @@ -8974,7 +8872,7 @@ } ], "dependencies": { - "escalade": "^3.1.1", + "escalade": "^3.1.2", "picocolors": "^1.0.0" }, "bin": { @@ -9031,9 +8929,9 @@ } }, "node_modules/vite": { - "version": "5.2.10", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.10.tgz", - "integrity": "sha512-PAzgUZbP7msvQvqdSD+ErD5qGnSFiGOoWmV5yAKUEI0kdhjbH6nMWVyZQC/hSc4aXwc0oJ9aEdIiF9Oje0JFCw==", + "version": "5.2.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.11.tgz", + "integrity": "sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==", "dependencies": { "esbuild": "^0.20.1", "postcss": "^8.4.38", @@ -9122,15 +9020,15 @@ } }, "node_modules/vitest": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.5.2.tgz", - "integrity": "sha512-l9gwIkq16ug3xY7BxHwcBQovLZG75zZL0PlsiYQbf76Rz6QGs54416UWMtC0jXeihvHvcHrf2ROEjkQRVpoZYw==", - "dependencies": { - "@vitest/expect": "1.5.2", - "@vitest/runner": "1.5.2", - "@vitest/snapshot": "1.5.2", - "@vitest/spy": "1.5.2", - "@vitest/utils": "1.5.2", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.0.tgz", + "integrity": "sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==", + "dependencies": { + "@vitest/expect": "1.6.0", + "@vitest/runner": "1.6.0", + "@vitest/snapshot": "1.6.0", + "@vitest/spy": "1.6.0", + "@vitest/utils": "1.6.0", "acorn-walk": "^8.3.2", "chai": "^4.3.10", "debug": "^4.3.4", @@ -9144,7 +9042,7 @@ "tinybench": "^2.5.1", "tinypool": "^0.8.3", "vite": "^5.0.0", - "vite-node": "1.5.2", + "vite-node": "1.6.0", "why-is-node-running": "^2.2.2" }, "bin": { @@ -9159,8 +9057,8 @@ "peerDependencies": { "@edge-runtime/vm": "*", "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "1.5.2", - "@vitest/ui": "1.5.2", + "@vitest/browser": "1.6.0", + "@vitest/ui": "1.6.0", "happy-dom": "*", "jsdom": "*" }, @@ -9186,9 +9084,9 @@ } }, "node_modules/vitest/node_modules/vite-node": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.5.2.tgz", - "integrity": "sha512-Y8p91kz9zU+bWtF7HGt6DVw2JbhyuB2RlZix3FPYAYmUyZ3n7iTp8eSyLyY6sxtPegvxQtmlTMhfPhUfCUF93A==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.0.tgz", + "integrity": "sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==", "dependencies": { "cac": "^6.7.14", "debug": "^4.3.4", @@ -9207,15 +9105,15 @@ } }, "node_modules/vue": { - "version": "3.4.25", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.25.tgz", - "integrity": "sha512-HWyDqoBHMgav/OKiYA2ZQg+kjfMgLt/T0vg4cbIF7JbXAjDexRf5JRg+PWAfrAkSmTd2I8aPSXtooBFWHB98cg==", + "version": "3.4.27", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.27.tgz", + "integrity": "sha512-8s/56uK6r01r1icG/aEOHqyMVxd1bkYcSe9j8HcKtr/xTOFWvnzIVTehNW+5Yt89f+DLBe4A569pnZLS5HzAMA==", "dependencies": { - "@vue/compiler-dom": "3.4.25", - "@vue/compiler-sfc": "3.4.25", - "@vue/runtime-dom": "3.4.25", - "@vue/server-renderer": "3.4.25", - "@vue/shared": "3.4.25" + "@vue/compiler-dom": "3.4.27", + "@vue/compiler-sfc": "3.4.27", + "@vue/runtime-dom": "3.4.27", + "@vue/server-renderer": "3.4.27", + "@vue/shared": "3.4.27" }, "peerDependencies": { "typescript": "*" @@ -9227,9 +9125,9 @@ } }, "node_modules/vue-component-type-helpers": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-2.0.14.tgz", - "integrity": "sha512-DInfgOyXlMyliyqAAD9frK28tTfch0+tMi4qoWJcZlRxUf+NFAtraJBnAsKLep+FOyLMiajkhfyEb3xLK08i7w==" + "version": "2.0.17", + "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-2.0.17.tgz", + "integrity": "sha512-2car49m8ciqg/JjgMBkx7o/Fd2A7fHESxNqL/2vJYFLXm4VwYO4yH0rexOi4a35vwNgDyvt17B07Vj126l9rAQ==" }, "node_modules/vue-eslint-parser": { "version": "9.4.2", @@ -9254,24 +9152,10 @@ "eslint": ">=6.0.0" } }, - "node_modules/vue-eslint-parser/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/vue-eslint-parser/node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "dependencies": { - "lru-cache": "^6.0.0" - }, + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", "bin": { "semver": "bin/semver.js" }, @@ -9279,11 +9163,6 @@ "node": ">=10" } }, - "node_modules/vue-eslint-parser/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, "node_modules/vue-hot-reload-api": { "version": "2.3.4", "resolved": "https://registry.npmjs.org/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz", @@ -9468,6 +9347,14 @@ "node": ">=8" } }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/wrap-ansi": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", @@ -9591,9 +9478,9 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/ws": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", - "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz", + "integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==", "engines": { "node": ">=10.0.0" }, From e7db6df5eacb3cf154aecc622d9c2e642055052a Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Tue, 14 May 2024 07:45:18 +0200 Subject: [PATCH 083/168] Auth: strict same site header (#13896) --- server/http_auth.go | 1 + 1 file changed, 1 insertion(+) diff --git a/server/http_auth.go b/server/http_auth.go index bd12e83494..160bd2ebd2 100644 --- a/server/http_auth.go +++ b/server/http_auth.go @@ -114,6 +114,7 @@ func loginHandler(auth auth.Auth) http.HandlerFunc { Path: "/", HttpOnly: true, Expires: time.Now().Add(lifetime), + SameSite: http.SameSiteStrictMode, }) } } From 142ce3620a0809eb3e3405d031ee7c7202f1fefd Mon Sep 17 00:00:00 2001 From: premultiply <4681172+premultiply@users.noreply.github.com> Date: Tue, 14 May 2024 10:54:38 +0000 Subject: [PATCH 084/168] solax-charger: fix enable --- charger/solax.go | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/charger/solax.go b/charger/solax.go index 2e3cfbd32a..118b553f07 100644 --- a/charger/solax.go +++ b/charger/solax.go @@ -48,6 +48,12 @@ const ( solaxRegActivePower = 0x000B // uint16 1W solaxRegTotalEnergy = 0x0010 // uint32s 0.1kWh solaxRegState = 0x001D // uint16 + + solaxCmdStop = 3 + solaxCmdStart = 4 + + solaxModeStop = 0 + solaxModeFast = 1 ) func init() { @@ -140,18 +146,17 @@ func (wb *Solax) Enabled() (bool, error) { return false, err } - return binary.BigEndian.Uint16(b) != 0, nil + return binary.BigEndian.Uint16(b) != solaxModeStop, nil } // Enable implements the api.Charger interface func (wb *Solax) Enable(enable bool) error { - var mode uint16 = 0 // "STOP" + var cmd uint16 = solaxCmdStop if enable { - mode = 1 // "FAST" + cmd = solaxCmdStart } - _, err := wb.conn.WriteSingleRegister(solaxRegDeviceMode, mode) - + _, err := wb.conn.WriteSingleRegister(solaxRegCommandControl, cmd) return err } From 72ab555412675d97ce23c6e2973deb88a6e36b6e Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Tue, 14 May 2024 20:28:20 +0200 Subject: [PATCH 085/168] Translations update from Hosted Weblate (#13844) * Translated using Weblate (Turkish) Currently translated at 100.0% (421 of 421 strings) Translated using Weblate (Turkish) Currently translated at 100.0% (421 of 421 strings) Translated using Weblate (Turkish) Currently translated at 100.0% (416 of 416 strings) Translated using Weblate (Turkish) Currently translated at 100.0% (416 of 416 strings) Translated using Weblate (Turkish) Currently translated at 99.7% (415 of 416 strings) Translated using Weblate (Turkish) Currently translated at 99.7% (415 of 416 strings) Translated using Weblate (Turkish) Currently translated at 99.7% (415 of 416 strings) Translated using Weblate (Turkish) Currently translated at 99.7% (415 of 416 strings) Co-authored-by: aerucu Translate-URL: https://hosted.weblate.org/projects/evcc/evcc/tr/ Translation: evcc/evcc * Translated using Weblate (French) Currently translated at 100.0% (416 of 416 strings) Co-authored-by: Jonas Translate-URL: https://hosted.weblate.org/projects/evcc/evcc/fr/ Translation: evcc/evcc * Translated using Weblate (Lithuanian) Currently translated at 100.0% (421 of 421 strings) Co-authored-by: RTTTC <94727758+RTTTC@users.noreply.github.com> Translate-URL: https://hosted.weblate.org/projects/evcc/evcc/lt/ Translation: evcc/evcc * fix toml --------- Co-authored-by: aerucu Co-authored-by: Jonas Co-authored-by: RTTTC <94727758+RTTTC@users.noreply.github.com> Co-authored-by: premultiply <4681172+premultiply@users.noreply.github.com> --- i18n/fr.toml | 10 ++++ i18n/lt.toml | 5 ++ i18n/tr.toml | 133 ++++++++++++++++++++++++++------------------------- 3 files changed, 84 insertions(+), 64 deletions(-) diff --git a/i18n/fr.toml b/i18n/fr.toml index a4c465ace2..1431773a5c 100644 --- a/i18n/fr.toml +++ b/i18n/fr.toml @@ -74,6 +74,16 @@ template = "Fabricant" titleChoice = "Que voulez-vous ajouter ?" validateSave = "Valider et enregistrer" +[config.options] + +[config.options.endianness] +big = "gros-boutiste" +little = "petit-boutiste" + +[config.options.schema] +http = "HTTP (non crypté)" +https = "HTTP (crypté)" + [config.pv] titleAdd = "Ajouter un compteur pour le solaire" titleEdit = "Modifier le compteur pour le solaire" diff --git a/i18n/lt.toml b/i18n/lt.toml index 118699e9eb..45b8d63ba6 100644 --- a/i18n/lt.toml +++ b/i18n/lt.toml @@ -493,6 +493,8 @@ vehicle = "Automobilis" [sessions.csv] chargedenergy = "Energija (kWh)" +chargeduration = "Trukmė" +co2perkwh = "CO₂/kWh" created = "Sukurta" finished = "Pabaigta" identifier = "Identifikatorius" @@ -500,6 +502,9 @@ loadpoint = "Įkroviklis" meterstart = "Skaitiklis pradžia (kWh)" meterstop = "Skaitiklis pabaiga (kWh)" odometer = "Odometras (km)" +price = "Kaina" +priceperkwh = "Kaina/kWh" +solarpercentage = "Saulės (%)" vehicle = "Automobilis" [sessions.filter] diff --git a/i18n/tr.toml b/i18n/tr.toml index e0132161ea..6016926e80 100644 --- a/i18n/tr.toml +++ b/i18n/tr.toml @@ -1,19 +1,19 @@ [batterySettings] -batteryLevel = "Batarya seviyesi" -capacity = "{total} içinde {energy}" +batteryLevel = "Doluluk oranı" +capacity = "{total}'in {energy}'si" control = "Batarya kontrolü" -discharge = "Hızlı modda ve planlanan doldurmada boşalmayı önleyin." +discharge = "Hızlı modda ve planlanan doldurmada boşalmayı önle." disclaimerHint = "Not:" -disclaimerText = "Bu ayarlar yalnızca güneş enerjisi modunu etkiler. Doldurma davranışı buna göre ayarlanır." -legendBottomName = "ev önceliği" -legendBottomSubline = "doldurma için kullanılmaz" -legendMiddleName = "önce araç" -legendMiddleSubline = "ev ikincil" -legendTitle = "Güneş enerjisi nasıl kullanılmalıdır?" +disclaimerText = "Bu ayarlar yalnızca güneş enerjisi yöntemini etkiler. Doldurma davranışı buna göre ayarlanır." +legendBottomName = "Evin önceliği var" +legendBottomSubline = "doldurmak için kullanma" +legendMiddleName = "Önce araç" +legendMiddleSubline = "sonra ev" +legendTitle = "Güneş enerjisi nasıl kullanılsın?" legendTopAutostart = "otomatik olarak başlar" -legendTopName = "batarya destekli doldurma" +legendTopName = "Enerji deposu destekli doldurma" legendTopSubline = "kesintisiz" -modalTitle = "Ev bataryası" +modalTitle = "Ev enerji deposu" [batterySettings.bufferStart] above = "{soc} üzerinde olduğunda" @@ -39,8 +39,8 @@ phasePowers = "Faz Gücü L1..L3" phaseVoltages = "Faz Voltajı L1..L3" power = "Güç" range = "Menzil" -soc = "Dolum durumu" -socLimit = "Dolum Sınırlaması" +soc = "Doluluk durumu" +socLimit = "Doluluk Sınırlaması" [config.form] example = "Örnek" @@ -56,7 +56,7 @@ titleEdit = "Elektrik Sayacını Düzenle" [config.main] addLoadpoint = "Doldurma noktası ekle" -addPvBattery = "Güneş enerjisi veya batarya ekle" +addPvBattery = "Güneş enerjisi veya enerji deposu ekle" addVehicle = "Araç ekle" edit = "düzenle" title = "Yapılandırma" @@ -131,7 +131,7 @@ greenEnergy = "Güneş Enerjisi" greenEnergySub1 = "evcc ile dolduruldu" greenEnergySub2 = "Ekim 2022'den beri" greenShare = "Güneş enerjisi payı" -greenShareSub1 = "güneş enerjisi ve batarya deposu" +greenShareSub1 = "güneş enerjisi ve enerji deposu" greenShareSub2 = "tarafından sağlanan enerji" power = "Doldurma gücü" powerSub1 = "{totalClients} katılımcıdan {activeClients} katılımcı" @@ -151,7 +151,7 @@ percentSelf = "{self} kWh güneşden" percentTitle = "Güneş Enerjisi" periodLabel = "Zaman aralığı" priceTitle = "Enerji fiyatı" -referenceGrid = "Şebeke" +referenceGrid = "şebeke" referenceLabel = "Referans verileri:" tabTitle = "Verilerim" @@ -163,8 +163,8 @@ total = "tüm zaman" [footer.sponsor] becomeSponsor = "Destekçi ol" confetti = "Konfeti istermisin?" -confettiPromise = "Stickerler ve dijital konfeti de var" -sticker = "… ya da evcc stickerları?" +confettiPromise = "Çıkartmalar ve dijital konfeti de var" +sticker = "… ya da evcc çıkartmaları?" supportUs = "Hedefimiz güneş enerjisi ile yakıt ikmalini gelenek haline getirmek. Bize yardım et ve evcc'yi maddi olarak destekle." thanks = "Teşekkürler {sponsor}! Katkın evcc'yi daha da geliştirmemize yardımcı oluyor." titleNoSponsor = "Bize destek ol" @@ -191,7 +191,7 @@ modalUpdateStatusStart = "Kurulum başladı:" [header] about = "evcc hakkında" blog = "Blog" -docs = "Belgelendirme" +docs = "Belgeler" github = "GitHub" login = "Araç Girişleri" logout = "Çıkış" @@ -215,7 +215,7 @@ secondaryActions = "Hâlâ bir çözüm bulamadın mı? Burada birkaç seçenek [help.restart] cancel = "İptal" confirm = "Evet, yeniden başlat!" -description = "Normal koşullarda yeniden başlatma gerekli olmamalı. Eğer evcc'yi sürekli olarak yeniden başlatman gerekiyorsa, bir hata bildirimi yap." +description = "Normal koşullarda yeniden başlatma gerekmemeli. Eğer evcc'yi sürekli olarak yeniden başlatman gerekiyorsa, bir hata bildirimi yap." disclaimer = "Not: evcc kendini sonlandıracak ve işletim sistemi tarafindan yeniden başlatılacağına güveniyor." modalTitle = "Yeniden başlatmak istediğine emin misin?" @@ -263,7 +263,7 @@ battery = "Batarya" batteryCharge = "Batarya doldurma" batteryDischarge = "Batarya boşaltma" batteryHold = "Batarya (kilitli)" -batteryTooltip = "{energy} / {total} ({soc})" +batteryTooltip = "{total} ({soc})'ın {energy}'ı" gridImport = "Şebeke kullanımı" homePower = "Tüketim" loadpoints = "Doldurma Cihazı| Doldurma Cihazı | {count} doldurma cihazları" @@ -313,7 +313,7 @@ label = "Asgari Akım" [main.loadpointSettings.minSoc] description = "Araç, güneş enerjisi modunda %{0} seviyesine hızlı doldurulur. Ardından güneş enerjisi fazlalığıyla devam eder. Karanlık havalarda dahi asgari bir menzil sağlamak için kullanışlıdır." -label = "Asgari dolum %" +label = "Asgari dolum oranı" [main.loadpointSettings.phasesConfigured] label = "Fazlar" @@ -340,10 +340,10 @@ co2Limit = "{co2} CO₂ sınırı" costLimitIgnore = "Bu zaman aralığında yapılandırılan {limit} yoksayılacak." currentPlan = "Etkin plan" descriptionEnergy = "{targetEnergy} ne zamana kadar araca doldurulmalı?" -descriptionSoc = "Araç ne zaman %{targetSoc} seviyesine şarj edilmeli?" -inactiveLabel = "Hedef zamanı" -notReachableInTime = "Hedefe zamanında ulaşılamaz. Tahmini bitiş: {endTime}." -onlyInPvMode = "Şarj planı sadece güneş enerjisi modunda çalışır." +descriptionSoc = "Araç ne zaman %{targetSoc} seviyesine doldurulmalı?" +inactiveLabel = "Hedeflenen zaman" +notReachableInTime = "Hedeflenen zamana ulaşılamaz. Tahmini bitiş: {endTime}." +onlyInPvMode = "Doldurma planı sadece güneş enerjisi modunda çalışır." planDuration = "Doldurma süresi" planPeriodLabel = "Zaman aralığı" planPeriodValue = "{start}'dan {end}'a kadar" @@ -352,19 +352,19 @@ preview = "Plan Önizleme" priceLimit = "{price} fiyat sınırı" remove = "Kaldır" setTargetTime = "yok" -targetIsAboveLimit = "Yapılandırılan {limit} seviyesindeki şarj sınırı bu zaman aralığında yoksayılacaktır." -targetIsAboveVehicleLimit = "Şarj hedefine ulaşmak için araç sınırını ({limit}) artır." +targetIsAboveLimit = "Yapılandırılan {limit} seviyesindeki doldurma sınırı bu zaman aralığında yok sayılacaktır." +targetIsAboveVehicleLimit = "Doldurma hedefine ulaşmak için araç sınırını ({limit}) artır." targetIsInThePast = "Gelecekte bir zaman seç, Marty." targetIsTooFarInTheFuture = "Gelecek hakkında daha fazla bilgi edindiğimizde planı uyarlayacağız." -title = "Hedef Zamanı" +title = "Hedeflenen Zaman" today = "bugün" tomorrow = "yarın" update = "Güncelle" vehicleCapacityDocs = "Nasıl yapılandırılacağını öğren." -vehicleCapacityRequired = "Şarj süresini tahmin etmek için araç batarya kapasitesi gereklidir." +vehicleCapacityRequired = "Doldurma süresini tahmin etmek için araç batarya kapasitesi gerekli." [main.targetChargePlan] -chargeDuration = "Şarj süresi" +chargeDuration = "Doldurma süresi" co2Label = "⌀ CO₂ emisyonu" priceLabel = "Enerji fiyatı" timeRange = "{day} {range} saat" @@ -376,20 +376,20 @@ noLimit = "yok" [main.vehicle] addVehicle = "Araç Ekle" -changeVehicle = "Araç değiştir" +changeVehicle = "Araçı değiştir" detectionActive = "Araç algılanıyor…" fallbackName = "Araç" moreActions = "Daha Fazla İşlem" none = "Araç Yok" notReachable = "Araca ulaşılamadı. Evcc'yi yeniden başlatmayı dene." -targetSoc = "Sınır" +targetSoc = "Doldurma Sınırı" temp = "Sıcaklık" -tempLimit = "Sıcaklık sınırı" +tempLimit = "Hedeflenen sıcaklık" unknown = "Misafir araç" -vehicleSoc = "Şarj" +vehicleSoc = "Doluluk seviyesi" [main.vehicleSoc] -charging = "şarj oluyor" +charging = "doluyor" connected = "bağlı" disconnected = "bağlantı kesildi" ready = "hazır" @@ -397,24 +397,24 @@ vehicleLimit = "Araç sınırı: %{soc}" vehicleTarget = "Araç sınırı: {soc}%" [main.vehicleStatus] -charging = "Şarj oluyor…" -cheapEnergyCharging = "Ucuz enerjiyle şarj oluyor: {price} (limit {limit})" -cleanEnergyCharging = "Temiz enerjiyle şarj oluyor: {co2} (sınır {limit})" +charging = "doluyor…" +cheapEnergyCharging = "Ucuz enerjiyle doluyor: {price} (limit {limit})" +cleanEnergyCharging = "Temiz enerjiyle doluyor: {co2} (sınır {limit})" climating = "Ön iklimlendirme algılandı." connected = "Bağlı." disconnected = "Bağlantı kesildi." -minCharge = "Asgari şarj %{soc}." +minCharge = " %{soc} kadar asgari dolum." pvDisable = "Yeterli güneş enerjisi fazlalığı yok. {remaining} içinde duraklatılıyor…" pvEnable = "Güneş enerjisi fazlalığı mevcut. {remaining} içinde başlatılıyor…" -scale1p = "{remaining} içinde 1 fazlı şarja geçiliyor…" -scale3p = "{remaining} içinde 3 fazlı şarja geçiliyor…" -targetChargeActive = "Hedef şarj aktif…" -targetChargePlanned = "Hedef şarj {time} başlıyor." -targetChargeWaitForVehicle = "Hedef şarj hazır. Araç bekleniyor…" +scale1p = "{remaining} içinde 1 faza düşürülüyor…" +scale3p = "{remaining} içinde 3 faza yükseltiliyor…" +targetChargeActive = "Doldurma planı aktif…" +targetChargePlanned = "Doldurma planı saat {time} başlayacak." +targetChargeWaitForVehicle = "Doldurma planı hazır. Araç bekleniyor…" unknown = "" vehicleLimitReached = "%{soc} araç sınırına ulaşıldı." vehicleTargetReached = "Araç sınırı {soc}% ulaşıldı." -waitForVehicle = "Hazır. Araç bekleniyor…" +waitForVehicle = "Doldurmaya hazır. Araç bekleniyor…" [notifications] dismissAll = "Bildirimleri kaldır" @@ -443,14 +443,14 @@ cancel = "İptal" co2 = "CO₂" date = "Zaman aralığı" delete = "Sil" -finished = "Tamamlandı" +finished = "Bitiş zamanı" meter = "Sayaç" meterstart = "Sayaç başlangıcı" meterstop = "Sayaç bitişi" odometer = "Kilometre" price = "Fiyat" -started = "Başladı" -title = "Şarj Oturumu" +started = "Başlama zamanı" +title = "Doldurma Oturumu" [sessions] avgPower = "⌀ Güç" @@ -461,29 +461,34 @@ csvMonth = "{month} CSV olarak indir" csvTotal = "CSV'nin tamamını indir" date = "Başlangıç" downloadCsv = "CSV olarak indir" -energy = "Şarj edilen" -loadpoint = "Şarj Noktası" -noData = "Bu ay henüz şarj oturumu yok." +energy = "Doldurulan" +loadpoint = "Doldurma Noktası" +noData = "Bu ay henüz doldurma oturumu yok." price = "Σ Fiyat" reallyDelete = "Bu oturumu gerçekten silmek istiyor musun?" solar = "Güneş Enerjisi" -title = "Şarj Oturumları" +title = "Doldurma Oturumları" total = "Toplam" vehicle = "Araç" [sessions.csv] chargedenergy = "Enerji (kWh)" +chargeduration = "Doldurma süresi" +co2perkwh = "CO₂/kWh" created = "Başlama zamanı" finished = "Bitiş zamanı" identifier = "Tanımlayıcı" -loadpoint = "Şarj Noktası" +loadpoint = "Doldurma Noktası" meterstart = "Sayaç başlangıcı (kWh)" meterstop = "Sayaç bitişi (kWh)" -odometer = "Kilometre sayacı (km)" +odometer = "Kilometre (km)" +price = "Fiyat" +priceperkwh = "Fiyat/kWh" +solarpercentage = "Güneş (%)" vehicle = "Araç" [sessions.filter] -allLoadpoints = "tüm şarj noktaları" +allLoadpoints = "tüm doldurma noktaları" allVehicles = "Tüm araçlar" filter = "Filtrele" @@ -504,12 +509,12 @@ auto = "Otomatik" label = "Dil" [settings.sponsorToken] -expires = "Sponsor jetonun {inXDays} sonra sona erecek. {getNewToken} ve yapılandırma dosyanızı güncelle." +expires = "Sponsor jetonun {inXDays} sonra sona erecek. {getNewToken} ve yapılandırma dosyanı güncelle." getNew = "Yeni bir tane al" hint = "Not: İleride bunu otomatik hale getireceğiz." [settings.telemetry] -label = "Telemetri" +label = "Uzölçüm" [settings.theme] auto = "Sistem" @@ -526,13 +531,13 @@ mi = "Mil" activeHours = "{total} saat içinde {charging}" activeHoursLabel = "Aktif saatler" applyToAll = "Heryerde uygulansın mı?" -batteryDescription = "Ev bataryasını şebekeden şarj eder." -cheapTitle = "Ucuz Şebeke Şarjı" -cleanTitle = "Temiz Şebeke Şarjı" +batteryDescription = "Ev enerji deposunu şebekeden doldurur." +cheapTitle = "Ucuz Şebeke Doldurması" +cleanTitle = "Temiz Şebeke Doldurması" co2Label = "CO₂ emisyonu" co2Limit = "CO₂ sınırı" -loadpointDescription = "Güneş enerjisi modunda hızlı şarjı geçici olarak etkinleştirir." -modalTitle = "Akıllı Şebeke Şarjı" +loadpointDescription = "Güneş enerjisi modunda hızlı doldurmayı geçici olarak etkinleştirir." +modalTitle = "Akıllı Şebeke Doldurması" none = "hiçbiri" priceLabel = "Enerji fiyatı" priceLimit = "Fiyat sınırı" @@ -544,7 +549,7 @@ configuration = "Yapılandırma" description = "Lütfen yapılandırma dosyanızı kontrol et. Hata mesajı yardımcı olmuyorsa, çözüm için {0}'e göz at." discussions = "GitHub Tartışmaları" fixAndRestart = "Lütfen sorunu düzelt ve ana makineyi yeniden başlat." -hint = "Not: Ayrıca hatalı bir cihaz da (inverter, sayaç, …) sebeb olabilir. Ağ bağlantılarını gözden geçir." +hint = "Not: Ayrıca hatalı bir cihaz da (güç çevirici, sayaç, …) sebeb olabilir. Ağ bağlantılarını gözden geçir." lineError = "{0} içinde hata bulundu." lineErrorLink = "{0}. satır" restartButton = "Yeniden Başlat" From f69a0d796c15851cf3b55339054705cc0d721297 Mon Sep 17 00:00:00 2001 From: andig Date: Wed, 15 May 2024 21:40:13 +0200 Subject: [PATCH 086/168] Push: fix missing template variables (#13917) --- push/hub.go | 10 +++++----- util/format.go | 25 ++++++++++++++----------- util/format_test.go | 1 + 3 files changed, 20 insertions(+), 16 deletions(-) diff --git a/push/hub.go b/push/hub.go index 9c5150eab6..61f858fddb 100644 --- a/push/hub.go +++ b/push/hub.go @@ -124,12 +124,12 @@ func (h *Hub) Run(events <-chan Event, valueChan chan util.Param) { continue } + if strings.TrimSpace(msg) == "" { + continue + } + for _, sender := range h.sender { - if strings.TrimSpace(msg) != "" { - go sender.Send(title, msg) - } else { - log.DEBUG.Printf("did not send empty message template for %s: %v", ev.Event, err) - } + go sender.Send(title, msg) } } } diff --git a/util/format.go b/util/format.go index 8f1cb5dc6a..4a26739b2e 100644 --- a/util/format.go +++ b/util/format.go @@ -9,6 +9,7 @@ import ( "time" "github.com/42atomys/sprout" + "golang.org/x/exp/maps" ) var re = regexp.MustCompile(`(?i)\${(\w+)(:([a-zA-Z0-9%.]+))?}`) @@ -73,27 +74,29 @@ func ReplaceFormatted(s string, kv map[string]interface{}) (string, error) { match, key, format := m[0], m[1], m[3] // find key and replacement value - val, ok := kv[strings.ToLower(key)] - if !ok { + var val *any + for k, v := range kv { + if strings.EqualFold(k, key) { + val = &v + break + } + } + + if val == nil { wanted = append(wanted, key) format = "%s" - val = "?" + val = PtrTo(any("?")) } // update all literal matches - new := FormatValue(format, val) + new := FormatValue(format, *val) s = strings.ReplaceAll(s, match, new) } // return missing keys if len(wanted) > 0 { - got := make([]string, 0) - for k := range kv { - got = append(got, k) - } - - err = fmt.Errorf("wanted: %v, got: %v", wanted, got) + return "", fmt.Errorf("wanted: %v, got: %v", wanted, maps.Keys(kv)) } - return s, err + return s, nil } diff --git a/util/format_test.go b/util/format_test.go index da21fdcd39..561da2bc37 100644 --- a/util/format_test.go +++ b/util/format_test.go @@ -37,6 +37,7 @@ func TestReplace(t *testing.T) { // regex tests {"foo", true, "${foo}", "true"}, {"foo", true, "${Foo}", "true"}, + {"Foo", true, "${foo}", "true"}, {"foo", "1", "abc${foo}${foo}", "abc11"}, {"foo", math.Pi, "${foo:%.2f}", "3.14"}, {"foo", math.Pi, "${foo:%.0f}%", "3%"}, From 606cd8f05d03e6b220f79cdd91ce6ae7cae5722e Mon Sep 17 00:00:00 2001 From: andig Date: Wed, 15 May 2024 21:46:35 +0200 Subject: [PATCH 087/168] chore: fix build --- .golangci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.golangci.yml b/.golangci.yml index 6369a2f2d0..ee6d9976f1 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -16,7 +16,6 @@ linters: enable: - dogsled - durationcheck - - exportloopref - gci - gofmt - goimports @@ -40,6 +39,7 @@ linters: # fixme # - bodyclose # - exhaustive + # - exportloopref # - gocritic # - godot # - gomoddirectives From c3b48b070eeeb312882391dd64d204a3373eed66 Mon Sep 17 00:00:00 2001 From: andig Date: Wed, 15 May 2024 21:53:15 +0200 Subject: [PATCH 088/168] Mercedes: use user instead of account --- vehicle/mercedes.go | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/vehicle/mercedes.go b/vehicle/mercedes.go index 805fba205d..cfb461aef6 100644 --- a/vehicle/mercedes.go +++ b/vehicle/mercedes.go @@ -21,12 +21,13 @@ func init() { // NewMercedesFromConfig creates a new vehicle func NewMercedesFromConfig(other map[string]interface{}) (api.Vehicle, error) { cc := struct { - embed `mapstructure:",squash"` - Tokens Tokens - Account string - VIN string - Cache time.Duration - Region string + embed `mapstructure:",squash"` + Tokens Tokens + User string + Account_ string `mapstructure:"account"` // TODO deprecated + VIN string + Cache time.Duration + Region string }{ Cache: interval, } @@ -40,8 +41,12 @@ func NewMercedesFromConfig(other map[string]interface{}) (api.Vehicle, error) { return nil, err } + if cc.User == "" && cc.Account_ != "" { + cc.User = cc.Account_ + } + log := util.NewLogger("mercedes").Redact(cc.Tokens.Access, cc.Tokens.Refresh) - identity, err := mercedes.NewIdentity(log, token, cc.Account, cc.Region) + identity, err := mercedes.NewIdentity(log, token, cc.User, cc.Region) if err != nil { return nil, err } From a50b421906342a49da9861d83202118a2ba4273c Mon Sep 17 00:00:00 2001 From: Martin Buchleitner Date: Wed, 15 May 2024 21:57:04 +0200 Subject: [PATCH 089/168] Wattpilot: fix reconnect issues (#13912) --- go.mod | 5 +---- go.sum | 10 ++-------- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index ef5dc3f569..afac8fe042 100644 --- a/go.mod +++ b/go.mod @@ -60,7 +60,7 @@ require ( github.com/libp2p/zeroconf/v2 v2.2.0 github.com/lorenzodonini/ocpp-go v0.18.0 github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 - github.com/mabunixda/wattpilot v1.7.0 + github.com/mabunixda/wattpilot v1.7.2 github.com/mitchellh/go-homedir v1.1.0 github.com/mitchellh/mapstructure v1.5.0 github.com/mlnoga/rct v0.1.2-0.20240421173556-1c5b75037e2f @@ -128,9 +128,6 @@ require ( github.com/go-logfmt/logfmt v0.6.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/gobwas/httphead v0.1.0 // indirect - github.com/gobwas/pool v0.2.1 // indirect - github.com/gobwas/ws v1.4.0 // indirect github.com/golang/mock v1.6.0 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/gorilla/websocket v1.5.1 // indirect diff --git a/go.sum b/go.sum index 40f5108547..254b1776fd 100644 --- a/go.sum +++ b/go.sum @@ -194,12 +194,6 @@ github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg= github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/goburrow/serial v0.1.0 h1:v2T1SQa/dlUqQiYIT8+Cu7YolfqAi3K96UmhwYyuSrA= github.com/goburrow/serial v0.1.0/go.mod h1:sAiqG0nRVswsm1C97xsttiYCzSLBmUZ/VSlVLZJ8haA= -github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= -github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= -github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= -github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= -github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs= -github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc= github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= @@ -397,8 +391,8 @@ github.com/lorenzodonini/ocpp-go v0.18.0/go.mod h1:ZynYDWGw6CslG3vyPuucLsy6AyE+h github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 h1:EnfXoSqDfSNJv0VBNqY/88RNnhSGYkrHaO0mmFGbVsc= github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40/go.mod h1:vy1vK6wD6j7xX6O6hXe621WabdtNkou2h7uRtTfRMyg= github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= -github.com/mabunixda/wattpilot v1.7.0 h1:dLbsfsZi8+H4m6DuQUQzCAmLqVbZOMHn+m31zi8OmWc= -github.com/mabunixda/wattpilot v1.7.0/go.mod h1:cfndLU/u8ANvy/HKNrT4ShsaNhPhIHlCRrDY8SyETFA= +github.com/mabunixda/wattpilot v1.7.2 h1:DZcyZSJvu3YTVhbQSKnr4w1T4zL3krKourKAekRaevA= +github.com/mabunixda/wattpilot v1.7.2/go.mod h1:9Gre9Jt8rVak0snzzlwLHCLxGfz36EmJ98fhgJ+jYkw= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= From d4c6ab7eb59788e1765e36ea2edd0d7970c7b45c Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Wed, 15 May 2024 21:59:25 +0200 Subject: [PATCH 090/168] UI: higher precision battery icon (#13909) --- .../js/components/Energyflow/BatteryIcon.vue | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/assets/js/components/Energyflow/BatteryIcon.vue b/assets/js/components/Energyflow/BatteryIcon.vue index c3199d8c45..cc961ec684 100644 --- a/assets/js/components/Energyflow/BatteryIcon.vue +++ b/assets/js/components/Energyflow/BatteryIcon.vue @@ -1,13 +1,15 @@ From 940ce81a84c3d7dbb5012a031083252788c56b17 Mon Sep 17 00:00:00 2001 From: andig Date: Wed, 15 May 2024 22:14:44 +0200 Subject: [PATCH 091/168] chore: upgrade go-sprout (#13841) --- cmd/configure/configure.go | 2 +- cmd/dump.go | 2 +- cmd/eebus.go | 2 +- go.mod | 4 +--- go.sum | 8 ++------ provider/http.go | 2 +- push/hub.go | 2 +- util/format.go | 2 +- util/templates/documentation.go | 2 +- util/templates/template.go | 2 +- util/templates/utils.go | 10 +--------- 11 files changed, 12 insertions(+), 26 deletions(-) diff --git a/cmd/configure/configure.go b/cmd/configure/configure.go index e824d4acc2..97f94f0667 100644 --- a/cmd/configure/configure.go +++ b/cmd/configure/configure.go @@ -5,8 +5,8 @@ import ( _ "embed" "text/template" - "github.com/42atomys/sprout" "github.com/evcc-io/evcc/util/templates" + "github.com/go-sprout/sprout" ) type device struct { diff --git a/cmd/dump.go b/cmd/dump.go index d346e54835..1c8253c53c 100644 --- a/cmd/dump.go +++ b/cmd/dump.go @@ -8,10 +8,10 @@ import ( "path/filepath" "text/template" - "github.com/42atomys/sprout" "github.com/evcc-io/evcc/core" "github.com/evcc-io/evcc/server" "github.com/evcc-io/evcc/util/config" + "github.com/go-sprout/sprout" "github.com/spf13/cobra" ) diff --git a/cmd/eebus.go b/cmd/eebus.go index a7940812b3..51977deb93 100644 --- a/cmd/eebus.go +++ b/cmd/eebus.go @@ -4,8 +4,8 @@ import ( "os" "text/template" - "github.com/42atomys/sprout" "github.com/evcc-io/evcc/charger/eebus" + "github.com/go-sprout/sprout" "github.com/spf13/cobra" ) diff --git a/go.mod b/go.mod index afac8fe042..e074ef66ab 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,6 @@ go 1.22.0 require ( dario.cat/mergo v1.0.0 - github.com/42atomys/sprout v0.2.0 github.com/AlecAivazis/survey/v2 v2.3.7 github.com/BurntSushi/toml v1.3.2 github.com/PuerkitoBio/goquery v1.9.2 @@ -32,6 +31,7 @@ require ( github.com/glebarez/sqlite v1.11.0 github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1 github.com/go-playground/validator/v10 v10.20.0 + github.com/go-sprout/sprout v0.3.1-0.20240510210334-9d4a544518d7 github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 github.com/godbus/dbus/v5 v5.1.0 github.com/gokrazy/updater v0.0.0-20240113102150-4ac511a17e33 @@ -134,7 +134,6 @@ require ( github.com/grid-x/serial v0.0.0-20211107191517-583c7356b3aa // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/holoplot/go-avahi v1.0.1 // indirect - github.com/huandu/xstrings v1.4.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf // indirect github.com/insomniacslk/xjson v0.0.0-20240314172816-ab1449dc107f // indirect @@ -171,7 +170,6 @@ require ( github.com/rogpeppe/go-internal v1.12.0 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect - github.com/shopspring/decimal v1.4.0 // indirect github.com/sirupsen/logrus v1.9.3 github.com/sourcegraph/conc v0.3.0 // indirect github.com/spali/go-slicereader v0.0.0-20201122145524-8e262e1a5127 // indirect diff --git a/go.sum b/go.sum index 254b1776fd..c5589537f8 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,6 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= -github.com/42atomys/sprout v0.2.0 h1:Fe8Wkc4xOI/z1pwaJffG2sElA+TtH2zgQGnvNNPbU8Q= -github.com/42atomys/sprout v0.2.0/go.mod h1:nTr5KxiTLYrnmEiZaJLuD/Ct+xod3KpHBmqhaZjQ1Lk= github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ= github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= @@ -183,6 +181,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/go-sprout/sprout v0.3.1-0.20240510210334-9d4a544518d7 h1:1g2VKjGbRV/da+uAyd4WMB4XYXTRImXXFNQlt5mxOzU= +github.com/go-sprout/sprout v0.3.1-0.20240510210334-9d4a544518d7/go.mod h1:BG7Zrds7XG7VZvLAkiT3pOK9rLQ6HSJIB4lAXzLdtaA= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= @@ -306,8 +306,6 @@ github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4Dvx github.com/holoplot/go-avahi v1.0.1 h1:XcqR2keL4qWRnlxHD5CAOdWpLFZJ+EOUK0vEuylfvvk= github.com/holoplot/go-avahi v1.0.1/go.mod h1:qH5psEKb0DK+BRplMfc+RY4VMOlbf6mqfxgpMy6aP0M= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU= -github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -582,8 +580,6 @@ github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA= github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= -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= github.com/simonvetter/modbus v1.6.0 h1:RDHJevtc7LDIVoHAbhDun8fy+QwnGe+ZU+sLm9ZZzjc= github.com/simonvetter/modbus v1.6.0/go.mod h1:hh90ZaTaPLcK2REj6/fpTbiV0J6S7GWmd8q+GVRObPw= diff --git a/provider/http.go b/provider/http.go index a8c1a190fa..ae1203181b 100644 --- a/provider/http.go +++ b/provider/http.go @@ -9,11 +9,11 @@ import ( "text/template" "time" - "github.com/42atomys/sprout" "github.com/evcc-io/evcc/provider/pipeline" "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/request" "github.com/evcc-io/evcc/util/transport" + "github.com/go-sprout/sprout" "github.com/gregjones/httpcache" "github.com/jpfielding/go-http-digest/pkg/digest" ) diff --git a/push/hub.go b/push/hub.go index 61f858fddb..1640dc6d14 100644 --- a/push/hub.go +++ b/push/hub.go @@ -5,9 +5,9 @@ import ( "strings" "text/template" - "github.com/42atomys/sprout" "github.com/evcc-io/evcc/core/vehicle" "github.com/evcc-io/evcc/util" + "github.com/go-sprout/sprout" ) // Event is a notification event diff --git a/util/format.go b/util/format.go index 4a26739b2e..30938b35a7 100644 --- a/util/format.go +++ b/util/format.go @@ -8,7 +8,7 @@ import ( "strings" "time" - "github.com/42atomys/sprout" + "github.com/go-sprout/sprout" "golang.org/x/exp/maps" ) diff --git a/util/templates/documentation.go b/util/templates/documentation.go index 1e636008bc..b1d248298a 100644 --- a/util/templates/documentation.go +++ b/util/templates/documentation.go @@ -7,7 +7,7 @@ import ( "strings" "text/template" - "github.com/42atomys/sprout" + "github.com/go-sprout/sprout" ) //go:embed documentation.tpl diff --git a/util/templates/template.go b/util/templates/template.go index b3fbffddd5..3db54350e0 100644 --- a/util/templates/template.go +++ b/util/templates/template.go @@ -9,8 +9,8 @@ import ( "strings" "text/template" - "github.com/42atomys/sprout" "github.com/evcc-io/evcc/util" + "github.com/go-sprout/sprout" ) // Template describes is a proxy device for use with cli and automated testing diff --git a/util/templates/utils.go b/util/templates/utils.go index 51d0082643..36b5a73c5d 100644 --- a/util/templates/utils.go +++ b/util/templates/utils.go @@ -6,9 +6,8 @@ import ( "net/url" "strings" "text/template" - "time" - "github.com/42atomys/sprout" + "github.com/go-sprout/sprout" "gopkg.in/yaml.v3" ) @@ -58,13 +57,6 @@ func FuncMap(tmpl *template.Template) *template.Template { return buf.String(), nil }, "urlEncode": url.QueryEscape, - "toDuration": func(v string) time.Duration { - d, err := time.ParseDuration(v) - if err != nil { - panic(err) - } - return d - }, } return tmpl.Funcs(sprout.TxtFuncMap()).Funcs(funcMap) From 2e643611600fae693466b44537d9665f53668456 Mon Sep 17 00:00:00 2001 From: andig Date: Thu, 16 May 2024 07:34:56 +0200 Subject: [PATCH 092/168] Ford: fix auth api (#13866) --- templates/definition/vehicle/ford.yaml | 4 +++ vehicle/ford.go | 6 ++-- vehicle/ford/identity.go | 41 ++++++++++++++++---------- 3 files changed, 33 insertions(+), 18 deletions(-) diff --git a/templates/definition/vehicle/ford.yaml b/templates/definition/vehicle/ford.yaml index d750004890..8c1f7213de 100644 --- a/templates/definition/vehicle/ford.yaml +++ b/templates/definition/vehicle/ford.yaml @@ -6,7 +6,11 @@ params: - preset: vehicle-identify - name: vin example: WF0FXX... + - name: domain + default: com + validvalues: ["de", "com"] render: | type: ford {{ include "vehicle-base" . }} {{ include "vehicle-identify" . }} + domain: {{ .domain }} diff --git a/vehicle/ford.go b/vehicle/ford.go index cee6f4fd52..77ed1892d2 100644 --- a/vehicle/ford.go +++ b/vehicle/ford.go @@ -29,9 +29,11 @@ func NewFordFromConfig(other map[string]interface{}) (api.Vehicle, error) { cc := struct { embed `mapstructure:",squash"` User, Password, VIN string + Domain string Cache time.Duration }{ - Cache: interval, + Domain: "com", + Cache: interval, } if err := util.DecodeOther(other, &cc); err != nil { @@ -47,7 +49,7 @@ func NewFordFromConfig(other map[string]interface{}) (api.Vehicle, error) { } log := util.NewLogger("ford").Redact(cc.User, cc.Password, cc.VIN) - identity := ford.NewIdentity(log, cc.User, cc.Password) + identity := ford.NewIdentity(log, cc.User, cc.Password, cc.Domain) err := identity.Login() if err != nil { diff --git a/vehicle/ford/identity.go b/vehicle/ford/identity.go index c096260822..a9712dc656 100644 --- a/vehicle/ford/identity.go +++ b/vehicle/ford/identity.go @@ -20,7 +20,7 @@ import ( const ( TokenURI = "https://api.mps.ford.com" - LoginUri = "https://login.ford.com/4566605f-43a7-400a-946e-89cc9fdb0bd7/B2C_1A_SignInSignUp_de-DE" + LoginUriTmpl = "https://login.ford.%s/4566605f-43a7-400a-946e-89cc9fdb0bd7/B2C_1A_SignInSignUp_de-DE" ClientID = "09852200-05fd-41f6-8c21-d36d3497dc64" ApplicationID = "1E8C7794-FF5F-49BC-9596-A1E0C86C5B19" ) @@ -31,14 +31,21 @@ var loginHeaders = map[string]string{ "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Mobile/15E148 Safari/604.1", } -var OAuth2Config = &oauth2.Config{ - ClientID: ClientID, - Endpoint: oauth2.Endpoint{ - AuthURL: fmt.Sprintf("%s/oauth2/v2.0/authorize", LoginUri), - TokenURL: fmt.Sprintf("%s/oauth2/v2.0/token", LoginUri), - }, - RedirectURL: "fordapp://userauthorized", - Scopes: []string{ClientID, "openid"}, +func NewOauth2Config(domain string) *oauth2.Config { + uri := loginUri(domain) + return &oauth2.Config{ + ClientID: ClientID, + Endpoint: oauth2.Endpoint{ + AuthURL: fmt.Sprintf("%s/oauth2/v2.0/authorize", uri), + TokenURL: fmt.Sprintf("%s/oauth2/v2.0/token", uri), + }, + RedirectURL: "fordapp://userauthorized", + Scopes: []string{ClientID, "openid"}, + } +} + +func loginUri(domain string) string { + return fmt.Sprintf(LoginUriTmpl, domain) } type Settings struct { @@ -48,16 +55,17 @@ type Settings struct { type Identity struct { *request.Helper - user, password string + user, password, domain string oauth2.TokenSource } // NewIdentity creates Ford identity -func NewIdentity(log *util.Logger, user, password string) *Identity { +func NewIdentity(log *util.Logger, user, password, domain string) *Identity { return &Identity{ Helper: request.NewHelper(log), user: user, password: password, + domain: domain, } } @@ -72,10 +80,11 @@ func (v *Identity) Login() error { // login authenticates with username/password to get new token func (v *Identity) login() (*oauth.Token, error) { - cv := oauth2.GenerateVerifier() + oc := NewOauth2Config(v.domain) + cv := oauth2.GenerateVerifier() state := lo.RandomString(16, lo.AlphanumericCharset) - uri := OAuth2Config.AuthCodeURL(state, oauth2.S256ChallengeOption(cv), + uri := oc.AuthCodeURL(state, oauth2.S256ChallengeOption(cv), oauth2.SetAuthURLParam("max_age", "3600"), oauth2.SetAuthURLParam("ui_locales", "de-DE"), oauth2.SetAuthURLParam("language_code", "de-DE"), @@ -121,7 +130,7 @@ func (v *Identity) login() (*oauth.Token, error) { } defer func() { v.Client.CheckRedirect = nil }() - uri2 := fmt.Sprintf("%s/SelfAsserted?tx=%s&p=B2C_1A_SignInSignUp_de-DE", LoginUri, settings.TransId) + uri2 := fmt.Sprintf("%s/SelfAsserted?tx=%s&p=B2C_1A_SignInSignUp_de-DE", loginUri(v.domain), settings.TransId) req, err = request.New(http.MethodPost, uri2, strings.NewReader(data.Encode()), request.URLEncoding, map[string]string{ "Accept": "*/*", "Accept-Language": "en-us", @@ -142,7 +151,7 @@ func (v *Identity) login() (*oauth.Token, error) { } } - uri3 := fmt.Sprintf("%s/api/CombinedSigninAndSignup/confirmed?rememberMe=false&csrf_token=%s&tx=%s&p=B2C_1A_SignInSignUp_de-DE", LoginUri, settings.Csrf, settings.TransId) + uri3 := fmt.Sprintf("%s/api/CombinedSigninAndSignup/confirmed?rememberMe=false&csrf_token=%s&tx=%s&p=B2C_1A_SignInSignUp_de-DE", loginUri(v.domain), settings.Csrf, settings.TransId) req, err = request.New(http.MethodGet, uri3, nil, request.URLEncoding, map[string]string{ "Origin": "https://login.ford.com", "Referer": uri, @@ -173,7 +182,7 @@ func (v *Identity) login() (*oauth.Token, error) { return nil, errors.New("could not obtain auth code- check user and password") } - tok, err := OAuth2Config.Exchange(ctx, code, oauth2.VerifierOption(cv)) + tok, err := oc.Exchange(ctx, code, oauth2.VerifierOption(cv)) // exchange code for api token var token oauth.Token From 03120d4e0dfcb216c365999a423ddb684561a4ad Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Thu, 16 May 2024 21:56:16 +0200 Subject: [PATCH 093/168] UI: battery color dark mode (#13937) --- assets/js/components/Energyflow/BatteryIcon.vue | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/assets/js/components/Energyflow/BatteryIcon.vue b/assets/js/components/Energyflow/BatteryIcon.vue index cc961ec684..c216399e87 100644 --- a/assets/js/components/Energyflow/BatteryIcon.vue +++ b/assets/js/components/Energyflow/BatteryIcon.vue @@ -2,9 +2,10 @@ - + From 0cb8ca0599d0950113fa0a759b9d929fdc928c54 Mon Sep 17 00:00:00 2001 From: FraBoCH Date: Thu, 16 May 2024 19:56:29 +0000 Subject: [PATCH 094/168] Translated using Weblate (French) Currently translated at 100.0% (421 of 421 strings) Co-authored-by: FraBoCH Translate-URL: https://hosted.weblate.org/projects/evcc/evcc/fr/ Translation: evcc/evcc --- i18n/fr.toml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/i18n/fr.toml b/i18n/fr.toml index 1431773a5c..be9f758042 100644 --- a/i18n/fr.toml +++ b/i18n/fr.toml @@ -498,6 +498,11 @@ meterstart = "Compteur début (kWh)" meterstop = "Compteur fin (kWh)" odometer = "Kilométrage (km)" vehicle = "Véhicule" +chargeduration = Durée +co2perkwh = "CO₂/kWh" +priceperkwh = Prix/kWh +price = Prix +solarpercentage = % solaire [sessions.filter] allLoadpoints = "tous les points de chargement" From 2056bd7a63c4818629fb98757fd5af9051d90c38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=BDiga=20Deisinger?= Date: Thu, 16 May 2024 19:56:30 +0000 Subject: [PATCH 095/168] Translated using Weblate (Slovenian) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (421 of 421 strings) Co-authored-by: Žiga Deisinger Translate-URL: https://hosted.weblate.org/projects/evcc/evcc/sl/ Translation: evcc/evcc --- i18n/sl.toml | 50 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/i18n/sl.toml b/i18n/sl.toml index 7a101a622d..41ee02b288 100644 --- a/i18n/sl.toml +++ b/i18n/sl.toml @@ -13,7 +13,7 @@ legendTitle = "Kako naj se uporablja sončna energija?" legendTopAutostart = "samodejno se zažene" legendTopName = "polnjenje, ki ga podpira baterija" legendTopSubline = "brez prekinitev" -modalTitle = "Nastavitve baterije" +modalTitle = "Domača baterija" [batterySettings.bufferStart] above = "ko je nad {soc}" @@ -200,6 +200,8 @@ primaryActions = "Nekaj ne deluje tako, kot bi moralo? Na teh mestih lahko poiš restartButton = "Ponovni zagon" restartDescription = "Ste poskusili izklopiti in znova vklopiti?" secondaryActions = "Še vedno ne morete rešiti svoje težave? Tukaj je nekaj zahtevnejših možnosti." +logsButton = "Prikaži loge" +logsDescription = "Preveri loge za napake." [help.restart] cancel = "Prekliči" @@ -215,6 +217,7 @@ invalid = "Geslo ni veljavno." login = "Prijava" password = "Geslo" title = "Avtentikacija" +reset = "Ponastavi geslo?" [main] vehicles = "Parkiranje" @@ -396,6 +399,7 @@ waitForVehicle = "Pripravljen. Čakam na vozilo..." [notifications] dismissAll = "Opusti vse" modalTitle = "Obvestila" +logs = "Prikaži celotne loge" [offline] message = "Ni povezan s strežnikom." @@ -457,6 +461,11 @@ meterstart = "Začetek merjenja (kWh)" meterstop = "Konec merjenja (kWh)" odometer = "Prevoženi kilometri (km)" vehicle = "Vozilo" +chargeduration = "Trajanje" +co2perkwh = "CO₂/kWh" +priceperkwh = "Cena/kWh" +price = "Cena" +solarpercentage = "Sončna energija (%)" [sessions.filter] allLoadpoints = "vsa polnilna mesta" @@ -464,7 +473,7 @@ allVehicles = "vsa vozila" filter = "Filter" [settings] -title = "Splošne nastavitve" +title = "Uporabniški vmesnik" [settings.gridDetails] co2 = "CO₂" @@ -531,3 +540,40 @@ title = "Napaka pri zagonu" cancel = "Prekliči" template = "Proizvajalec" test = "Test" + + +[config.system] +restart = "Ponovni zagon" +restartRequiredDescription = "Prosimo, ponovno zaženite, da vidite učinek." +restartRequiredMessage = "Konfiguracija je bila spremenjena." +restartingDescription = "Prosim počakajte..." +restartingMessage = "Ponovni zagon evcc." +logs = "Logi" + +[log] +showAll = "Pokaži vse vnose" +areaLabel = "Filtriraj glede na območje" +areas = "Vsa območja" +download = "Prenesi vse loge" +levelLabel = "Filtriraj po ravni logov" +noResults = "Ni ujemajočih vnosov v logih." +search = "Iskanje" +title = "Logi" +update = "Samodejno posodabljanje" + +[config.options.endianness] +big = "big-endian" +little = "little-endian" + +[config.options.schema] +http = "HTTP (nešifrirano)" +https = "HTTPS (šifrirano)" + +[config.section] +general = "Splošno" +system = "Sistem" + +[settings.fullscreen] +enter = "Vstopi v celozaslonski način" +exit = "Izhod iz celozaslonskega načina" +label = "Celozaslonski način" \ No newline at end of file From d2293ae0d18201fd547837dabe5469367aeec5a3 Mon Sep 17 00:00:00 2001 From: gallegonovato Date: Thu, 16 May 2024 19:56:31 +0000 Subject: [PATCH 096/168] Translated using Weblate (Spanish) Currently translated at 100.0% (421 of 421 strings) Co-authored-by: gallegonovato Translate-URL: https://hosted.weblate.org/projects/evcc/evcc/es/ Translation: evcc/evcc --- i18n/es.toml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/i18n/es.toml b/i18n/es.toml index 2282c872a5..a3ea131de0 100644 --- a/i18n/es.toml +++ b/i18n/es.toml @@ -501,6 +501,11 @@ meterstart = "Inicio de metro (kWh)" meterstop = "Parón de metro (kWh)" odometer = "Kilometraje (km)" vehicle = "Vehículo" +co2perkwh = "CO₂/kWh" +chargeduration = «Duración» +price = "Precio" +priceperkwh = "Precio/kWh" +solarpercentage = "Solar (%)" [sessions.filter] allLoadpoints = "todos los puntos de recarga" From 3880d23d96ecf65267603d9285599b3a8b14576f Mon Sep 17 00:00:00 2001 From: amtssp Date: Thu, 16 May 2024 19:56:32 +0000 Subject: [PATCH 097/168] Translated using Weblate (Danish) Currently translated at 100.0% (421 of 421 strings) Co-authored-by: amtssp Translate-URL: https://hosted.weblate.org/projects/evcc/evcc/da/ Translation: evcc/evcc --- i18n/da.toml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/i18n/da.toml b/i18n/da.toml index 15add36079..f66af98bcd 100644 --- a/i18n/da.toml +++ b/i18n/da.toml @@ -486,6 +486,11 @@ meterstart = "Målerstart (kWh)" meterstop = "Målerstop (kWh)" odometer = "Kilometertal (km)" vehicle = "Køretøj" +chargeduration = "Varighed" +co2perkwh = "CO₂/kWh" +price = "Pris" +priceperkwh = "Pris/kWh" +solarpercentage = "Sol (%)" [sessions.filter] allLoadpoints = "alle ladepunkter" @@ -559,3 +564,12 @@ title = "Fejl ved opstart" cancel = "Afbryd" template = "Producent" test = "Test" + + +[config.options.endianness] +big = "big-endian" +little = "little-endian" + +[config.options.schema] +http = "HTTP (ikke-krypteret)" +https = "HTTPS (krypteret)" \ No newline at end of file From 71dc77e3f940c28d4f9453ffc0c8e121f56c6fde Mon Sep 17 00:00:00 2001 From: aerucu Date: Thu, 16 May 2024 19:56:33 +0000 Subject: [PATCH 098/168] Translated using Weblate (Turkish) Currently translated at 100.0% (421 of 421 strings) Co-authored-by: aerucu Translate-URL: https://hosted.weblate.org/projects/evcc/evcc/tr/ Translation: evcc/evcc --- i18n/tr.toml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/i18n/tr.toml b/i18n/tr.toml index 6016926e80..15ce07d82b 100644 --- a/i18n/tr.toml +++ b/i18n/tr.toml @@ -83,8 +83,8 @@ http = "HTTP (şifrelenmemiş)" https = "HTTPS (şifrelenmiş)" [config.pv] -titleAdd = "Güneş Enerjisi (GE) Sayacı Ekle" -titleEdit = "Güneş Enerjisi (GE) Sayacını Düzenle" +titleAdd = "GES Sayacı Ekle" +titleEdit = "GES Sayacını Düzenle" [config.section] general = "Genel" @@ -325,10 +325,10 @@ phases_3 = "3 fazlı" phases_3_hint = "({min}'dan {max}'a kadar)" [main.mode] -minpv = "Asg.+Güneş" +minpv = "Asg.+GES" now = "Hızlı" off = "Kapalı" -pv = "Güneş" +pv = "GES" [main.provider] login = "giriş yap" @@ -404,8 +404,8 @@ climating = "Ön iklimlendirme algılandı." connected = "Bağlı." disconnected = "Bağlantı kesildi." minCharge = " %{soc} kadar asgari dolum." -pvDisable = "Yeterli güneş enerjisi fazlalığı yok. {remaining} içinde duraklatılıyor…" -pvEnable = "Güneş enerjisi fazlalığı mevcut. {remaining} içinde başlatılıyor…" +pvDisable = "Yeterli fazlalık yok. {remaining} içinde duraklatılıyor…" +pvEnable = "Fazlalık mevcut. {remaining} içinde başlatılıyor…" scale1p = "{remaining} içinde 1 faza düşürülüyor…" scale3p = "{remaining} içinde 3 faza yükseltiliyor…" targetChargeActive = "Doldurma planı aktif…" From e837bfdeef9493be1e03cf4a78779ab5bfc53e46 Mon Sep 17 00:00:00 2001 From: Jonas Gate Date: Thu, 16 May 2024 19:56:35 +0000 Subject: [PATCH 099/168] Translated using Weblate (Swedish) Currently translated at 100.0% (421 of 421 strings) Co-authored-by: Jonas Gate Translate-URL: https://hosted.weblate.org/projects/evcc/evcc/sv/ Translation: evcc/evcc --- i18n/sv.toml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/i18n/sv.toml b/i18n/sv.toml index dc5ac453e5..719ca49934 100644 --- a/i18n/sv.toml +++ b/i18n/sv.toml @@ -486,6 +486,11 @@ meterstart = "Mätare start (kWh)" meterstop = "Mätare slut (kWh)" odometer = "Mätarställning (km)" vehicle = "Fordon" +chargeduration = "Varaktighet" +co2perkwh = "CO₂/kWh" +price = "Pris" +priceperkwh = "Pris/kWh" +solarpercentage = "Sol (%)" [sessions.filter] allLoadpoints = "alla laddplatser" @@ -565,3 +570,12 @@ title = "Startfel" cancel = "Avbryt" template = "Tillverkare" test = "Test" + + +[config.options.schema] +http = "HTTP (okrypterad)" +https = "HTTPS (krypterad)" + +[config.options.endianness] +big = "big-endian" +little = "little-endian" \ No newline at end of file From a4bce77f1245b078a045e0cf9cb632a9f463ae1a Mon Sep 17 00:00:00 2001 From: interstart Date: Thu, 16 May 2024 19:56:35 +0000 Subject: [PATCH 100/168] Translated using Weblate (Hungarian) Currently translated at 100.0% (421 of 421 strings) Co-authored-by: interstart Translate-URL: https://hosted.weblate.org/projects/evcc/evcc/hu/ Translation: evcc/evcc --- i18n/hu.toml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/i18n/hu.toml b/i18n/hu.toml index f9b9c5aaed..fb3c578894 100644 --- a/i18n/hu.toml +++ b/i18n/hu.toml @@ -479,6 +479,11 @@ meterstart = "Fogyasztásmérő kezdeti állás (kWh)" meterstop = "Fogyasztásmérő befejező állás (kWh)" odometer = "Óraállás (km)" vehicle = "Jármű" +chargeduration = Időtartam +co2perkwh = CO₂/kWh +price = Ár +priceperkwh = Ár/kWh +solarpercentage = Napenergia (%) [sessions.filter] allLoadpoints = "minden töltőpont" From e576fb207a76ec39727ca68c97e98324341842fc Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Thu, 16 May 2024 22:05:24 +0200 Subject: [PATCH 101/168] UI: fix visualization label animation (#13939) --- assets/js/components/Energyflow/Visualization.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/js/components/Energyflow/Visualization.vue b/assets/js/components/Energyflow/Visualization.vue index e373c03781..1622861e67 100644 --- a/assets/js/components/Energyflow/Visualization.vue +++ b/assets/js/components/Energyflow/Visualization.vue @@ -269,7 +269,7 @@ html.dark .grid-import { overflow: hidden; } .visualization--ready :deep(.label-bar) { - transition-property: width, opacity; + transition-property: flex-basis, opacity; transition-duration: var(--evcc-transition-medium), var(--evcc-transition-fast); transition-timing-function: linear, ease; } From df5d6ece8173d73e483c9edc91fb6facd317e760 Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Fri, 17 May 2024 10:07:36 +0200 Subject: [PATCH 102/168] UI i18n: percentage formatting (#13880) --- assets/js/components/BatterySettingsModal.vue | 2 +- assets/js/components/ChargingPlan.vue | 4 ++-- assets/js/components/ChargingPlanArrival.vue | 6 +++++- .../components/ChargingPlanSettingsEntry.vue | 1 + assets/js/components/ChargingPlanWarnings.vue | 2 +- assets/js/components/ChargingSessionModal.vue | 5 ++--- assets/js/components/Config/DeviceTags.vue | 5 ++--- assets/js/components/Energyflow/Energyflow.vue | 2 +- assets/js/components/LimitEnergySelect.vue | 3 ++- assets/js/components/LimitSocSelect.vue | 2 +- assets/js/components/LoadpointSessionInfo.vue | 2 +- assets/js/components/Savings.vue | 2 +- assets/js/components/SavingsTile.vue | 13 +++++++++++-- assets/js/components/Vehicle.vue | 4 ++-- assets/js/components/VehicleSoc.vue | 6 +++++- assets/js/components/VehicleStatus.test.js | 6 +++--- assets/js/components/VehicleStatus.vue | 6 ++++-- assets/js/mixins/formatter.js | 12 +++++++++++- assets/js/utils/energyOptions.js | 4 ++-- i18n/ar.toml | 4 ++-- i18n/bg.toml | 4 ++-- i18n/ca.toml | 14 +++++++------- i18n/cs.toml | 12 ++++++------ i18n/da.toml | 12 ++++++------ i18n/de.toml | 14 +++++++------- i18n/en.toml | 14 +++++++------- i18n/es.toml | 14 +++++++------- i18n/fi.toml | 14 +++++++------- i18n/fr.toml | 14 +++++++------- i18n/hr.toml | 4 ++-- i18n/hu.toml | 14 +++++++------- i18n/it.toml | 14 +++++++------- i18n/lb.toml | 14 +++++++------- i18n/lt.toml | 14 +++++++------- i18n/nl.toml | 14 +++++++------- i18n/no.toml | 14 +++++++------- i18n/pl.toml | 14 +++++++------- i18n/pt.toml | 14 +++++++------- i18n/ro.toml | 14 +++++++------- i18n/ru.toml | 8 ++++---- i18n/sl.toml | 14 +++++++------- i18n/sv.toml | 14 +++++++------- i18n/tr.toml | 18 +++++++++--------- i18n/uk.toml | 16 ++++++++-------- i18n/zh-Hans.toml | 14 +++++++------- tests/vehicle-settings.spec.js | 2 +- 46 files changed, 224 insertions(+), 195 deletions(-) diff --git a/assets/js/components/BatterySettingsModal.vue b/assets/js/components/BatterySettingsModal.vue index 2140db998e..9fa382ba66 100644 --- a/assets/js/components/BatterySettingsModal.vue +++ b/assets/js/components/BatterySettingsModal.vue @@ -436,7 +436,7 @@ export default { return { transform: `scale(${scale})` }; }, fmtSoc(soc) { - return `${Math.round(soc)}%`; + return this.fmtPercentage(soc); }, async changeDischargeControl(e) { try { diff --git a/assets/js/components/ChargingPlan.vue b/assets/js/components/ChargingPlan.vue index 7fb993577e..3e4060a3b7 100644 --- a/assets/js/components/ChargingPlan.vue +++ b/assets/js/components/ChargingPlan.vue @@ -176,7 +176,7 @@ export default { return this.targetChargeEnabled || this.minSocEnabled; }, minSocLabel: function () { - return `${Math.round(this.minSoc)}%`; + return this.fmtPercentage(this.minSoc); }, modalId: function () { return `chargingPlanModal_${this.id}`; @@ -204,7 +204,7 @@ export default { }, targetSocLabel: function () { if (this.socBasedPlanning) { - return `${Math.round(this.effectivePlanSoc)}%`; + return this.fmtPercentage(this.effectivePlanSoc); } return fmtEnergy( this.planEnergy, diff --git a/assets/js/components/ChargingPlanArrival.vue b/assets/js/components/ChargingPlanArrival.vue index a42f09e5c7..a94270a998 100644 --- a/assets/js/components/ChargingPlanArrival.vue +++ b/assets/js/components/ChargingPlanArrival.vue @@ -20,7 +20,11 @@ - {{ $t("main.loadpointSettings.minSoc.description", [selectedMinSoc || "x"]) }} + {{ + $t("main.loadpointSettings.minSoc.description", [ + selectedMinSoc ? fmtPercentage(selectedMinSoc) : "x", + ]) + }}
diff --git a/assets/js/components/ChargingPlanSettingsEntry.vue b/assets/js/components/ChargingPlanSettingsEntry.vue index df877066a6..50bfd04de2 100644 --- a/assets/js/components/ChargingPlanSettingsEntry.vue +++ b/assets/js/components/ChargingPlanSettingsEntry.vue @@ -176,6 +176,7 @@ export default { this.capacity || 100, this.socPerKwh, this.fmtKWh, + this.fmtPercentage, "-" ); // remove the first entry (0) diff --git a/assets/js/components/ChargingPlanWarnings.vue b/assets/js/components/ChargingPlanWarnings.vue index bdb0cbc9f6..48899ba245 100644 --- a/assets/js/components/ChargingPlanWarnings.vue +++ b/assets/js/components/ChargingPlanWarnings.vue @@ -129,7 +129,7 @@ export default { }, methods: { fmtSoc(soc) { - return `${Math.round(soc)}%`; + return this.fmtPercentage(soc); }, }, }; diff --git a/assets/js/components/ChargingSessionModal.vue b/assets/js/components/ChargingSessionModal.vue index 6721878ac0..d5f8c2d466 100644 --- a/assets/js/components/ChargingSessionModal.vue +++ b/assets/js/components/ChargingSessionModal.vue @@ -79,9 +79,8 @@ {{ $t("sessions.solar") }} - {{ fmtNumber(session.solarPercentage, 1) }}% ({{ - fmtKWh(solarEnergy, solarEnergy >= 1e3) - }}) + {{ fmtPercentage(session.solarPercentage, 1) }} + ({{ fmtKWh(solarEnergy, solarEnergy >= 1e3) }}) diff --git a/assets/js/components/Config/DeviceTags.vue b/assets/js/components/Config/DeviceTags.vue index bc1ea75847..594621eca4 100644 --- a/assets/js/components/Config/DeviceTags.vue +++ b/assets/js/components/Config/DeviceTags.vue @@ -41,7 +41,8 @@ export default { case "chargedEnergy": return this.fmtKWh(value * 1e3); case "soc": - return `${this.fmtNumber(value, 1)}%`; + case "socLimit": + return this.fmtPercentage(value, 1); case "odometer": case "range": return `${this.fmtNumber(value, 0)} km`; @@ -53,8 +54,6 @@ export default { return value.map((v) => this.fmtKw(v)).join(", "); case "chargeStatus": return value; - case "socLimit": - return `${this.fmtNumber(value)}%`; } return value; }, diff --git a/assets/js/components/Energyflow/Energyflow.vue b/assets/js/components/Energyflow/Energyflow.vue index 857044bf9d..5d7ef22cf5 100644 --- a/assets/js/components/Energyflow/Energyflow.vue +++ b/assets/js/components/Energyflow/Energyflow.vue @@ -288,7 +288,7 @@ export default { return this.pv.map(({ power }) => this.fmtKw(power, this.powerInKw)); }, batteryFmt() { - return (soc) => `${Math.round(soc)}%`; + return (soc) => this.fmtPercentage(soc, 0); }, co2Available() { return this.smartCostType === CO2_TYPE; diff --git a/assets/js/components/LimitEnergySelect.vue b/assets/js/components/LimitEnergySelect.vue index 27a1a3d8a9..1cad71da2a 100644 --- a/assets/js/components/LimitEnergySelect.vue +++ b/assets/js/components/LimitEnergySelect.vue @@ -57,6 +57,7 @@ export default { this.capacity || 100, this.socPerKwh, this.fmtKWh, + this.fmtPercentage, this.$t("main.targetEnergy.noLimit") ); }, @@ -75,7 +76,7 @@ export default { return fmtEnergy(value, this.step, this.fmtKWh, this.$t("main.targetEnergy.noLimit")); }, fmtSoc: function (value) { - return `+${Math.round(value)}%`; + return `+${this.fmtPercentage(value)}`; }, }, }; diff --git a/assets/js/components/LimitSocSelect.vue b/assets/js/components/LimitSocSelect.vue index 753bf111a0..8ce4183af7 100644 --- a/assets/js/components/LimitSocSelect.vue +++ b/assets/js/components/LimitSocSelect.vue @@ -67,7 +67,7 @@ export default { return null; }, formatSoc: function (value) { - return this.heating ? this.fmtTemperature(value) : `${Math.round(value)}%`; + return this.heating ? this.fmtTemperature(value) : this.fmtPercentage(value); }, formatKm: function (value) { return `${this.fmtNumber(value, 0)} ${distanceUnit()}`; diff --git a/assets/js/components/LoadpointSessionInfo.vue b/assets/js/components/LoadpointSessionInfo.vue index 7c904e3ea5..46d6448e99 100644 --- a/assets/js/components/LoadpointSessionInfo.vue +++ b/assets/js/components/LoadpointSessionInfo.vue @@ -117,7 +117,7 @@ export default { return this.valueSm !== undefined; }, solarFormatted() { - return `${this.fmtNumber(this.sessionSolarPercentage, 1)}%`; + return this.fmtPercentage(this.sessionSolarPercentage, 1); }, priceFormatted() { return `${this.fmtMoney(this.sessionPrice, this.currency)} ${this.fmtCurrencySymbol( diff --git a/assets/js/components/Savings.vue b/assets/js/components/Savings.vue index 324cd3b952..210658abce 100644 --- a/assets/js/components/Savings.vue +++ b/assets/js/components/Savings.vue @@ -228,7 +228,7 @@ export default { return `${docsPrefix()}/docs/reference/configuration/tariffs`; }, percent() { - return Math.round(this.solarPercentage) || 0; + return this.fmtPercentage(this.solarPercentage || 0); }, regionOptions() { return co2Reference.regions.map((r) => ({ diff --git a/assets/js/components/SavingsTile.vue b/assets/js/components/SavingsTile.vue index c01c6a303b..36972b3c32 100644 --- a/assets/js/components/SavingsTile.vue +++ b/assets/js/components/SavingsTile.vue @@ -15,11 +15,13 @@

{{ title }}

- + {{ value }} - {{ unit }} + + {{ unit }} +
@@ -37,10 +39,12 @@ import "@h2d2/shopicons/es/regular/receivepayment"; import "@h2d2/shopicons/es/regular/car3"; import "@h2d2/shopicons/es/regular/eco1"; import AnimatedNumber from "./AnimatedNumber.vue"; +import formatter from "../mixins/formatter"; export default { name: "SavingsTile", components: { AnimatedNumber }, + mixins: [formatter], props: { title: String, icon: String, @@ -50,6 +54,11 @@ export default { sub1: String, sub2: String, }, + computed: { + leadingUnit() { + return this.unit === "%" && this.hasLeadingPercentageSign(); + }, + }, };